Best Practices Using a Perl Client Module?

I am the developer for a large content management system that produces free websites from content entered in simple forms for Toastmasters public speaking clubs. Users also have the option to enter a custom domain name that they registered on their own (+ point to our server) into one of the form inputs and the system will use that domain name instead of the system default subdomain.domain format for their generated website.

We already handle the default domains via a wild card cert. I am exploring using some sort of ACME protocol access (via LE) for the 800+ custom domain names that the system also handles. I have posted previously exploring doing manual approaches for getting the certs, and I have basically come to the conclusion that the best approach for us is to use one of the available Perl client modules to allow us to seamlessly handle getting new certs, renewing them, and getting rid of them without any needed user intervention.

  1. The ones that I see are available via CPAN and MetaCPAN are: Net::ACME, Protocol::ACME, Crypt::LE, WWW::LetsEncrypt, Mojo::ACME … Can anyone make a recommendation for the simple one cert at a time case (like what I need)? Can anyone share experiences with these? I really do not want to have to test each one if I can avoid it.

  2. My presumed workflow (via Perl script) for a new cert is as follows… (Does this sound reasonable?)
    a. Detect new, changed, or deleted domain name entered in settings form for website.
    b. If changed, then check for existing cert, and if found ditch it.
    c. If domain was deleted then stop here.
    d. Create SSL key via call to OpenSSL
    e. Make call via ACME client function to get new cert w/ SSL key.
    f. Handle challenges. (how?)
    g. Save cert file in server folder used for all SSL certs
    h. Do a soft restart of Apache.
    i. Refresh the website page.

Thanks in advance!

check out this package from someone (not me) who has been doing this a while

Andrei

I'll try to answer to the question based on Crypt::LE and logic :slight_smile:

First, normally you need to account for the behavioural patterns of people - given the form which can be used, they might tend to be doing this repeatedly (add domain, remove domain, add domain, remove domain). Which with the workflow above will likely lead to an excessive amount of calls to Let's Encrypt and hitting a rate limit. So it's good to back up the process with some sort of DB-kept log on your side to detect "odd" cases.

For example, if there are use cases that names are getting "removed" in a sense of disabling SSL for those but they are still pointing to your systems and there is a chance they might be re-added, you might just disable the SSL configuration bit for that "removed" name but keep the certificate as-is.

Note that you are attempting to remove an existing certificate too early in your workflow - you don't want to end up without it if any of the further steps fail or if someone decides to restart the server while the process of updating the certificate is still running. Referencing missing certificate in your configuration will prevent successful restart.

You can indeed create a key by running openssl command or you can let the client/library do that. This is true for both the account key and the domain key. Please note that "to get new cert w/ SSL key" sounds a bit wrong - you need an account key and a CSR, but you are not sending your domain key to LE in the process.

Challenges can be handled by the client, by some custom subroutine in your script or you can use your own custom module to handle them (see the provided Crypt::LE::Challenge::Simple for example). Same with the completion (see Crypt::LE::Complete::Simple). During the custom completion process you can put the certificate wherever you want and initiate a restart if you like. If completion is handled by the client, you can use a custom set exit code to detect the issuance and take the necessary actions.

1 Like

ok, thanks

I am trying to get something going w/ Crypt::LE Running into problems.

(The following dumps to the browser screen)

# Reference: http://search.cpan.org/~perler/Crypt-OpenSSL-RSA-0.28/RSA.pm
use Crypt::OpenSSL::Random;
use Crypt::OpenSSL::RSA;
my $rsa = Crypt::OpenSSL::RSA->generate_key(4096); 
print "Private key is:<br>", $rsa->get_private_key_string();
print "<br><br>Public key (in PKCS1 format) is:<br>", $rsa->get_public_key_string();
# Reference: http://search.cpan.org/~leader/Crypt-LE-0.23/lib/Crypt/LE.pm
use Crypt::LE;
use Crypt::LE ':errors';
my $le = Crypt::LE->new();
print $le->load_account_key($rsa->get_private_key_string()) == OK or die "Could not load the account key: " . $le->error_details;
print $le->load_csr_key($rsa->get_private_key_string()) == OK or die "Could not load the csr key: " . $le->error_details;
print "<br><br>CSR is:<br>", $le->generate_csr(['mydomain.org'], KEY_RSA, 4096);

