From 175279b9814fc9be5243a353a59338e375280e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 13 Jul 2026 11:24:31 +0000 Subject: [PATCH 1/4] scep: surface CertRep failInfo and drop unused renewal new_key param The renewal entry points took a new_key argument that was ignored ((void)new_key): the renewed public key is already carried in the CSR and the pkiMessage is signed with current_key. Remove the dead parameter from wolfcert_scep_renewal_req and _renewal_req_ex. Also parse the RFC 8894 section 3.2.1.4 failInfo attribute out of a FAILURE CertRep and populate WolfCertScepResult.fail_info, which was previously always left at -1. wolfcert_scep_parse_pki_message gains an out_fail_info output; all non-consuming callers pass NULL. The SCEP poll roundtrip test now asserts the client surfaces failInfo=4 (badCertId) for an unknown transaction. --- src/internal.h | 2 +- src/scep/scep_client.c | 17 ++++++++++++----- src/scep/scep_msg.c | 16 ++++++++++++++-- src/scep/scep_server.c | 2 +- tests/integration/test_scep_poll_roundtrip.c | 3 +++ tests/integration/test_scep_roundtrip.c | 2 +- tests/unit/test_scep_msg.c | 6 +++--- wolfcert/scep.h | 8 +++++--- 8 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/internal.h b/src/internal.h index 7231459..4c7372f 100644 --- a/src/internal.h +++ b/src/internal.h @@ -322,7 +322,7 @@ WOLFCERT_TEST_VIS int wolfcert_scep_parse_pki_message(const uint8_t* pki_der, size_t* out_tid_len, uint8_t** out_sender_nonce, size_t* out_snonce_len, uint8_t** out_recipient_nonce,size_t* out_rnonce_len, char** out_message_type, char** out_pki_status, uint8_t** out_signer_cert, size_t* out_signer_cert_len, - void* heap); + char** out_fail_info, void* heap); /* Build the GetNextCACert response (RFC 8894 section 4.6.1): wrap the next CA * certificate in a degenerate certs-only SignedData and sign that with the diff --git a/src/scep/scep_client.c b/src/scep/scep_client.c index 904f667..a3d49c8 100644 --- a/src/scep/scep_client.c +++ b/src/scep/scep_client.c @@ -367,6 +367,7 @@ static int do_scep_round_trip(const WolfCertServerCfg* srv, WolfCertBuffer resp_env = { 0 }; WolfCertBuffer inner = { 0 }; char* status = NULL; + char* fail_info = NULL; char* resp_mt = NULL; uint8_t* rx_tid = NULL; size_t rx_tid_len = 0; @@ -378,7 +379,7 @@ static int do_scep_round_trip(const WolfCertServerCfg* srv, size_t rx_signer_len = 0; 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, heap); + &resp_mt, &status, &rx_signer, &rx_signer_len, &fail_info, heap); WOLFCERT_XFREE(resp, heap); WOLFCERT_XFREE(rx_sn, heap); @@ -435,6 +436,14 @@ static int do_scep_round_trip(const WolfCertServerCfg* srv, } else if (status == NULL || strcmp(status, "0") != 0) { out->status = WOLFCERT_SCEP_STATUS_FAILURE; + /* RFC 8894 section 3.2.1.4: a FAILURE CertRep carries a failInfo + * PrintableString of "0".."4". Surface it to the caller; leave the + * default -1 when the server omitted the attribute. */ + if (fail_info != NULL && + fail_info[0] >= '0' && fail_info[0] <= '4' && + fail_info[1] == '\0') { + out->fail_info = fail_info[0] - '0'; + } } else { /* status "0" is SUCCESS: de-envelop the CertRep and convert the @@ -453,6 +462,7 @@ static int do_scep_round_trip(const WolfCertServerCfg* srv, WOLFCERT_XFREE(rx_tid, heap); WOLFCERT_XFREE(status, heap); + WOLFCERT_XFREE(fail_info, heap); wolfcert_buffer_free(&inner); wolfcert_buffer_free(&resp_env); return rc; @@ -572,11 +582,9 @@ int wolfcert_scep_renewal_req_ex(const WolfCertServerCfg* srv, 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 WolfCertKey* new_key, const uint8_t* csr_der, size_t csr_der_len, WolfCertScepResult* out) { - (void)new_key; if (srv == NULL || ra_cert == NULL || ca_bundle == NULL || current_cert == NULL || current_key == NULL || csr_der == NULL || out == NULL) @@ -613,7 +621,6 @@ int wolfcert_scep_renewal_req(const WolfCertServerCfg* srv, const uint8_t* ra_cert, size_t ra_cert_len, const uint8_t* current_cert, size_t current_cert_len, const WolfCertKey* current_key, - const WolfCertKey* new_key, const uint8_t* csr_der, size_t csr_der_len, WolfCertBuffer* out_cert_pem) { @@ -626,7 +633,7 @@ int wolfcert_scep_renewal_req(const WolfCertServerCfg* srv, int rc = wolfcert_scep_renewal_req_ex(srv, caps, ra_cert, ra_cert_len, ra_cert, ra_cert_len, current_cert, current_cert_len, - current_key, new_key, + current_key, csr_der, csr_der_len, &r); if (rc != WOLFCERT_OK) { diff --git a/src/scep/scep_msg.c b/src/scep/scep_msg.c index 7564586..b6964d1 100644 --- a/src/scep/scep_msg.c +++ b/src/scep/scep_msg.c @@ -558,7 +558,7 @@ WOLFCERT_TEST_VIS int wolfcert_scep_verify_next_ca_response(const uint8_t* resp_ rc = wolfcert_scep_parse_pki_message(resp_der, resp_len, &content, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - &signer, &signer_len, heap); + &signer, &signer_len, NULL, heap); /* Bind the rollover message to the trusted current CA: its SignedData * signer must share that CA's public key. */ @@ -581,7 +581,7 @@ WOLFCERT_TEST_VIS int wolfcert_scep_parse_pki_message(const uint8_t* pki_der, size_t* out_tid_len, uint8_t** out_sender_nonce, size_t* out_snonce_len, uint8_t** out_recipient_nonce,size_t* out_rnonce_len, char** out_message_type, char** out_pki_status, uint8_t** out_signer_cert, size_t* out_signer_cert_len, - void* heap) + char** out_fail_info, void* heap) { PKCS7* p7 = wc_PKCS7_New(heap, WOLFCERT_DEVID_SOFTWARE); if (p7 == NULL) @@ -656,6 +656,9 @@ WOLFCERT_TEST_VIS int wolfcert_scep_parse_pki_message(const uint8_t* pki_der, if (out_pki_status) *out_pki_status = NULL; + if (out_fail_info) + *out_fail_info = NULL; + for (PKCS7DecodedAttrib* a = p7->decodedAttrib; a != NULL; a = a->next) { /* PKCS7DecodedAttrib.value can arrive in either of two shapes * depending on the wolfSSL version / producer: @@ -725,6 +728,15 @@ WOLFCERT_TEST_VIS int wolfcert_scep_parse_pki_message(const uint8_t* pki_der, *out_pki_status = s; } } + else if (a->oidSz == sizeof(OID_FAIL_INFO) && + memcmp(a->oid, OID_FAIL_INFO, a->oidSz) == 0 && out_fail_info) { + char* s = (char*)WOLFCERT_XMALLOC(vlen + 1, heap); + if (s) { + memcpy(s, v + off, vlen); + s[vlen] = '\0'; + *out_fail_info = s; + } + } else if (a->oidSz == sizeof(OID_TRANS_ID) && memcmp(a->oid, OID_TRANS_ID, a->oidSz) == 0 && out_transaction_id) { uint8_t* b = (uint8_t*)WOLFCERT_XMALLOC(vlen, heap); diff --git a/src/scep/scep_server.c b/src/scep/scep_server.c index 21f6d28..e62a7ac 100644 --- a/src/scep/scep_server.c +++ b/src/scep/scep_server.c @@ -734,7 +734,7 @@ static int handle_pki_op(WolfCertServer* s, int fd, const ScepRequest* req) int rc = wolfcert_scep_parse_pki_message(req->body, req->body_len, &env, &tid, &tid_len, &snonce, &snonce_len, &rnonce, &rnonce_len, - &mt, &ps, &signer_cert, &signer_cert_len, s->heap); + &mt, &ps, &signer_cert, &signer_cert_len, NULL, s->heap); if (rc != WOLFCERT_OK) { send_text(s, fd, 400, "Bad Request", "text/plain", ""); goto out; diff --git a/tests/integration/test_scep_poll_roundtrip.c b/tests/integration/test_scep_poll_roundtrip.c index aeb6fef..9645264 100644 --- a/tests/integration/test_scep_poll_roundtrip.c +++ b/tests/integration/test_scep_poll_roundtrip.c @@ -147,6 +147,9 @@ static int poll_path(WolfCertServer* s) &r3); REQUIRE(rc == WOLFCERT_OK); REQUIRE(r3.status == WOLFCERT_SCEP_STATUS_FAILURE); + /* The server reports failInfo "4" (badCertId) for an unknown transaction; + * the client must surface it in the result. */ + REQUIRE(r3.fail_info == 4); wolfcert_scep_result_free(&r1); wolfcert_scep_result_free(&r2); diff --git a/tests/integration/test_scep_roundtrip.c b/tests/integration/test_scep_roundtrip.c index ed5041e..9020fc4 100644 --- a/tests/integration/test_scep_roundtrip.c +++ b/tests/integration/test_scep_roundtrip.c @@ -242,7 +242,7 @@ int main(void) char *w_mt = NULL, *w_st = NULL; REQUIRE(wolfcert_scep_parse_pki_message(wmsg.data, wmsg.len, &wenv, &w_tid, &w_tidl, &w_sn, &w_snl, &w_rn, &w_rnl, - &w_mt, &w_st, &w_sc, &w_scl, NULL) == WOLFCERT_OK); + &w_mt, &w_st, &w_sc, &w_scl, NULL, NULL) == WOLFCERT_OK); REQUIRE(w_rn != NULL); /* present */ REQUIRE(w_rnl == sizeof(rnonce)); REQUIRE(memcmp(w_rn, rnonce, sizeof(rnonce)) == 0); /* unchanged */ diff --git a/tests/unit/test_scep_msg.c b/tests/unit/test_scep_msg.c index 88f3a8c..06678bd 100644 --- a/tests/unit/test_scep_msg.c +++ b/tests/unit/test_scep_msg.c @@ -186,7 +186,7 @@ static int check_no_envelope(const uint8_t* ca_der, size_t ca_len, /* The message must still verify and parse, returning an empty envelope. */ prc = wolfcert_scep_parse_pki_message(pki.data, pki.len, &env, &rx_tid, &rx_tid_len, &rx_sn, &rx_sn_len, &rx_rn, &rx_rn_len, - &mt, &status, NULL, NULL, NULL); + &mt, &status, NULL, NULL, NULL, NULL); REQUIRE(prc == WOLFCERT_OK); REQUIRE(env.len == 0 && env.data == NULL); REQUIRE(status != NULL && strcmp(status, "2") == 0); @@ -426,7 +426,7 @@ static int test_next_ca_response_is_signed(void) /* A signed SignedData verifies here; an unsigned degenerate bundle does * not, because the parser rejects degenerate SignedData. */ rc = wolfcert_scep_parse_pki_message(resp.data, resp.len, &content, - NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); REQUIRE(rc == WOLFCERT_OK); REQUIRE(content.data != NULL && content.len > 0); @@ -602,7 +602,7 @@ static int test_signer_is_verified_cert(void) REQUIRE(wolfcert_scep_parse_pki_message(msg, msg_len, &env, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - &signer, &signer_len, NULL) == WOLFCERT_OK); + &signer, &signer_len, NULL, NULL) == WOLFCERT_OK); /* The parser must surface the cert that signed, not cert[0]. */ REQUIRE(signer != NULL); diff --git a/wolfcert/scep.h b/wolfcert/scep.h index 56c9bdb..acbb1c4 100644 --- a/wolfcert/scep.h +++ b/wolfcert/scep.h @@ -123,14 +123,17 @@ WOLFCERT_API int wolfcert_scep_pkcs_req(const WolfCertServerCfg* srv, /* RenewalReq: re-enroll using an existing cert/key to sign the pkiMessage. * `ra_cert` is the envelope target; `ca_bundle` is the trusted GetCACert bundle - * the response signer is checked against (see wolfcert_scep_pkcs_req_ex). */ + * the response signer is checked against (see wolfcert_scep_pkcs_req_ex). + * + * The renewed key pair is conveyed entirely inside `csr_der`: the CSR carries + * the new public key and `current_key` signs the enclosing pkiMessage. There + * is deliberately no separate new-key argument. */ WOLFCERT_API int wolfcert_scep_renewal_req_ex(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* current_cert, size_t current_cert_len, const WolfCertKey* current_key, - const WolfCertKey* new_key, const uint8_t* csr_der, size_t csr_der_len, WolfCertScepResult* out); @@ -139,7 +142,6 @@ WOLFCERT_API int wolfcert_scep_renewal_req(const WolfCertServerCfg* srv, const uint8_t* ra_cert, size_t ra_cert_len, const uint8_t* current_cert, size_t current_cert_len, const WolfCertKey* current_key, - const WolfCertKey* new_key, const uint8_t* csr_der, size_t csr_der_len, WolfCertBuffer* out_cert_pem); From 73cb7de9561e565049b1359db05d14483f7ac8a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 13 Jul 2026 11:29:39 +0000 Subject: [PATCH 2/4] scep: add wolfcert_scep_verify_ca_fingerprint trust-bootstrap helper Add a public helper to check a GetCACert response against a fingerprint obtained out of band before it is trusted as a CA anchor. It hashes the whole DER certificate with SHA-256 (or SHA-1 / SHA-512) and compares in constant time; WOLFCERT_SCEP_FP_AUTO selects the digest from the expected length. Returns WOLFCERT_ERR_AUTH on mismatch, WOLFCERT_ERR_UNSUPPORTED when the digest is not compiled into wolfSSL. MD5 is intentionally not offered. Unit-tested in test_scep_msg for match, single-bit tamper, AUTO dispatch, and length/argument misuse. --- docs/ARCHITECTURE.md | 8 ++++ src/scep/scep_client.c | 79 +++++++++++++++++++++++++++++++++++ tests/unit/test_scep_msg.c | 84 ++++++++++++++++++++++++++++++++++++++ wolfcert/scep.h | 29 +++++++++++++ 4 files changed, 200 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a1a57c2..018d41a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -190,6 +190,14 @@ or `FAILURE`. `WOLFCERT_ERR_UNSUPPORTED` — this is a protocol constraint, not a wolfCert limitation. Use EST for Ed25519 / Ed448 / ML-DSA. +**Trust bootstrap.** A `GetCACert` response is only trustworthy once its +fingerprint has been checked against a value obtained out of band. +`wolfcert_scep_verify_ca_fingerprint` hashes the DER CA certificate (SHA-256, +or SHA-1 / SHA-512, with `WOLFCERT_SCEP_FP_AUTO` selecting the algorithm from +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. + **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 diff --git a/src/scep/scep_client.c b/src/scep/scep_client.c index a3d49c8..b98e3cc 100644 --- a/src/scep/scep_client.c +++ b/src/scep/scep_client.c @@ -32,6 +32,13 @@ #include #include #include +#ifndef NO_SHA +#include +#endif +#include +#ifdef WOLFSSL_SHA512 +#include +#endif #include #include @@ -224,6 +231,78 @@ int wolfcert_scep_get_ca_cert_enc(const WolfCertServerCfg* srv, WolfCertEncoding return rc; } +/* Constant-time buffer comparison: returns 0 iff the two buffers are equal. + * Fingerprints are not secret, but a timing-independent compare keeps the + * trust check uniform and avoids leaking match position. */ +static int ct_diff(const uint8_t* a, const uint8_t* b, size_t n) +{ + uint8_t d = 0; + for (size_t i = 0; i < n; ++i) + d |= (uint8_t)(a[i] ^ b[i]); + return d; +} + +int wolfcert_scep_verify_ca_fingerprint(const uint8_t* ca_der, size_t ca_der_len, + const uint8_t* expected, size_t expected_len, + WolfCertScepFpAlg alg) +{ + /* SHA-512 (64 bytes) is the widest digest we produce. */ + uint8_t digest[64]; + size_t digest_len; + int rc; + + if (ca_der == NULL || ca_der_len == 0 || expected == NULL || expected_len == 0) + return WOLFCERT_ERR_BAD_ARG; + + /* AUTO: identify the algorithm from the supplied fingerprint length. */ + if (alg == WOLFCERT_SCEP_FP_AUTO) { + switch (expected_len) { + case 20: alg = WOLFCERT_SCEP_FP_SHA1; break; /* SHA-1 */ + case 32: alg = WOLFCERT_SCEP_FP_SHA256; break; /* SHA-256 */ + case 64: alg = WOLFCERT_SCEP_FP_SHA512; break; /* SHA-512 */ + default: + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "scep", + "fingerprint length does not match SHA-1/SHA-256/SHA-512"); + } + } + + switch (alg) { + case WOLFCERT_SCEP_FP_SHA256: + digest_len = WC_SHA256_DIGEST_SIZE; + rc = wc_Sha256Hash(ca_der, (word32)ca_der_len, digest); + break; +#ifndef NO_SHA + case WOLFCERT_SCEP_FP_SHA1: + digest_len = WC_SHA_DIGEST_SIZE; + rc = wc_ShaHash(ca_der, (word32)ca_der_len, digest); + break; +#endif +#ifdef WOLFSSL_SHA512 + case WOLFCERT_SCEP_FP_SHA512: + digest_len = WC_SHA512_DIGEST_SIZE; + rc = wc_Sha512Hash(ca_der, (word32)ca_der_len, digest); + break; +#endif + default: + return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "scep", + "requested fingerprint digest is not compiled into wolfSSL"); + } + + if (rc != 0) + return WOLFCERT_ERR_WC(rc, "scep", "fingerprint hash"); + + /* An explicit algorithm with a mismatched length is a caller error. */ + if (expected_len != digest_len) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "scep", + "expected fingerprint length does not match the digest size"); + + if (ct_diff(expected, digest, digest_len) != 0) + return WOLFCERT_ERR(WOLFCERT_ERR_AUTH, "scep", + "CA certificate fingerprint mismatch"); + + return WOLFCERT_OK; +} + /* ---- PKCSReq / RenewalReq ---------------------------------------------- */ static int pick_hash_oid(const WolfCertScepCaps* caps) diff --git a/tests/unit/test_scep_msg.c b/tests/unit/test_scep_msg.c index 06678bd..bf865aa 100644 --- a/tests/unit/test_scep_msg.c +++ b/tests/unit/test_scep_msg.c @@ -43,6 +43,13 @@ #include #include #include +#ifndef NO_SHA +#include +#endif +#include +#ifdef WOLFSSL_SHA512 +#include +#endif #include #include @@ -718,10 +725,87 @@ static int test_signer_subject_fallback(void) return 0; } +/* All fingerprint assertions run against a caller-owned CA DER so the buffer is + * freed exactly once by test_ca_fingerprint no matter which REQUIRE fires - the + * same ownership split as check_no_envelope / test_non_success_has_no_envelope. */ +static int check_ca_fingerprint(const uint8_t* ca_der, size_t ca_len) +{ + uint8_t sha256[WC_SHA256_DIGEST_SIZE]; + uint8_t tampered[WC_SHA256_DIGEST_SIZE]; +#ifndef NO_SHA + uint8_t sha1[WC_SHA_DIGEST_SIZE]; +#endif +#ifdef WOLFSSL_SHA512 + uint8_t sha512[WC_SHA512_DIGEST_SIZE]; +#endif + + REQUIRE(wc_Sha256Hash(ca_der, (word32)ca_len, sha256) == 0); + + /* Explicit SHA-256 and AUTO (by length) both accept the real digest. */ + REQUIRE(wolfcert_scep_verify_ca_fingerprint(ca_der, ca_len, sha256, + sizeof(sha256), WOLFCERT_SCEP_FP_SHA256) == WOLFCERT_OK); + REQUIRE(wolfcert_scep_verify_ca_fingerprint(ca_der, ca_len, sha256, + sizeof(sha256), WOLFCERT_SCEP_FP_AUTO) == WOLFCERT_OK); + + /* A single flipped bit must be rejected as a mismatch. */ + memcpy(tampered, sha256, sizeof(tampered)); + tampered[0] ^= 0x01; + REQUIRE(wolfcert_scep_verify_ca_fingerprint(ca_der, ca_len, tampered, + sizeof(tampered), WOLFCERT_SCEP_FP_SHA256) == WOLFCERT_ERR_AUTH); + + /* Explicit algorithm with a length that doesn't match the digest -> BAD_ARG. */ + REQUIRE(wolfcert_scep_verify_ca_fingerprint(ca_der, ca_len, sha256, 20, + WOLFCERT_SCEP_FP_SHA256) == WOLFCERT_ERR_BAD_ARG); + /* AUTO with an unrecognized length -> BAD_ARG. */ + REQUIRE(wolfcert_scep_verify_ca_fingerprint(ca_der, ca_len, sha256, 33, + WOLFCERT_SCEP_FP_AUTO) == WOLFCERT_ERR_BAD_ARG); + /* NULL / zero inputs -> BAD_ARG. */ + REQUIRE(wolfcert_scep_verify_ca_fingerprint(NULL, 0, sha256, sizeof(sha256), + WOLFCERT_SCEP_FP_SHA256) == WOLFCERT_ERR_BAD_ARG); + +#ifndef NO_SHA + REQUIRE(wc_ShaHash(ca_der, (word32)ca_len, sha1) == 0); + REQUIRE(wolfcert_scep_verify_ca_fingerprint(ca_der, ca_len, sha1, + sizeof(sha1), WOLFCERT_SCEP_FP_SHA1) == WOLFCERT_OK); + REQUIRE(wolfcert_scep_verify_ca_fingerprint(ca_der, ca_len, sha1, + sizeof(sha1), WOLFCERT_SCEP_FP_AUTO) == WOLFCERT_OK); +#endif + +#ifdef WOLFSSL_SHA512 + REQUIRE(wc_Sha512Hash(ca_der, (word32)ca_len, sha512) == 0); + REQUIRE(wolfcert_scep_verify_ca_fingerprint(ca_der, ca_len, sha512, + sizeof(sha512), WOLFCERT_SCEP_FP_SHA512) == WOLFCERT_OK); + REQUIRE(wolfcert_scep_verify_ca_fingerprint(ca_der, ca_len, sha512, + sizeof(sha512), WOLFCERT_SCEP_FP_AUTO) == WOLFCERT_OK); +#endif + + return 0; +} + +/* wolfcert_scep_verify_ca_fingerprint: correct digest verifies, a tampered one + * is rejected, AUTO dispatches on length, and length/argument misuse is caught. */ +static int test_ca_fingerprint(void) +{ + uint8_t* ca_der = NULL; + uint8_t* key_der = NULL; + size_t ca_len = 0, key_len = 0; + int rc; + + REQUIRE(make_ca(&ca_der, &ca_len, &key_der, &key_len) == 0); + + rc = check_ca_fingerprint(ca_der, ca_len); + + free(ca_der); + free(key_der); + return rc; +} + int main(void) { REQUIRE(test_static_mem_init() == 0); REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); + if (test_ca_fingerprint()) + return 1; if (test_non_success_has_no_envelope()) return 1; if (test_signer_subject_fallback()) diff --git a/wolfcert/scep.h b/wolfcert/scep.h index acbb1c4..cf4840c 100644 --- a/wolfcert/scep.h +++ b/wolfcert/scep.h @@ -55,6 +55,35 @@ WOLFCERT_API int wolfcert_scep_get_ca_cert_enc(const WolfCertServerCfg* srv, WolfCertEncoding enc, WolfCertBuffer* out_ca); +/* Digest algorithm for wolfcert_scep_verify_ca_fingerprint. AUTO selects the + * algorithm from the expected fingerprint length (20 => SHA-1, 32 => SHA-256, + * 64 => SHA-512). SHA-256 is always available; SHA-1 and SHA-512 depend on the + * wolfSSL build and yield WOLFCERT_ERR_UNSUPPORTED when absent. MD5 is + * intentionally not offered. */ +typedef enum { + WOLFCERT_SCEP_FP_AUTO = 0, + WOLFCERT_SCEP_FP_SHA256 = 1, + WOLFCERT_SCEP_FP_SHA1 = 2, + WOLFCERT_SCEP_FP_SHA512 = 3 +} WolfCertScepFpAlg; + +/* Verify that a CA/RA certificate matches a fingerprint obtained out of band, + * the standard SCEP trust-bootstrap check on a GetCACert response before it is + * used as a trust anchor. The fingerprint is a hash over the whole DER-encoded + * certificate `ca_der` (pass one certificate, e.g. the leaf of a GetCACert + * bundle), compared in constant time against `expected`. + * + * Returns WOLFCERT_OK on match, WOLFCERT_ERR_AUTH on mismatch, + * WOLFCERT_ERR_BAD_ARG on NULL/zero inputs or an `expected_len` that does not + * match the chosen algorithm (or any known length under AUTO), and + * WOLFCERT_ERR_UNSUPPORTED when the requested digest is not compiled into + * wolfSSL. */ +WOLFCERT_API int wolfcert_scep_verify_ca_fingerprint(const uint8_t* ca_der, + size_t ca_der_len, + const uint8_t* expected, + size_t expected_len, + WolfCertScepFpAlg alg); + /* RFC 8894 section 3.2.1.3 pkiStatus values returned by the server in a CertRep * pkiMessage. PENDING means the enrollment was accepted but is waiting * for manual approval; the caller re-queries later via From 52fbf187cb62ff560b75fee630ec5bf71af3ecac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 13 Jul 2026 11:45:14 +0000 Subject: [PATCH 3/4] scep: add base64 HTTP GET PKIOperation fallback (RFC 8894 4.1) When the passed caps show the CA does not advertise POSTPKIOperation, carry the pkiMessage base64-encoded and percent-escaped in a GET `message` query parameter instead of POSTing it. run_pki_op chooses POST (the default, and whenever caps are unknown) or GET from the caps; a GET URL longer than WOLFCERT_SCEP_MAX_GET_URL (8 KiB) is refused with WOLFCERT_ERR_UNSUPPORTED so the caller falls back to a POST-capable CA. The in-tree test server learns to accept GET PKIOperation: it decodes the message parameter and dispatches it exactly like a POST body. Its query field gets a dedicated WOLFCERT_HTTP_QUERY_SZ (8 KiB) so the encoded message fits, and the client HTTP path ceiling is raised to match. Tests: test_scep_roundtrip enrolls once over the GET path end to end; test_scep_msg unit-tests URL construction and the oversize rejection. docs/ARCHITECTURE.md and the docs/EMBEDDED.md tunables table updated. --- docs/ARCHITECTURE.md | 13 +- docs/EMBEDDED.md | 26 ++-- src/http.c | 8 +- src/internal.h | 40 +++++- src/scep/scep_client.c | 117 ++++++++++++++++-- src/scep/scep_server.c | 157 ++++++++++++++++++++---- tests/integration/test_scep_roundtrip.c | 107 ++++++++++++++++ tests/unit/test_scep_msg.c | 40 ++++++ 8 files changed, 450 insertions(+), 58 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 018d41a..8661f3c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -181,10 +181,15 @@ whose payload is an EnvelopedData, built on wolfSSL's `wc_PKCS7` API. Each round trip envelopes the payload to the RA/CA cert's public key, signs it (for `PKCSReq` with a transient self-signed cert whose key matches the one -being enrolled), POSTs it, and parses the response. The server's `pkiStatus` -maps to a `WolfCertScepResult.status` of `SUCCESS` (cert in `cert_pem`), -`PENDING` (poll with `GetCertInitial`, quoting the returned transaction ID), -or `FAILURE`. +being enrolled), sends it, and parses the response. The pkiMessage is POSTed by +default; when the passed `caps` shows the CA does **not** advertise +`POSTPKIOperation`, the client falls back to the RFC 8894 section 4.1 HTTP GET +form, carrying the message base64-encoded and percent-escaped in the `message` +query parameter (refusing, with `WOLFCERT_ERR_UNSUPPORTED`, to build a URL +longer than `WOLFCERT_SCEP_MAX_GET_URL`). The in-tree test server accepts both. +The server's `pkiStatus` maps to a `WolfCertScepResult.status` of `SUCCESS` +(cert in `cert_pem`), `PENDING` (poll with `GetCertInitial`, quoting the +returned transaction ID), or `FAILURE`. **SCEP is RSA-only.** The entry points reject non-RSA keys with `WOLFCERT_ERR_UNSUPPORTED` — this is a protocol constraint, not a wolfCert diff --git a/docs/EMBEDDED.md b/docs/EMBEDDED.md index c735982..4da57b0 100644 --- a/docs/EMBEDDED.md +++ b/docs/EMBEDDED.md @@ -75,7 +75,7 @@ miscompile. |----------|----------------|--------------|--------------| | wolfSSL `Cert` (CSR / cert build) | **heap** (`wc_CertNew`) | ~20+ KB | `altNames[16384]` | | wolfSSL `DecodedCert` (cert parse) | **stack**, transient | several KB | parse scratch | -| HTTP request handling | **stack** | 2-3 KB | request read buffer | +| HTTP request handling | **stack** (EST + client); **heap** (SCEP server) | 2-3 KB stack | request read buffer | The good news: wolfCert never stack-allocates a `Cert`. Every CSR/cert build path (`src/csr.c`, `src/ca_issue.c`, `src/scep/scep_msg.c`) obtains @@ -121,24 +121,32 @@ Trade-offs: ## 2. wolfCert HTTP stack buffers The in-tree test server and HTTP client size a few request-handling -buffers on the stack. They are tunable with `#ifndef`-guarded macros in -`src/internal.h`; override via `-D` or your `user_settings`-style config -header. +buffers with `#ifndef`-guarded macros in `src/internal.h`; override via `-D` +or your `user_settings`-style config header. The EST server and the client +place these on the stack; the SCEP server, whose GET `PKIOperation` read +buffer is by far the largest, allocates it on the **heap** (freed as soon as +the request completes) so it never counts against the stack budget. | Macro | Default | Buffer | |-------|---------|--------| -| `WOLFCERT_HTTP_REQ_BUF_SZ` | `2048` | server request-header read buffer (`est_server.c`, `scep_server.c`) | -| `WOLFCERT_HTTP_PATH_SZ` | `512` | server request `path` / `query` fields; the SCEP `full` reconstruction buffer is `2 ×` this | +| `WOLFCERT_HTTP_REQ_BUF_SZ` | `2048` | server request-header read buffer. The SCEP server adds `WOLFCERT_HTTP_QUERY_SZ` to it (so a base64 GET `PKIOperation` fits) and allocates the result on the heap; the EST server keeps its `2048`-byte buffer on the stack (`est_server.c`, `scep_server.c`) | +| `WOLFCERT_HTTP_PATH_SZ` | `512` | EST server request `path` field (the SCEP server points `path`/`query` into its heap read buffer instead) | +| `WOLFCERT_HTTP_QUERY_SZ` | `8192` | sized to hold a base64 GET `PKIOperation` message; on the SCEP server it extends the heap read buffer (`REQ_BUF_SZ + QUERY_SZ`) that `query` points into | | `WOLFCERT_HTTP_AUTH_BUF_SZ` | `512` | client Basic-auth header line (`http.c`) | +| `WOLFCERT_HTTP_MAX_PATH_LEN` | `8192` | client-side ceiling on a request URL's path+query (`http.c`) | +| `WOLFCERT_SCEP_MAX_GET_URL` | `8192` | client cap on a GET `PKIOperation` URL; a larger message is refused with `WOLFCERT_ERR_UNSUPPORTED` so the caller POSTs (`internal.h`) | Shrinking `WOLFCERT_HTTP_REQ_BUF_SZ` lowers the largest request header -block the server accepts; `WOLFCERT_HTTP_PATH_SZ` lowers the longest -request path/query; `WOLFCERT_HTTP_AUTH_BUF_SZ` lowers the longest -Basic-auth credential the client can send. Example: +block the server accepts; `WOLFCERT_HTTP_PATH_SZ` / `WOLFCERT_HTTP_QUERY_SZ` +lower the longest request path / query; `WOLFCERT_HTTP_AUTH_BUF_SZ` lowers the +longest Basic-auth credential the client can send. A POST-only SCEP deployment +can trim `WOLFCERT_HTTP_QUERY_SZ` (and, on the client, `WOLFCERT_SCEP_MAX_GET_URL` +and `WOLFCERT_HTTP_MAX_PATH_LEN`) back down. Example: ```c #define WOLFCERT_HTTP_REQ_BUF_SZ 768 #define WOLFCERT_HTTP_PATH_SZ 128 +#define WOLFCERT_HTTP_QUERY_SZ 256 #define WOLFCERT_HTTP_AUTH_BUF_SZ 128 ``` diff --git a/src/http.c b/src/http.c index d08821b..5296a46 100644 --- a/src/http.c +++ b/src/http.c @@ -42,7 +42,13 @@ #include #define WOLFCERT_HTTP_MAX_HOST_LEN 256 -#define WOLFCERT_HTTP_MAX_PATH_LEN 2048 +/* Large enough to carry an RFC 8894 GET PKIOperation whose base64 pkiMessage + * rides in the query string (bounded by WOLFCERT_SCEP_MAX_GET_URL). The parsed + * path is heap-allocated and the request head buffer is sized to it, so this is + * only a sanity ceiling. */ +#ifndef WOLFCERT_HTTP_MAX_PATH_LEN +#define WOLFCERT_HTTP_MAX_PATH_LEN 8192 +#endif #define WOLFCERT_HTTP_DEFAULT_MAX_BODY (64 * 1024) #define WOLFCERT_HTTP_READ_CHUNK 2048 diff --git a/src/internal.h b/src/internal.h index 4c7372f..643ab3c 100644 --- a/src/internal.h +++ b/src/internal.h @@ -46,16 +46,26 @@ #include #include -/* ---- tunable stack-buffer sizes ----------------------------------------- * - * These bound the HTTP request-handling stack footprint of the test server - * and client. Override them (compiler -D or user_settings) to shrink stack - * usage on constrained targets; see docs/EMBEDDED.md. Shrinking lowers the - * largest request header block / path / Basic-auth credential accepted. */ +/* ---- tunable HTTP request-buffer sizes ---------------------------------- * + * These bound the HTTP request-handling buffers of the test server and client. + * The EST server and the client keep them on the stack; the SCEP server + * heap-allocates its (larger) read buffer, so shrinking these lowers its heap + * churn rather than its stack. Override them (compiler -D or user_settings) on + * constrained targets; see docs/EMBEDDED.md. Shrinking lowers the largest + * request header block / path / Basic-auth credential accepted. */ #ifndef WOLFCERT_HTTP_REQ_BUF_SZ #define WOLFCERT_HTTP_REQ_BUF_SZ 2048 /* server request header read buffer */ #endif #ifndef WOLFCERT_HTTP_PATH_SZ -#define WOLFCERT_HTTP_PATH_SZ 512 /* server request path / query field */ +#define WOLFCERT_HTTP_PATH_SZ 512 /* EST server request path field */ +#endif +/* The query size is separate from the path: an RFC 8894 GET PKIOperation + * carries the whole base64/percent-encoded pkiMessage in the `message=` query + * parameter, so the buffer must be large enough to hold one. The SCEP server + * adds this to REQ_BUF_SZ for its heap read buffer (query points into it); + * trim it on a POST-only deployment to reclaim that buffer. */ +#ifndef WOLFCERT_HTTP_QUERY_SZ +#define WOLFCERT_HTTP_QUERY_SZ 8192 /* SCEP GET pkiMessage query capacity */ #endif #ifndef WOLFCERT_HTTP_AUTH_BUF_SZ #define WOLFCERT_HTTP_AUTH_BUF_SZ 512 /* client Basic-auth header line */ @@ -86,6 +96,17 @@ #define WOLFCERT_SCEP_MAX_MSG_SZ (64 * 1024) #endif +/* Upper bound on the length of a base64/percent-encoded PKIOperation GET URL. + * RFC 8894 section 4.1 lets a client fall back to HTTP GET when the CA does not + * advertise POSTPKIOperation, carrying the pkiMessage in the `message` query + * parameter. GET has no standard length limit but servers commonly cap request + * lines around 8 KiB, so refuse to build a longer URL and surface a clear + * error instead (a large message needs a POSTPKIOperation-capable CA). Override + * with a compiler -D or user_settings if a deployment allows longer GETs. */ +#ifndef WOLFCERT_SCEP_MAX_GET_URL +#define WOLFCERT_SCEP_MAX_GET_URL (8 * 1024) +#endif + /* ---- key ---------------------------------------------------------------- */ struct WolfCertKey { @@ -305,6 +326,13 @@ int wolfcert_scep_issuer_and_subject(const uint8_t* issuer_cert_der, size_t issu WOLFCERT_TEST_VIS int wolfcert_scep_envelop(const uint8_t* ra_cert_der, size_t ra_cert_len, const uint8_t* payload, size_t payload_len, int enc_oid, WolfCertBuffer* out_der, void* heap); + +/* Build the RFC 8894 section 4.1 GET PKIOperation URL: + * base?operation=PKIOperation&message= + * Returns WOLFCERT_ERR_UNSUPPORTED when the encoded URL would exceed + * WOLFCERT_SCEP_MAX_GET_URL. Exposed for white-box testing. */ +WOLFCERT_TEST_VIS int wolfcert_scep_build_pki_get_url(const char* base, + const uint8_t* pki_msg, size_t pki_len, void* heap, char** out_url); int wolfcert_scep_deenvelop(const uint8_t* recipient_cert_der, size_t recipient_cert_len, const uint8_t* recipient_key_der, size_t recipient_key_len, const uint8_t* env_der, size_t env_len, diff --git a/src/scep/scep_client.c b/src/scep/scep_client.c index b98e3cc..6ed3245 100644 --- a/src/scep/scep_client.c +++ b/src/scep/scep_client.c @@ -317,22 +317,117 @@ static int pick_hash_oid(const WolfCertScepCaps* caps) return SHA256h; } +/* Percent-encode `in` into a freshly allocated NUL-terminated string, escaping + * every byte outside the RFC 3986 unreserved set so the base64 pkiMessage is + * safe inside a URL query value. Returns NULL on allocation failure. */ +static char* url_encode(const uint8_t* in, size_t in_len, void* heap) +{ + static const char HEX[] = "0123456789ABCDEF"; + /* Worst case each byte expands to "%XX" (3 chars), plus the NUL. */ + char* out = (char*)WOLFCERT_XMALLOC(in_len * 3 + 1, heap); + if (out == NULL) + return NULL; + + size_t o = 0; + for (size_t i = 0; i < in_len; ++i) { + unsigned char c = in[i]; + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || c == '_' || c == '.' || c == '~') { + out[o++] = (char)c; + } + else { + out[o++] = '%'; + out[o++] = HEX[c >> 4]; + out[o++] = HEX[c & 0x0F]; + } + } + out[o] = '\0'; + + return out; +} + +/* Build the HTTP GET URL for a PKIOperation fallback (RFC 8894 section 4.1): + * base?operation=PKIOperation&message= + * Returns WOLFCERT_OK with *out_url owned by the caller, WOLFCERT_ERR_MEMORY, + * or WOLFCERT_ERR_UNSUPPORTED when the encoded URL would exceed + * WOLFCERT_SCEP_MAX_GET_URL (message too large for GET). */ +WOLFCERT_TEST_VIS int wolfcert_scep_build_pki_get_url(const char* base, + const uint8_t* pki_msg, + size_t pki_len, void* heap, char** out_url) +{ + WolfCertBuffer b64 = { 0 }; + int rc = wolfcert_base64_encode(pki_msg, pki_len, &b64, heap); + if (rc != WOLFCERT_OK) + return rc; + + char* enc = url_encode(b64.data, b64.len, heap); + wolfcert_buffer_free(&b64); + if (enc == NULL) + return WOLFCERT_ERR_MEMORY; + + char* head = append_query(base, "PKIOperation", heap); + if (head == NULL) { + WOLFCERT_XFREE(enc, heap); + return WOLFCERT_ERR_MEMORY; + } + + size_t need = strlen(head) + strlen("&message=") + strlen(enc) + 1; + if (need > WOLFCERT_SCEP_MAX_GET_URL) { + WOLFCERT_XFREE(enc, heap); + WOLFCERT_XFREE(head, heap); + return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "scep", + "pkiMessage too large for an HTTP GET; the CA must advertise " + "POSTPKIOperation"); + } + + char* url = (char*)WOLFCERT_XMALLOC(need, heap); + if (url == NULL) { + WOLFCERT_XFREE(enc, heap); + WOLFCERT_XFREE(head, heap); + return WOLFCERT_ERR_MEMORY; + } + snprintf(url, need, "%s&message=%s", head, enc); + + WOLFCERT_XFREE(enc, heap); + WOLFCERT_XFREE(head, heap); + *out_url = url; + + return WOLFCERT_OK; +} + static int run_pki_op(const WolfCertServerCfg* srv, + const WolfCertScepCaps* caps, const uint8_t* pki_msg, size_t pki_len, uint8_t** out_resp, size_t* out_resp_len) { void* heap = srv->heap ? srv->heap : wolfcert_default_heap(); - char* url = append_query(srv->server_url, "PKIOperation", heap); - if (url == NULL) - return WOLFCERT_ERR_MEMORY; - WolfCertHttpRequest req = { - .method = "POST", - .url = url, - .content_type = "application/x-pki-message", - .body = pki_msg, - .body_len = pki_len, - }; + /* 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 }; + 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 }; @@ -437,7 +532,7 @@ static int do_scep_round_trip(const WolfCertServerCfg* srv, uint8_t* resp = NULL; size_t resp_len = 0; - rc = run_pki_op(srv, pki.data, pki.len, &resp, &resp_len); + rc = run_pki_op(srv, caps, pki.data, pki.len, &resp, &resp_len); wolfcert_buffer_free(&pki); if (rc != WOLFCERT_OK) diff --git a/src/scep/scep_server.c b/src/scep/scep_server.c index e62a7ac..2e5a851 100644 --- a/src/scep/scep_server.c +++ b/src/scep/scep_server.c @@ -45,14 +45,19 @@ #include typedef struct { - char method[8]; - char path[WOLFCERT_HTTP_PATH_SZ]; - char query[WOLFCERT_HTTP_PATH_SZ]; - size_t content_length; - uint8_t* body; - size_t body_len; - int connection_close; - void* heap; + /* rawbuf owns the request-line + header bytes read off the wire. It is + * heap-allocated (REQ_BUF_SZ + QUERY_SZ) so an RFC 8894 GET PKIOperation, + * whose base64 pkiMessage rides in the query string, never lands on the + * stack. path and query point into it (NUL-terminated in place). */ + char* rawbuf; + const char* path; + const char* query; + uint8_t* body; + void* heap; + size_t content_length; + size_t body_len; + char method[8]; + int connection_close; } ScepRequest; /* Pending-queue entry: one per PKCSReq / RenewalReq held under @@ -101,7 +106,9 @@ WOLFCERT_TEST_VIS void wolfcert_scep_server_set_faults(WolfCertServer* s, static void free_req(ScepRequest* r) { + /* path/query point into rawbuf, so freeing rawbuf reclaims them too. */ WOLFCERT_XFREE(r->body, r->heap); + WOLFCERT_XFREE(r->rawbuf, r->heap); memset(r, 0, sizeof(*r)); } @@ -124,12 +131,23 @@ static int read_line(const char** p, const char* end, char** ls, size_t* ll) static int read_request(WolfCertServer* s, int fd, ScepRequest* out, void* heap) { + /* Large enough to hold the request line + headers, including a GET + * PKIOperation whose base64 pkiMessage lives in the query string. Heap- + * allocated (owned by free_req) to keep it off the request-handling stack; + * path and query end up pointing into it. */ + size_t buf_sz = WOLFCERT_HTTP_REQ_BUF_SZ + WOLFCERT_HTTP_QUERY_SZ; + char* buf; + size_t n = 0; + memset(out, 0, sizeof(*out)); out->heap = heap; - char buf[WOLFCERT_HTTP_REQ_BUF_SZ]; - size_t n = 0; - while (n < sizeof(buf) - 1) { - ssize_t r = wolfcert_io_recv(s, fd, buf + n, sizeof(buf) - 1 - n); + out->rawbuf = (char*)WOLFCERT_XMALLOC(buf_sz, heap); + if (out->rawbuf == NULL) + return WOLFCERT_ERR_MEMORY; + buf = out->rawbuf; + + while (n < buf_sz - 1) { + ssize_t r = wolfcert_io_recv(s, fd, buf + n, buf_sz - 1 - n); if (r <= 0) return WOLFCERT_ERR_IO; @@ -161,22 +179,19 @@ static int read_request(WolfCertServer* s, int fd, ScepRequest* out, void* heap) if (sp2 == NULL) return WOLFCERT_ERR_PROTOCOL; - size_t pqlen = (size_t)(sp2 - sp1 - 1); - char full[2 * WOLFCERT_HTTP_PATH_SZ]; - if (pqlen >= sizeof(full)) - return WOLFCERT_ERR_PROTOCOL; - - memcpy(full, sp1 + 1, pqlen); - full[pqlen] = '\0'; - - char* qs = strchr(full, '?'); + /* Split the request-target in place: NUL the trailing space, then the '?' + * (if any). path and query point into buf (== out->rawbuf), which lives + * until free_req, so no copy and no large stack buffers are needed. */ + *sp2 = '\0'; + out->path = sp1 + 1; + char* qs = strchr(sp1 + 1, '?'); if (qs != NULL) { *qs = '\0'; - strncpy(out->query, qs + 1, sizeof(out->query) - 1); - out->query[sizeof(out->query) - 1] = '\0'; + out->query = qs + 1; + } + else { + out->query = sp2; /* empty query string */ } - strncpy(out->path, full, sizeof(out->path) - 1); - out->path[sizeof(out->path) - 1] = '\0'; while (read_line(&p, end, &line, &llen) == 0 && llen > 0) { if (llen > 14 && strncasecmp(line, "Content-Length", 14) == 0) { @@ -781,6 +796,92 @@ static int handle_pki_op(WolfCertServer* s, int fd, const ScepRequest* req) return rc; } +static int hexval(int c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +/* Return the value of query parameter `key` (the text just past "key="), or + * NULL if absent. `key` is matched as a whole parameter name -- at the start of + * the query or immediately after a '&' -- so "message" is not matched inside + * "mymessage". The value runs to the next '&' or the end of the string. */ +static const char* query_param(const char* query, const char* key) +{ + size_t key_len = strlen(key); + const char* p = query; + + while (*p != '\0') { + if (strncmp(p, key, key_len) == 0 && p[key_len] == '=') + return p + key_len + 1; + p = strchr(p, '&'); + if (p == NULL) + break; + ++p; + } + + return NULL; +} + +/* RFC 8894 section 4.1 GET PKIOperation: the pkiMessage is carried + * base64-encoded and percent-escaped in the `message` query parameter. Decode + * it into req->body (owned; freed by free_req) and dispatch exactly like a + * POSTed pkiMessage. Sends its own 400 on a malformed message. */ +static int handle_pki_op_get(WolfCertServer* s, int fd, ScepRequest* req) +{ + const char* m = query_param(req->query, "message"); + if (m == NULL) { + send_text(s, fd, 400, "Bad Request", "text/plain", ""); + return WOLFCERT_ERR_PROTOCOL; + } + size_t enc_len = strcspn(m, "&"); + + /* Percent-decoding never grows the input. */ + uint8_t* b64 = (uint8_t*)WOLFCERT_XMALLOC(enc_len + 1, req->heap); + if (b64 == NULL) { + send_text(s, fd, 500, "Server Error", "text/plain", ""); + return WOLFCERT_ERR_MEMORY; + } + + size_t o = 0; + for (size_t i = 0; i < enc_len; ) { + if (m[i] == '%') { + int hi = (i + 2 < enc_len) ? hexval((unsigned char)m[i + 1]) : -1; + int lo = (i + 2 < enc_len) ? hexval((unsigned char)m[i + 2]) : -1; + if (hi < 0 || lo < 0) { + WOLFCERT_XFREE(b64, req->heap); + send_text(s, fd, 400, "Bad Request", "text/plain", ""); + return WOLFCERT_ERR_PROTOCOL; + } + b64[o++] = (uint8_t)((hi << 4) | lo); + i += 3; + } + else { + b64[o++] = (uint8_t)m[i]; + i += 1; + } + } + + WolfCertBuffer der = { 0 }; + int rc = wolfcert_base64_decode(b64, o, &der, req->heap); + WOLFCERT_XFREE(b64, req->heap); + if (rc != WOLFCERT_OK) { + send_text(s, fd, 400, "Bad Request", "text/plain", ""); + return rc; + } + + /* A GET carries its pkiMessage in the query, not a body; free any body a + * bogus Content-Length made read_request allocate before installing the + * decoded message, so it is not leaked (XFREE(NULL) is a no-op). */ + WOLFCERT_XFREE(req->body, req->heap); + req->body = der.data; + req->body_len = der.len; + + return handle_pki_op(s, fd, req); +} + static int handle_request(WolfCertServer* s, int fd) { ScepRequest req = { 0 }; @@ -795,13 +896,12 @@ static int handle_request(WolfCertServer* s, int fd) if (req.connection_close) s->keep_alive = 0; - const char* op = strstr(req.query, "operation="); + const char* op = query_param(req.query, "operation"); if (op == NULL) { send_text(s, fd,400, "Bad Request", "text/plain", ""); free_req(&req); return WOLFCERT_ERR_PROTOCOL; } - op += 10; if (strncmp(op, "GetCACaps", 9) == 0 && strcmp(req.method, "GET") == 0) { handle_get_ca_caps(s, fd); @@ -815,6 +915,9 @@ static int handle_request(WolfCertServer* s, int fd) else if (strncmp(op, "PKIOperation", 12) == 0 && strcmp(req.method, "POST") == 0) { rc = handle_pki_op(s, fd, &req); } + else if (strncmp(op, "PKIOperation", 12) == 0 && strcmp(req.method, "GET") == 0) { + rc = handle_pki_op_get(s, fd, &req); + } else { send_text(s, fd,404, "Not Found", "text/plain", ""); } diff --git a/tests/integration/test_scep_roundtrip.c b/tests/integration/test_scep_roundtrip.c index 9020fc4..3848b57 100644 --- a/tests/integration/test_scep_roundtrip.c +++ b/tests/integration/test_scep_roundtrip.c @@ -39,6 +39,11 @@ #include #include #include +#include +#include +#include +#include +#include #define REQUIRE(cond) \ do { \ @@ -54,6 +59,70 @@ static void* server_thread(void* arg) return NULL; } +/* Send a raw HTTP/1.1 GET to the plain-HTTP SCEP server on the loopback port + * and return the numeric response status (or -1 on transport failure). An + * optional body (with a matching Content-Length) is sent when `body` is + * non-NULL. Used to exercise the server's malformed-GET rejection branches and + * the GET-with-body free path, which the client API cannot produce. */ +static int raw_http_status(uint16_t port, const char* target, const char* body) +{ + struct sockaddr_in addr; + struct timeval tv; + char req[512]; + char resp[128]; + size_t body_len = body != NULL ? strlen(body) : 0; + int fd; + int n; + ssize_t r; + int status = -1; + + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) + return -1; + + tv.tv_sec = 5; + tv.tv_usec = 0; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) { + close(fd); + return -1; + } + + if (body_len > 0) + n = snprintf(req, sizeof(req), + "GET %s HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n" + "Content-Length: %zu\r\n\r\n%s", + target, body_len, body); + else + n = snprintf(req, sizeof(req), + "GET %s HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n", + target); + if (n < 0 || (size_t)n >= sizeof(req)) { + close(fd); + return -1; + } + if (write(fd, req, (size_t)n) != (ssize_t)n) { + close(fd); + return -1; + } + + r = read(fd, resp, sizeof(resp) - 1); + if (r > 0) { + resp[r] = '\0'; + if (strncmp(resp, "HTTP/1.1 ", 9) == 0) + status = atoi(resp + 9); + } + + close(fd); + return status; +} + int main(void) { REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); @@ -107,6 +176,44 @@ int main(void) WOLFSSL_FILETYPE_ASN1) == WOLFSSL_SUCCESS); wolfSSL_CertManagerFree(cm); + /* ---- HTTP GET PKIOperation fallback (RFC 8894 section 4.1) ------------ + * With a caps set that does not advertise POSTPKIOperation, the client + * must carry the pkiMessage base64-encoded in a GET query; the server + * decodes it and issues exactly as for POST. */ + WolfCertScepCaps caps_get = caps; + caps_get.post_pki_operation = 0; + WolfCertKey* dkg = NULL; + REQUIRE(wolfcert_key_generate(&kcfg, &dkg) == WOLFCERT_OK); + WolfCertCertMeta meta_get = { .subject_dn = "CN=device-scep-get" }; + WolfCertBuffer csr_get = { 0 }; + REQUIRE(wolfcert_csr_build(dkg, &meta_get, &csr_get) == WOLFCERT_OK); + WolfCertBuffer issued_get = { 0 }; + REQUIRE(wolfcert_scep_pkcs_req(&cli, &caps_get, ca_der->buffer, ca_der->length, + dkg, csr_get.data, csr_get.len, &issued_get) + == WOLFCERT_OK); + REQUIRE(memmem(issued_get.data, issued_get.len, "BEGIN CERTIFICATE", 17) != NULL); + wolfcert_buffer_free(&issued_get); + wolfcert_buffer_free(&csr_get); + wolfcert_key_free(dkg); + + /* Negative GET PKIOperation branches (RFC 8894 section 4.1): the server must + * reject each malformed request with 400. These cannot be produced by the + * client API, so drive the running server over a raw socket. */ + REQUIRE(raw_http_status(wolfcert_server_port(s), + "/scep?operation=PKIOperation", NULL) == 400); /* no message= */ + REQUIRE(raw_http_status(wolfcert_server_port(s), + "/scep?operation=PKIOperation&message=%ZZ", NULL) == 400); /* bad %-escape */ + REQUIRE(raw_http_status(wolfcert_server_port(s), + "/scep?operation=PKIOperation&message=@@@@", NULL) == 400); /* bad base64 */ + + /* A GET carrying a spurious Content-Length body: read_request allocates + * req->body for it, and handle_pki_op_get must free that before installing + * the decoded message or it leaks (caught under ASan). "QUJD" is valid + * base64 so the decode succeeds and the free path runs; the payload is not a + * real pkiMessage, so the request is rejected with 400. */ + REQUIRE(raw_http_status(wolfcert_server_port(s), + "/scep?operation=PKIOperation&message=QUJD", "XYZ") == 400); /* body freed */ + #ifdef WOLFCERT_HAVE_ED25519 /* Ed25519 signer must be rejected cleanly (RFC 8894 requires RSA). */ WolfCertKeyCfg edcfg = { .type = WOLFCERT_KEY_ED25519, .param = 0, diff --git a/tests/unit/test_scep_msg.c b/tests/unit/test_scep_msg.c index bf865aa..7f33f02 100644 --- a/tests/unit/test_scep_msg.c +++ b/tests/unit/test_scep_msg.c @@ -800,12 +800,52 @@ static int test_ca_fingerprint(void) return rc; } +/* wolfcert_scep_build_pki_get_url: builds a well-formed GET URL for small + * messages and refuses one that would exceed WOLFCERT_SCEP_MAX_GET_URL. */ +static int test_pki_get_url(void) +{ + const char* base = "http://ca.example/scep"; + const char* pfx = "http://ca.example/scep?operation=PKIOperation&message="; + int prefix_ok, has_message, rc; + + uint8_t small[64]; + for (size_t i = 0; i < sizeof(small); i++) + small[i] = (uint8_t)(i * 7 + 3); /* spans bytes that must be escaped */ + + char* url = NULL; + REQUIRE(wolfcert_scep_build_pki_get_url(base, small, sizeof(small), NULL, &url) + == WOLFCERT_OK); + REQUIRE(url != NULL); + /* Capture the assertions, then free url before the REQUIREs so a failing + * check cannot leak the heap-allocated URL. */ + prefix_ok = (strncmp(url, pfx, strlen(pfx)) == 0); + has_message = (strlen(url) > strlen(pfx)); /* the message is present */ + WOLFCERT_XFREE(url, NULL); /* url came from the wolfCert heap, not malloc */ + REQUIRE(prefix_ok); + REQUIRE(has_message); + + /* A pkiMessage too large for a GET must be refused so the caller POSTs. */ + size_t big_len = WOLFCERT_SCEP_MAX_GET_URL; /* base64 alone exceeds the cap */ + uint8_t* big = (uint8_t*)malloc(big_len); + REQUIRE(big != NULL); + memset(big, 0xA5, big_len); + char* url2 = NULL; + rc = wolfcert_scep_build_pki_get_url(base, big, big_len, NULL, &url2); + free(big); + WOLFCERT_XFREE(url2, NULL); /* NULL on the expected UNSUPPORTED path */ + REQUIRE(rc == WOLFCERT_ERR_UNSUPPORTED); + + return 0; +} + int main(void) { REQUIRE(test_static_mem_init() == 0); REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); if (test_ca_fingerprint()) return 1; + if (test_pki_get_url()) + return 1; if (test_non_success_has_no_envelope()) return 1; if (test_signer_subject_fallback()) From 136c414a8722831f11df681b07a5af5dc5abfcc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 15 Jul 2026 09:24:37 +0200 Subject: [PATCH 4/4] scep,est: use wc_ConstantCompare for constant-time comparisons wolfSSL now exposes wc_ConstantCompare as a public API (alongside wc_ForceZero). Replace the three hand-rolled XOR-accumulate constant-time comparisons with it: - the CA fingerprint check (scep_client.c, dropping the local ct_diff), - the challenge-password check (scep_server.c), and - the HTTP Basic-auth credential check (est_server.c). Each keeps its existing length-mismatch guard, and wc_ConstantCompare returns 0 iff equal, so behaviour is unchanged. --- src/est/est_server.c | 10 ++++------ src/scep/scep_client.c | 13 +------------ src/scep/scep_server.c | 11 ++++------- 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/src/est/est_server.c b/src/est/est_server.c index c1d7e49..dbda31d 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -35,6 +35,7 @@ #include #include #include +#include #include #include @@ -520,12 +521,9 @@ static int check_basic_auth(const WolfCertServer* s, const char* auth_header) * does not leak the length of the matching prefix. */ const char* tok = auth_header + EST_BASIC_AUTH_SCHEME_LEN; int ok = (strlen(tok) == enc.len); - if (ok) { - unsigned acc = 0; - for (size_t i = 0; i < enc.len; ++i) - acc |= (unsigned)((unsigned char)tok[i] ^ enc.data[i]); - ok = (acc == 0); - } + if (ok) + ok = (wc_ConstantCompare((const byte*)tok, enc.data, + (int)enc.len) == 0); wolfcert_buffer_free(&enc); return ok; diff --git a/src/scep/scep_client.c b/src/scep/scep_client.c index 6ed3245..9ea963f 100644 --- a/src/scep/scep_client.c +++ b/src/scep/scep_client.c @@ -231,17 +231,6 @@ int wolfcert_scep_get_ca_cert_enc(const WolfCertServerCfg* srv, WolfCertEncoding return rc; } -/* Constant-time buffer comparison: returns 0 iff the two buffers are equal. - * Fingerprints are not secret, but a timing-independent compare keeps the - * trust check uniform and avoids leaking match position. */ -static int ct_diff(const uint8_t* a, const uint8_t* b, size_t n) -{ - uint8_t d = 0; - for (size_t i = 0; i < n; ++i) - d |= (uint8_t)(a[i] ^ b[i]); - return d; -} - int wolfcert_scep_verify_ca_fingerprint(const uint8_t* ca_der, size_t ca_der_len, const uint8_t* expected, size_t expected_len, WolfCertScepFpAlg alg) @@ -296,7 +285,7 @@ int wolfcert_scep_verify_ca_fingerprint(const uint8_t* ca_der, size_t ca_der_len return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "scep", "expected fingerprint length does not match the digest size"); - if (ct_diff(expected, digest, digest_len) != 0) + if (wc_ConstantCompare(expected, digest, (int)digest_len) != 0) return WOLFCERT_ERR(WOLFCERT_ERR_AUTH, "scep", "CA certificate fingerprint mismatch"); diff --git a/src/scep/scep_server.c b/src/scep/scep_server.c index 2e5a851..87ca841 100644 --- a/src/scep/scep_server.c +++ b/src/scep/scep_server.c @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -438,13 +439,9 @@ static int check_challenge(const uint8_t* csr_der, size_t csr_len, size_t elen = strlen(expected); int ok = (dc.cPwd != NULL) && ((size_t)dc.cPwdLen == elen); - if (ok) { - unsigned acc = 0; - for (size_t i = 0; i < elen; ++i) { - acc |= (unsigned)(dc.cPwd[i] ^ expected[i]); - } - ok = (acc == 0); - } + if (ok) + ok = (wc_ConstantCompare((const byte*)dc.cPwd, + (const byte*)expected, (int)elen) == 0); wc_FreeDecodedCert(&dc); return ok ? WOLFCERT_OK : WOLFCERT_ERR_AUTH;