RFC 7030, the silliEST edition. Brought to you by Brandon @ cyb3r.sh
This server runs a two-tier PKI (root + intermediate CA) and speaks just enough of EST (RFC 7030) to complete this workshop.
Our goals are:
Instead of using a production EST client, we're going to use common CLI tools like:
curl, openssl, and
base64 in order to showcase how simple the protocol is and to aid in our understanding of the protocol.
silliest.lab.cyb3r.sh with a publicly-trusted certificate, so your browser doesn't complain. The EST endpoints live at silliest-ca.lab.cyb3r.sh and use a certificate signed by an ephemeral root CA.Use this string as the HTTP Basic Auth password when calling
/simpleenroll (RFC 7030 sec 3.2.3). The username can be anything.
9138439cafb7fe25d90f54831e914291
/cacerts (sec 4.1)The first request is the bootstrap. You don't trust the server's TLS cert
yet, so use curl -k to disable validation for this one call.
The response is a base64-encoded "certs-only" PKCS#7 carrying the CA certificates.
curl -k https://silliest-ca.lab.cyb3r.sh/.well-known/est/cacerts -o cacerts.b64
It's worth calling out that "certs-only" PKCS#7 is literally base64 encoded DER. A binary format that anyone who has worked with certificates should be familiar with.
RFC 7030 sec 4.1.3 requires /cacerts to return the root and every intermediate a client needs to build a chain back to the trust anchor. Since our CA is two tiers, we should have received two certificates. Let's extract them.
# list the certs inside the cacerts response
base64 -d cacerts.b64 | openssl pkcs7 -inform DER -print_certs -noout
# Convert to a PEM bundle, then split into separate root + intermediate files
base64 -d cacerts.b64 | openssl pkcs7 -inform DER -print_certs -out ca.pem
awk '/-----BEGIN/{n++; f="ca-"n".pem"} f{print > f}' ca.pem
mv ca-1.pem root.pem
mv ca-2.pem intermediate.pem
From here on, use --cacert root.pem instead of -k.
intermediate.pem separately because we'll need it
ourselves in Step 5 when presenting our leaf certificate during an mTLS handshake.In production, EST clients either have the root provisioned out-of-band or will disable validation on the first check, then compare the root against a known valid fingerprint (that was provisioned out of band). The intermediate is then validated implicitly by its signature chain back to that root.
# Does the root fingerprint match what we expect?
openssl x509 -in root.pem -noout -fingerprint
# Can we validate the intermediate against the root?
openssl verify -CAfile root.pem intermediate.pem
Expected Root CA SHA-256 Fingerprint: 7e:41:47:d1:5e:22:bb:6c:7f:66:c3:1e:5c:6a:27:d1:fe:74:e2:51:97:04:7f:ce:79:59:25:4a:c5:b1:92:42
Finally, take a look at the intermediate and root certificates key usage and issuer.
openssl x509 -in root.pem -noout -subject -issuer -ext keyUsage
openssl x509 -in intermediate.pem -noout -subject -issuer -ext keyUsage
Two things to note, 1) The RootCA's issuer is itself. Trust has to start somewhere. 2) Both the intermediate and root can be used for certificate signing, but the root would generally only be used to sign the intermediate. All leaf certificates should be signed by the intermediate. Revoking an intermediate certificate is easier than revoking the root.
# generate a key pair
openssl genrsa -out client.key 2048
# create a new CSR
openssl req -new -key client.key -out client.csr -subj "/CN=workshop-attendee-cTaYr"
# optionally, view the CSR you just created
openssl req -in client.csr -noout -text
/simpleenroll (sec 4.2)As with the CACerts, we enroll by simply sending a PKCS#10 CSR. Once again, this is literally just base64 encoded DER.
# Base64 encode the DER
openssl req -in client.csr -outform DER | base64 > client.csr.b64
POST the encoded CSR and authenticate using basic auth with the challenge above as the password.
# Send an HTTPS request with appropriate headers and our CSR
curl --cacert root.pem \
-u "workshop:9138439cafb7fe25d90f54831e914291" \
-H "Content-Type: application/pkcs10" \
-H "Content-Transfer-Encoding: base64" \
--data-binary @client.csr.b64 \
https://silliest-ca.lab.cyb3r.sh/.well-known/est/simpleenroll \
-o client.p7.b64
The response is a base64 PKCS#7 containing your shiny new certificate. Per RFC 7030 sec 4.2.3 only the end-entity cert is returned. The client is expected to build the chain from /cacerts.
base64 -d client.p7.b64 | openssl pkcs7 -inform DER -print_certs -out client.pem
Note that unlike our intermediate and root, the leaf certificate is not valid for certificate signing. It is valid for digital signature, which is necessary for TLS operations.
openssl x509 -in client.pem -noout -subject -issuer -dates -ext keyUsage
1h0m0s). Once expired, the TLS handshake on /mtls-test and /simplereenroll will fail. Re-enroll via /simpleenroll (Step 3) or renew before expiry via Step 6.Before using the cert, let's check that we can build the chain back to the root.
-CAfile supplies the trust anchor;
-untrusted supplies the intermediate(s) needed to bridge the gap.
openssl verify -CAfile root.pem -untrusted intermediate.pem client.pem
A failure here means either the intermediate doesn't match, the cert is expired, or the root you trusted isn't the one that signed our intermediate.
The /mtls-test page requires you to present your chain in the
handshake, not just the leaf. This is what TLS servers do when they present their own cert.
cat client.pem intermediate.pem > client-chain.pem
curl --cacert root.pem --cert client-chain.pem --key client.key \
https://silliest-ca.lab.cyb3r.sh/mtls-test
Send only the leaf (--cert client.pem) and the request will fail.
/simplereenroll (sec 4.2.2)Reenrollment authenticates with the existing certificate via mTLS. No challenge
required. The format is otherwise identical to /simpleenroll.
First, the simple case. Renew the cert without rekeying. Sign a new CSR with the existing key. Then send a request to simplereenroll in the same format as our earlier enrollment request.
openssl req -new -key client.key -out reenroll.csr -subj "/CN=workshop-attendee-cTaYr"
openssl req -in reenroll.csr -outform DER | base64 > reenroll.csr.b64
curl --cacert root.pem \
--cert client.pem --key client.key \
-H "Content-Type: application/pkcs10" \
-H "Content-Transfer-Encoding: base64" \
--data-binary @reenroll.csr.b64 \
https://silliest-ca.lab.cyb3r.sh/.well-known/est/simplereenroll \
-o reenrolled.p7.b64
base64 -d reenrolled.p7.b64 | openssl pkcs7 -inform DER -print_certs -out reenrolled.pem
openssl x509 -in reenrolled.pem -noout -subject -issuer -dates
Or rekey at the same time by generating a new keypair and binding it to the new CSR. The mTLS handshake will still use the old cert and key (the only one we have right now), but the CSR carries the new public key.
# new keypair, new CSR signed with the new key
openssl genrsa -out client-new.key 2048
openssl req -new -key client-new.key -out reenroll-rekey.csr -subj "/CN=workshop-attendee-cTaYr"
openssl req -in reenroll-rekey.csr -outform DER | base64 > reenroll-rekey.csr.b64
curl --cacert root.pem \
--cert client.pem --key client.key \
-H "Content-Type: application/pkcs10" \
-H "Content-Transfer-Encoding: base64" \
--data-binary @reenroll-rekey.csr.b64 \
https://silliest-ca.lab.cyb3r.sh/.well-known/est/simplereenroll \
-o reenrolled-rekey.p7.b64
base64 -d reenrolled-rekey.p7.b64 | openssl pkcs7 -inform DER -print_certs -out reenrolled-rekey.pem
openssl x509 -in reenrolled-rekey.pem -noout -subject -issuer -dates
# confirm the rekeyed cert is bound to client-new.key (matching md5s)
openssl x509 -in reenrolled-rekey.pem -noout -pubkey | openssl md5
openssl pkey -in client-new.key -pubout | openssl md5
From this point on you'd swap to client-new.key + reenrolled-rekey.pem for any subsequent mTLS work (and discard the old key).
openssl s_clientTo watch the raw TLS handshake and inspect the cert chain the server presents:
openssl s_client -connect silliest-ca.lab.cyb3r.sh:443 -showcerts < /dev/null
To watch an mTLS handshake with your client cert:
openssl s_client -connect silliest-ca.lab.cyb3r.sh:443 \
-CAfile root.pem -cert client-chain.pem -key client.key < /dev/null