When we introduced wolfCert, we said it speaks two certificate enrollment protocols, EST and SCEP, both over HTTP(S). That one line hides a lot of detail. This post opens it up: what each protocol actually is, how they differ, how wolfCert implements and exposes them, and why the design leans hard toward embedded and bare-metal targets.
If you only remember one thing: EST is the modern, TLS-native successor, already supported across a wide range of current PKI tooling. SCEP is the long-established protocol that a huge amount of deployed infrastructure still speaks. wolfCert supports both because real deployments still need both.
The problem both protocols solve
Every device that speaks TLS requires a certificate, and a certificate is only useful if a trusted Certificate Authority (CA) signed it. That creates a chicken-and-egg problem on a factory line or in the field: the device holds a freshly generated key pair, but nobody has vouched for it yet. Certificate enrollment is the automated handshake that fixes this. Stripped to its essentials, both EST and SCEP walk the same path:
- Bootstrap trust: fetch the CA (and any Registration Authority (RA)) certificates so the device knows who it is talking to.
- Prove possession: generate a key pair on the device and wrap its public half in a Certificate Signing Request (CSR), self-signed to prove the device holds the matching private key.
- Enroll: submit the CSR, authenticate somehow, and receive a signed end-entity certificate.
- Renew (re-enroll): before the certificate expires, do it again using the current certificate as proof of identity.
The differences are all in how each step happens: what carries the messages, how the device authenticates, which key algorithms are allowed, and how trust is bootstrapped. That is where the two protocols diverge.
SCEP (RFC 8894)
In general
SCEP, the Simple Certificate Enrollment Protocol, began as an IETF Internet-Draft from Cisco (the long-lived draft-nourse-scep) in the early 2000s and was finally standardized as RFC 8894 in 2020. It is the protocol behind a huge amount of deployed infrastructure: mobile device management (MDM), VPN and network gear, and enterprise PKI such as Microsoft’s NDES (Network Device Enrollment Service).
SCEP predates ubiquitous TLS, so it does not lean on the transport for security. Instead, each request and response is a PKCS#7 / CMS structure that is signed and encrypted at the message layer, then carried over plain HTTP. A device signs its request with its new key, encrypts it to the CA/RA’s certificate, and the CA replies with a signed, encrypted response. The message layer, not the pipe, provides confidentiality and authenticity.
A SCEP exchange is a small set of message types:
- GetCACaps: ask the server which capabilities it supports, such as whether PKI operations can be POSTed, whether renewal is allowed, which SHA-2 hashes and content-encryption ciphers it accepts, and whether it offers CA roll-over.
- GetCACert: fetch the CA (and RA) certificate(s) that anchor trust and provide the encryption target.
- PKCSReq: the enrollment request itself, the CSR enveloped and signed.
- CertRep: the server’s response, carrying a status of SUCCESS, FAILURE, or PENDING.
- GetCertInitial: poll for a request that came back PENDING (a human has to approve it).
- RenewalReq / GetNextCACert: re-enroll an existing identity and pick up the roll-over CA before the current one expires.
Two SCEP details matter in practice. First, trust is bootstrapped out of band: because the initial GetCACert arrives over unauthenticated HTTP, the client is expected to verify the returned CA certificate against a fingerprint delivered through a separate channel before trusting it. Second, enrollment is authenticated with a shared challengePassword embedded in the CSR: simple, but a shared secret with the usual caveats (it can be replayed or leaked, and is often reused across a whole fleet).
Use cases
- Enrolling devices into an existing SCEP-based PKI you do not control (MDM platforms, NDES).
- Network and VPN appliances where SCEP is the established, expected protocol.
- Fleets already provisioned around SCEP that need a modern, maintained client.
Caveats
- RSA only. RFC 8894’s PKCS#7 signing and enveloping are defined around RSA. wolfCert follows the spec and rejects non-RSA keys.
- Limited crypto agility. The PKCS#7 message envelope constrains algorithm choice; many servers still sign their CertRep with SHA-1, and content encryption is AES-128-CBC with a fallback to 3DES-CBC for legacy peers that do not advertise AES.
- Shared-secret enrollment. challengePassword is convenient but is a bearer secret; protect it accordingly.
How wolfCert exposes SCEP
wolfCert gives you the SCEP message types directly as small primitives in wolfcert/scep.h, so you assemble exactly the flow your PKI needs. A minimal enroll is: fetch the CA, (optionally) query capabilities, generate an RSA key and CSR, then send PKCSReq (the challengePassword is supplied through your WolfCertServerCfg):
WolfCertServerCfg srv = { .protocol = WOLFCERT_PROTO_SCEP, .server_url = url };
/* 1) GetCACert, then verify it against a fingerprint you got out of band. */
WolfCertBuffer ca_pem = { 0 };
wolfcert_scep_get_ca_cert(&srv, &ca_pem);
/* wolfcert_scep_verify_ca_fingerprint(ca_der, ca_der_len, expected, len,
* WOLFCERT_SCEP_FP_SHA256); <- do this! */
/* 2) Ask what the server supports (AES vs 3DES, SHA-256, renewal, ...). */
WolfCertScepCaps caps = { 0 };
wolfcert_scep_get_ca_caps(&srv, &caps);
/* 3) Generate an RSA key and build the CSR. */
WolfCertKeyCfg kcfg = { .type = WOLFCERT_KEY_RSA, .param = 2048,
.dev_id = WOLFCERT_DEVID_SOFTWARE };
WolfCertKey* key = NULL;
wolfcert_key_generate(&kcfg, &key);
WolfCertCertMeta meta = { .subject_dn = "CN=device-2" };
WolfCertBuffer csr = { 0 };
wolfcert_csr_build(key, &meta, &csr);
/* 4) PKCSReq -> issued certificate (PEM). */
WolfCertBuffer cert = { 0 };
wolfcert_scep_pkcs_req(&srv, &caps, ca_der, ca_der_len,
key, csr.data, csr.len, &cert);
The richer wolfcert_scep_pkcs_req_ex returns a WolfCertScepResult that distinguishes SUCCESS / FAILURE / PENDING and hands back the transactionID. When a deployment requires manual approval, you echo that transaction ID into wolfcert_scep_get_cert_initial to poll until the certificate is issued. wolfcert_scep_renewal_req re-enrolls using the current certificate and key to sign the message, and wolfcert_scep_get_next_ca_cert picks up the roll-over CA, refusing any substituted roll-over certificate whose signer does not match a CA you already trust. The full example lives in examples/enroll_scep.c.
EST (RFC 7030)
In general
EST, Enrollment over Secure Transport, specified in RFC 7030, is the IETF’s modern answer to SCEP. Its key design move is to put the security in the transport: every EST exchange runs over TLS, and it is TLS itself, not a message-layer envelope, that authenticates the server, encrypts the channel, and optionally authenticates the client. The enrollment payloads themselves are simple CMS structures, which keeps the protocol far leaner than SCEP’s nested PKCS#7.
EST is a small set of RESTful endpoints under a /.well-known/est/ path:
- /cacerts: fetch the CA chain as PKCS#7. Because it runs over authenticated TLS, there is no separate out-of-band fingerprint dance.
- /simpleenroll: POST a CSR, get back the issued certificate.
- /simplereenroll: renew, using the current certificate/key as the TLS client credential.
- /csrattrs: ask the server which attributes it wants in the CSR (it can even pin the key algorithm and signature hash).
Client authentication is flexible: an EST client can present a TLS client certificate (an existing factory or bootstrap identity) or fall back to HTTP Basic credentials over the TLS channel. When a request needs manual approval, the server answers /simpleenroll with 202 Accepted and a Retry-After hint (the EST analogue of SCEP’s PENDING) and the client re-POSTs the identical request later.
Two modern touches make EST especially flexible on the client side. The /csrattrs endpoint lets the server dictate policy (“use a P-384 key, sign with SHA-384, include this attribute”), so a device does not have to hard-code CA policy. And with TLS 1.3 post-handshake authentication (RFC 8446), a device can open an anonymous TLS connection, fetch /cacerts, and only present its client identity when the enroll step actually needs it.
Use cases
- Modern PKI deployments, where EST is already supported by a wide range of current tooling and you want the TLS-native option without a separate trust-bootstrap step.
- All key types, including elliptic-curve, edwards-curve, and notably post-quantum certificates: EST is the right protocol for Ed25519, Ed448, and ML-DSA enrollment. Because EST puts no restriction on the key algorithm, future schemes (SLH-DSA, Falcon, …) can slot in the same way.
- Renewal flows where a device already holds a certificate and re-enrolls over mutual TLS with no shared secret in sight.
Caveats
- Server authentication is mandatory. RFC 7030 requires the client to authenticate the EST server. wolfCert enforces this: an EST enroll without a trust anchor is refused rather than handing your CSR and credentials to an unverified server.
- On-device key generation is the point. EST’s optional /serverkeygen (the server generates your private key and ships it to you) runs against the whole reason wolfCert exists, so wolfCert generates keys on the device and does not use it.
- Check your server’s EST support. Some products gate EST behind a specific edition (step-ca, for example, ships EST only in its commercial build), so confirm the endpoint is exposed before you commit. EST itself is broadly supported across modern PKI tooling.
How wolfCert exposes EST
The one-shot EST API mirrors the endpoints. Generate a key (any supported type, here Ed25519), build a CSR, and POST it to /simpleenroll against a trust-anchored server:
/* Any key type EST allows, Ed25519 here; ML-DSA works the same way. */
WolfCertKeyCfg kcfg = { .type = WOLFCERT_KEY_ED25519, .param = 0,
.dev_id = WOLFCERT_DEVID_SOFTWARE };
WolfCertKey* key = NULL;
wolfcert_key_generate(&kcfg, &key);
WolfCertCertMeta meta = { .subject_dn = "CN=device-42,O=wolfSSL" };
WolfCertBuffer csr = { 0 };
wolfcert_csr_build(key, &meta, &csr);
/* verify_server is on because we supplied a trust anchor, EST requires it. */
WolfCertServerCfg srv = {
.protocol = WOLFCERT_PROTO_EST,
.server_url = "https://ra.example/.well-known/est",
.trust_anchors = trust_pem, .trust_anchors_len = trust_pem_len,
.verify_server = 1,
};
WolfCertBuffer cert = { 0 };
wolfcert_est_simple_enroll(&srv, csr.data, csr.len, &cert);
For deployments with manual approval, the _ex entry points surface the 202 Accepted / Retry-After case explicitly instead of hiding it behind an error code:
WolfCertEstResult r = { 0 };
wolfcert_est_simple_enroll_ex(&srv, csr.data, csr.len, &r);
if (r.status == WOLFCERT_EST_STATUS_PENDING) {
/* Wait r.retry_after_sec (or your own floor), then re-POST the same CSR. */
}
else if (r.status == WOLFCERT_EST_STATUS_SUCCESS) {
/* r.cert_pem holds the issued certificate. */
}
/csrattrs is a first-class citizen: wolfcert_est_parse_csr_attrs decodes the server’s attribute list, and wolfcert_csr_attrs_apply overlays any key-algorithm and hash the server pinned onto your WolfCertKeyCfg / WolfCertCertMeta before you build the CSR (explicit caller choices always win). Renewal is wolfcert_est_simple_reenroll, which uses your current certificate and key as the TLS client credential. The full walkthrough is in examples/enroll_est.c.
For repeated operations there is a keep-alive session that carries several EST requests over one TCP+TLS connection, including the TLS 1.3 post-handshake-auth pattern: open anonymously, fetch /cacerts, then let the server request the client identity only at enroll time. Every session entry point has a non-blocking variant (wolfcert_est_session_*_nb) that returns WOLFCERT_ERR_WANT_READ / WANT_WRITE and hands you the socket fd for any poll/epoll/kqueue event loop. The same asynchronous style is coming to SCEP: an async SCEP API is currently in review.
Choosing between SCEP and EST
| SCEP (RFC 8894) | EST (RFC 7030) | |
|---|---|---|
| Security model | Message-layer PKCS#7 (sign + envelope) over HTTP | TLS-native; transport provides confidentiality and auth |
| Key algorithms | RSA only | RSA, ECC, Ed25519, Ed448, ML-DSA, … (no protocol-level restriction) |
| Trust bootstrap | Out-of-band CA fingerprint check | Authenticated TLS to the server |
| Client auth | Shared challengePassword | TLS client cert or HTTP Basic over TLS |
| Message format | Nested PKCS#7 / CMS | Lean CMS payloads |
| Pending / approval | pkiStatus=PENDING + GetCertInitial poll | 202 Accepted + Retry-After re-POST |
| Renewal | RenewalReq | /simplereenroll over mTLS |
| Best when | You must integrate with an existing SCEP-only PKI | You want the leaner, TLS-native protocol with modern, future-proof (PQC) algorithms |

