ZeroSSL API – The missing examples

02Apr21

The @ Company uses a lot of SSL certificates, and we’ve been using ZeroSSL and its Certbot wrapper zerossl-bot to automate how we manage certs. But we wanted more control over the process, which has driven us towards the ZeroSSL API. Sadly the docs don’t provide usage examples, which has made it quite a journey to figure out how things work.

After LOTS of trial and error I have a script that generates and downloads a certificate, which I’ll walk through block by block below. The whole thing is at this gist.

I’m using bash, so we begin with a shebang:

#!/bin/bash

I’ll probably end up writing the final script in Python, or maybe even put together a Go or Dart app, to provide error checking and retry logic.

I’m using the Digital Ocean API for DNS (other DNS providers with APIs are available), as it’s fast and well documented. Both APIs need keys, which should be stored in a secrets manager rather than a script:

# API keys for ZeroSSL and Digital Ocean
# These particular keys are fake random hex
ZEROSSL_KEY='0f027ac0f3b24ddb3c4412f11fa1e746'
DO_KEY='a3e6ee004fd7c352af61f0465765030b5d162acc94c24fdbb42f7a8c81e897a3'

The script sets a base (sub)domain, and takes a single parameter of the certificate name to be created:

# Set root domain and take CN from params
DOMAIN=subdomain.example.com
CERT_NAME="$1"."$DOMAIN"

We need a certificate signing request (CSR). The ZeroSSL docs point to a web form that generates CSRs, but anybody using an API will want to have a more automated way of doing that bit, such as:

# Create CSR and Private Key
openssl req -new -newkey rsa:2048 -nodes -out "$CERT_NAME".csr \
            -keyout "$CERT_NAME".key \
            -subj "/C=GB/ST=London/L=London/O=Example/OU=Testing/CN=$CERT_NAME" \
            &>/dev/null

With the CSR prepared it’s time for the first call against the ZeroSSL API to draft a certificate. The hard part here was figuring out how to pass a CSR into the API, but thankfully recent versions of curl have an option to URL encode data directly, and I’m using the @ operator to pull in the CSR from the file generated in the last step:

# Draft certificate at ZeroSSL
curl -s -X POST https://api.zerossl.com/certificates?access_key="$ZEROSSL_KEY" \
        --data-urlencode certificate_csr@"$CERT_NAME".csr \
        -d certificate_domains="$CERT_NAME" \
        -d certificate_validity_days=90 \
        -o "$CERT_NAME".resp

The response then needs to be parsed to extract the certificate ID and parameters for validation, in this case for the DNS CNAME method. Since it seems necessary to modify DNS to make use of the HTTP(S) methods this look like the simplest way.

I tried using jq to parse the JSON, but it’s an extra dependency, and seemed to sometimes mangle CNAME parameters. The combination of sed and awk isn’t great, but does appear to work for this limited use case:

# Extract CNAME parameters from ZeroSSL response
ID=$(< "$CERT_NAME".resp  python3 -c "import sys, json; print(json.load(sys.stdin)['id'])")
CNAME_HOST=$(< "$CERT_NAME".resp sed -e 's/[{}]/''/g' \
        | awk -v RS=',"' -F: '/^cname_validation_p1/ {print $2}' \
        | sed -e 's/"//g' | sed -s s/".$DOMAIN"//g)
CNAME_ALIAS=$(< "$CERT_NAME".resp sed -e 's/[{}]/''/g' \
        | awk -v RS=',"' -F: '/^cname_validation_p2/ {print $2}' \
        | sed -s 's/"//g')

The CNAME can then be added to DNS for validation:

# Add DNS CNAME at Digital Ocean for verification
curl -s -X POST -H "Content-Type: application/json" \
        -H "Authorization: Bearer $DO_KEY" \
        -d '{"type":"CNAME","name":"'"$CNAME_HOST"'","data":"'"$CNAME_ALIAS"'.","priority":null,"port":null,"ttl":1800,"weight":null,"flags":null,"tag":null}' \
        https://api.digitalocean.com/v2/domains/"$DOMAIN"/records \
        -o "$CERT_NAME".name

Wait a moment for it to be ready:

# Wait for DNS record to propagate
sleep 30

Then call for validation:

# Validate certificate at ZeroSSL
curl -s -X POST https://api.zerossl.com/certificates/"$ID"/challenges?access_key="$ZEROSSL_KEY" \
        -d validation_method=CNAME_CSR_HASH \
        -o "$CERT_NAME".vald

Wait again for the certificate to be issued:

# Wait for cert to be issued
sleep 30

Then get the certificate. Using jq here to prettify the JSON:

# Get the cert
curl -s https://api.zerossl.com/certificates/"$ID"/download/return?access_key="$ZEROSSL_KEY" \
        | jq -r '."certificate.crt"' > "$CERT_NAME".crt

Finally tidy up DNS by removing the validation CNAME:

DNSID=$(< "$CERT_NAME".name python3 -c "import sys, json; print(json.load(sys.stdin)['domain_record']['id'])")
echo "$DNSID"

# Delete the verification CNAME
curl -s -X DELETE -H "Content-Type: application/json" \
        -H "Authorization: Bearer $DO_KEY" \
        https://api.digitalocean.com/v2/domains/"$DOMAIN"/records/"$DNSID"

Update 15 Apr 2021

I ended up doing a Python version, with some better error checking and retry logic.

Update 21 Mar 2022

I’m no longer using the REST API for ZeroSSL, having switched to a fork of the acme-dns-tiny script (which takes care of DNS pieces with the Digital Ocean API).



2 Responses to “ZeroSSL API – The missing examples”

  1. 1 Anonymous coward

    Might be worth adding some basic error handling if there are delays in the dns propagation.

    • Absolutely. That’s the next job, and probably why I’ll redo it in a proper language that has JSON support. I’m using Python anyway, so I might as well use that. Though I’m also keen to sharpen my Go and to learn my way into Dart. If there had been curl examples I’d probably already be doing the stuff to make it work rather than just having a barely functional prototype. It took me minutes to get the Digital Ocean stuff working using their curl examples. It took me hours to get the ZeroSSL bits working, and those hours would have been better spent on resilience.


Leave a reply to Anonymous coward Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.