My posts are usually notes and reference materials for myself, which I publish here with the hope that others might find them useful.
As of this writing, the Let's Encrypt Upcoming Features page indicates that ECDSA (Elliptic Curve Digital Signature Algorithm, as opposed to ECDH, Elliptic Curve Diffie-Hellman) root and intermediate certificates are coming in Q1 2019, but there is not yet an indication when Let's Encrypt will support generating EC certificates for end users. Fortunately, however, Let's Encrypt will sign an EC certificate passed to it in a Certificate Signing Request (CSR).
So, in the meantime, if we want an EC certificate from Let's Encrypt, we need to create our own certificate, and then ask Let's Encrypt to sign it.
Fortunately, the process is not difficult.
In this example, we will generate a private key using ECDSA with the P-384 (secp384r1) curve, which has near-universal browser support back to IE11 (hence, its inclusion in Mozilla's "Modern" compatibility requirements).
First, we generate the private key with OpenSSL. The OpenSSL command we will use is
man openssl), which is used for "EC parameter manipulation and generation," and passing configuration parameters to that command (
openssl ecparam -help).
openssl ecparam -genkey -name secp384r1 -out privkey.pem
-genkeyoption tells OpenSSL to generate an EC key.
-nameparam tells OpenSSL which curve to use.
-outparam tells OpenSSL to write the output to a file.
Note that OpenSSL writes its output in PEM format by default.
We can check that OpenSSL did the right thing with the
ec command, which processes EC keys:
openssl ec -in privkey.pem -noout -text
-inis the input file
-noouttells OpenSSL not to output the key, which would just pointlessly print
-texttells OpenSSL to write out information about the key in plaintext format
If all goes well, and the key was created correctly, OpenSSL will show something like the following:
read EC key Private-Key: (384 bit) priv: [redacted] pub: [omitted] ASN1 OID: secp384r1 NIST CURVE: P-384
This confirms that the key was created with the P-384 curve.
Next, we must create an OpenSSL configuration file with parameters specific to the domain for which we wish to obtain a TLS certificate. In this example, we will enter the following configuration into a file
[ req ] prompt = no encrypt_key = no default_md = sha512 distinguished_name = dname req_extensions = reqext [ dname ] CN = example.com emailAddress = email@example.com [ reqext ] subjectAltName = DNS:example.com, DNS:*.example.com
A brief explanation of these configuration options:
In the Required (
[ req ]) section:
prompt = notells OpenSSL to get as much configuration as it can from the configuration file
encrypt_key = notells OpenSSL not to encrypt the private key with a password. (Encrypted private keys are supported by Nginx, but I don't use them.)
default_md = sha512tells OpenSSL to sign the CSR with SHA512. (AFAIK, Let's Encrypt only supports RSA with SHA256 for its signatures, but that doesn't mean we can't use stronger encryption on the CSR.)
distinguished_name = dnametells OpenSSL to look for a
[ dname ]section for Distinguished Name configuration options.
req_extensions = reqexttells OpenSSL to look for a
[ reqext ]section for Requested Extensions configuration options, which is where Subject Alternative Names (SANs) are configured.
In the Distinguished Name (
[ dname ]) section:
CN = example.comspecifices the Common Name for the certificate.
emailAddress = firstname.lastname@example.org be obvious.
In the Requested Extensions (
[ reqext ]) section, the
subjectAltName provides the list of SANs for the certificate. (Chrome, as of v58, requires the Common Name to be included in the list of SANs).
Let's Encrypt v2 supports wildcard domains, so in this example I have requested a single-level wildcard for hosts off of the apex (
The final client-side step is to generate the Certificate Signing Request using OpenSSL, which we will then pass to Let's Encrypt to sign, and return to us the signed certificate.
The OpenSSL command needed to generate a CSR is
man openssl and
openssl req -help).
openssl req -new -config openssl.cnf -key privkey.pem -out csr.pem
-newtells OpenSSL that we are creating a CSR (and not examining an existing CSR)
-config openssl.cnfspecifies the configuration file we created above
-key privkey.pemspecifies the private key we generated above
-out csr.pemtells OpenSSL to write the CSR to an output file (instead of stdout)
We can verify that we generated the CSR correctly:
openssl req -in csr.pem -noout -text -verify
-verifyrequests OpenSSL verify the signature on the CSR
This should produce expected results in the output:
verify OK Certificate Request: Data: Version: 1 (0x0) Subject: CN = example.com, emailAddress = email@example.com Subject Public Key Info: Public Key Algorithm: id-ecPublicKey Public-Key: (384 bit) pub: [omitted] ASN1 OID: secp384r1 NIST CURVE: P-384 Attributes: Requested Extensions: X509v3 Subject Alternative Name: DNS:example.com, DNS:*.example.com Signature Algorithm: ecdsa-with-SHA512 [omitted]
The last step is to pass the CSR to Let's Encrypt with an ACME client,
certbot being the most common client.
The command-line options passed to the
certbot client vary depending on our setup, with whom our domain is registered, etc. We will generally need to use the
certonly command, and we may be able to use one of the certbot DNS plugins.
example.com is registered with AWS Route 53, we can use the corresponding plugin to handle verification, which is extremely convenient, requiring no manual intervention in the process. (Configuring the Route 53 DNS plugin with AWS credentials is beyond the scope of this article.)
It is generally advisable to do a
--dry-run first to make sure everything is in order.
certbot certonly --dry-run --dns-route53 --domain "example.com" --domain "*.example.com" --csr csr.pem
- N.B., quotes are required around the wildcard to prevent shell glob expansion, and in general they are a good idea.
certbotthat we already have a certificate that we just need Let's Encrypt to sign for us.
certbot client will check that the list of domains requested on the command line match the domains listed in the certificate, and it will use the Route 53 DNS plugin to verify our ownership of the domain, and let us know if anything is wrong.
If nothing is wrong, it will tell you so:
IMPORTANT NOTES: - The dry run was successful.
The real command immediately follows:
certbot certonly --dns-route53 --domain "example.com" --domain "*.example.com" --csr csr.pem
After a (long) delay, the client will produce as output:
- The signed certificate:
- The root and intermediate certificates:
- The certificate + intermediates:
At this point, the CSR
csr.pem can be deleted.
If we are curious, we can inspect the certificates returned by the client with OpenSSL using the
openssl x509 -in 0001_chain.pem -noout -text
Alas, we will discover, as described above, that Let's Encrypt has signed our certificate with a SHA256 signature. (In addition to being more secure, SHA512 performs better than SHA256 on modern 64-bit CPUs.) But our public key should still use ECDSA.
These files are nondescript, so we should move and organize them in a more informative way.
On Debian Linux, I like to create subdirectories for my domains, keeping the private key in
/etc/ssl/private/example.com/privkey.pem, and the certs:
At this point, we have our certificates in hand. We can configure our web server of choice to use them. As a brief example, an Nginx configuration snippet, which includes modified rules generated by Mozilla Observatory for its "Modern" configuration, follows:
ssl_certificate_key /etc/ssl/private/example.com/privkey.pem; ssl_certificate /etc/ssl/certs/example.com/fullchain.pem; ssl_trusted_certificate /etc/ssl/certs/example.com/chain.pem; # Share 50 MB session cache ssl_session_cache shared:SSL:50m; # Disable session tickets ssl_session_tickets off; # Use only TLSv1.2+ ssl_protocols TLSv1.2 TLSv1.3; # Safe ciphers ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-\ RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; ssl_prefer_server_ciphers on; # Safe curves ssl_ecdh_curve secp521r1:secp384r1:prime256v1; # OCSP Stapling ssl_stapling on; ssl_stapling_verify on; # CloudFlare for DNS lookup resolver 184.108.40.206; add_header Content-Security-Policy "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; frame-ancestors 'none'"; add_header X-Content-Type-Options "nosniff"; add_header X-Frame-Options "sameorigin"; add_header X-XSS-Protection "1; mode=block"; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
If we have done everything correctly, when we inspect the certificate with a web browser like Chrome, it will confirm that it is an EC certificate:
Additionally, Mozilla Observatory will give us an A+ grade!
We should also create a cron job / systemd timer to renew our certificate. This is an exercise left to the reader.