Obtaining an Elliptic Curve certificate from Let's Encrypt
Tags: letsencrypt, openssl
Published: 2018-09-25 21:48:18

---
title: Obtaining an Elliptic Curve certificate from Let's Encrypt
published: true
description: Let's Encrypt does not currently generate Elliptic Curve certificates. Here's how to obtain one.
tags: letsencrypt, openssl
---

*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"](https://soatok.blog/2022/05/19/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](https://wiki.mozilla.org/Security/Server_Side_TLS#Recommended_configurations) as measured by [Mozilla Observatory](https://observatory.mozilla.org).

As of this writing, the Let's Encrypt [Upcoming Features](https://letsencrypt.org/upcoming-features/) page indicates that ECDSA ([Elliptic Curve Digital Signature Algorithm](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm), as opposed to ECDH, [Elliptic Curve Diffie-Hellman](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman)) 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 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
```

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](https://www.openssl.org/docs/manmaster/man1/req.html#CONFIGURATION_FILE_FORMAT):

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](http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_password_file), 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](https://en.wikipedia.org/wiki/Subject_Alternative_Name) (SANs) are configured.

In the Distinguished Name (`[ dname ]`) section:
* `CN = example.com` specifices the [Common Name](https://www.ssl.com/faqs/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](https://support.google.com/chrome/a/answer/7391219?hl=en)).

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](https://certbot.eff.org/docs/using.html#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` 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.
```

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: 

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

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](https://crypto.stackexchange.com/questions/26336/sha512-faster-than-sha256).) 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](https://mozilla.github.io/server-side-tls/ssl-config-generator/), follows:

```nginx
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:

![Chrome certificate inspector](https://i.imgur.com/6N2USsV.png "Chrome certificate inspector")

Additionally, [Mozilla Observatory](https://observatory.mozilla.org/) will give us an A+ grade!

![Mozilla Observatory grade](https://i.imgur.com/aTi2Yxr.png "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.