My posts are usually notes and reference materials for myself, which I publish here with the hope that others might find them useful.
EDIT 23 May 2022: See this article: "Guidance for Choosing an Elliptic Curve Signature Algorithm in 2022"
Elliptic Curve (EC) certificates, wherein the public key uses elliptic curve cryptography, besides having a cool name, are required for "Modern" compatibility as measured by Mozilla Observatory.
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).
Generate the private key
First, we generate the private key with OpenSSL. The OpenSSL command we will use is ecparam
(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
- The
-genkey
option tells OpenSSL to generate an EC key. - The
-name
param tells OpenSSL which curve to use. - The
-out
param 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
-
-in
is the input file -
-noout
tells OpenSSL not to output the key, which would just pointlessly printprivkey.pem
to stdout. -
-text
tells 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.
Create the OpenSSL configuration for the certificate
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 openssl.cnf
:
[ req ]
prompt = no
encrypt_key = no
default_md = sha512
distinguished_name = dname
req_extensions = reqext
[ dname ]
CN = example.com
emailAddress = admin@example.com
[ reqext ]
subjectAltName = DNS:example.com, DNS:*.example.com
A brief explanation of these configuration options:
In the Required ([ req ]
) section:
-
prompt = no
tells OpenSSL to get as much configuration as it can from the configuration file -
encrypt_key = no
tells 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 = sha512
tells 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 = dname
tells OpenSSL to look for a[ dname ]
section for Distinguished Name configuration options. -
req_extensions = reqext
tells 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.com
specifices the Common Name for the certificate. -
emailAddress = admin@example.com
should 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 (*.example.com
).
Create a Certificate Signing Request
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 req
(man openssl
and openssl req -help
).
openssl req -new -config openssl.cnf -key privkey.pem -out csr.pem
-
-new
tells OpenSSL that we are creating a CSR (and not examining an existing CSR) -
-config openssl.cnf
specifies the configuration file we created above -
-key privkey.pem
specifies the private key we generated above -
-out csr.pem
tells 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
-
-verify
requests 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 = admin@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]
Ask Let's Encrypt to sign our certificate
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.
E.g., if 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.
-
--csr csr.pem
tellscertbot
that we already have a certificate that we just need Let's Encrypt to sign for us.
The 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:
0000_cert.pem
- The root and intermediate certificates:
0000_chain.pem
- The certificate + intermediates:
0001_chain.pem
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 x509
command:
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:
/etc/ssl/certs/example.com/cert.pem
/etc/ssl/certs/example.com/chain.pem
/etc/ssl/certs/example.com/fullchain.pem
BONUS ROUND: Configure the web server
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 1.1.1.1;
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!
Postscript
We should also create a cron job / systemd timer to renew our certificate. This is an exercise left to the reader.
Top comments (3)
LetsEncrypt have revoked around 3 million certs last night due to a bug that they found. Are you impacted by this, Check out ?
DevTo
[+] dev.to/dineshrathee12/letsencrypt-...
GitHub
[+] github.com/dineshrathee12/Let-s-En...
LetsEncryptCommunity
[+] community.letsencrypt.org/t/letsen...
As of Oct 2020, Let's Encrypt still signs the EC cert you generate with a RSA intermediate. This works fine in all browsers but it is not HIPPA or NIST compliant. If you do this, you will fail tests that check for those things.
Thank you for this article. But one thing is missing: Automatic renewal.
Would be nice, if you could add it.