This does not generate the CSR for me–I get an error 3… (“mydomain.org is a dummy name… I do use a real domain name”)

Any thoughts?

First of all - always use “use strict”. :slight_smile: If you had that in your script, you would have received the following message on an attempt to run it:

Bareword “KEY_RSA” not allowed while “strict subs”

Additionally, to get the details about the code 3 (INVALID_DATA), you could call $le->error_details after generate_csr call and it would return “Unsupported key type” hinting about the problem.

This is because you are using KEY_RSA without importing it. Normally generate_csr does not need specifying the key type and size (it defaults to RSA and 4096), but if you want to specify them explicitly, you need to import ‘:keys’ too:

use Crypt::LE qw<:errors :keys>;
use strict;

Note that you only need to “use” library once.

Finally, keep in mind that generate_csr will return the result code (such as OK if you made the changes outlined above) - to get the CSR itself you need to call $le->csr;

NB: You don’t have to use Crypt::OpenSSL::RSA to generate a key (there is a generate_account_key call that can do it for you), though of course you can generate it in whichever way you like :slight_smile: Although you might want to call import_random_seed if you intend to generate that by yourself.

P.S. Almost forgot - you are trying to use the same private key for both the account and the domain - that won’t work (Let’s Encrypt will return an error for CSRs signed with account keys).

Ok thanks for the catch.

Ok, please excuse me as I am a bit new to the RSA SSL stuff. Don’t I need both a public and private key pair? My understanding is that generate_account_key only gives a private key… Am I missing something? Is the public key saved on the LE server and I just get the private key back?

I need to save this information on our server for later reference. I will be doing this create sequence for over 800 domain names in a loop–must have individual certs for each domain name. (have to be independent) I thought I would be saving to key files.

Again, please excuse me for my newness. On our server, we have some certs already that were gotten via “non-LE” means, and we only have one key file for each domain, so I assumed I only needed one key file per domain.

Let's Encrypt's ACME protocol also has a notion of an "account", which other CAs might not have (or they probably do, but the account on other CAs has a password and is accessed via a web browser, as opposed to the Let's Encrypt account which has a keypair and is accessed via an ACME client application).

Ok, got it.–that makes sense. Thank you!

Right now, what I see on our server for the “non-LE” certs are .key files, .csr files, and .crt files.

So, it looks like I need to generate 2 types of key files: one for LE account access and one for the csr file. I use those to set up the account and I get back the contents of the .crt file. Correct?

So, my question is then, that since I am only getting back private keys from the Crypt:LE functions that generate keys (per the doc), I am assuming that I would save those private keys in .key files on our server for future re-use. For example, maybe something like mydomain.le.key and mydomain.csr.key I am also assuming I would use those keys in the future when renewing the cert. (correct?)

Right.

You will also need them in order to provide the service at all, because the server needs the private key that corresponds to the certificate. :slight_smile:

The public key that is the subject of the certificate is listed in its entirety in the certificate itself and so it doesn't need to be saved in a separate file. (It could also be derived from the private key if that were ever necessary, which it's normally not.)

[quote]I am also assuming I would use those keys in the future when renewing the cert. (correct?)
[/quote]

You need the account key to renew the cert (or else you need to register a new account, which is also permitted). There was a proposal that you would also need to prove that you controlled the key that the cert refers to, but this was never implemented.

A bit more about the Perl Crypt::LE module usage …

Ok, so I am now generating/getting LE account keys via Crypt::LE using the generate_account_key() / account_key() methods. I am also now generating/getting a CSR via the generate_csr() / csr() methods.

Does the generate_csr() method create the key for the CSR automatically? Don’t I need to store a key for that in addition to the CSR for future use? I see a csr_key() method… I am assuming that I would use this to get the key so I can save it for future use?

Also, I am assuming that I only need the load_xxx() methods to load the account key / csr from a file when they were previously generated, correct? In other words, do I need to use a load after a generate, or does the key / csr get kept internal to the module from a generate?

Again, thanks much for the assistance!

