DEV Community

Benjamin Black
Benjamin Black

Posted on • Edited on

Obtaining an Elliptic Curve certificate from Let's Encrypt

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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • -in is the input file
  • -noout tells OpenSSL not to output the key, which would just pointlessly print privkey.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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • -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
Enter fullscreen mode Exit fullscreen mode
  • -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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • N.B., quotes are required around the wildcard to prevent shell glob expansion, and in general they are a good idea.
  • --csr csr.pem tells certbot 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.
Enter fullscreen mode Exit fullscreen mode

The real command immediately follows:

certbot certonly --dns-route53 --domain "example.com" --domain "*.example.com" --csr csr.pem
Enter fullscreen mode Exit fullscreen mode

After a (long) delay, the client will produce as output:

  1. The signed certificate: 0000_cert.pem
  2. The root and intermediate certificates: 0000_chain.pem
  3. 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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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:

Chrome certificate inspector

Additionally, Mozilla Observatory will give us an A+ grade!

Mozilla Observatory 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)

Collapse
 
dineshrathee12 profile image
Dinesh Rathee

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...

Collapse
 
dhuv profile image
Dhaval Patel

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.

Collapse
 
softcreatr profile image
Sascha Greuel

Thank you for this article. But one thing is missing: Automatic renewal.

Would be nice, if you could add it.