[root@localhost /lab]# silliEST

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.

This landing page is served from 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.

Your enrollment challenge

Use this string as the HTTP Basic Auth password when calling /simpleenroll (RFC 7030 sec 3.2.3). The username can be anything.

2bb5f520755efda4a7c245a556a6d9e3

For simplicity, this server only supports basic auth, production EST servers often support digest and bearer authentication as well.

Step 1 - Bootstrap trust via /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

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

Step 2 - Generate a keypair and a CSR (RFC 2986)

# 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

Step 3 - Enroll via /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.

Heads up: issued certs are short-lived (TTL: 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.

Step 4 - Verify the issued cert chains back to the root

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.

Step 5 - Use the cert at /mtls-test (mTLS - Mutual TLS)

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:

Step 6 - Reenroll via /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

Peek under the hood with openssl s_client

To 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

References