Yes, unless it was explicitly provided (via load_csr_key).

Yes, you do - that will be the "domain" key you use with the certificate.

Yes, indeed.

Correct - once generated and stored, next time you load them (you can either give the filename to an appropriate function to load that from file or you can use a reference to serve the key/CSR from some variable you already loaded it to by some other means).

NB: Keep in mind that the library comes with a client application le.pl - you can use it as a reference point to implement your own workflow, or to just run the client app command line from your script and let it handle the challenges, save keys, etc (eliminating the need to use the library directly). See https://zerossl.com/usage.html

@leader:

Thanks. One thing that may not be immediately clear is that this is part of a much bigger system (11,000+ Toastmasters public speaking clubs and districts) that I do updates and maintenance coding for. For that reason, while using a client application like le.pl may be fine for initial development and testing, I am inclined to want to use the library module in order to tightly integrate everything. I will have to put my own wrapper functions around your library, anyway, so what I actually run is kind of immaterial.

As I am going to need to manage the certs for 800+ domain names in the system, I will likely save the cert issued date in our MySQL db for each domain/website to simplify renewals while keeping good performance. (So I can just trigger off of an elapsed time from saved cert issued date.) I am anticipating that sometimes the ACME calls may lag a bit (not due to Crypt::LE), so I do not want to rely strictly on your library calls to the ACME API for triggering cert renewals at the right time.

As I am getting keys now, and I think I understand that part of your library module’s interface, I will work towards getting the challenges piece working next. I will likely be back here with some questions on that.

@leader:

Ok, a question related to the http challenge in Crypt::LE:

Is the folder /.well-known/acme-challenge supposed to be located off the server root or HTML root? (just want to make sure before I start making the token file)

Is there anyway to change the location of this folder?

HTML root for the appropriate vhost. This is where Let’s Encrypt servers will be looking for it. Having said that, it does not mean that it should physically exist as an actual filesystem path - you can proxy it to some other place for example.

Simply put - for the library (or provided client, or your script) it does not make any difference where those files are going to be put, as long as the request to your webserver from verification servers for /.well-known/acme-challenge/[token] sends the expected data back. Technically speaking, no one stops you from processing that specific path with some custom code (mod_perl handler or whatever) and fetch the content of those “verification files” from the database, memory cache or anything you like.

@leader: Ok, good point. I did not think about proxying the location.

@leader:

Ok, I am now able to successfully complete challenges and generate a cert. Great!

I have not tested the generated cert, but I will be doing that shortly.

A few questions as I look forward to roll my development code in a fully working scheme:

  1. Do I need to register both www.mydomain.org and domain.org variants on certs to ensure both are covered? Keep in mind I am a bit new w/ the HTTPS stuff.

  2. I do not see any methods in your Perl module for checking time remaining on a cert or renewing the cert. Can you please advise if I am missing something?

  3. Do I only need to accept TOS when I register or renew? (or do I need to be constantly checking for a change in TOS?)

Thanks again for all your assistance!

Yes. You can include up to 100 domain names on one certificate if needed. Keep in mind that each will need to be verified.

There is such function in the client (see verify_crt_file for example), but not in the library indeed. I guess it might be a good idea to place that into the next version of the library, to make it easier to use. The renewal itself is effectively the same process as the initial issuance - you just trigger it when you see it's time to renew (for example with that code I mentioned above).

TOS may change and you will need to re-accept it. However, you can safely call accept_tos() - if there was no change, it will just return OK status. If you want to specifically check if there was a change, you can use tos_changed().

@leader:

Something I am noticing… It appears that if I load the csr key from a file, then load the csr from a file,
$le->csr_key() is blank.

This is intentional - you might only need to load "CSR key" if you are about to generate and sign a new CSR (by default the key will be generated for you along with the CSR itself). If you are loading a pre-made CSR though, that means it has already been signed with some key, not necessarily matching the one you might attempt to load separately. Considering that it is not possible to extract that key from CSR anyway, the key is reset to avoid the confusion (otherwise you might for example by mistake try to save that key and try using that with the certificate obtained with a CSR signed with a completely different key).

@leader:

I would suggest documenting this a bit better.