RFC 7030, the workshop edition
This server runs a two-tier PKI (root + intermediate CA) and speaks just enough of EST (RFC 7030) to let you enroll a client certificate, then use that certificate to reach an mTLS authenticated page.
You won't use a "real" EST client. Just common CLI tools like:
curl, openssl, and
base64. Each step below maps to a specific section of the RFC.
silliest.lab.cyb3r.sh with a publicly-trusted certificate, so your browser doesn't complain. EST endpoints live at silliest-ca.lab.cyb3r.sh instead. That hostname presents the silliEST CA-signed cert, establishing trust with this CA and receiving a client certificate from it is the goal of this workshop.Use this string as the HTTP Basic Auth password when calling
/simpleenroll (RFC 7030 sec 3.2.3). The username can be anything.
2bb5f520755efda4a7c245a556a6d9e3
/cacerts (sec 4.1)The first request is the bootstrap. You don't trust the server's TLS cert
yet, so use -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
# list the certs inside the cacerts response
base64 -d cacerts.b64 | openssl pkcs7 -inform DER -print_certs -noout
# decode -> 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.
Why only the root? By convention, TLS speakers present their
leaf cert plus the intermediate(s) they're signed by during the handshake, so the
client only needs the root in its trust store to build and validate the chain.
We hold onto intermediate.pem separately because we'll need it ourselves
in Step 5 when presenting our leaf certificate during an mTLS handshake.
# Does the root fingerprint match what we expect?
openssl x509 -in root.pem -noout -fingerprint
# Can we validated the intermediate against the root?
openssl verify -CAfile root.pem intermediate.pem
Expected Root CA SHA-256 Fingerprint: ec:c5:29:34:af:0d:41:38:34:80:cb:2f:52:fd:a8:ab:a3:ea:37:f2:3c:8a:60:92:21:8e:95:a5:01:68:cf:f6
/cacerts to return the root
and every intermediate a client needs to build a chain back to the trust
anchor. In production, EST clients either have the root provisioned out-of-band
or will disable validation on the first check, but 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.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 a leaf.
# 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"
# optionally, view the CSR you just created
openssl req -in client.csr -noout -text
/simpleenroll (sec 4.2)POST the CSR as base64 encoded DER and authenticate using basic auth with the challenge above as the password.
# Base64 encode the DER
openssl req -in client.csr -outform DER | base64 > client.csr.b64
# Send an HTTPS request with appropriate headers and our CSR
curl --cacert root.pem \
-u "workshop:2bb5f520755efda4a7c245a556a6d9e3" \
-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
# extract the client cert from the pkcs7 bundle
base64 -d client.p7.b64 | openssl pkcs7 -inform DER -print_certs -out client.pem
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.
openssl x509 -in client.pem -noout -subject -issuer -dates ext keyUsage
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.
1h0m0s). Once expired, the TLS handshake on /mtls-test and /simplereenroll will fail. Re-bootstrap via /simpleenroll (Step 3) or renew before expiry via Step 6.Before using the cert, sanity-check that openssl can build the chain
client -> intermediate -> 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 the cert doesn't chain to our root. Likely because 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 server will reject the
request with a 401 explaining what's missing.
To visit /mtls-test from a browser, export a .p12
that carries the leaf, key, and intermediate together, then trust root.pem:
openssl pkcs12 -export -in client.pem -inkey client.key \
-certfile intermediate.pem -out client.p12 -passout pass:
/simplereenroll (sec 4.2.2)Reenrollment authenticates with the existing certificate via mTLS. No challenge
required. The format is otherwise identical to /simpleenroll.
# reuse client.key, or generate a new one for rekey-on-reenroll
openssl req -new -key client.key -out reenroll.csr -subj "/CN=workshop-attendee"
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
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 -servername silliest-ca.lab.cyb3r.sh \
-CAfile root.pem -cert client-chain.pem -key client.key < /dev/null