diff --git a/CLAUDE.md b/CLAUDE.md index 355519e..28d06b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -166,11 +166,12 @@ Minimal orientation: (`` and `"internal.h"`) -> wolfSSL (``) -> system headers. - **Session vs one-shot APIs.** The non-blocking session variants - (`wolfcert_http_session_request_nb`, `wolfcert_est_session_*_nb`) are - the only non-blocking entry points; one-shot - `wolfcert_http_request` / `wolfcert_est_*` / `wolfcert_scep_*` + (`wolfcert_http_session_request_nb`, `wolfcert_est_session_*_nb`, + `wolfcert_scep_session_*_nb`) are the only non-blocking entry points; + one-shot `wolfcert_http_request` / `wolfcert_est_*` / `wolfcert_scep_*` calls are blocking by design. DNS + initial TCP connect remain - synchronous even in non-blocking mode. + synchronous even in non-blocking mode. The SCEP session (unlike EST) + does not require TLS, since SCEP authenticates at the pkiMessage layer. - **SCEP is RSA-only** (per RFC 8894). The SCEP entry points reject non-RSA keys with `WOLFCERT_ERR_UNSUPPORTED`. EST is the right protocol for Ed25519 / Ed448 / ML-DSA. diff --git a/Makefile.am b/Makefile.am index 720e9a3..5480067 100644 --- a/Makefile.am +++ b/Makefile.am @@ -179,12 +179,14 @@ test_scep_msg_SOURCES = tests/unit/test_scep_msg.c test_scep_msg_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/src test_scep_msg_LDADD = libwolfcert.la $(WOLFSSL_LIBS) if WOLFCERT_HAVE_SERVER -check_PROGRAMS += test_scep_roundtrip test_scep_poll_roundtrip +check_PROGRAMS += test_scep_roundtrip test_scep_poll_roundtrip test_scep_async_roundtrip test_scep_roundtrip_SOURCES = tests/integration/test_scep_roundtrip.c test_scep_roundtrip_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/src test_scep_roundtrip_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread test_scep_poll_roundtrip_SOURCES = tests/integration/test_scep_poll_roundtrip.c test_scep_poll_roundtrip_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread +test_scep_async_roundtrip_SOURCES = tests/integration/test_scep_async_roundtrip.c +test_scep_async_roundtrip_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread endif endif diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8661f3c..80f98a3 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -203,6 +203,17 @@ the fingerprint length) and constant-time-compares it, returning `WOLFCERT_ERR_AUTH` on mismatch. Verify the bundle this way before using it as the `ca_bundle` trust set for enrollment. +**Keep-alive / async sessions.** Alongside the one-shot calls, PKCSReq, +RenewalReq and GetCertInitial have session variants +(`wolfcert_scep_session_pkcs_req_ex`/`_nb`, etc.) that reuse one connection, +built on the same non-blocking HTTP session as EST (see the session section +below). Fetch caps + the CA cert with the one-shot getters first, then open a +session with `wolfcert_scep_session_open` (blocking) or +`wolfcert_scep_session_open_async` (event-loop). Each round trip is +prepared (envelope + sign), sent, and its CertRep parsed as one logical step; +in async mode only the HTTP transport is pumped through `WANT_READ`/`WANT_WRITE` +while the crypto stays synchronous. + **Signed attributes.** The CertRep carries the full RFC 8894 §3.1 signed-attribute set (including `recipientNonce`) — up to 9 entries alongside the CMS auto-defaults. wolfSSL's PKCS#7 encoder grows its signed-attribute @@ -297,7 +308,10 @@ for (;;) { Key points: - **Only the session API is non-blocking.** One-shot `wolfcert_http_request` / - `wolfcert_est_*` / `wolfcert_scep_*` calls are blocking by design. + `wolfcert_est_*` / `wolfcert_scep_*` calls are blocking by design. Both EST + (`wolfcert_est_session_*_nb`) and SCEP (`wolfcert_scep_session_*_nb`) offer + non-blocking session variants; the SCEP session additionally does not require + TLS, since SCEP authenticates at the pkiMessage layer. - **DNS and the initial TCP connect stay synchronous.** Only socket operations *after* the session is open are non-blocking; if you can't afford a blocking connect, resolve and connect the socket yourself. diff --git a/src/est/est_client.c b/src/est/est_client.c index fc1b36f..1d3a1fc 100644 --- a/src/est/est_client.c +++ b/src/est/est_client.c @@ -422,19 +422,11 @@ static int est_session_open_common(const WolfCertServerCfg* srv, int nonblocking "the server certificate (RFC 7030)"); } - size_t origin_len = strlen(u.scheme) + 3 + strlen(u.host) + 16; - char* origin = (char*)WOLFCERT_XMALLOC(origin_len, heap); - if (origin == NULL) { - wolfcert_http_url_free(&u); - return WOLFCERT_ERR_MEMORY; - } - - if ((u.tls && u.port == 443) || (!u.tls && u.port == 80)) - snprintf(origin, origin_len, "%s://%s", u.scheme, u.host); - else - snprintf(origin, origin_len, "%s://%s:%d", u.scheme, u.host, u.port); - + char* origin = NULL; + rc = wolfcert_http_url_origin(&u, heap, &origin); wolfcert_http_url_free(&u); + if (rc != WOLFCERT_OK) + return rc; WolfCertEstSession* s = (WolfCertEstSession*)WOLFCERT_XMALLOC(sizeof(*s), heap); if (s == NULL) { diff --git a/src/http.c b/src/http.c index 1941069..82daa44 100644 --- a/src/http.c +++ b/src/http.c @@ -73,6 +73,33 @@ WOLFCERT_TEST_VIS void wolfcert_http_url_free(WolfCertUrl* u) u->scheme = u->host = u->path = NULL; } +/* Build the "scheme://host[:port]" origin for a parsed URL into a freshly + * allocated buffer (owned by the caller, free with WOLFCERT_XFREE). The default + * port (443 for TLS, 80 otherwise) is omitted. Shared by the EST and SCEP + * session opens so the two origin builders cannot drift. */ +WOLFCERT_TEST_VIS int wolfcert_http_url_origin(const WolfCertUrl* u, void* heap, + char** out_origin) +{ + size_t origin_len; + char* origin; + + if (u == NULL || u->scheme == NULL || u->host == NULL || out_origin == NULL) + return WOLFCERT_ERR_BAD_ARG; + + origin_len = strlen(u->scheme) + 3 + strlen(u->host) + 16; + origin = (char*)WOLFCERT_XMALLOC(origin_len, heap); + if (origin == NULL) + return WOLFCERT_ERR_MEMORY; + + if ((u->tls && u->port == 443) || (!u->tls && u->port == 80)) + snprintf(origin, origin_len, "%s://%s", u->scheme, u->host); + else + snprintf(origin, origin_len, "%s://%s:%d", u->scheme, u->host, u->port); + + *out_origin = origin; + return WOLFCERT_OK; +} + static char* dup_range(const char* s, const char* e, void* heap) { size_t n = (size_t)(e - s); diff --git a/src/internal.h b/src/internal.h index 71ed19d..f1d3900 100644 --- a/src/internal.h +++ b/src/internal.h @@ -274,6 +274,8 @@ typedef struct { WOLFCERT_TEST_VIS int wolfcert_http_url_parse(const char* url, WolfCertUrl* out, void* heap); WOLFCERT_TEST_VIS void wolfcert_http_url_free (WolfCertUrl* u); +WOLFCERT_TEST_VIS int wolfcert_http_url_origin(const WolfCertUrl* u, void* heap, + char** out_origin); /* Base64 helpers. `_encode` is single-line (no newlines) - the right * default for HTTP header values and for wolfCert's own server-side diff --git a/src/scep/scep_client.c b/src/scep/scep_client.c index 2be0e9d..023556d 100644 --- a/src/scep/scep_client.c +++ b/src/scep/scep_client.c @@ -387,6 +387,30 @@ WOLFCERT_TEST_VIS int wolfcert_scep_build_pki_get_url(const char* base, return WOLFCERT_OK; } +/* Decide the PKIOperation transport (RFC 8894 section 4.1) and build the + * request URL: POST when the CA advertises POSTPKIOperation (assumed when caps + * are unknown), otherwise a base64 GET carrying the message in the query. Sets + * *out_url (owned by caller) and *out_use_post. Shared by the one-shot and + * session clients. */ +static int scep_build_transport(const char* server_url, const WolfCertScepCaps* caps, + const uint8_t* pki_msg, size_t pki_len, void* heap, + char** out_url, int* out_use_post) +{ + int use_post = (caps == NULL || caps->post_pki_operation); + *out_use_post = use_post; + + if (use_post) { + char* url = append_query(server_url, "PKIOperation", heap); + if (url == NULL) + return WOLFCERT_ERR_MEMORY; + *out_url = url; + return WOLFCERT_OK; + } + + return wolfcert_scep_build_pki_get_url(server_url, pki_msg, pki_len, heap, + out_url); +} + static int run_pki_op(const WolfCertServerCfg* srv, const WolfCertScepCaps* caps, const uint8_t* pki_msg, size_t pki_len, @@ -394,35 +418,23 @@ static int run_pki_op(const WolfCertServerCfg* srv, { void* heap = srv->heap ? srv->heap : wolfcert_default_heap(); - /* RFC 8894 section 4.1: POST the pkiMessage when the CA advertises - * POSTPKIOperation (assumed when caps are unknown), otherwise fall back to - * carrying it base64-encoded in an HTTP GET query. */ - int use_post = (caps == NULL || caps->post_pki_operation); - char* url = NULL; - WolfCertHttpRequest req = { 0 }; + int use_post = 0; + int rc = scep_build_transport(srv->server_url, caps, pki_msg, pki_len, heap, + &url, &use_post); + if (rc != WOLFCERT_OK) + return rc; + + WolfCertHttpRequest req = { .method = use_post ? "POST" : "GET", .url = url }; if (use_post) { - url = append_query(srv->server_url, "PKIOperation", heap); - if (url == NULL) - return WOLFCERT_ERR_MEMORY; - req.method = "POST"; - req.url = url; req.content_type = "application/x-pki-message"; req.body = pki_msg; req.body_len = pki_len; } - else { - int grc = wolfcert_scep_build_pki_get_url(srv->server_url, pki_msg, - pki_len, heap, &url); - if (grc != WOLFCERT_OK) - return grc; - req.method = "GET"; - req.url = url; - } fill_common(srv, &req); WolfCertHttpResponse resp = { 0 }; - int rc = wolfcert_http_request(&req, &resp); + rc = wolfcert_http_request(&req, &resp); WOLFCERT_XFREE(url, heap); if (rc != WOLFCERT_OK) @@ -441,26 +453,28 @@ static int run_pki_op(const WolfCertServerCfg* srv, return WOLFCERT_OK; } -/* Core round-trip used by PKCSReq / RenewalReq / GetCertInitial. Builds a - * pkiMessage wrapping `envelope_content`, POSTs it, parses the CertRep, - * and fills `out`. Caller retains ownership of `txid_override`; when NULL - * a fresh 16-byte hex transactionID is generated. */ -static int do_scep_round_trip(const WolfCertServerCfg* srv, - const WolfCertScepCaps* caps, - const uint8_t* ra_cert, size_t ra_cert_len, - const uint8_t* ca_bundle, size_t ca_bundle_len, - const uint8_t* signer_cert, size_t signer_cert_len, - const uint8_t* signer_key, size_t signer_key_len, - const char* msg_type, - const uint8_t* envelope_content, size_t envelope_content_len, - const uint8_t* txid_override, size_t txid_override_len, - WolfCertScepResult* out) +/* Shared SCEP round-trip sizes. */ +#define SCEP_NONCE_SZ 16 +#define SCEP_TXID_MAX 128 /* holds a 32-hex generated txid or any server echo */ + +/* Build the enveloped + signed pkiMessage for one SCEP round trip. Produces the + * DER message in *out_pki, copies the effective transactionID into txid_buf + * (a fresh 32-hex value when txid_override is NULL, else the override) and the + * senderNonce into out_nonce - both retained by the caller to validate the + * CertRep after the transport completes. */ +static int scep_prepare(void* heap, const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* signer_cert, size_t signer_cert_len, + const uint8_t* signer_key, size_t signer_key_len, + const char* msg_type, + const uint8_t* envelope_content, size_t envelope_content_len, + const uint8_t* txid_override, size_t txid_override_len, + WolfCertBuffer* out_pki, + uint8_t* txid_buf, size_t txid_buf_cap, size_t* out_txid_len, + uint8_t* out_nonce) { - void* heap = srv->heap ? srv->heap : wolfcert_default_heap(); int hash_oid = pick_hash_oid(caps); int enc_oid; - out->heap = heap; - out->fail_info = -1; /* RFC 8894: the GetCACaps "AES" keyword advertises AES-128-CBC as the * content cipher. Honour it when offered; otherwise fall back to the @@ -479,6 +493,10 @@ static int do_scep_round_trip(const WolfCertServerCfg* srv, #endif } + if (txid_override != NULL && txid_override_len > txid_buf_cap) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "scep", + "transactionID longer than the internal buffer"); + WolfCertBuffer env = { 0 }; int rc = wolfcert_scep_envelop(ra_cert, ra_cert_len, envelope_content, envelope_content_len, @@ -487,48 +505,69 @@ static int do_scep_round_trip(const WolfCertServerCfg* srv, return rc; WC_RNG rng; - wc_InitRng_ex(&rng, heap, WOLFCERT_DEVID_SOFTWARE); - - uint8_t txid_gen[16], nonce[16]; - wc_RNG_GenerateBlock(&rng, txid_gen, sizeof(txid_gen)); - wc_RNG_GenerateBlock(&rng, nonce, sizeof(nonce)); + uint8_t txid_gen[16]; + int rng_rc = wc_InitRng_ex(&rng, heap, WOLFCERT_DEVID_SOFTWARE); + if (rng_rc != 0) { + wolfcert_buffer_free(&env); + return WOLFCERT_ERR_WC(rng_rc, "scep", "RNG init failed"); + } + /* The transactionID and the anti-replay senderNonce must both come from the + * RNG. Ignoring a failure here would build the pkiMessage over + * uninitialized stack memory and silently weaken replay protection, so + * surface any generation error instead. */ + rng_rc = wc_RNG_GenerateBlock(&rng, txid_gen, sizeof(txid_gen)); + if (rng_rc == 0) + rng_rc = wc_RNG_GenerateBlock(&rng, out_nonce, SCEP_NONCE_SZ); wc_FreeRng(&rng); - - static const char HEX[] = "0123456789abcdef"; - char txid_generated[33]; - for (size_t i = 0; i < sizeof(txid_gen); ++i) { - txid_generated[i*2] = HEX[txid_gen[i] >> 4]; - txid_generated[i*2+1] = HEX[txid_gen[i] & 0x0F]; + if (rng_rc != 0) { + wolfcert_buffer_free(&env); + return WOLFCERT_ERR_WC(rng_rc, "scep", "RNG generate failed"); } - txid_generated[32] = '\0'; - const uint8_t* txid = txid_override ? txid_override : (const uint8_t*)txid_generated; - size_t txid_len = txid_override ? txid_override_len : 32; + size_t txid_len; + if (txid_override != NULL) { + memcpy(txid_buf, txid_override, txid_override_len); + txid_len = txid_override_len; + } + else { + static const char HEX[] = "0123456789abcdef"; + for (size_t i = 0; i < sizeof(txid_gen); ++i) { + txid_buf[i*2] = (uint8_t)HEX[txid_gen[i] >> 4]; + txid_buf[i*2+1] = (uint8_t)HEX[txid_gen[i] & 0x0F]; + } + txid_len = sizeof(txid_gen) * 2; /* 32 hex chars */ + } + *out_txid_len = txid_len; WolfCertScepAttrs attrs = { - .transaction_id = txid, .transaction_id_len = txid_len, - .sender_nonce = nonce, .sender_nonce_len = sizeof(nonce), + .transaction_id = txid_buf, .transaction_id_len = txid_len, + .sender_nonce = out_nonce, .sender_nonce_len = SCEP_NONCE_SZ, .message_type = msg_type, }; - WolfCertBuffer pki = { 0 }; rc = wolfcert_scep_build_pki_message(env.data, env.len, signer_cert, signer_cert_len, signer_key, signer_key_len, - hash_oid, &attrs, &pki, heap); + hash_oid, &attrs, out_pki, heap); wolfcert_buffer_free(&env); - if (rc != WOLFCERT_OK) - return rc; - - uint8_t* resp = NULL; - size_t resp_len = 0; - rc = run_pki_op(srv, caps, pki.data, pki.len, &resp, &resp_len); - - wolfcert_buffer_free(&pki); - if (rc != WOLFCERT_OK) - return rc; + return rc; +} +/* Validate and consume a CertRep response: require messageType 3 with the + * transactionID we sent, bind the signer to the CA/RA bundle, check the + * recipientNonce echoes our senderNonce, then fill `out` with SUCCESS (+ the + * issued cert in cert_pem), PENDING, or FAILURE. Reads but does not free + * `resp`; `txid`/`nonce` are the values scep_prepare produced. */ +static int scep_finish(void* heap, + const uint8_t* resp, size_t resp_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const uint8_t* signer_cert, size_t signer_cert_len, + const uint8_t* signer_key, size_t signer_key_len, + const uint8_t* txid, size_t txid_len, + const uint8_t* nonce, + WolfCertScepResult* out) +{ WolfCertBuffer resp_env = { 0 }; WolfCertBuffer inner = { 0 }; char* status = NULL; @@ -542,11 +581,10 @@ static int do_scep_round_trip(const WolfCertServerCfg* srv, size_t rx_rn_len = 0; uint8_t* rx_signer = NULL; size_t rx_signer_len = 0; - rc = wolfcert_scep_parse_pki_message(resp, resp_len, &resp_env, + int rc = wolfcert_scep_parse_pki_message(resp, resp_len, &resp_env, &rx_tid, &rx_tid_len, &rx_sn, &rx_sn_len, &rx_rn, &rx_rn_len, &resp_mt, &status, &rx_signer, &rx_signer_len, &fail_info, heap); - WOLFCERT_XFREE(resp, heap); WOLFCERT_XFREE(rx_sn, heap); /* RFC 8894: an enrollment response is a CertRep (messageType 3) whose @@ -581,8 +619,8 @@ static int do_scep_round_trip(const WolfCertServerCfg* srv, * means the response cannot be tied to our request (stale / replayed / * cross-talk / cannot verify) -> reject. */ if (rc == WOLFCERT_OK && - (rx_rn == NULL || rx_rn_len != sizeof(nonce) || - memcmp(rx_rn, nonce, sizeof(nonce)) != 0)) { + (rx_rn == NULL || rx_rn_len != SCEP_NONCE_SZ || + memcmp(rx_rn, nonce, SCEP_NONCE_SZ) != 0)) { rc = WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "scep", "CertRep recipientNonce missing or does not echo " "senderNonce"); @@ -633,6 +671,52 @@ static int do_scep_round_trip(const WolfCertServerCfg* srv, return rc; } +/* One-shot SCEP round trip for PKCSReq / RenewalReq / GetCertInitial: build the + * enveloped + signed pkiMessage (scep_prepare), POST or base64-GET it + * (run_pki_op), then parse + verify the CertRep and fill `out` (scep_finish). + * Caller retains ownership of `txid_override`; when NULL a fresh 32-hex + * transactionID is generated. */ +static int do_scep_round_trip(const WolfCertServerCfg* srv, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const uint8_t* signer_cert, size_t signer_cert_len, + const uint8_t* signer_key, size_t signer_key_len, + const char* msg_type, + const uint8_t* envelope_content, size_t envelope_content_len, + const uint8_t* txid_override, size_t txid_override_len, + WolfCertScepResult* out) +{ + void* heap = srv->heap ? srv->heap : wolfcert_default_heap(); + out->heap = heap; + out->fail_info = -1; + + WolfCertBuffer pki = { 0 }; + uint8_t txid[SCEP_TXID_MAX]; + size_t txid_len = 0; + uint8_t nonce[SCEP_NONCE_SZ]; + int rc = scep_prepare(heap, caps, ra_cert, ra_cert_len, + signer_cert, signer_cert_len, signer_key, signer_key_len, + msg_type, envelope_content, envelope_content_len, + txid_override, txid_override_len, + &pki, txid, sizeof(txid), &txid_len, nonce); + if (rc != WOLFCERT_OK) + return rc; + + uint8_t* resp = NULL; + size_t resp_len = 0; + rc = run_pki_op(srv, caps, pki.data, pki.len, &resp, &resp_len); + wolfcert_buffer_free(&pki); + if (rc != WOLFCERT_OK) + return rc; + + rc = scep_finish(heap, resp, resp_len, ca_bundle, ca_bundle_len, + signer_cert, signer_cert_len, signer_key, signer_key_len, + txid, txid_len, nonce, out); + WOLFCERT_XFREE(resp, heap); + return rc; +} + /* Serialize the private key half of a WolfCertKey to DER for PKCS#7 use. * SCEP requires RSA (RFC 8894), so the caller has already validated * key->type == WOLFCERT_KEY_RSA. */ @@ -938,3 +1022,651 @@ int wolfcert_scep_get_next_ca_cert(const WolfCertServerCfg* srv, wolfcert_http_response_free(&resp); return rc; } + +/* ---- keep-alive / async SCEP session ----------------------------------- */ + +/* Which PKIOperation occupies the session's single in-flight slot. Set when a + * round trip begins; used to reject an async resume that names a different + * operation than the one already in progress. */ +enum scep_session_op { + SCEP_SESS_OP_PKCS_REQ = 0, + SCEP_SESS_OP_RENEWAL, + SCEP_SESS_OP_GET_CERT_INITIAL +}; + +struct WolfCertScepSession { + WolfCertHttpSession* http; + char* server_url; /* full SCEP endpoint URL, owned */ + void* heap; + int nonblocking; /* opened via _open_async (_nb calls) vs _open (_ex) */ + + /* Async in-flight state: one round trip at a time. */ + int in_active; + int in_op; /* enum scep_session_op, valid when in_active */ + char* in_url; /* owned request URL */ + WolfCertBuffer in_pki; /* built pkiMessage, owned (POST body) */ + WolfCertHttpRequest in_req; + WolfCertHttpResponse in_resp; + + /* Captured at begin, consumed by the finish phase after the pumps: */ + uint8_t* in_ca_bundle; size_t in_ca_bundle_len; /* owned copy */ + uint8_t* in_signer; size_t in_signer_len; /* owned copy */ + uint8_t* in_signer_key; size_t in_signer_key_len; /* owned copy, zeroized */ + uint8_t in_txid[SCEP_TXID_MAX]; size_t in_txid_len; + uint8_t in_nonce[SCEP_NONCE_SZ]; + WolfCertScepResult* in_out; +}; + +static uint8_t* dup_buf(const uint8_t* src, size_t len, void* heap) +{ + if (src == NULL || len == 0) + return NULL; + uint8_t* p = (uint8_t*)WOLFCERT_XMALLOC(len, heap); + if (p != NULL) + memcpy(p, src, len); + return p; +} + +static int scep_session_open_common(const WolfCertServerCfg* srv, int nonblocking, + WolfCertScepSession** out) +{ + if (srv == NULL || srv->server_url == NULL || out == NULL) + return WOLFCERT_ERR_BAD_ARG; + + void* heap = srv->heap ? srv->heap : wolfcert_default_heap(); + + /* Split the SCEP URL into scheme://host[:port] for the HTTP session vs the + * path we keep for building per-operation query strings. Unlike EST there + * is deliberately no TLS-required gate: RFC 8894 authenticates at the + * pkiMessage layer and commonly runs over plaintext http://. */ + WolfCertUrl u; + int rc = wolfcert_http_url_parse(srv->server_url, &u, heap); + if (rc != WOLFCERT_OK) + return rc; + + /* SCEP does not require TLS, but if the caller did choose an https:// + * endpoint, refuse to run it unverified: verify_server is the sole peer- + * verification switch, so opening with it off would perform a silent, + * unauthenticated TLS handshake. Plaintext http:// stays allowed. */ + if (u.tls && !srv->verify_server) { + wolfcert_http_url_free(&u); + return WOLFCERT_ERR(WOLFCERT_ERR_TLS, "scep", + "TLS SCEP endpoint requires server authentication: set verify_server " + "or use a plaintext http:// URL"); + } + + char* origin = NULL; + rc = wolfcert_http_url_origin(&u, heap, &origin); + wolfcert_http_url_free(&u); + if (rc != WOLFCERT_OK) + return rc; + + WolfCertScepSession* s = (WolfCertScepSession*)WOLFCERT_XMALLOC(sizeof(*s), heap); + if (s == NULL) { + WOLFCERT_XFREE(origin, heap); + return WOLFCERT_ERR_MEMORY; + } + + memset(s, 0, sizeof(*s)); + s->heap = heap; + s->nonblocking = nonblocking; + s->server_url = wolfcert_strdup(srv->server_url, heap); + if (s->server_url == NULL) { + WOLFCERT_XFREE(s, heap); + WOLFCERT_XFREE(origin, heap); + return WOLFCERT_ERR_MEMORY; + } + + WolfCertHttpSessionCfg hcfg = { + .base_url = origin, + .trust_anchors = srv->trust_anchors, + .trust_anchors_len = srv->trust_anchors_len, + .verify_server = srv->verify_server, + .timeout_ms = srv->timeout_ms, + .max_response_bytes = srv->max_response_bytes, + .client_cert = srv->client_cert, + .client_cert_len = srv->client_cert_len, + .client_key = srv->client_key, + .client_key_len = srv->client_key_len, + .nonblocking = nonblocking, + .connect_cb = srv->connect_cb, + .connect_ctx = srv->connect_ctx, + .heap = heap, + }; + rc = wolfcert_http_session_open(&hcfg, &s->http); + + WOLFCERT_XFREE(origin, heap); + if (rc != WOLFCERT_OK) { + WOLFCERT_XFREE(s->server_url, heap); + WOLFCERT_XFREE(s, heap); + return rc; + } + + *out = s; + return WOLFCERT_OK; +} + +int wolfcert_scep_session_open(const WolfCertServerCfg* srv, WolfCertScepSession** out) +{ + return scep_session_open_common(srv, 0, out); +} + +int wolfcert_scep_session_open_async(const WolfCertServerCfg* srv, WolfCertScepSession** out) +{ + return scep_session_open_common(srv, 1, out); +} + +int wolfcert_scep_session_fd(const WolfCertScepSession* s) +{ + return s != NULL ? wolfcert_http_session_fd(s->http) : -1; +} + +static void scep_async_reset(WolfCertScepSession* s) +{ + WOLFCERT_XFREE(s->in_url, s->heap); + s->in_url = NULL; + wolfcert_buffer_free(&s->in_pki); + wolfcert_http_response_free(&s->in_resp); + memset(&s->in_req, 0, sizeof(s->in_req)); + memset(&s->in_resp, 0, sizeof(s->in_resp)); + + WOLFCERT_XFREE(s->in_ca_bundle, s->heap); + s->in_ca_bundle = NULL; + s->in_ca_bundle_len = 0; + WOLFCERT_XFREE(s->in_signer, s->heap); + s->in_signer = NULL; + s->in_signer_len = 0; + if (s->in_signer_key != NULL) { + wc_ForceZero(s->in_signer_key, (word32)s->in_signer_key_len); + WOLFCERT_XFREE(s->in_signer_key, s->heap); + s->in_signer_key = NULL; + } + s->in_signer_key_len = 0; + s->in_txid_len = 0; + s->in_out = NULL; + s->in_active = 0; +} + +void wolfcert_scep_session_close(WolfCertScepSession* s) +{ + if (s == NULL) + return; + + scep_async_reset(s); + if (s->http) + wolfcert_http_session_close(s->http); + + WOLFCERT_XFREE(s->server_url, s->heap); + WOLFCERT_XFREE(s, s->heap); +} + +/* Build the request state for one round trip into the session. Copies the + * finish-phase inputs (ca_bundle, signer cert/key) so they outlive the pumps, + * prepares the pkiMessage, and picks POST/GET transport. */ +static int scep_session_begin(WolfCertScepSession* s, const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const uint8_t* signer_cert, size_t signer_cert_len, + const uint8_t* signer_key, size_t signer_key_len, + const char* msg_type, int op, + const uint8_t* envelope_content, size_t envelope_content_len, + const uint8_t* txid_override, size_t txid_override_len, + WolfCertScepResult* out) +{ + void* heap = s->heap; + out->heap = heap; + out->fail_info = -1; + + /* These three buffers are copied with dup_buf below, which returns NULL for + * a zero length as well as for an allocation failure. Reject an empty one + * up front so the NULL check that follows is unambiguously OOM. All internal + * callers pass non-empty values (the pending-PKCSReq path derives a signer + * cert rather than passing length 0), so this only fires on malformed input + * such as a non-NULL signer cert with length 0. */ + if (ca_bundle_len == 0 || signer_cert_len == 0 || signer_key_len == 0) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "scep", + "ca_bundle, signer cert and signer key must be non-empty"); + + s->in_ca_bundle = dup_buf(ca_bundle, ca_bundle_len, heap); + s->in_ca_bundle_len = ca_bundle_len; + s->in_signer = dup_buf(signer_cert, signer_cert_len, heap); + s->in_signer_len = signer_cert_len; + s->in_signer_key = dup_buf(signer_key, signer_key_len, heap); + s->in_signer_key_len = signer_key_len; + if (s->in_ca_bundle == NULL || s->in_signer == NULL || s->in_signer_key == NULL) { + scep_async_reset(s); + return WOLFCERT_ERR_MEMORY; + } + + WolfCertBuffer pki = { 0 }; + int rc = scep_prepare(heap, caps, ra_cert, ra_cert_len, + signer_cert, signer_cert_len, signer_key, signer_key_len, + msg_type, envelope_content, envelope_content_len, + txid_override, txid_override_len, + &pki, s->in_txid, sizeof(s->in_txid), &s->in_txid_len, + s->in_nonce); + if (rc != WOLFCERT_OK) { + scep_async_reset(s); + return rc; + } + + char* url = NULL; + int use_post = 0; + rc = scep_build_transport(s->server_url, caps, pki.data, pki.len, heap, + &url, &use_post); + if (rc != WOLFCERT_OK) { + wolfcert_buffer_free(&pki); + scep_async_reset(s); + return rc; + } + + s->in_pki = pki; /* ownership moves to the session */ + s->in_url = url; + /* Note: no max_response_bytes here - the HTTP session request path uses the + * cap fixed at session open (WolfCertHttpSessionCfg.max_response_bytes), not + * a per-request one, so setting it on in_req would be silently ignored. */ + s->in_req = (WolfCertHttpRequest){ + .method = use_post ? "POST" : "GET", + .url = url, + .heap = heap, + }; + if (use_post) { + s->in_req.content_type = "application/x-pki-message"; + s->in_req.body = s->in_pki.data; + s->in_req.body_len = s->in_pki.len; + } + s->in_out = out; + s->in_op = op; + s->in_active = 1; + + return WOLFCERT_OK; +} + +/* Run scep_finish on a completed transport result and clear the in-flight + * state. `rc` is the transport return (already past any WANT_* handling). */ +static int scep_session_finish(WolfCertScepSession* s, int rc) +{ + if (rc != WOLFCERT_OK) { + scep_async_reset(s); + return rc; + } + + if (s->in_resp.status_code != 200) { + scep_async_reset(s); + return WOLFCERT_ERR_HTTP; + } + + rc = scep_finish(s->heap, s->in_resp.body, s->in_resp.body_len, + s->in_ca_bundle, s->in_ca_bundle_len, + s->in_signer, s->in_signer_len, + s->in_signer_key, s->in_signer_key_len, + s->in_txid, s->in_txid_len, s->in_nonce, s->in_out); + scep_async_reset(s); + return rc; +} + +static int scep_session_drive_sync(WolfCertScepSession* s) +{ + int rc = wolfcert_http_session_request(s->http, &s->in_req, &s->in_resp); + return scep_session_finish(s, rc); +} + +static int scep_session_drive_nb(WolfCertScepSession* s) +{ + int rc = wolfcert_http_session_request_nb(s->http, &s->in_req, &s->in_resp); + if (rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE) + return rc; + return scep_session_finish(s, rc); +} + +/* Validate a resumed async _nb poll: the operation must match the one captured + * at begin and the result pointer must be the same object. Shared by the three + * _nb entry points. */ +static int scep_session_resume_check(const WolfCertScepSession* s, int expected_op, + const WolfCertScepResult* out) +{ + if (s->in_op != expected_op) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "scep", + "a different SCEP operation is already in flight on this session"); + if (out != s->in_out) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "scep", + "result pointer differs from the in-flight request; pass the " + "same WolfCertScepResult* to each poll call"); + return WOLFCERT_OK; +} + +/* ---- per-operation begin helpers (run only when starting a round trip) --- */ + +static int scep_session_begin_pkcs_req(WolfCertScepSession* s, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const WolfCertKey* new_key, const uint8_t* csr_der, size_t csr_der_len, + WolfCertScepResult* out) +{ + /* Set the fail_info sentinel before the RSA-key check and the crypto below, + * so an early error return carries -1 like the one-shot APIs (the entry + * point only memset out to 0; scep_session_begin, which also sets -1, runs + * after this crypto). */ + out->fail_info = -1; + + if (new_key->type != WOLFCERT_KEY_RSA) + return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "scep", + "SCEP (RFC 8894) requires an RSA signer for pkiMessage"); + + void* heap = s->heap; + uint8_t* signer_der = NULL; + size_t signer_len = 0; + int rc = wolfcert_scep_self_signed_rsa((RsaKey*)new_key->impl, + csr_der, csr_der_len, + &signer_der, &signer_len, heap); + if (rc != WOLFCERT_OK) + return rc; + + uint8_t* key_der = NULL; + size_t key_der_len = 0; + rc = rsa_key_to_der(new_key, heap, &key_der, &key_der_len); + if (rc != WOLFCERT_OK) { + WOLFCERT_XFREE(signer_der, heap); + return rc; + } + + rc = scep_session_begin(s, caps, ra_cert, ra_cert_len, ca_bundle, ca_bundle_len, + signer_der, signer_len, key_der, key_der_len, + "19", SCEP_SESS_OP_PKCS_REQ, + csr_der, csr_der_len, NULL, 0, out); + + WOLFCERT_XFREE(signer_der, heap); + wc_ForceZero(key_der, (word32)key_der_len); + WOLFCERT_XFREE(key_der, heap); + return rc; +} + +static int scep_session_begin_renewal(WolfCertScepSession* s, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const uint8_t* current_cert, size_t current_cert_len, + const WolfCertKey* current_key, const uint8_t* csr_der, size_t csr_der_len, + WolfCertScepResult* out) +{ + out->fail_info = -1; /* sentinel before the crypto below (see pkcs_req) */ + + if (current_key->type != WOLFCERT_KEY_RSA) + return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "scep", + "SCEP (RFC 8894) requires an RSA signer for pkiMessage"); + + void* heap = s->heap; + uint8_t* key_der = NULL; + size_t key_der_len = 0; + int rc = rsa_key_to_der(current_key, heap, &key_der, &key_der_len); + if (rc != WOLFCERT_OK) + return rc; + + rc = scep_session_begin(s, caps, ra_cert, ra_cert_len, ca_bundle, ca_bundle_len, + current_cert, current_cert_len, key_der, key_der_len, + "17", SCEP_SESS_OP_RENEWAL, + csr_der, csr_der_len, NULL, 0, out); + + wc_ForceZero(key_der, (word32)key_der_len); + WOLFCERT_XFREE(key_der, heap); + return rc; +} + +static int scep_session_begin_get_cert_initial(WolfCertScepSession* s, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const uint8_t* signer_cert, size_t signer_cert_len, + const WolfCertKey* signer_key, + const uint8_t* csr_der, size_t csr_der_len, + const uint8_t* transaction_id, size_t transaction_id_len, + WolfCertScepResult* out) +{ + out->fail_info = -1; /* sentinel before the crypto below (see pkcs_req) */ + + if (signer_key->type != WOLFCERT_KEY_RSA) + return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "scep", + "SCEP (RFC 8894) requires an RSA signer for pkiMessage"); + + void* heap = s->heap; + WolfCertBuffer ias = { 0 }; + int rc = wolfcert_scep_issuer_and_subject(ra_cert, ra_cert_len, + csr_der, csr_der_len, &ias, heap); + if (rc != WOLFCERT_OK) + return rc; + + uint8_t* key_der = NULL; + size_t key_der_len = 0; + rc = rsa_key_to_der(signer_key, heap, &key_der, &key_der_len); + if (rc != WOLFCERT_OK) { + wolfcert_buffer_free(&ias); + return rc; + } + + /* Pending PKCSReq: regenerate the same transient self-signed cert the + * original request used. RenewalReq callers pass their existing cert. */ + uint8_t* derived = NULL; + size_t derived_len = 0; + const uint8_t* eff = signer_cert; + size_t eff_len = signer_cert_len; + if (signer_cert == NULL) { + rc = wolfcert_scep_self_signed_rsa((RsaKey*)signer_key->impl, + csr_der, csr_der_len, + &derived, &derived_len, heap); + if (rc != WOLFCERT_OK) { + wolfcert_buffer_free(&ias); + wc_ForceZero(key_der, (word32)key_der_len); + WOLFCERT_XFREE(key_der, heap); + return rc; + } + eff = derived; + eff_len = derived_len; + } + + rc = scep_session_begin(s, caps, ra_cert, ra_cert_len, ca_bundle, ca_bundle_len, + eff, eff_len, key_der, key_der_len, + "20", SCEP_SESS_OP_GET_CERT_INITIAL, ias.data, ias.len, + transaction_id, transaction_id_len, out); + + wolfcert_buffer_free(&ias); + wc_ForceZero(key_der, (word32)key_der_len); + WOLFCERT_XFREE(key_der, heap); + WOLFCERT_XFREE(derived, heap); + return rc; +} + +/* ---- session PKCSReq ---------------------------------------------------- */ + +int wolfcert_scep_session_pkcs_req_ex(WolfCertScepSession* s, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const WolfCertKey* new_key, const uint8_t* csr_der, size_t csr_der_len, + WolfCertScepResult* out) +{ + if (s == NULL || caps == NULL || ra_cert == NULL || ra_cert_len == 0 || + ca_bundle == NULL || ca_bundle_len == 0 || new_key == NULL || + csr_der == NULL || csr_der_len == 0 || out == NULL) + return WOLFCERT_ERR_BAD_ARG; + + if (s->nonblocking) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "scep", + "blocking _ex call on an async session; use the _nb variant"); + + memset(out, 0, sizeof(*out)); + int rc = scep_session_begin_pkcs_req(s, caps, ra_cert, ra_cert_len, + ca_bundle, ca_bundle_len, + new_key, csr_der, csr_der_len, out); + if (rc != WOLFCERT_OK) + return rc; + return scep_session_drive_sync(s); +} + +int wolfcert_scep_session_pkcs_req_nb(WolfCertScepSession* s, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const WolfCertKey* new_key, const uint8_t* csr_der, size_t csr_der_len, + WolfCertScepResult* out) +{ + if (s == NULL || out == NULL) + return WOLFCERT_ERR_BAD_ARG; + + if (!s->nonblocking) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "scep", + "non-blocking _nb call on a blocking session; use the _ex variant"); + + if (!s->in_active) { + if (caps == NULL || ra_cert == NULL || ra_cert_len == 0 || + ca_bundle == NULL || ca_bundle_len == 0 || new_key == NULL || + csr_der == NULL || csr_der_len == 0) + return WOLFCERT_ERR_BAD_ARG; + memset(out, 0, sizeof(*out)); + int rc = scep_session_begin_pkcs_req(s, caps, ra_cert, ra_cert_len, + ca_bundle, ca_bundle_len, + new_key, csr_der, csr_der_len, out); + if (rc != WOLFCERT_OK) + return rc; + } + else { + int rc = scep_session_resume_check(s, SCEP_SESS_OP_PKCS_REQ, out); + if (rc != WOLFCERT_OK) + return rc; + } + return scep_session_drive_nb(s); +} + +/* ---- session RenewalReq ------------------------------------------------- */ + +int wolfcert_scep_session_renewal_req_ex(WolfCertScepSession* s, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const uint8_t* current_cert, size_t current_cert_len, + const WolfCertKey* current_key, const uint8_t* csr_der, size_t csr_der_len, + WolfCertScepResult* out) +{ + if (s == NULL || caps == NULL || ra_cert == NULL || ra_cert_len == 0 || + ca_bundle == NULL || ca_bundle_len == 0 || current_cert == NULL || + current_cert_len == 0 || current_key == NULL || csr_der == NULL || + csr_der_len == 0 || out == NULL) + return WOLFCERT_ERR_BAD_ARG; + + if (s->nonblocking) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "scep", + "blocking _ex call on an async session; use the _nb variant"); + + memset(out, 0, sizeof(*out)); + int rc = scep_session_begin_renewal(s, caps, ra_cert, ra_cert_len, + ca_bundle, ca_bundle_len, + current_cert, current_cert_len, + current_key, csr_der, csr_der_len, out); + if (rc != WOLFCERT_OK) + return rc; + return scep_session_drive_sync(s); +} + +int wolfcert_scep_session_renewal_req_nb(WolfCertScepSession* s, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const uint8_t* current_cert, size_t current_cert_len, + const WolfCertKey* current_key, const uint8_t* csr_der, size_t csr_der_len, + WolfCertScepResult* out) +{ + if (s == NULL || out == NULL) + return WOLFCERT_ERR_BAD_ARG; + + if (!s->nonblocking) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "scep", + "non-blocking _nb call on a blocking session; use the _ex variant"); + + if (!s->in_active) { + if (caps == NULL || ra_cert == NULL || ra_cert_len == 0 || ca_bundle == NULL || + ca_bundle_len == 0 || current_cert == NULL || current_cert_len == 0 || + current_key == NULL || csr_der == NULL || csr_der_len == 0) + return WOLFCERT_ERR_BAD_ARG; + memset(out, 0, sizeof(*out)); + int rc = scep_session_begin_renewal(s, caps, ra_cert, ra_cert_len, + ca_bundle, ca_bundle_len, + current_cert, current_cert_len, + current_key, csr_der, csr_der_len, out); + if (rc != WOLFCERT_OK) + return rc; + } + else { + int rc = scep_session_resume_check(s, SCEP_SESS_OP_RENEWAL, out); + if (rc != WOLFCERT_OK) + return rc; + } + return scep_session_drive_nb(s); +} + +/* ---- session GetCertInitial (poll) ------------------------------------- */ + +int wolfcert_scep_session_get_cert_initial_ex(WolfCertScepSession* s, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const uint8_t* signer_cert, size_t signer_cert_len, + const WolfCertKey* signer_key, + const uint8_t* csr_der, size_t csr_der_len, + const uint8_t* transaction_id, size_t transaction_id_len, + WolfCertScepResult* out) +{ + if (s == NULL || caps == NULL || ra_cert == NULL || ra_cert_len == 0 || + ca_bundle == NULL || ca_bundle_len == 0 || signer_key == NULL || + csr_der == NULL || csr_der_len == 0 || transaction_id == NULL || + transaction_id_len == 0 || out == NULL) + return WOLFCERT_ERR_BAD_ARG; + + if (s->nonblocking) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "scep", + "blocking _ex call on an async session; use the _nb variant"); + + memset(out, 0, sizeof(*out)); + int rc = scep_session_begin_get_cert_initial(s, caps, ra_cert, ra_cert_len, + ca_bundle, ca_bundle_len, signer_cert, signer_cert_len, signer_key, + csr_der, csr_der_len, transaction_id, transaction_id_len, out); + if (rc != WOLFCERT_OK) + return rc; + return scep_session_drive_sync(s); +} + +int wolfcert_scep_session_get_cert_initial_nb(WolfCertScepSession* s, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const uint8_t* signer_cert, size_t signer_cert_len, + const WolfCertKey* signer_key, + const uint8_t* csr_der, size_t csr_der_len, + const uint8_t* transaction_id, size_t transaction_id_len, + WolfCertScepResult* out) +{ + if (s == NULL || out == NULL) + return WOLFCERT_ERR_BAD_ARG; + + if (!s->nonblocking) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "scep", + "non-blocking _nb call on a blocking session; use the _ex variant"); + + if (!s->in_active) { + if (caps == NULL || ra_cert == NULL || ra_cert_len == 0 || ca_bundle == NULL || + ca_bundle_len == 0 || signer_key == NULL || csr_der == NULL || + csr_der_len == 0 || transaction_id == NULL || transaction_id_len == 0) + return WOLFCERT_ERR_BAD_ARG; + memset(out, 0, sizeof(*out)); + int rc = scep_session_begin_get_cert_initial(s, caps, ra_cert, ra_cert_len, + ca_bundle, ca_bundle_len, signer_cert, signer_cert_len, signer_key, + csr_der, csr_der_len, transaction_id, transaction_id_len, out); + if (rc != WOLFCERT_OK) + return rc; + } + else { + int rc = scep_session_resume_check(s, SCEP_SESS_OP_GET_CERT_INITIAL, out); + if (rc != WOLFCERT_OK) + return rc; + } + return scep_session_drive_nb(s); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8317c15..c6b3cdd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -108,4 +108,8 @@ if(WOLFCERT_ENABLE_SCEP AND WOLFCERT_ENABLE_SERVER) add_executable(test_scep_poll_roundtrip integration/test_scep_poll_roundtrip.c) target_link_libraries(test_scep_poll_roundtrip PRIVATE wolfcert Threads::Threads) add_test(NAME scep_poll_roundtrip COMMAND test_scep_poll_roundtrip) + + add_executable(test_scep_async_roundtrip integration/test_scep_async_roundtrip.c) + target_link_libraries(test_scep_async_roundtrip PRIVATE wolfcert Threads::Threads) + add_test(NAME scep_async_roundtrip COMMAND test_scep_async_roundtrip) endif() diff --git a/tests/integration/test_scep_async_roundtrip.c b/tests/integration/test_scep_async_roundtrip.c new file mode 100644 index 0000000..29eb5f6 --- /dev/null +++ b/tests/integration/test_scep_async_roundtrip.c @@ -0,0 +1,1102 @@ +/* + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfCert. + * + * wolfCert is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfCert is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfCert. If not, see . + */ + +/* + * End-to-end non-blocking SCEP session driven through a real poll(2) loop. + * Exercises: + * - wolfcert_scep_session_open_async over plaintext HTTP, + * - wolfcert_scep_session_pkcs_req_nb (direct enrollment -> SUCCESS), + * - a PENDING enrollment followed by wolfcert_scep_session_get_cert_initial_nb + * on the same keep-alive connection (-> SUCCESS), + * all pumped through poll() between WOLFCERT_ERR_WANT_READ / _WANT_WRITE + * returns. The async counterpart to test_scep_roundtrip / test_scep_poll_roundtrip. + */ + +#define _POSIX_C_SOURCE 200809L +#define _DEFAULT_SOURCE +#define _DARWIN_C_SOURCE /* expose memmem on macOS */ +#define _GNU_SOURCE + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define REQUIRE(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \ + return 1; \ + } \ + } while (0) + +/* Like REQUIRE but jumps to a function-local `cleanup:` label (setting ret=1) + * so a scenario holding open sessions/allocations releases them on failure. */ +#define REQUIRE_CLEAN(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \ + ret = 1; goto cleanup; \ + } \ + } while (0) + +static void* server_thread(void* arg) +{ + wolfcert_server_run((WolfCertServer*)arg); + return NULL; +} + +/* Generous poll ceiling so a legitimately slow WANT_READ/WANT_WRITE wait on a + * loaded CI host is not mistaken for a hang. */ +#define SCEP_ASYNC_POLL_TIMEOUT_MS 30000 + +/* poll() until the session's fd is ready for the direction it asked for. + * Returns 0 on ready, -1 on timeout or poll error - both are fatal for the pump + * but are logged distinctly so a timeout is not confused with a syscall error. */ +static int wait_ready(int fd, int rc) +{ + struct pollfd p = { + .fd = fd, + .events = (rc == WOLFCERT_ERR_WANT_WRITE) ? POLLOUT : POLLIN, + }; + int n = poll(&p, 1, SCEP_ASYNC_POLL_TIMEOUT_MS); + if (n > 0) + return 0; + fprintf(stderr, "wait_ready: %s\n", + n == 0 ? "timed out" : strerror(errno)); + return -1; +} + +static int pump_pkcs_req(WolfCertScepSession* s, const WolfCertScepCaps* caps, + const uint8_t* ca_der, size_t ca_len, + const WolfCertKey* key, + const uint8_t* csr, size_t csr_len, + WolfCertScepResult* out) +{ + int fd = wolfcert_scep_session_fd(s); + for (;;) { + int rc = wolfcert_scep_session_pkcs_req_nb(s, caps, ca_der, ca_len, + ca_der, ca_len, key, csr, csr_len, out); + if (rc == WOLFCERT_OK) + return 0; + if (rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE) { + if (wait_ready(fd, rc) != 0) + return -1; + continue; + } + fprintf(stderr, "pkcs_req rc=%d (%s)\n", rc, wolfcert_strerror(rc)); + return -1; + } +} + +static int pump_get_cert_initial(WolfCertScepSession* s, const WolfCertScepCaps* caps, + const uint8_t* ca_der, size_t ca_len, + const WolfCertKey* key, + const uint8_t* csr, size_t csr_len, + const uint8_t* tid, size_t tid_len, + WolfCertScepResult* out) +{ + int fd = wolfcert_scep_session_fd(s); + for (;;) { + int rc = wolfcert_scep_session_get_cert_initial_nb(s, caps, ca_der, ca_len, + ca_der, ca_len, NULL, 0, key, csr, csr_len, tid, tid_len, out); + if (rc == WOLFCERT_OK) + return 0; + if (rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE) { + if (wait_ready(fd, rc) != 0) + return -1; + continue; + } + fprintf(stderr, "get_cert_initial rc=%d (%s)\n", rc, wolfcert_strerror(rc)); + return -1; + } +} + +static int pump_renewal_req(WolfCertScepSession* s, const WolfCertScepCaps* caps, + const uint8_t* ca_der, size_t ca_len, + const uint8_t* cur_cert, size_t cur_cert_len, + const WolfCertKey* key, + const uint8_t* csr, size_t csr_len, + WolfCertScepResult* out) +{ + int fd = wolfcert_scep_session_fd(s); + for (;;) { + int rc = wolfcert_scep_session_renewal_req_nb(s, caps, ca_der, ca_len, + ca_der, ca_len, cur_cert, cur_cert_len, key, csr, csr_len, out); + if (rc == WOLFCERT_OK) + return 0; + if (rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE) { + if (wait_ready(fd, rc) != 0) + return -1; + continue; + } + fprintf(stderr, "renewal_req rc=%d (%s)\n", rc, wolfcert_strerror(rc)); + return -1; + } +} + +/* Fetch caps + CA cert (blocking one-shots), generate a fresh key + CSR. */ +static int bootstrap(const WolfCertServerCfg* cli, const char* cn, + WolfCertScepCaps* caps, WolfCertBuffer* ca_pem, + DerBuffer** ca_der, WolfCertKey** key, WolfCertBuffer* csr) +{ + if (wolfcert_scep_get_ca_caps(cli, caps) != WOLFCERT_OK) + return -1; + if (wolfcert_scep_get_ca_cert(cli, ca_pem) != WOLFCERT_OK) + return -1; + if (wc_PemToDer(ca_pem->data, (long)ca_pem->len, CERT_TYPE, + ca_der, NULL, NULL, NULL) != 0) { + wolfcert_buffer_free(ca_pem); + return -1; + } + + WolfCertKeyCfg kcfg = { .type = WOLFCERT_KEY_RSA, .param = 2048, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + if (wolfcert_key_generate(&kcfg, key) != WOLFCERT_OK) { + wc_FreeDer(ca_der); + wolfcert_buffer_free(ca_pem); + return -1; + } + WolfCertCertMeta meta = { .subject_dn = cn }; + if (wolfcert_csr_build(*key, &meta, csr) != WOLFCERT_OK) { + wolfcert_key_free(*key); + *key = NULL; + wc_FreeDer(ca_der); + wolfcert_buffer_free(ca_pem); + return -1; + } + return 0; +} + +/* Scenario A: an auto-approving server issues immediately over an async + * PKCSReq, then a blocking session runs enroll + RenewalReq to completion. + * Every allocation is released through the single cleanup: label so a REQUIRE + * failure cannot leak the open sessions or buffers. */ +static int async_enroll_path(WolfCertServer* s) +{ + char url[128]; + WolfCertServerCfg cli; + WolfCertScepCaps caps = { 0 }; + WolfCertBuffer ca_pem = { 0 }; + DerBuffer* ca_der = NULL; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertScepSession* sess = NULL; + WolfCertScepResult r = { 0 }; + WolfCertScepSession* bsess = NULL; + WolfCertKeyCfg kcfg2 = { .type = WOLFCERT_KEY_RSA, .param = 2048, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertKey* dk2 = NULL; + WolfCertCertMeta meta2 = { .subject_dn = "CN=sync-scep-1" }; + WolfCertBuffer csr2 = { 0 }; + WolfCertScepResult rb = { 0 }; + DerBuffer* rb_der = NULL; + WolfCertScepResult rbr = { 0 }; + int ret = 1; + + snprintf(url, sizeof(url), "http://127.0.0.1:%u/scep", wolfcert_server_port(s)); + cli = (WolfCertServerCfg){ .protocol = WOLFCERT_PROTO_SCEP, .server_url = url }; + + REQUIRE_CLEAN(bootstrap(&cli, "CN=async-scep-1", &caps, &ca_pem, &ca_der, + &dk, &csr) == 0); + + REQUIRE_CLEAN(wolfcert_scep_session_open_async(&cli, &sess) == WOLFCERT_OK); + REQUIRE_CLEAN(wolfcert_scep_session_fd(sess) >= 0); + + REQUIRE_CLEAN(pump_pkcs_req(sess, &caps, ca_der->buffer, ca_der->length, + dk, csr.data, csr.len, &r) == 0); + REQUIRE_CLEAN(r.status == WOLFCERT_SCEP_STATUS_SUCCESS); + REQUIRE_CLEAN(memmem(r.cert_pem.data, r.cert_pem.len, "BEGIN CERTIFICATE", 17) != NULL); + + /* Close the async session before opening the blocking one: the single- + * threaded test server serves one connection at a time. */ + wolfcert_scep_session_close(sess); + sess = NULL; + + REQUIRE_CLEAN(wolfcert_scep_session_open(&cli, &bsess) == WOLFCERT_OK); + REQUIRE_CLEAN(wolfcert_key_generate(&kcfg2, &dk2) == WOLFCERT_OK); + REQUIRE_CLEAN(wolfcert_csr_build(dk2, &meta2, &csr2) == WOLFCERT_OK); + REQUIRE_CLEAN(wolfcert_scep_session_pkcs_req_ex(bsess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + dk2, csr2.data, csr2.len, &rb) == WOLFCERT_OK); + REQUIRE_CLEAN(rb.status == WOLFCERT_SCEP_STATUS_SUCCESS); + + /* Blocking-session RenewalReq on the same keep-alive connection, covering + * wolfcert_scep_session_renewal_req_ex. */ + REQUIRE_CLEAN(wc_PemToDer(rb.cert_pem.data, (long)rb.cert_pem.len, CERT_TYPE, + &rb_der, NULL, NULL, NULL) == 0); + REQUIRE_CLEAN(wolfcert_scep_session_renewal_req_ex(bsess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + rb_der->buffer, rb_der->length, dk2, csr2.data, csr2.len, &rbr) + == WOLFCERT_OK); + REQUIRE_CLEAN(rbr.status == WOLFCERT_SCEP_STATUS_SUCCESS); + + ret = 0; +cleanup: + if (sess != NULL) + wolfcert_scep_session_close(sess); + if (bsess != NULL) + wolfcert_scep_session_close(bsess); + wolfcert_scep_result_free(&r); + wolfcert_scep_result_free(&rb); + wolfcert_scep_result_free(&rbr); + if (rb_der != NULL) + wc_FreeDer(&rb_der); + wolfcert_buffer_free(&csr2); + if (dk2 != NULL) + wolfcert_key_free(dk2); + if (ca_der != NULL) + wc_FreeDer(&ca_der); + wolfcert_buffer_free(&ca_pem); + wolfcert_buffer_free(&csr); + if (dk != NULL) + wolfcert_key_free(dk); + return ret; +} + +/* Scenario B: an approval-required server returns PENDING, then issues on the + * follow-up GetCertInitial - both round trips async on one keep-alive session. */ +static int async_poll_path(WolfCertServer* s) +{ + char url[128]; + WolfCertServerCfg cli; + WolfCertScepCaps caps = { 0 }; + WolfCertBuffer ca_pem = { 0 }; + DerBuffer* ca_der = NULL; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertScepSession* sess = NULL; + WolfCertScepResult r1 = { 0 }; + WolfCertScepResult r2 = { 0 }; + int ret = 1; + + snprintf(url, sizeof(url), "http://127.0.0.1:%u/scep", wolfcert_server_port(s)); + cli = (WolfCertServerCfg){ .protocol = WOLFCERT_PROTO_SCEP, .server_url = url }; + + REQUIRE_CLEAN(bootstrap(&cli, "CN=async-scep-poll", &caps, &ca_pem, &ca_der, + &dk, &csr) == 0); + + REQUIRE_CLEAN(wolfcert_scep_session_open_async(&cli, &sess) == WOLFCERT_OK); + + /* First round trip: PENDING with a transactionID. */ + REQUIRE_CLEAN(pump_pkcs_req(sess, &caps, ca_der->buffer, ca_der->length, + dk, csr.data, csr.len, &r1) == 0); + REQUIRE_CLEAN(r1.status == WOLFCERT_SCEP_STATUS_PENDING); + REQUIRE_CLEAN(r1.transaction_id != NULL && r1.transaction_id_len > 0); + + /* Second round trip on the same connection: poll -> SUCCESS. */ + REQUIRE_CLEAN(pump_get_cert_initial(sess, &caps, ca_der->buffer, ca_der->length, + dk, csr.data, csr.len, + r1.transaction_id, r1.transaction_id_len, &r2) == 0); + REQUIRE_CLEAN(r2.status == WOLFCERT_SCEP_STATUS_SUCCESS); + REQUIRE_CLEAN(memmem(r2.cert_pem.data, r2.cert_pem.len, "BEGIN CERTIFICATE", 17) != NULL); + + ret = 0; +cleanup: + if (sess != NULL) + wolfcert_scep_session_close(sess); + wolfcert_scep_result_free(&r1); + wolfcert_scep_result_free(&r2); + if (ca_der != NULL) + wc_FreeDer(&ca_der); + wolfcert_buffer_free(&ca_pem); + wolfcert_buffer_free(&csr); + if (dk != NULL) + wolfcert_key_free(dk); + return ret; +} + +/* Scenario C: async enroll, then an async RenewalReq re-using the same + * keep-alive session. current_cert/current_key sign the renewal pkiMessage; + * the auto-approving server issues immediately. Covers + * wolfcert_scep_session_renewal_req_nb. */ +static int async_renewal_path(WolfCertServer* s) +{ + char url[128]; + WolfCertServerCfg cli; + WolfCertScepCaps caps = { 0 }; + WolfCertBuffer ca_pem = { 0 }; + DerBuffer* ca_der = NULL; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertScepSession* sess = NULL; + WolfCertScepResult r1 = { 0 }; + DerBuffer* cur_der = NULL; + WolfCertScepResult r2 = { 0 }; + int ret = 1; + + snprintf(url, sizeof(url), "http://127.0.0.1:%u/scep", wolfcert_server_port(s)); + cli = (WolfCertServerCfg){ .protocol = WOLFCERT_PROTO_SCEP, .server_url = url }; + + REQUIRE_CLEAN(bootstrap(&cli, "CN=async-scep-renew", &caps, &ca_pem, &ca_der, + &dk, &csr) == 0); + + REQUIRE_CLEAN(wolfcert_scep_session_open_async(&cli, &sess) == WOLFCERT_OK); + + /* First round trip: an initial enrollment to obtain the cert we then renew. */ + REQUIRE_CLEAN(pump_pkcs_req(sess, &caps, ca_der->buffer, ca_der->length, + dk, csr.data, csr.len, &r1) == 0); + REQUIRE_CLEAN(r1.status == WOLFCERT_SCEP_STATUS_SUCCESS); + + REQUIRE_CLEAN(wc_PemToDer(r1.cert_pem.data, (long)r1.cert_pem.len, CERT_TYPE, + &cur_der, NULL, NULL, NULL) == 0); + + /* Second round trip on the same connection: RenewalReq signed by the cert + * just issued (the CSR carries the - here unchanged - public key). */ + REQUIRE_CLEAN(pump_renewal_req(sess, &caps, ca_der->buffer, ca_der->length, + cur_der->buffer, cur_der->length, + dk, csr.data, csr.len, &r2) == 0); + REQUIRE_CLEAN(r2.status == WOLFCERT_SCEP_STATUS_SUCCESS); + REQUIRE_CLEAN(memmem(r2.cert_pem.data, r2.cert_pem.len, "BEGIN CERTIFICATE", 17) != NULL); + + ret = 0; +cleanup: + if (sess != NULL) + wolfcert_scep_session_close(sess); + wolfcert_scep_result_free(&r1); + wolfcert_scep_result_free(&r2); + if (cur_der != NULL) + wc_FreeDer(&cur_der); + if (ca_der != NULL) + wc_FreeDer(&ca_der); + wolfcert_buffer_free(&ca_pem); + wolfcert_buffer_free(&csr); + if (dk != NULL) + wolfcert_key_free(dk); + return ret; +} + +/* Open a loopback listener that completes the TCP handshake (via the kernel + * backlog) but never accepts, reads, or answers, so a non-blocking HTTP request + * sent to it is guaranteed to stay in WOLFCERT_ERR_WANT_READ. Returns the + * listening fd (>= 0) and writes the bound port to *out_port, or -1 on error. */ +static int black_hole_listener(uint16_t* out_port) +{ + struct sockaddr_in addr; + socklen_t addr_len = sizeof(addr); + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) + return -1; + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; /* ephemeral */ + if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0 || + listen(fd, 16) != 0 || + getsockname(fd, (struct sockaddr*)&addr, &addr_len) != 0) { + close(fd); + return -1; + } + + *out_port = ntohs(addr.sin_port); + return fd; +} + +/* Scenario D: session misuse guards. Covers (1) the mode guards that reject an + * _ex call on an async session and an _nb call on a blocking session, and (2) + * the in_op guard that rejects starting a different operation while one is + * already in flight. The in-flight state is held deterministically by pointing + * the guard sessions at a black-hole listener (accepts the connect, never + * answers) instead of the real server, so the first non-blocking pump always + * returns WANT_* regardless of host speed. */ +static int async_guard_path(WolfCertServer* s) +{ + char real_url[128]; + char bh_url[128]; + WolfCertServerCfg cli_real; + WolfCertServerCfg cli; + WolfCertScepCaps caps = { 0 }; + WolfCertBuffer ca_pem = { 0 }; + DerBuffer* ca_der = NULL; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertScepSession* bsess = NULL; + WolfCertScepSession* asess = NULL; + WolfCertScepResult rbad = { 0 }; + WolfCertScepResult r1 = { 0 }; + WolfCertScepResult r2 = { 0 }; + uint16_t bh_port = 0; + int bh_fd = -1; + int rc = 0; + int ret = 1; + + /* Real server: fetch caps + CA and generate a valid key/CSR so the + * pkiMessage the in-flight guard builds is well-formed. */ + snprintf(real_url, sizeof(real_url), "http://127.0.0.1:%u/scep", + wolfcert_server_port(s)); + cli_real = (WolfCertServerCfg){ .protocol = WOLFCERT_PROTO_SCEP, + .server_url = real_url }; + REQUIRE_CLEAN(bootstrap(&cli_real, "CN=async-scep-guard", &caps, &ca_pem, + &ca_der, &dk, &csr) == 0); + + /* Point the guard sessions at a black hole so a non-blocking request stays + * in flight deterministically (see black_hole_listener). */ + bh_fd = black_hole_listener(&bh_port); + REQUIRE_CLEAN(bh_fd >= 0); + snprintf(bh_url, sizeof(bh_url), "http://127.0.0.1:%u/scep", (unsigned)bh_port); + cli = (WolfCertServerCfg){ .protocol = WOLFCERT_PROTO_SCEP, .server_url = bh_url }; + + /* Mode guard: an _nb call on a blocking session is rejected. */ + REQUIRE_CLEAN(wolfcert_scep_session_open(&cli, &bsess) == WOLFCERT_OK); + REQUIRE_CLEAN(wolfcert_scep_session_pkcs_req_nb(bsess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + dk, csr.data, csr.len, &rbad) == WOLFCERT_ERR_BAD_ARG); + wolfcert_scep_session_close(bsess); + bsess = NULL; + + /* Mode guard: an _ex call on an async session is rejected. */ + REQUIRE_CLEAN(wolfcert_scep_session_open_async(&cli, &asess) == WOLFCERT_OK); + REQUIRE_CLEAN(wolfcert_scep_session_pkcs_req_ex(asess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + dk, csr.data, csr.len, &rbad) == WOLFCERT_ERR_BAD_ARG); + + /* in_op guard: begin a PKCSReq against the black hole and leave it in + * flight, then attempt a different operation on the same session. The peer + * completes the TCP connect but never answers, so the first pump always + * returns WANT_* with the request in flight - the in-flight state is + * independent of host speed, closing the timing hole a real server would + * open by occasionally replying before the client's first read. */ + rc = wolfcert_scep_session_pkcs_req_nb(asess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + dk, csr.data, csr.len, &r1); + REQUIRE_CLEAN(rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE); + + /* out-pointer guard: resuming the in-flight PKCSReq (same operation) with a + * different WolfCertScepResult* than the one captured at begin is rejected, + * rather than writing the eventual result to the wrong object. */ + REQUIRE_CLEAN(wolfcert_scep_session_pkcs_req_nb(asess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + dk, csr.data, csr.len, &r2) == WOLFCERT_ERR_BAD_ARG); + + /* in_op guard: a different operation while one is in flight is rejected. */ + REQUIRE_CLEAN(wolfcert_scep_session_renewal_req_nb(asess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + ca_der->buffer, ca_der->length, dk, csr.data, csr.len, &r2) + == WOLFCERT_ERR_BAD_ARG); + REQUIRE_CLEAN(wolfcert_scep_session_get_cert_initial_nb(asess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + NULL, 0, dk, csr.data, csr.len, csr.data, csr.len, &r2) + == WOLFCERT_ERR_BAD_ARG); + + ret = 0; +cleanup: + if (bsess != NULL) + wolfcert_scep_session_close(bsess); + if (asess != NULL) + wolfcert_scep_session_close(asess); /* resets any in-flight PKCSReq cleanly */ + if (bh_fd >= 0) + close(bh_fd); + wolfcert_scep_result_free(&r1); + wolfcert_scep_result_free(&r2); + wolfcert_scep_result_free(&rbad); + if (ca_der != NULL) + wc_FreeDer(&ca_der); + wolfcert_buffer_free(&ca_pem); + wolfcert_buffer_free(&csr); + if (dk != NULL) + wolfcert_key_free(dk); + return ret; +} + +/* Scenario E: the blocking mirror of scenario B. A blocking session drives + * PKCSReq -> PENDING then GetCertInitial -> SUCCESS on one keep-alive + * connection, covering wolfcert_scep_session_get_cert_initial_ex. */ +static int blocking_poll_path(WolfCertServer* s) +{ + char url[128]; + WolfCertServerCfg cli; + WolfCertScepCaps caps = { 0 }; + WolfCertBuffer ca_pem = { 0 }; + DerBuffer* ca_der = NULL; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertScepSession* sess = NULL; + WolfCertScepResult r1 = { 0 }; + WolfCertScepResult r2 = { 0 }; + int ret = 1; + + snprintf(url, sizeof(url), "http://127.0.0.1:%u/scep", wolfcert_server_port(s)); + cli = (WolfCertServerCfg){ .protocol = WOLFCERT_PROTO_SCEP, .server_url = url }; + + REQUIRE_CLEAN(bootstrap(&cli, "CN=sync-scep-poll", &caps, &ca_pem, &ca_der, + &dk, &csr) == 0); + + REQUIRE_CLEAN(wolfcert_scep_session_open(&cli, &sess) == WOLFCERT_OK); + + REQUIRE_CLEAN(wolfcert_scep_session_pkcs_req_ex(sess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + dk, csr.data, csr.len, &r1) == WOLFCERT_OK); + REQUIRE_CLEAN(r1.status == WOLFCERT_SCEP_STATUS_PENDING); + REQUIRE_CLEAN(r1.transaction_id != NULL && r1.transaction_id_len > 0); + + REQUIRE_CLEAN(wolfcert_scep_session_get_cert_initial_ex(sess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + NULL, 0, dk, csr.data, csr.len, + r1.transaction_id, r1.transaction_id_len, &r2) == WOLFCERT_OK); + REQUIRE_CLEAN(r2.status == WOLFCERT_SCEP_STATUS_SUCCESS); + REQUIRE_CLEAN(memmem(r2.cert_pem.data, r2.cert_pem.len, "BEGIN CERTIFICATE", 17) != NULL); + + ret = 0; +cleanup: + if (sess != NULL) + wolfcert_scep_session_close(sess); + wolfcert_scep_result_free(&r1); + wolfcert_scep_result_free(&r2); + if (ca_der != NULL) + wc_FreeDer(&ca_der); + wolfcert_buffer_free(&ca_pem); + wolfcert_buffer_free(&csr); + if (dk != NULL) + wolfcert_key_free(dk); + return ret; +} + +/* Scenario F: force the base64 HTTP GET PKIOperation transport (RFC 8894 4.1) + * over a session by clearing post_pki_operation in the caps, covering the + * session's use_post==0 wiring in scep_session_begin (the in-tree server + * otherwise always advertises POSTPKIOperation). Blocking for brevity. */ +static int session_get_transport_path(WolfCertServer* s) +{ + char url[128]; + WolfCertServerCfg cli; + WolfCertScepCaps caps = { 0 }; + WolfCertBuffer ca_pem = { 0 }; + DerBuffer* ca_der = NULL; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertScepSession* sess = NULL; + WolfCertScepResult r = { 0 }; + int ret = 1; + + snprintf(url, sizeof(url), "http://127.0.0.1:%u/scep", wolfcert_server_port(s)); + cli = (WolfCertServerCfg){ .protocol = WOLFCERT_PROTO_SCEP, .server_url = url }; + + REQUIRE_CLEAN(bootstrap(&cli, "CN=sync-scep-get", &caps, &ca_pem, &ca_der, + &dk, &csr) == 0); + + caps.post_pki_operation = 0; /* force the base64 GET transport */ + + REQUIRE_CLEAN(wolfcert_scep_session_open(&cli, &sess) == WOLFCERT_OK); + + REQUIRE_CLEAN(wolfcert_scep_session_pkcs_req_ex(sess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + dk, csr.data, csr.len, &r) == WOLFCERT_OK); + REQUIRE_CLEAN(r.status == WOLFCERT_SCEP_STATUS_SUCCESS); + REQUIRE_CLEAN(memmem(r.cert_pem.data, r.cert_pem.len, "BEGIN CERTIFICATE", 17) != NULL); + + ret = 0; +cleanup: + if (sess != NULL) + wolfcert_scep_session_close(sess); + wolfcert_scep_result_free(&r); + if (ca_der != NULL) + wc_FreeDer(&ca_der); + wolfcert_buffer_free(&ca_pem); + wolfcert_buffer_free(&csr); + if (dk != NULL) + wolfcert_key_free(dk); + return ret; +} + +/* Scenario G: the TLS transport guard. An https:// SCEP session with + * verify_server unset is refused before any connect; plaintext http:// (used by + * every other scenario) is accepted. Needs no server - the guard fires during + * URL parsing in session open. */ +static int tls_guard_path(void) +{ + WolfCertServerCfg cli = { .protocol = WOLFCERT_PROTO_SCEP, + .server_url = "https://127.0.0.1:8443/scep", + .verify_server = 0 }; + WolfCertScepSession* sess = NULL; + + REQUIRE(wolfcert_scep_session_open(&cli, &sess) == WOLFCERT_ERR_TLS); + REQUIRE(sess == NULL); + REQUIRE(wolfcert_scep_session_open_async(&cli, &sess) == WOLFCERT_ERR_TLS); + REQUIRE(sess == NULL); + + /* NULL-input contract of the session accessors. */ + REQUIRE(wolfcert_scep_session_fd(NULL) == -1); + wolfcert_scep_session_close(NULL); /* no-op, must not crash */ + + /* NULL srv / server_url are rejected by both opens. */ + REQUIRE(wolfcert_scep_session_open(NULL, &sess) == WOLFCERT_ERR_BAD_ARG); + REQUIRE(wolfcert_scep_session_open_async(NULL, &sess) == WOLFCERT_ERR_BAD_ARG); + WolfCertServerCfg no_url = { .protocol = WOLFCERT_PROTO_SCEP, .server_url = NULL }; + REQUIRE(wolfcert_scep_session_open(&no_url, &sess) == WOLFCERT_ERR_BAD_ARG); + return 0; +} + +#ifdef WOLFCERT_HAVE_ED25519 +/* Scenario H: SCEP is RSA-only (RFC 8894); each session begin helper rejects a + * non-RSA signer with WOLFCERT_ERR_UNSUPPORTED before any network I/O. */ +static int non_rsa_reject_path(WolfCertServer* s) +{ + char url[128]; + WolfCertServerCfg cli; + WolfCertScepCaps caps = { 0 }; + WolfCertBuffer ca_pem = { 0 }; + DerBuffer* ca_der = NULL; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertKeyCfg ecfg = { .type = WOLFCERT_KEY_ED25519, .param = 0, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertKey* ek = NULL; + WolfCertScepSession* sess = NULL; + WolfCertScepResult r = { 0 }; + int ret = 1; + + snprintf(url, sizeof(url), "http://127.0.0.1:%u/scep", wolfcert_server_port(s)); + cli = (WolfCertServerCfg){ .protocol = WOLFCERT_PROTO_SCEP, .server_url = url }; + REQUIRE_CLEAN(bootstrap(&cli, "CN=nonrsa", &caps, &ca_pem, &ca_der, &dk, &csr) == 0); + REQUIRE_CLEAN(wolfcert_key_generate(&ecfg, &ek) == WOLFCERT_OK); + REQUIRE_CLEAN(wolfcert_scep_session_open(&cli, &sess) == WOLFCERT_OK); + + REQUIRE_CLEAN(wolfcert_scep_session_pkcs_req_ex(sess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + ek, csr.data, csr.len, &r) == WOLFCERT_ERR_UNSUPPORTED); + REQUIRE_CLEAN(wolfcert_scep_session_renewal_req_ex(sess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + ca_der->buffer, ca_der->length, ek, csr.data, csr.len, &r) + == WOLFCERT_ERR_UNSUPPORTED); + REQUIRE_CLEAN(wolfcert_scep_session_get_cert_initial_ex(sess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + NULL, 0, ek, csr.data, csr.len, csr.data, csr.len, &r) + == WOLFCERT_ERR_UNSUPPORTED); + + ret = 0; +cleanup: + if (sess != NULL) + wolfcert_scep_session_close(sess); + wolfcert_scep_result_free(&r); + if (ek != NULL) + wolfcert_key_free(ek); + if (ca_der != NULL) + wc_FreeDer(&ca_der); + wolfcert_buffer_free(&ca_pem); + wolfcert_buffer_free(&csr); + if (dk != NULL) + wolfcert_key_free(dk); + return ret; +} +#endif /* WOLFCERT_HAVE_ED25519 */ + +/* Scenario I: the non-NULL signer_cert branch of session GetCertInitial. On an + * approval-required server: enroll -> PENDING -> poll (signer_cert NULL, the + * derive branch) yields a cert; renew that cert -> PENDING -> poll passing the + * issued cert as signer_cert (the non-derive branch) -> SUCCESS. */ +static int blocking_renewal_poll_path(WolfCertServer* s) +{ + char url[128]; + WolfCertServerCfg cli; + WolfCertScepCaps caps = { 0 }; + WolfCertBuffer ca_pem = { 0 }; + DerBuffer* ca_der = NULL; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertScepSession* sess = NULL; + WolfCertScepResult r1 = { 0 }; + WolfCertScepResult r2 = { 0 }; + DerBuffer* issued_der = NULL; + WolfCertScepResult r3 = { 0 }; + WolfCertScepResult r4 = { 0 }; + int ret = 1; + + snprintf(url, sizeof(url), "http://127.0.0.1:%u/scep", wolfcert_server_port(s)); + cli = (WolfCertServerCfg){ .protocol = WOLFCERT_PROTO_SCEP, .server_url = url }; + REQUIRE_CLEAN(bootstrap(&cli, "CN=sync-renew-poll", &caps, &ca_pem, &ca_der, + &dk, &csr) == 0); + REQUIRE_CLEAN(wolfcert_scep_session_open(&cli, &sess) == WOLFCERT_OK); + + /* Enroll -> PENDING, then poll (signer_cert NULL) -> issued cert. */ + REQUIRE_CLEAN(wolfcert_scep_session_pkcs_req_ex(sess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + dk, csr.data, csr.len, &r1) == WOLFCERT_OK); + REQUIRE_CLEAN(r1.status == WOLFCERT_SCEP_STATUS_PENDING); + REQUIRE_CLEAN(wolfcert_scep_session_get_cert_initial_ex(sess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + NULL, 0, dk, csr.data, csr.len, + r1.transaction_id, r1.transaction_id_len, &r2) == WOLFCERT_OK); + REQUIRE_CLEAN(r2.status == WOLFCERT_SCEP_STATUS_SUCCESS); + REQUIRE_CLEAN(wc_PemToDer(r2.cert_pem.data, (long)r2.cert_pem.len, CERT_TYPE, + &issued_der, NULL, NULL, NULL) == 0); + + /* Renew -> PENDING, then poll passing the issued cert as signer_cert + * (the non-NULL / non-derive branch) -> SUCCESS. */ + REQUIRE_CLEAN(wolfcert_scep_session_renewal_req_ex(sess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + issued_der->buffer, issued_der->length, dk, csr.data, csr.len, &r3) + == WOLFCERT_OK); + REQUIRE_CLEAN(r3.status == WOLFCERT_SCEP_STATUS_PENDING); + REQUIRE_CLEAN(wolfcert_scep_session_get_cert_initial_ex(sess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + issued_der->buffer, issued_der->length, dk, csr.data, csr.len, + r3.transaction_id, r3.transaction_id_len, &r4) == WOLFCERT_OK); + REQUIRE_CLEAN(r4.status == WOLFCERT_SCEP_STATUS_SUCCESS); + REQUIRE_CLEAN(memmem(r4.cert_pem.data, r4.cert_pem.len, "BEGIN CERTIFICATE", 17) != NULL); + + ret = 0; +cleanup: + if (sess != NULL) + wolfcert_scep_session_close(sess); + wolfcert_scep_result_free(&r1); + wolfcert_scep_result_free(&r2); + wolfcert_scep_result_free(&r3); + wolfcert_scep_result_free(&r4); + if (issued_der != NULL) + wc_FreeDer(&issued_der); + if (ca_der != NULL) + wc_FreeDer(&ca_der); + wolfcert_buffer_free(&ca_pem); + wolfcert_buffer_free(&csr); + if (dk != NULL) + wolfcert_key_free(dk); + return ret; +} + +/* A listener thread that accepts one connection and answers HTTP 500, so a + * session request against it returns WOLFCERT_ERR_HTTP. Polls first so it can + * never block join() indefinitely if no client shows up. */ +static void* stub_500_thread(void* arg) +{ + int lfd = *(int*)arg; + struct pollfd p = { .fd = lfd, .events = POLLIN }; + int cfd; + if (poll(&p, 1, SCEP_ASYNC_POLL_TIMEOUT_MS) <= 0) + return NULL; + cfd = accept(lfd, NULL, NULL); + if (cfd >= 0) { + static const char resp[] = + "HTTP/1.1 500 Internal Server Error\r\n" + "Content-Length: 0\r\nConnection: close\r\n\r\n"; + char buf[2048]; + (void)recv(cfd, buf, sizeof(buf), 0); /* read the request head */ + (void)send(cfd, resp, sizeof(resp) - 1, 0); /* answer 500 */ + while (recv(cfd, buf, sizeof(buf), MSG_DONTWAIT) > 0) + ; /* drain for a clean close */ + close(cfd); + } + return NULL; +} + +/* Scenario J: a session whose HTTP peer answers a non-200 status surfaces + * WOLFCERT_ERR_HTTP. bootstrap runs against the real server; the enrolling + * request goes to the 500 stub. */ +static int non_200_path(WolfCertServer* s) +{ + char real_url[128]; + char stub_url[128]; + WolfCertServerCfg cli_real; + WolfCertServerCfg cli; + WolfCertScepCaps caps = { 0 }; + WolfCertBuffer ca_pem = { 0 }; + DerBuffer* ca_der = NULL; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertScepSession* sess = NULL; + WolfCertScepResult r = { 0 }; + uint16_t port = 0; + int lfd = -1; + pthread_t stub; + int stub_started = 0; + int ret = 1; + + snprintf(real_url, sizeof(real_url), "http://127.0.0.1:%u/scep", + wolfcert_server_port(s)); + cli_real = (WolfCertServerCfg){ .protocol = WOLFCERT_PROTO_SCEP, + .server_url = real_url }; + REQUIRE_CLEAN(bootstrap(&cli_real, "CN=sync-500", &caps, &ca_pem, &ca_der, + &dk, &csr) == 0); + + lfd = black_hole_listener(&port); /* just need a listening socket to accept on */ + REQUIRE_CLEAN(lfd >= 0); + REQUIRE_CLEAN(pthread_create(&stub, NULL, stub_500_thread, &lfd) == 0); + stub_started = 1; + snprintf(stub_url, sizeof(stub_url), "http://127.0.0.1:%u/scep", (unsigned)port); + cli = (WolfCertServerCfg){ .protocol = WOLFCERT_PROTO_SCEP, .server_url = stub_url }; + + REQUIRE_CLEAN(wolfcert_scep_session_open(&cli, &sess) == WOLFCERT_OK); + REQUIRE_CLEAN(wolfcert_scep_session_pkcs_req_ex(sess, &caps, + ca_der->buffer, ca_der->length, ca_der->buffer, ca_der->length, + dk, csr.data, csr.len, &r) == WOLFCERT_ERR_HTTP); + + ret = 0; +cleanup: + if (sess != NULL) + wolfcert_scep_session_close(sess); + if (stub_started) + pthread_join(stub, NULL); + if (lfd >= 0) + close(lfd); + wolfcert_scep_result_free(&r); + if (ca_der != NULL) + wc_FreeDer(&ca_der); + wolfcert_buffer_free(&ca_pem); + wolfcert_buffer_free(&csr); + if (dk != NULL) + wolfcert_key_free(dk); + return ret; +} + +/* Scenario K: argument-validation guards of the session request APIs. NULL + * pointers, zero lengths, a NULL caps, and an over-long transactionID all return + * WOLFCERT_ERR_BAD_ARG before any network I/O; also covers the renewal / + * get_cert_initial mode guards (Scenario D covers pkcs_req). NULL srv and the + * accessor NULL-safety are covered in tls_guard_path. */ +static int negative_args_path(WolfCertServer* s) +{ + char url[128]; + WolfCertServerCfg cli; + WolfCertScepCaps caps = { 0 }; + WolfCertBuffer ca_pem = { 0 }; + DerBuffer* ca_der = NULL; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertScepSession* sess = NULL; + WolfCertScepResult r = { 0 }; + const uint8_t* ca = NULL; + size_t ca_len = 0; + uint8_t big_tid[200]; /* > SCEP_TXID_MAX (128), which is client-internal */ + int ret = 1; + + snprintf(url, sizeof(url), "http://127.0.0.1:%u/scep", wolfcert_server_port(s)); + cli = (WolfCertServerCfg){ .protocol = WOLFCERT_PROTO_SCEP, .server_url = url }; + REQUIRE_CLEAN(bootstrap(&cli, "CN=negargs", &caps, &ca_pem, &ca_der, &dk, &csr) == 0); + REQUIRE_CLEAN(wolfcert_scep_session_open(&cli, &sess) == WOLFCERT_OK); + ca = ca_der->buffer; + ca_len = ca_der->length; + + /* PKCSReq: NULL session, NULL out, NULL/zero required arg, NULL caps. */ + REQUIRE_CLEAN(wolfcert_scep_session_pkcs_req_ex(NULL, &caps, ca, ca_len, + ca, ca_len, dk, csr.data, csr.len, &r) == WOLFCERT_ERR_BAD_ARG); + REQUIRE_CLEAN(wolfcert_scep_session_pkcs_req_ex(sess, &caps, ca, ca_len, + ca, ca_len, dk, csr.data, csr.len, NULL) == WOLFCERT_ERR_BAD_ARG); + REQUIRE_CLEAN(wolfcert_scep_session_pkcs_req_ex(sess, &caps, NULL, ca_len, + ca, ca_len, dk, csr.data, csr.len, &r) == WOLFCERT_ERR_BAD_ARG); + REQUIRE_CLEAN(wolfcert_scep_session_pkcs_req_ex(sess, &caps, ca, 0, + ca, ca_len, dk, csr.data, csr.len, &r) == WOLFCERT_ERR_BAD_ARG); + REQUIRE_CLEAN(wolfcert_scep_session_pkcs_req_ex(sess, NULL, ca, ca_len, + ca, ca_len, dk, csr.data, csr.len, &r) == WOLFCERT_ERR_BAD_ARG); + + /* RenewalReq: a NULL op-specific required arg (current_cert). */ + REQUIRE_CLEAN(wolfcert_scep_session_renewal_req_ex(sess, &caps, ca, ca_len, + ca, ca_len, NULL, 0, dk, csr.data, csr.len, &r) == WOLFCERT_ERR_BAD_ARG); + + /* GetCertInitial: NULL transactionID, then an over-long transactionID. */ + REQUIRE_CLEAN(wolfcert_scep_session_get_cert_initial_ex(sess, &caps, ca, ca_len, + ca, ca_len, NULL, 0, dk, csr.data, csr.len, NULL, 0, &r) + == WOLFCERT_ERR_BAD_ARG); + memset(big_tid, 0x11, sizeof(big_tid)); + REQUIRE_CLEAN(wolfcert_scep_session_get_cert_initial_ex(sess, &caps, ca, ca_len, + ca, ca_len, NULL, 0, dk, csr.data, csr.len, big_tid, sizeof(big_tid), &r) + == WOLFCERT_ERR_BAD_ARG); + + /* Mode-guard sibling coverage: _nb on a blocking session is rejected. */ + REQUIRE_CLEAN(wolfcert_scep_session_renewal_req_nb(sess, &caps, ca, ca_len, + ca, ca_len, ca, ca_len, dk, csr.data, csr.len, &r) == WOLFCERT_ERR_BAD_ARG); + REQUIRE_CLEAN(wolfcert_scep_session_get_cert_initial_nb(sess, &caps, ca, ca_len, + ca, ca_len, NULL, 0, dk, csr.data, csr.len, csr.data, csr.len, &r) + == WOLFCERT_ERR_BAD_ARG); + + ret = 0; +cleanup: + if (sess != NULL) + wolfcert_scep_session_close(sess); + wolfcert_scep_result_free(&r); + if (ca_der != NULL) + wc_FreeDer(&ca_der); + wolfcert_buffer_free(&ca_pem); + wolfcert_buffer_free(&csr); + if (dk != NULL) + wolfcert_key_free(dk); + return ret; +} + +int main(void) +{ + REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); + + /* Scenario A: auto-approve server. */ + WolfCertServerCfgSrv cfg = { .protocol = WOLFCERT_PROTO_SCEP, + .bind_host = "127.0.0.1", .bind_port = 0 }; + WolfCertServer* s1 = NULL; + REQUIRE(wolfcert_server_start(&cfg, &s1) == WOLFCERT_OK); + pthread_t t1; + REQUIRE(pthread_create(&t1, NULL, server_thread, s1) == 0); + int rc = async_enroll_path(s1); + wolfcert_server_stop(s1); + pthread_join(t1, NULL); + wolfcert_server_free(s1); + if (rc != 0) + return rc; + + /* Scenario B: approval-required server (PENDING -> poll). */ + WolfCertServerCfgSrv cfg_pending = { .protocol = WOLFCERT_PROTO_SCEP, + .bind_host = "127.0.0.1", .bind_port = 0, + .scep_require_approval = 1 }; + WolfCertServer* s2 = NULL; + REQUIRE(wolfcert_server_start(&cfg_pending, &s2) == WOLFCERT_OK); + pthread_t t2; + REQUIRE(pthread_create(&t2, NULL, server_thread, s2) == 0); + rc = async_poll_path(s2); + wolfcert_server_stop(s2); + pthread_join(t2, NULL); + wolfcert_server_free(s2); + if (rc != 0) + return rc; + + /* Scenario C: auto-approve server, async enroll -> async RenewalReq. */ + WolfCertServerCfgSrv cfg_renew = { .protocol = WOLFCERT_PROTO_SCEP, + .bind_host = "127.0.0.1", .bind_port = 0 }; + WolfCertServer* s3 = NULL; + REQUIRE(wolfcert_server_start(&cfg_renew, &s3) == WOLFCERT_OK); + pthread_t t3; + REQUIRE(pthread_create(&t3, NULL, server_thread, s3) == 0); + rc = async_renewal_path(s3); + wolfcert_server_stop(s3); + pthread_join(t3, NULL); + wolfcert_server_free(s3); + if (rc != 0) + return rc; + + /* Scenario D: auto-approve server, session misuse guards. */ + WolfCertServerCfgSrv cfg_guard = { .protocol = WOLFCERT_PROTO_SCEP, + .bind_host = "127.0.0.1", .bind_port = 0 }; + WolfCertServer* s4 = NULL; + REQUIRE(wolfcert_server_start(&cfg_guard, &s4) == WOLFCERT_OK); + pthread_t t4; + REQUIRE(pthread_create(&t4, NULL, server_thread, s4) == 0); + rc = async_guard_path(s4); + wolfcert_server_stop(s4); + pthread_join(t4, NULL); + wolfcert_server_free(s4); + if (rc != 0) + return rc; + + /* Scenario E: approval-required server, blocking PENDING -> GetCertInitial. */ + WolfCertServerCfgSrv cfg_bpoll = { .protocol = WOLFCERT_PROTO_SCEP, + .bind_host = "127.0.0.1", .bind_port = 0, + .scep_require_approval = 1 }; + WolfCertServer* s5 = NULL; + REQUIRE(wolfcert_server_start(&cfg_bpoll, &s5) == WOLFCERT_OK); + pthread_t t5; + REQUIRE(pthread_create(&t5, NULL, server_thread, s5) == 0); + rc = blocking_poll_path(s5); + wolfcert_server_stop(s5); + pthread_join(t5, NULL); + wolfcert_server_free(s5); + if (rc != 0) + return rc; + + /* Scenario F: auto-approve server, session over the base64 GET transport. */ + WolfCertServerCfgSrv cfg_get = { .protocol = WOLFCERT_PROTO_SCEP, + .bind_host = "127.0.0.1", .bind_port = 0 }; + WolfCertServer* s6 = NULL; + REQUIRE(wolfcert_server_start(&cfg_get, &s6) == WOLFCERT_OK); + pthread_t t6; + REQUIRE(pthread_create(&t6, NULL, server_thread, s6) == 0); + rc = session_get_transport_path(s6); + wolfcert_server_stop(s6); + pthread_join(t6, NULL); + wolfcert_server_free(s6); + if (rc != 0) + return rc; + + /* Scenario G: TLS transport guard (no server needed). */ + if (tls_guard_path()) + return 1; + +#ifdef WOLFCERT_HAVE_ED25519 + /* Scenario H: auto-approve server, non-RSA signer rejection. */ + WolfCertServerCfgSrv cfg_nonrsa = { .protocol = WOLFCERT_PROTO_SCEP, + .bind_host = "127.0.0.1", .bind_port = 0 }; + WolfCertServer* s7 = NULL; + REQUIRE(wolfcert_server_start(&cfg_nonrsa, &s7) == WOLFCERT_OK); + pthread_t t7; + REQUIRE(pthread_create(&t7, NULL, server_thread, s7) == 0); + rc = non_rsa_reject_path(s7); + wolfcert_server_stop(s7); + pthread_join(t7, NULL); + wolfcert_server_free(s7); + if (rc != 0) + return rc; +#endif + + /* Scenario I: approval-required server, renewal PENDING -> poll with a + * caller-supplied signer_cert. */ + WolfCertServerCfgSrv cfg_rpoll = { .protocol = WOLFCERT_PROTO_SCEP, + .bind_host = "127.0.0.1", .bind_port = 0, + .scep_require_approval = 1 }; + WolfCertServer* s8 = NULL; + REQUIRE(wolfcert_server_start(&cfg_rpoll, &s8) == WOLFCERT_OK); + pthread_t t8; + REQUIRE(pthread_create(&t8, NULL, server_thread, s8) == 0); + rc = blocking_renewal_poll_path(s8); + wolfcert_server_stop(s8); + pthread_join(t8, NULL); + wolfcert_server_free(s8); + if (rc != 0) + return rc; + + /* Scenario J: auto-approve server for bootstrap; enrolling request hits a + * 500 stub -> WOLFCERT_ERR_HTTP. */ + WolfCertServerCfgSrv cfg_500 = { .protocol = WOLFCERT_PROTO_SCEP, + .bind_host = "127.0.0.1", .bind_port = 0 }; + WolfCertServer* s9 = NULL; + REQUIRE(wolfcert_server_start(&cfg_500, &s9) == WOLFCERT_OK); + pthread_t t9; + REQUIRE(pthread_create(&t9, NULL, server_thread, s9) == 0); + rc = non_200_path(s9); + wolfcert_server_stop(s9); + pthread_join(t9, NULL); + wolfcert_server_free(s9); + if (rc != 0) + return rc; + + /* Scenario K: auto-approve server, argument-validation guards. */ + WolfCertServerCfgSrv cfg_neg = { .protocol = WOLFCERT_PROTO_SCEP, + .bind_host = "127.0.0.1", .bind_port = 0 }; + WolfCertServer* s10 = NULL; + REQUIRE(wolfcert_server_start(&cfg_neg, &s10) == WOLFCERT_OK); + pthread_t t10; + REQUIRE(pthread_create(&t10, NULL, server_thread, s10) == 0); + rc = negative_args_path(s10); + wolfcert_server_stop(s10); + pthread_join(t10, NULL); + wolfcert_server_free(s10); + if (rc != 0) + return rc; + + wolfcert_cleanup(); + printf("OK\n"); + return 0; +} diff --git a/tests/unit/test_http.c b/tests/unit/test_http.c index 5d559c5..80d51c0 100644 --- a/tests/unit/test_http.c +++ b/tests/unit/test_http.c @@ -79,6 +79,42 @@ static int test_url_parser(void) return 0; } +/* wolfcert_http_url_origin: default-port omission, non-default port, and the + * BAD_ARG guard - the helper is shared by the EST and SCEP session opens. */ +static int test_url_origin(void) +{ + WolfCertUrl u; + char* origin = NULL; + + /* Default ports (https:443, http:80) are omitted. */ + REQUIRE(wolfcert_http_url_parse("https://ca.example.com/scep", &u, NULL) == WOLFCERT_OK); + REQUIRE(wolfcert_http_url_origin(&u, NULL, &origin) == WOLFCERT_OK); + REQUIRE(strcmp(origin, "https://ca.example.com") == 0); + WOLFCERT_XFREE(origin, NULL); origin = NULL; + wolfcert_http_url_free(&u); + + REQUIRE(wolfcert_http_url_parse("http://host.example/x", &u, NULL) == WOLFCERT_OK); + REQUIRE(wolfcert_http_url_origin(&u, NULL, &origin) == WOLFCERT_OK); + REQUIRE(strcmp(origin, "http://host.example") == 0); + WOLFCERT_XFREE(origin, NULL); origin = NULL; + wolfcert_http_url_free(&u); + + /* A non-default port is included. */ + REQUIRE(wolfcert_http_url_parse("http://host.example:8080/x", &u, NULL) == WOLFCERT_OK); + REQUIRE(wolfcert_http_url_origin(&u, NULL, &origin) == WOLFCERT_OK); + REQUIRE(strcmp(origin, "http://host.example:8080") == 0); + WOLFCERT_XFREE(origin, NULL); origin = NULL; + wolfcert_http_url_free(&u); + + /* NULL url and NULL out are rejected. */ + REQUIRE(wolfcert_http_url_origin(NULL, NULL, &origin) == WOLFCERT_ERR_BAD_ARG); + REQUIRE(wolfcert_http_url_parse("https://h/x", &u, NULL) == WOLFCERT_OK); + REQUIRE(wolfcert_http_url_origin(&u, NULL, NULL) == WOLFCERT_ERR_BAD_ARG); + wolfcert_http_url_free(&u); + + return 0; +} + struct srv_ctx { int port; }; static void* srv_thread(void* arg) @@ -480,6 +516,8 @@ int main(void) REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); if (test_url_parser()) return 1; + if (test_url_origin()) + return 1; if (test_loopback_http()) return 1; if (test_session_retry_after_reset()) diff --git a/wolfcert/scep.h b/wolfcert/scep.h index b7e3bb1..cc9c6b4 100644 --- a/wolfcert/scep.h +++ b/wolfcert/scep.h @@ -220,6 +220,106 @@ WOLFCERT_API int wolfcert_scep_get_next_ca_cert(const WolfCertServerCfg* srv, size_t current_ca_len, WolfCertBuffer* out_next_ca_pem); +/* ---- keep-alive / async SCEP session ----------------------------------- + * + * A single TCP (optionally TLS) connection that carries several SCEP + * PKIOperation round trips, mirroring the EST session API. SCEP authenticates + * at the pkiMessage layer, so - unlike EST - the session does NOT require TLS: + * a plaintext http:// endpoint is accepted. + * + * Fetch the CA capabilities and RA/CA certificate first with the one-shot + * wolfcert_scep_get_ca_caps / wolfcert_scep_get_ca_cert, then drive the + * enrolling round trips over the session. Open with wolfcert_scep_session_open + * for a blocking connection, or wolfcert_scep_session_open_async for a + * non-blocking one whose *_nb calls return WOLFCERT_ERR_WANT_READ / + * WOLFCERT_ERR_WANT_WRITE (poll wolfcert_scep_session_fd(), then call again + * with the same arguments - and in particular the same WolfCertScepResult* out + * pointer, which the session captures on the first call; a later poll that + * passes a different out is rejected with WOLFCERT_ERR_BAD_ARG). DNS + the + * initial TCP connect inside session_open stay synchronous even in async mode. + * + * The blocking *_ex calls run to completion in one call and must be paired with + * wolfcert_scep_session_open; the *_nb calls require wolfcert_scep_session_open_async. + * A mismatched pairing is rejected with WOLFCERT_ERR_BAD_ARG. + * + * Transport auth: a plaintext http:// session is accepted, but an https:// + * session requires srv->verify_server (an unverified TLS handshake is refused). + * The session authenticates the enrollment at the pkiMessage layer (the CMS + * signature bound to the CA/RA bundle, plus the PKCS#9 challengePassword) and + * via optional mTLS; it does NOT apply HTTP Basic auth, so srv->username / + * srv->password are ignored by the session API (they are an EST-oriented + * transport credential). */ +typedef struct WolfCertScepSession WolfCertScepSession; + +/* NOTE (transport, differs from the EST session): a plaintext http:// URL is + * accepted with no error - SCEP does not require TLS. An https:// URL still + * requires srv->verify_server (an unverified TLS handshake is refused). The EST + * session, by contrast, rejects plaintext outright. A zero-initialized + * WolfCertServerCfg pointed at an http:// endpoint therefore opens fine here. */ +WOLFCERT_API int wolfcert_scep_session_open(const WolfCertServerCfg* srv, + WolfCertScepSession** out); +WOLFCERT_API int wolfcert_scep_session_open_async(const WolfCertServerCfg* srv, + WolfCertScepSession** out); +WOLFCERT_API void wolfcert_scep_session_close(WolfCertScepSession* s); + +/* Socket fd of the backing HTTP session - hand to poll/epoll/kqueue. */ +WOLFCERT_API int wolfcert_scep_session_fd(const WolfCertScepSession* s); + +/* PKCSReq over the session. See wolfcert_scep_pkcs_req_ex for the argument + * contract; the result shape (status / cert_pem / transaction_id / fail_info) + * is identical. */ +WOLFCERT_API int wolfcert_scep_session_pkcs_req_ex(WolfCertScepSession* s, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const WolfCertKey* new_key, const uint8_t* csr_der, size_t csr_der_len, + WolfCertScepResult* out); +WOLFCERT_API int wolfcert_scep_session_pkcs_req_nb(WolfCertScepSession* s, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const WolfCertKey* new_key, const uint8_t* csr_der, size_t csr_der_len, + WolfCertScepResult* out); + +/* RenewalReq over the session. The renewed public key is carried in `csr_der` + * and `current_key` signs the pkiMessage (see wolfcert_scep_renewal_req_ex). */ +WOLFCERT_API int wolfcert_scep_session_renewal_req_ex(WolfCertScepSession* s, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const uint8_t* current_cert, size_t current_cert_len, + const WolfCertKey* current_key, const uint8_t* csr_der, size_t csr_der_len, + WolfCertScepResult* out); +WOLFCERT_API int wolfcert_scep_session_renewal_req_nb(WolfCertScepSession* s, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const uint8_t* current_cert, size_t current_cert_len, + const WolfCertKey* current_key, const uint8_t* csr_der, size_t csr_der_len, + WolfCertScepResult* out); + +/* GetCertInitial (poll a PENDING enrollment) over the session. See + * wolfcert_scep_get_cert_initial for the argument contract; pass the + * transactionID the prior request returned. */ +WOLFCERT_API int wolfcert_scep_session_get_cert_initial_ex(WolfCertScepSession* s, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const uint8_t* signer_cert, size_t signer_cert_len, + const WolfCertKey* signer_key, + const uint8_t* csr_der, size_t csr_der_len, + const uint8_t* transaction_id, size_t transaction_id_len, + WolfCertScepResult* out); +WOLFCERT_API int wolfcert_scep_session_get_cert_initial_nb(WolfCertScepSession* s, + const WolfCertScepCaps* caps, + const uint8_t* ra_cert, size_t ra_cert_len, + const uint8_t* ca_bundle, size_t ca_bundle_len, + const uint8_t* signer_cert, size_t signer_cert_len, + const WolfCertKey* signer_key, + const uint8_t* csr_der, size_t csr_der_len, + const uint8_t* transaction_id, size_t transaction_id_len, + WolfCertScepResult* out); + #ifdef __cplusplus } #endif