From b64c134e472b37fc367eff96eefd3012d1dfcc63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 30 Sep 2026 13:54:05 +0200 Subject: [PATCH 1/9] scep: require a pkcsPKIEnvelope on GetCertInitial The test server ignored a de-enveloping failure for messageType 20 and dispatched the poll anyway, matching the pending entry by transactionID alone. A signed GetCertInitial with absent or non-envelope content could therefore drain a parked request and trigger issuance. Route every PKIOperation whose envelope does not decrypt to the CA through the signed FAILURE CertRep path, as RFC 8894 requires the pkcsPKIEnvelope for all of them. The envelope alone does not bind the poll to its request: the CA certificate is public, so anyone can build one. The poll's signer must now carry the same public key as the parked CSR, the rule PKCSReq already enforces. A mismatch gets FAILURE/badCertId and leaves the entry queued. The poll roundtrip test now posts an unenveloped, a malformed and a validly enveloped GetCertInitial signed by a different key for a pending transaction, expects each to be refused, and checks the legitimate poll still succeeds afterwards. Fixes F-11063. --- Makefile.am | 1 + src/scep/scep_server.c | 24 +++-- tests/CMakeLists.txt | 1 + tests/integration/test_scep_poll_roundtrip.c | 95 ++++++++++++++++++++ 4 files changed, 113 insertions(+), 8 deletions(-) diff --git a/Makefile.am b/Makefile.am index 295d8de..a97c2c4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -197,6 +197,7 @@ 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_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/src 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 diff --git a/src/scep/scep_server.c b/src/scep/scep_server.c index 76147fb..0573a46 100644 --- a/src/scep/scep_server.c +++ b/src/scep/scep_server.c @@ -816,11 +816,12 @@ static int handle_enroll(WolfCertServer* s, int fd, const char* mt, } /* Handle messageType=20 (GetCertInitial): poll for a pending enrollment. - * Test-server policy: the first poll for a known transactionID issues - * the cert and drains the queue entry; subsequent polls for unknown - * transactionIDs return pkiStatus=2 (FAILURE) rather than pretending - * to be pending forever. */ + * Test-server policy: the first poll for a known transactionID, signed with + * the parked CSR's key, issues the cert and drains the queue entry. Any + * other poll gets pkiStatus=2 (FAILURE) and leaves the queue unchanged. */ static int handle_get_cert_initial(WolfCertServer* s, int fd, + const uint8_t* signer_cert, + size_t signer_cert_len, const uint8_t* tid, size_t tid_len, const uint8_t* snonce, size_t snonce_len) { @@ -831,6 +832,14 @@ static int handle_get_cert_initial(WolfCertServer* s, int fd, "4" /* badCertId: no such transaction */); } + /* Only the key that parked the request may release it. */ + if (signer_cert == NULL || + signer_matches_csr(signer_cert, signer_cert_len, + e->csr_der, e->csr_len, s->heap) != WOLFCERT_OK) { + return send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len, + "4" /* badCertId */); + } + /* Approve on first poll. A production implementation would hold * requests until an admin acts on a queue; for the test server a * single round trip through pending is enough to exercise the @@ -953,9 +962,7 @@ static int handle_pki_op(WolfCertServer* s, int fd, const ScepRequest* req) rc = wolfcert_scep_deenvelop(s->ca.cert_der, s->ca.cert_der_len, s->ca.key_der, s->ca.key_der_len, env.data, env.len, &csr, s->heap); - if (rc != WOLFCERT_OK && strcmp(mt, "20") != 0) { - /* Decryption matters for 19/17 (CSR inside); for 20 the payload - * is IssuerAndSubject which the server matches by txid anyway. */ + if (rc != WOLFCERT_OK) { const char* fail_info = rc == WOLFCERT_ERR_UNSUPPORTED ? "0" /* badAlg */ : "2" /* badRequest */; @@ -975,7 +982,8 @@ static int handle_pki_op(WolfCertServer* s, int fd, const ScepRequest* req) tid, tid_len, snonce, snonce_len); } else if (strcmp(mt, "20") == 0) { - rc = handle_get_cert_initial(s, fd, tid, tid_len, snonce, snonce_len); + rc = handle_get_cert_initial(s, fd, signer_cert, signer_cert_len, + tid, tid_len, snonce, snonce_len); } else if (strcmp(mt, "21") == 0 && s->cfg.scep_enable_get_cert) { const uint8_t* gc_signer = signer_cert; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bf823ce..39dd337 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -139,6 +139,7 @@ if(WOLFCERT_ENABLE_SCEP AND WOLFCERT_ENABLE_SERVER) add_test(NAME scep_roundtrip COMMAND test_scep_roundtrip) add_executable(test_scep_poll_roundtrip integration/test_scep_poll_roundtrip.c) + target_include_directories(test_scep_poll_roundtrip PRIVATE ${CMAKE_SOURCE_DIR}/src) target_link_libraries(test_scep_poll_roundtrip PRIVATE wolfcert Threads::Threads) add_test(NAME scep_poll_roundtrip COMMAND test_scep_poll_roundtrip) diff --git a/tests/integration/test_scep_poll_roundtrip.c b/tests/integration/test_scep_poll_roundtrip.c index 1aace50..48e4b77 100644 --- a/tests/integration/test_scep_poll_roundtrip.c +++ b/tests/integration/test_scep_poll_roundtrip.c @@ -36,9 +36,11 @@ #include #include #include +#include "internal.h" #include #include +#include #include #include @@ -59,6 +61,61 @@ static void* server_thread(void* arg) return NULL; } +/* POST a GetCertInitial for `tid` whose signed content is `content` rather + * than a pkcsPKIEnvelope, and return the CertRep's pkiStatus and failInfo. */ +static int post_unenveloped_poll(const char* url, WolfCertKey* dk, + const WolfCertBuffer* csr, + const uint8_t* tid, size_t tid_len, + const uint8_t* content, size_t content_len, + char** out_status, char** out_fail_info) +{ + uint8_t* signer = NULL; + size_t signer_len = 0; + uint8_t key_der[4096]; + int key_len; + uint8_t snonce[16]; + char pki_url[160]; + WolfCertScepAttrs attrs; + WolfCertBuffer pki = { 0 }; + WolfCertBuffer env = { 0 }; + WolfCertHttpResponse resp = { 0 }; + + REQUIRE(wolfcert_scep_self_signed_rsa((RsaKey*)dk->impl, csr->data, + csr->len, &signer, &signer_len, + NULL) == WOLFCERT_OK); + key_len = wc_RsaKeyToDer((RsaKey*)dk->impl, key_der, sizeof(key_der)); + REQUIRE(key_len > 0); + + memset(snonce, 0xC3, sizeof(snonce)); + memset(&attrs, 0, sizeof(attrs)); + attrs.transaction_id = tid; + attrs.transaction_id_len = tid_len; + attrs.sender_nonce = snonce; + attrs.sender_nonce_len = sizeof(snonce); + attrs.message_type = "20"; + REQUIRE(wolfcert_scep_build_pki_message(content, content_len, + signer, signer_len, + key_der, (size_t)key_len, 0, + &attrs, &pki, NULL) == WOLFCERT_OK); + + snprintf(pki_url, sizeof(pki_url), "%s?operation=PKIOperation", url); + WolfCertHttpRequest req = { .method = "POST", .url = pki_url, + .content_type = "application/x-pki-message", + .body = pki.data, .body_len = pki.len }; + REQUIRE(wolfcert_http_request(&req, &resp) == WOLFCERT_OK); + REQUIRE(resp.status_code == 200); + REQUIRE(wolfcert_scep_parse_pki_message(resp.body, resp.body_len, &env, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, out_status, + NULL, NULL, out_fail_info, NULL) == WOLFCERT_OK); + + wolfcert_buffer_free(&env); + wolfcert_http_response_free(&resp); + wolfcert_buffer_free(&pki); + wc_ForceZero(key_der, sizeof(key_der)); + WOLFCERT_XFREE(signer, NULL); + return 0; +} + static int poll_path(WolfCertServer* s) { char url[128]; @@ -102,6 +159,44 @@ static int poll_path(WolfCertServer* s) REQUIRE(legacy_rc == WOLFCERT_ERR_PENDING); wolfcert_buffer_free(&legacy_out); + /* A poll whose signed content is absent or not an envelope must be + * refused, and must leave the pending entry for the real poll below. */ + static const uint8_t not_env[] = { 0x04, 0x03, 'a', 'b', 'c' }; + const uint8_t* bad_content[2] = { NULL, not_env }; + size_t bad_len[2] = { 0, sizeof(not_env) }; + for (int i = 0; i < 2; i++) { + char* status = NULL; + char* fail_info = NULL; + REQUIRE(post_unenveloped_poll(url, dk, &csr, r1.transaction_id, + r1.transaction_id_len, bad_content[i], + bad_len[i], &status, &fail_info) == 0); + int refused = status != NULL && strcmp(status, "2") == 0 && + fail_info != NULL && strcmp(fail_info, "2") == 0; + WOLFCERT_XFREE(status, NULL); + WOLFCERT_XFREE(fail_info, NULL); + REQUIRE(refused); + } + + /* A valid poll signed by another key must not release the entry either. */ + WolfCertKey* other = NULL; + WolfCertBuffer other_csr = { 0 }; + WolfCertScepResult rx = { 0 }; + REQUIRE(wolfcert_key_generate(&kcfg, &other) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build(other, &meta, &other_csr) == WOLFCERT_OK); + rc = wolfcert_scep_get_cert_initial(&cli, &caps, + ca_der->buffer, ca_der->length, + ca_der->buffer, ca_der->length, + NULL, 0, + other, other_csr.data, other_csr.len, + r1.transaction_id, r1.transaction_id_len, + &rx); + REQUIRE(rc == WOLFCERT_OK); + REQUIRE(rx.status == WOLFCERT_SCEP_STATUS_FAILURE); + REQUIRE(rx.fail_info == 4); + wolfcert_scep_result_free(&rx); + wolfcert_buffer_free(&other_csr); + wolfcert_key_free(other); + /* Step 2: GetCertInitial with the same transactionID -> SUCCESS. * signer_cert=NULL so the client regenerates the transient * "SCEP Enrollee" self-signed cert that PKCSReq used. */ From 211fad5f1bff119999b71292a52f6e9b7f27197e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 30 Sep 2026 14:02:24 +0200 Subject: [PATCH 2/9] scep: answer a CA issuance failure with a CertRep FAILURE The server parses the CSR without verification for its SPKI and challenge checks, so a CSR with a broken self-signature is only refused inside wolfcert_ca_issue. That branch replied with a plain HTTP 400, leaving the client without a signed CertRep, its transactionID, recipientNonce or failInfo, and surfacing a PKI rejection as a transport error. Reply with pkiStatus FAILURE and failInfo badRequest instead and close the connection, matching the other rejection paths. The integration test enrolls with a CSR whose signature has one bit flipped and expects the FAILURE CertRep. A server that requires approval used to park such a CSR and answer PENDING for a request that could never issue. It now verifies the CSR signature before parking and refuses it on the PKCSReq with the same FAILURE/badRequest, as the EST server does. The poll test sends a bad-signature PKCSReq to the approval server, expects FAILURE, and then a poll for its transactionID gets badCertId since nothing was parked. A CA that runs out of memory while verifying or issuing is the server's own fault, so both paths answer HTTP 500, like the file's other allocation failures, instead of a final FAILURE. The signer/CSR key checks on PKCSReq and GetCertInitial do the same, rather than answering badRequest or badCertId. The SCEP signature check and the EST server's identical csr_verify are now one wolfcert_csr_verify in ca_issue.c. A poll used to drain its pending entry before issuing, so an out-of-memory issuance lost the request and every later poll got badCertId. The poll now issues from the parked entry and keeps it only when the server ran out of memory before the CA issued. Once a certificate exists the entry goes, so a retry cannot issue a second one. A test-only fault runs the signer check, the issuance or the reply after it out of memory. The poll test checks that a PKCSReq failed in the signer check can be resent, that a poll failed in the signer check or the issuance can be polled again, and that one failed after issuing gets badCertId. Fixes F-12110. --- cli/wolfcert_server.c | 3 +- src/ca_issue.c | 18 +++ src/est/est_server.c | 21 +-- src/internal.h | 6 + src/scep/scep_server.c | 157 +++++++++++++------ tests/integration/test_scep_poll_roundtrip.c | 84 ++++++++++ tests/integration/test_scep_roundtrip.c | 37 +++++ wolfcert/server.h | 10 +- 8 files changed, 261 insertions(+), 75 deletions(-) diff --git a/cli/wolfcert_server.c b/cli/wolfcert_server.c index a6055e7..ee0e162 100644 --- a/cli/wolfcert_server.c +++ b/cli/wolfcert_server.c @@ -78,7 +78,8 @@ static void print_usage(FILE* out) " /simplereenroll also needs --tls-client-ca and a\n" " KEEP_PEER_CERT wolfSSL\n" " --scep-require-approval Defer SCEP PKCSReq/RenewalReq (pkiStatus=PENDING); issue\n" - " on first GetCertInitial with the same transactionID\n" + " on the first GetCertInitial with the same\n" + " transactionID, signed with the CSR's key\n" " --scep-enable-next-ca Advertise + answer GetNextCACert (RFC 8894 section 4.7),\n" " generating a roll-over CA on first request\n" " --scep-enable-get-cert Answer GetCert (RFC 8894 section 3.3.4), returning a\n" diff --git a/src/ca_issue.c b/src/ca_issue.c index 22c3027..3e098de 100644 --- a/src/ca_issue.c +++ b/src/ca_issue.c @@ -743,6 +743,24 @@ WOLFCERT_TEST_VIS int wolfcert_copy_csr_subject(const DecodedCert* dc, Cert* nc) #undef COPY_SUBJ_E #undef COPY_SUBJ +/* Parse the PKCS#10 request and check its self-signature. */ +int wolfcert_csr_verify(const uint8_t* csr_der, size_t csr_len, void* heap) +{ + DecodedCert dc; + int rc; + + wc_InitDecodedCert(&dc, (byte*)csr_der, (word32)csr_len, heap); + rc = wc_ParseCert(&dc, CERTREQ_TYPE, VERIFY, NULL); + wc_FreeDecodedCert(&dc); + if (rc == MEMORY_E) + return WOLFCERT_ERR_WC(rc, "ca", "ParseCert(CSR)"); + if (rc != 0) + return WOLFCERT_ERR(WOLFCERT_ERR_PARSE, "ca", + "CSR does not parse or verify (%d)", rc); + + return WOLFCERT_OK; +} + int wolfcert_ca_issue(WolfCertCa* ca, const uint8_t* csr_der, size_t csr_len, uint8_t** out_cert, size_t* out_len) diff --git a/src/est/est_server.c b/src/est/est_server.c index efe4a61..61efcda 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -1016,24 +1016,6 @@ static int reenroll_identity_check(WolfCertServer* s, #endif } -/* Parse the PKCS#10 request and check its self-signature. */ -static int csr_verify(const uint8_t* csr_der, size_t csr_len, void* heap) -{ - DecodedCert dc; - int rc; - - wc_InitDecodedCert(&dc, (byte*)csr_der, (word32)csr_len, heap); - rc = wc_ParseCert(&dc, CERTREQ_TYPE, VERIFY, NULL); - wc_FreeDecodedCert(&dc); - if (rc == MEMORY_E) - return WOLFCERT_ERR_WC(rc, "est", "ParseCert(CSR)"); - if (rc != 0) - return WOLFCERT_ERR(WOLFCERT_ERR_PARSE, "est", - "CSR does not parse or verify (%d)", rc); - - return WOLFCERT_OK; -} - static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, int reenroll) { @@ -1130,7 +1112,8 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, EstPriv* p = (EstPriv*)s->priv; uint8_t h[32]; /* The reenroll identity check has already verified the CSR. */ - rc = reenroll ? WOLFCERT_OK : csr_verify(csr.data, csr.len, s->heap); + rc = reenroll ? WOLFCERT_OK : + wolfcert_csr_verify(csr.data, csr.len, s->heap); if (rc == WOLFCERT_ERR_MEMORY) { send_error(s, fd, 500, "Server Error", "cannot check the CSR\n"); wolfcert_buffer_free(&csr); diff --git a/src/internal.h b/src/internal.h index 17098af..85292ef 100644 --- a/src/internal.h +++ b/src/internal.h @@ -163,6 +163,7 @@ void wolfcert_buffer_free_secure(WolfCertBuffer* buf); /* Rebuild an issued certificate's subject from a decoded CSR. */ WOLFCERT_TEST_VIS int wolfcert_copy_csr_subject(const DecodedCert* dc, Cert* nc); +int wolfcert_csr_verify(const uint8_t* csr_der, size_t csr_len, void* heap); WOLFCERT_TEST_VIS int wolfcert_ca_issue(WolfCertCa* ca, const uint8_t* csr_der, size_t csr_len, uint8_t** out_cert, size_t* out_len); @@ -255,6 +256,11 @@ WOLFCERT_TEST_VIS void wolfcert_scep_server_set_getcert_fault(WolfCertServer* s, WOLFCERT_TEST_VIS void wolfcert_scep_server_set_faults(WolfCertServer* s, int omit_recipient_nonce, int sign_with_wrong_key, int rng_fail); + +/* Out of memory: 1 fails wolfcert_ca_issue, 2 the reply after issuing, 3 the + * signer/CSR key match; 0 clears it. */ +WOLFCERT_TEST_VIS void wolfcert_scep_server_set_oom_fault(WolfCertServer* s, + int when); #endif /* ---- error reporting --------------------------------------------------- */ diff --git a/src/scep/scep_server.c b/src/scep/scep_server.c index 0573a46..c7f8204 100644 --- a/src/scep/scep_server.c +++ b/src/scep/scep_server.c @@ -114,6 +114,7 @@ typedef struct { int fault_rng_fail; int fault_getcert_wrong_cert; int fault_getcert_no_signer; + int fault_oom; WolfCertCa wrong_ca; int wrong_ca_ready; #endif @@ -139,6 +140,12 @@ WOLFCERT_TEST_VIS void wolfcert_scep_server_set_getcert_fault(WolfCertServer* s, p->fault_getcert_wrong_cert = wrong_cert; p->fault_getcert_no_signer = no_signer; } + +WOLFCERT_TEST_VIS void wolfcert_scep_server_set_oom_fault(WolfCertServer* s, + int when) +{ + ((ScepPriv*)s->priv)->fault_oom = when; +} #endif static void free_req(ScepRequest* r) @@ -717,21 +724,60 @@ static const ScepIssued* issued_find(ScepPriv* p, void* heap, return NULL; } -/* Issue the cert and answer with a success CertRep. */ +/* Answer a rejected pkiMessage with a signed CertRep carrying pkiStatus + * FAILURE and failInfo, per RFC 8894 section 3.2.1. */ +static int send_pki_failure(WolfCertServer* s, int fd, + const uint8_t* tid, size_t tid_len, + const uint8_t* snonce, size_t snonce_len, + const char* fail_info) +{ + /* A FAILURE CertRep carries no messageData, hence no envelope target. */ + return send_cert_rep(s, fd, NULL, 0, NULL, 0, + tid, tid_len, snonce, snonce_len, "2", fail_info); +} + +/* Issue the cert and answer with a success CertRep. *did_issue, when given, is + * set once the CA has issued, whatever the reply's outcome. */ static int issue_and_reply(WolfCertServer* s, int fd, const uint8_t* csr, size_t csr_len, const uint8_t* env_target, size_t env_target_len, const uint8_t* tid, size_t tid_len, - const uint8_t* snonce, size_t snonce_len) + const uint8_t* snonce, size_t snonce_len, + int* did_issue) { uint8_t* issued = NULL; size_t issued_len = 0; int rc = wolfcert_ca_issue(&s->ca, csr, csr_len, &issued, &issued_len); + + if (did_issue != NULL) + *did_issue = 0; +#if defined(WOLFCERT_BUILD_TESTING) + if (rc == WOLFCERT_OK && ((ScepPriv*)s->priv)->fault_oom == 1) { + WOLFCERT_XFREE(issued, s->heap); + issued = NULL; + rc = WOLFCERT_ERR_MEMORY; + } +#endif + if (rc == WOLFCERT_ERR_MEMORY) { + send_text(s, fd, 500, "Server Error", "text/plain", ""); + return rc; + } if (rc != WOLFCERT_OK) { - send_text(s, fd, 400, "Bad CSR", "text/plain", ""); + int send_rc; + + /* Closed by the non-OK return; the flag is for the header. */ + s->keep_alive = 0; + send_rc = send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len, + "2" /* badRequest */); + if (send_rc != WOLFCERT_OK) + WOLFCERT_LOG_DBG("scep", "CertRep send failed: %d", send_rc); + return rc; } + if (did_issue != NULL) + *did_issue = 1; + if (s->cfg.scep_enable_get_cert) { int reg_rc = issued_record((ScepPriv*)s->priv, s->heap, issued, issued_len); @@ -740,6 +786,14 @@ static int issue_and_reply(WolfCertServer* s, int fd, reg_rc); } +#if defined(WOLFCERT_BUILD_TESTING) + if (((ScepPriv*)s->priv)->fault_oom == 2) { + WOLFCERT_XFREE(issued, s->heap); + send_text(s, fd, 500, "Server Error", "text/plain", ""); + return WOLFCERT_ERR_MEMORY; + } +#endif + rc = send_cert_rep(s, fd, issued, issued_len, env_target, env_target_len, tid, tid_len, snonce, snonce_len, "0", NULL); @@ -748,18 +802,6 @@ static int issue_and_reply(WolfCertServer* s, int fd, return rc; } -/* Answer a rejected pkiMessage with a signed CertRep carrying pkiStatus - * FAILURE and failInfo, per RFC 8894 section 3.2.1. */ -static int send_pki_failure(WolfCertServer* s, int fd, - const uint8_t* tid, size_t tid_len, - const uint8_t* snonce, size_t snonce_len, - const char* fail_info) -{ - /* A FAILURE CertRep carries no messageData, hence no envelope target. */ - return send_cert_rep(s, fd, NULL, 0, NULL, 0, - tid, tid_len, snonce, snonce_len, "2", fail_info); -} - /* Handle messageType=19 (PKCSReq) or 17 (RenewalReq) freshly arrived. */ static int handle_enroll(WolfCertServer* s, int fd, const char* mt, const WolfCertBuffer* csr, @@ -772,13 +814,23 @@ static int handle_enroll(WolfCertServer* s, int fd, const char* mt, size_t env_target_len = signer_cert ? signer_cert_len : s->ca.cert_der_len; /* Enforce signer/CSR SPKI match. */ - if (signer_cert != NULL && - signer_matches_csr(signer_cert, signer_cert_len, - csr->data, csr->len, s->heap) != WOLFCERT_OK) { - /* Report the failure as a CertRep, then close the connection. */ - s->keep_alive = 0; - return send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len, - "2" /* badRequest */); + if (signer_cert != NULL) { + int mrc = signer_matches_csr(signer_cert, signer_cert_len, + csr->data, csr->len, s->heap); +#if defined(WOLFCERT_BUILD_TESTING) + if (mrc == WOLFCERT_OK && ((ScepPriv*)s->priv)->fault_oom == 3) + mrc = WOLFCERT_ERR_MEMORY; +#endif + if (mrc == WOLFCERT_ERR_MEMORY) { + send_text(s, fd, 500, "Server Error", "text/plain", ""); + return mrc; + } + if (mrc != WOLFCERT_OK) { + /* Report the failure as a CertRep, then close the connection. */ + s->keep_alive = 0; + return send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len, + "2" /* badRequest */); + } } if (check_challenge(csr->data, csr->len, s->cfg_challenge, @@ -793,6 +845,19 @@ static int handle_enroll(WolfCertServer* s, int fd, const char* mt, /* Defer issuance; return pkiStatus=3 (PENDING). The client polls * with GetCertInitial (messageType 20) referencing this txid. */ ScepPriv* p = (ScepPriv*)s->priv; + + /* Refuse now a CSR that could never issue, rather than park it. */ + int vrc = wolfcert_csr_verify(csr->data, csr->len, s->heap); + if (vrc == WOLFCERT_ERR_MEMORY) { + send_text(s, fd, 500, "Server Error", "text/plain", ""); + return vrc; + } + if (vrc != WOLFCERT_OK) { + s->keep_alive = 0; + return send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len, + "2" /* badRequest */); + } + if (pending_find(p, tid, tid_len) == NULL) { int add = pending_add(p, s->heap, tid, tid_len, csr->data, csr->len, @@ -812,7 +877,7 @@ static int handle_enroll(WolfCertServer* s, int fd, const char* mt, return issue_and_reply(s, fd, csr->data, csr->len, env_target, env_target_len, - tid, tid_len, snonce, snonce_len); + tid, tid_len, snonce, snonce_len, NULL); } /* Handle messageType=20 (GetCertInitial): poll for a pending enrollment. @@ -833,9 +898,18 @@ static int handle_get_cert_initial(WolfCertServer* s, int fd, } /* Only the key that parked the request may release it. */ - if (signer_cert == NULL || - signer_matches_csr(signer_cert, signer_cert_len, - e->csr_der, e->csr_len, s->heap) != WOLFCERT_OK) { + int mrc = signer_cert == NULL ? WOLFCERT_ERR_AUTH : + signer_matches_csr(signer_cert, signer_cert_len, + e->csr_der, e->csr_len, s->heap); +#if defined(WOLFCERT_BUILD_TESTING) + if (mrc == WOLFCERT_OK && p->fault_oom == 3) + mrc = WOLFCERT_ERR_MEMORY; +#endif + if (mrc == WOLFCERT_ERR_MEMORY) { + send_text(s, fd, 500, "Server Error", "text/plain", ""); + return mrc; + } + if (mrc != WOLFCERT_OK) { return send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len, "4" /* badCertId */); } @@ -845,32 +919,15 @@ static int handle_get_cert_initial(WolfCertServer* s, int fd, * single round trip through pending is enough to exercise the * RFC 8894 section 3.3.3 flow end-to-end. */ e->polls++; - uint8_t* csr_copy = (uint8_t*)WOLFCERT_XMALLOC(e->csr_len, s->heap); - if (csr_copy == NULL) { - send_text(s, fd, 500, "Server Error", "text/plain", ""); - return WOLFCERT_ERR_MEMORY; - } + int did_issue = 0; + int rc = issue_and_reply(s, fd, e->csr_der, e->csr_len, + e->signer_cert_der, e->signer_cert_len, + tid, tid_len, snonce, snonce_len, &did_issue); - memcpy(csr_copy, e->csr_der, e->csr_len); - size_t csr_len_local = e->csr_len; - uint8_t* tgt = (uint8_t*)WOLFCERT_XMALLOC(e->signer_cert_len, s->heap); - if (tgt == NULL) { - WOLFCERT_XFREE(csr_copy, s->heap); - send_text(s, fd, 500, "Server Error", "text/plain", ""); - return WOLFCERT_ERR_MEMORY; - } - - memcpy(tgt, e->signer_cert_der, e->signer_cert_len); - size_t tgt_len = e->signer_cert_len; - - pending_remove(p, s->heap, e); - - int rc = issue_and_reply(s, fd, csr_copy, csr_len_local, - tgt, tgt_len, - tid, tid_len, snonce, snonce_len); + /* Out of memory before issuing keeps the request for another poll. */ + if (rc != WOLFCERT_ERR_MEMORY || did_issue) + pending_remove(p, s->heap, e); - WOLFCERT_XFREE(csr_copy, s->heap); - WOLFCERT_XFREE(tgt, s->heap); return rc; } diff --git a/tests/integration/test_scep_poll_roundtrip.c b/tests/integration/test_scep_poll_roundtrip.c index 48e4b77..0140211 100644 --- a/tests/integration/test_scep_poll_roundtrip.c +++ b/tests/integration/test_scep_poll_roundtrip.c @@ -194,9 +194,51 @@ static int poll_path(WolfCertServer* s) REQUIRE(rx.status == WOLFCERT_SCEP_STATUS_FAILURE); REQUIRE(rx.fail_info == 4); wolfcert_scep_result_free(&rx); + + /* A CSR with a broken signature is refused on the PKCSReq, not parked. */ + other_csr.data[other_csr.len - 1] ^= 0x01; + rc = wolfcert_scep_pkcs_req_ex(&cli, &caps, + ca_der->buffer, ca_der->length, + ca_der->buffer, ca_der->length, + other, other_csr.data, other_csr.len, &rx); + REQUIRE(rc == WOLFCERT_OK); + REQUIRE(rx.status == WOLFCERT_SCEP_STATUS_FAILURE); + REQUIRE(rx.fail_info == 2); + other_csr.data[other_csr.len - 1] ^= 0x01; + WolfCertScepResult ry = { 0 }; + rc = wolfcert_scep_get_cert_initial(&cli, &caps, + ca_der->buffer, ca_der->length, + ca_der->buffer, ca_der->length, + NULL, 0, + other, other_csr.data, other_csr.len, + rx.transaction_id, rx.transaction_id_len, + &ry); + REQUIRE(rc == WOLFCERT_OK); + REQUIRE(ry.status == WOLFCERT_SCEP_STATUS_FAILURE); + REQUIRE(ry.fail_info == 4); + wolfcert_scep_result_free(&rx); + wolfcert_scep_result_free(&ry); wolfcert_buffer_free(&other_csr); wolfcert_key_free(other); + /* Out of memory in the signer check or the issuance answers 500 and + * keeps the entry. */ + static const int oom_at[2] = { 3 /* signer check */, 1 /* issuance */ }; + for (int i = 0; i < 2; i++) { + WolfCertScepResult rm = { 0 }; + wolfcert_scep_server_set_oom_fault(s, oom_at[i]); + rc = wolfcert_scep_get_cert_initial(&cli, &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, &rm); + wolfcert_scep_server_set_oom_fault(s, 0); + REQUIRE(rc != WOLFCERT_OK); + wolfcert_scep_result_free(&rm); + } + /* Step 2: GetCertInitial with the same transactionID -> SUCCESS. * signer_cert=NULL so the client regenerates the transient * "SCEP Enrollee" self-signed cert that PKCSReq used. */ @@ -228,6 +270,48 @@ static int poll_path(WolfCertServer* s) wc_FreeDer(&issued_der); wolfSSL_CertManagerFree(cm); + /* Out of memory after issuing must not leave the entry to issue again. */ + WolfCertScepResult r7 = { 0 }; + WolfCertScepResult r8 = { 0 }; + WolfCertScepResult r9 = { 0 }; + wolfcert_scep_server_set_oom_fault(s, 3); + rc = wolfcert_scep_pkcs_req_ex(&cli, &caps, + ca_der->buffer, ca_der->length, + ca_der->buffer, ca_der->length, + dk, csr.data, csr.len, &r7); + wolfcert_scep_server_set_oom_fault(s, 0); + REQUIRE(rc != WOLFCERT_OK); + wolfcert_scep_result_free(&r7); + rc = wolfcert_scep_pkcs_req_ex(&cli, &caps, + ca_der->buffer, ca_der->length, + ca_der->buffer, ca_der->length, + dk, csr.data, csr.len, &r7); + REQUIRE(rc == WOLFCERT_OK); + REQUIRE(r7.status == WOLFCERT_SCEP_STATUS_PENDING); + wolfcert_scep_server_set_oom_fault(s, 2); + rc = wolfcert_scep_get_cert_initial(&cli, &caps, + ca_der->buffer, ca_der->length, + ca_der->buffer, ca_der->length, + NULL, 0, + dk, csr.data, csr.len, + r7.transaction_id, r7.transaction_id_len, + &r8); + wolfcert_scep_server_set_oom_fault(s, 0); + REQUIRE(rc != WOLFCERT_OK); + rc = wolfcert_scep_get_cert_initial(&cli, &caps, + ca_der->buffer, ca_der->length, + ca_der->buffer, ca_der->length, + NULL, 0, + dk, csr.data, csr.len, + r7.transaction_id, r7.transaction_id_len, + &r9); + REQUIRE(rc == WOLFCERT_OK); + REQUIRE(r9.status == WOLFCERT_SCEP_STATUS_FAILURE); + REQUIRE(r9.fail_info == 4); + wolfcert_scep_result_free(&r7); + wolfcert_scep_result_free(&r8); + wolfcert_scep_result_free(&r9); + /* Step 3: Polling an unknown transactionID -> FAILURE. */ uint8_t bogus_tid[32]; memset(bogus_tid, 0x5A, sizeof(bogus_tid)); diff --git a/tests/integration/test_scep_roundtrip.c b/tests/integration/test_scep_roundtrip.c index ff6b247..26a1dfc 100644 --- a/tests/integration/test_scep_roundtrip.c +++ b/tests/integration/test_scep_roundtrip.c @@ -385,6 +385,40 @@ static int check_pubkey_txid(const WolfCertServerCfg* cli, return rc; } +/* A CSR whose self-signature is broken gets past the SPKI and challenge checks + * and is only refused at issuance, which must still answer with a CertRep. */ +static int check_bad_csr_sig(const WolfCertServerCfg* cli, + const WolfCertScepCaps* caps, + const WolfCertKey* key, + const uint8_t* csr, size_t csr_len, + const uint8_t* ca_der_buf, size_t ca_der_len) +{ + WolfCertScepResult r = { 0 }; + uint8_t* bad = NULL; + int rc = WOLFCERT_OK; + + bad = (uint8_t*)WOLFCERT_XMALLOC(csr_len, NULL); + if (bad == NULL) + rc = WOLFCERT_ERR_MEMORY; + if (rc == WOLFCERT_OK) { + memcpy(bad, csr, csr_len); + bad[csr_len - 1] ^= 0x01; + rc = wolfcert_scep_pkcs_req_ex(cli, caps, ca_der_buf, ca_der_len, + ca_der_buf, ca_der_len, key, + bad, csr_len, &r); + if (rc != WOLFCERT_OK) + fprintf(stderr, "bad CSR signature: rc=%d (%s)\n", rc, + wolfcert_strerror(rc)); + } + if (rc == WOLFCERT_OK && (r.status != WOLFCERT_SCEP_STATUS_FAILURE || + r.fail_info != 2)) + rc = -1; + + wolfcert_scep_result_free(&r); + WOLFCERT_XFREE(bad, NULL); + return rc; +} + /* The content-cipher checks force an AES-CBC cipher, so they only exist when * wolfSSL can supply one. */ #if defined(HAVE_AES_CBC) && \ @@ -1428,6 +1462,9 @@ int main(void) REQUIRE(check_pubkey_txid(&cli, &caps, &kcfg, ca_der->buffer, ca_der->length) == WOLFCERT_OK); + REQUIRE(check_bad_csr_sig(&cli, &caps, dk, csr.data, csr.len, + ca_der->buffer, ca_der->length) == WOLFCERT_OK); + /* ---- RSA-4096 enrollment ---------------------------------------------- */ rc = check_rsa4096(&cli, &caps, ca_der->buffer, ca_der->length); if (rc != WOLFCERT_OK) diff --git a/wolfcert/server.h b/wolfcert/server.h index 1a2e6e2..334598f 100644 --- a/wolfcert/server.h +++ b/wolfcert/server.h @@ -76,11 +76,11 @@ typedef struct { size_t tls_client_ca_pem_len; /* SCEP manual-approval mode. When set, PKCSReq/RenewalReq return - * pkiStatus=PENDING instead of issuing immediately; the client must - * poll with GetCertInitial. The test server's built-in policy auto- - * approves a pending request on the first poll that quotes its - * transactionID, which is enough to exercise the pending -> issued - * transition end-to-end without an admin UI. */ + * pkiStatus=PENDING instead of issuing immediately, or FAILURE when the + * CSR signature does not verify; the client must poll with + * GetCertInitial. The test server's built-in policy auto-approves a + * pending request on the first poll that quotes its transactionID and + * is signed with the parked CSR's key. */ int scep_require_approval; /* SCEP CA roll-over. When set, the server advertises GetNextCACert From 19561d0b382db7616feb4e21fa2feee0f1677858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 30 Sep 2026 14:34:14 +0200 Subject: [PATCH 3/9] scep: require AES-128-CBC for SCEP builds RFC 8894 section 2.9 makes AES128-CBC mandatory-to-implement, but the build only rejected NO_AES. A wolfSSL built with NO_AES_128, NO_AES_CBC or NO_AES_DECRYPT still configured with SCEP enabled. The client then refused an explicit AES-128 request and, in AUTO mode, sent 3DES even to a CA advertising AES, while the test server silently dropped to 3DES and stopped advertising SCEPStandard. Reject such a wolfSSL whenever SCEP is enabled, in check_config.h, configure.ac and CMake alike, and drop the AES-128 guards from the client cipher selection and the 3DES branch from the server. The 3DES fallback for a CA that does not advertise AES is unchanged. The check_config.h rule sits with the other wolfSSL feature checks, so WOLFCERT_NO_WOLFSSL_FEATURE_CHECK skips it as it skips NO_AES. A new neg-no-aes128 wolfSSL config and no-aes128-scep case in assert-configure-fails.sh keep both build systems rejecting it. EMBEDDED.md, user_settings.h.example and the Zephyr WOLFCERT_SCEP help state the requirement, and the SCEP tests lose the AES-128 guards that are now always true. Fixes F-12894. --- CLAUDE.md | 3 +++ CMakeLists.txt | 17 ++++++++++++ configure.ac | 14 ++++++++++ docs/CI.md | 8 +++--- docs/EMBEDDED.md | 5 ++-- examples/user_settings.h.example | 3 ++- scripts/ci/assert-configure-fails.sh | 4 ++- scripts/ci/build-wolfssl.sh | 6 ++++- src/scep/scep_client.c | 24 +++++------------ src/scep/scep_server.c | 14 +++------- tests/integration/test_scep_roundtrip.c | 36 +++---------------------- tests/unit/test_scep_msg.c | 11 +------- wolfcert/check_config.h | 5 ++++ zephyr/Kconfig | 4 ++- 14 files changed, 73 insertions(+), 81 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5fac90d..597f220 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,6 +32,9 @@ configure time if the installed wolfSSL lacks any of `HAVE_PKCS7`, `WOLFSSL_KEY_GEN`, `WOLF_CRYPTO_CB`, `WOLFSSL_BASE64_ENCODE`, `WOLFSSL_ALT_NAMES`, or `WOLFSSL_CERT_NAME_ALL`, or if it was built with `NO_AES` / `NO_SHA256`, or if it provides neither TLS 1.2 nor TLS 1.3. +With SCEP enabled it also needs AES-128-CBC encrypt and decrypt +(`NO_AES_128`, `NO_AES_CBC` or `NO_AES_DECRYPT` hard-fail), since RFC 8894 +makes it mandatory-to-implement. It also link-probes the `WOLFSSL_ASN_API` helpers it calls, which a shared libwolfssl exports only under one of `WOLFSSL_PUBLIC_ASN` (the lean choice), `OPENSSL_EXTRA`, `OPENSSL_EXTRA_X509_SMALL` or `WOLFSSL_TEST_CERT`; a static diff --git a/CMakeLists.txt b/CMakeLists.txt index 30c8e8e..7141436 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -227,6 +227,23 @@ else() "the rsa:* key type will return WOLFCERT_ERR_UNSUPPORTED.") endif() +# AES-128-CBC: mandatory-to-implement for SCEP (RFC 8894 section 2.9). +if(WOLFCERT_ENABLE_SCEP) + unset(_have_aes128cbc CACHE) + check_c_source_compiles("${_wc_wolfssl_prologue} + #if !defined(WOLFSSL_AES_128) || !defined(HAVE_AES_CBC) || !defined(HAVE_AES_DECRYPT) + #error noaes128cbc + #endif + int main(void) { return 0; } + " _have_aes128cbc) + if(NOT _have_aes128cbc) + message(FATAL_ERROR + "wolfCert SCEP requires AES-128-CBC (RFC 8894). Rebuild wolfSSL " + "without NO_AES_128, NO_AES_CBC or NO_AES_DECRYPT, or set " + "-DWOLFCERT_ENABLE_SCEP=OFF for an EST-only build.") + endif() +endif() + # TLS: the HTTPS transport needs at least TLS 1.2 or TLS 1.3; the endpoints pin # their floor to whichever lower version is available. unset(_have_tls12 CACHE) diff --git a/configure.ac b/configure.ac index 3a3aea3..31b57d1 100644 --- a/configure.ac +++ b/configure.ac @@ -269,6 +269,20 @@ AS_IF([test "x$have_rsa" = "xno"], RSA and the rsa:* key type will return WOLFCERT_ERR_UNSUPPORTED.])])]) AM_CONDITIONAL([WOLFCERT_HAVE_RSA], [test "x$have_rsa" = "xyes"]) +# AES-128-CBC: mandatory-to-implement for SCEP (RFC 8894 section 2.9). +AS_IF([test "x$enable_scep" = "xyes"], + [AC_MSG_CHECKING([whether wolfSSL provides AES-128-CBC]) + AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM(WOLFCERT_WOLFSSL_PROLOGUE, + [[#if !defined(WOLFSSL_AES_128) || !defined(HAVE_AES_CBC) || !defined(HAVE_AES_DECRYPT) + #error noaes128cbc + #endif]])], + [AC_MSG_RESULT([yes])], + [AC_MSG_RESULT([no]) + AC_MSG_ERROR([wolfCert SCEP requires AES-128-CBC (RFC 8894). Rebuild + wolfSSL without NO_AES_128, NO_AES_CBC or NO_AES_DECRYPT, or configure + with --disable-scep.])])]) + # TLS: the HTTPS transport needs at least TLS 1.2 or TLS 1.3; the endpoints pin # their floor to whichever lower version is available. AC_MSG_CHECKING([whether wolfSSL provides TLS 1.2]) diff --git a/docs/CI.md b/docs/CI.md index 0f8a624..cee0e21 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -59,10 +59,10 @@ make -j && make check The negative-config gate asserts wolfCert's configure hard-fails on an unsupportable wolfSSL. It covers the cases a *buildable* wolfSSL can express -(`no-rsa-scep`, `no-pkcs7`, `no-public-asn`); wolfSSL itself refuses to drop -AES / SHA-256 / all TLS / all key algorithms, so wolfCert's compile-time -`#error` guards for those (`wolfcert/check_config.h`) are validated at compile -time, not by this gate. +(`no-rsa-scep`, `no-pkcs7`, `no-public-asn`, `no-aes128-scep`); wolfSSL +itself refuses to drop AES / SHA-256 / all TLS / all key algorithms, so +wolfCert's compile-time `#error` guards for those (`wolfcert/check_config.h`) +are validated at compile time, not by this gate. ```sh scripts/ci/assert-configure-fails.sh # all cases, both build systems diff --git a/docs/EMBEDDED.md b/docs/EMBEDDED.md index abb466f..d67e2ba 100644 --- a/docs/EMBEDDED.md +++ b/docs/EMBEDDED.md @@ -33,8 +33,9 @@ never `#define ... 0`, because the code tests presence with `#ifdef`.** result at compile time, so a contradictory or incomplete config fails with a clear `#error` rather than a confusing downstream error. It enforces the same rules the configure step does: at least one key algorithm, SCEP requires RSA -(RFC 8894), and the wolfSSL feature set wolfCert depends on (PKCS#7, cert -gen/req/ext, key gen, CryptoCb, base64 encode, alt names, +and AES-128-CBC (both RFC 8894, so `NO_AES_128`, `NO_AES_CBC` and +`NO_AES_DECRYPT` rule SCEP out), and the wolfSSL feature set wolfCert depends +on (PKCS#7, cert gen/req/ext, key gen, CryptoCb, base64 encode, alt names, `WOLFSSL_CERT_NAME_ALL`, AES, SHA-256, and TLS 1.2 or 1.3). The header you copy documents the matching wolfSSL configure flags. diff --git a/examples/user_settings.h.example b/examples/user_settings.h.example index 19fdbd6..e75eb73 100644 --- a/examples/user_settings.h.example +++ b/examples/user_settings.h.example @@ -44,7 +44,8 @@ * value of 0 still counts as enabled. * - At least one key algorithm must be enabled. * - SCEP is RSA-only (RFC 8894): enabling WOLFCERT_HAVE_SCEP requires - * WOLFCERT_HAVE_RSA. + * WOLFCERT_HAVE_RSA, and a wolfSSL with AES-128-CBC encrypt and decrypt + * (no NO_AES_128, NO_AES_CBC or NO_AES_DECRYPT). * * The enabled set must also be backed by the wolfSSL you link against. wolfCert * requires a wolfSSL (>= 5.9.4) built with at least: diff --git a/scripts/ci/assert-configure-fails.sh b/scripts/ci/assert-configure-fails.sh index 55cc970..c99438e 100755 --- a/scripts/ci/assert-configure-fails.sh +++ b/scripts/ci/assert-configure-fails.sh @@ -20,7 +20,8 @@ # of this gate. # # Scope: only misconfigurations that a *buildable* wolfSSL can express are -# covered here (RSA-off-with-SCEP, PKCS7 missing, ASN helpers not exported). +# covered here (RSA-off-with-SCEP, PKCS7 missing, ASN helpers not exported, +# AES-128 off with SCEP). # wolfSSL's own configure refuses to drop AES / SHA-256 / all TLS / all key # algorithms, so wolfCert's compile-time #error guards for those # (check_config.h) can't be fed by a real wolfSSL build and are not exercised @@ -56,6 +57,7 @@ CASE_TABLE=( "no-rsa-scep:neg-no-rsa:::SCEP is RSA-only" "no-pkcs7:neg-no-pkcs7:::HAVE_PKCS7|missing a required feature" "no-public-asn:neg-no-public-asn:::does not export its ASN helpers" + "no-aes128-scep:neg-no-aes128:::requires AES-128-CBC" ) lookup_case() { diff --git a/scripts/ci/build-wolfssl.sh b/scripts/ci/build-wolfssl.sh index 9b5d9eb..1749317 100755 --- a/scripts/ci/build-wolfssl.sh +++ b/scripts/ci/build-wolfssl.sh @@ -62,7 +62,7 @@ KNOWN_CONFIGS=( # wolfSSL's own configure refuses to drop AES/SHA-256/all-TLS/all-key-algs # (those are cascade-required), so wolfCert's compile-time #error guards for # them in check_config.h cannot be fed by a real wolfSSL build. - neg-no-rsa neg-no-pkcs7 neg-no-public-asn + neg-no-rsa neg-no-pkcs7 neg-no-public-asn neg-no-aes128 ) # Emit the configure argument list (one per line) for a config name. @@ -167,6 +167,10 @@ resolve_flags() { # No ASN-export macro -> "does not export its ASN helpers". _base_flags printf '%s\n' 'CPPFLAGS=-DWOLFSSL_ALT_NAMES -DWOLFSSL_CERT_NAME_ALL -DKEEP_PEER_CERT -DWOLFSSL_HAVE_TLS_UNIQUE' ;; + neg-no-aes128) + # NO_AES_128 with SCEP still requested -> "requires AES-128-CBC". + _base_flags + printf '%s\n' 'CPPFLAGS=-DWOLFSSL_ALT_NAMES -DWOLFSSL_CERT_NAME_ALL -DKEEP_PEER_CERT -DWOLFSSL_HAVE_TLS_UNIQUE -DWOLFSSL_PUBLIC_ASN -DNO_AES_128' ;; *) echo "ERROR: unknown wolfSSL config '$cfg'." >&2 echo " Known: ${KNOWN_CONFIGS[*]}" >&2 diff --git a/src/scep/scep_client.c b/src/scep/scep_client.c index b28c238..ad46efb 100644 --- a/src/scep/scep_client.c +++ b/src/scep/scep_client.c @@ -727,18 +727,13 @@ static int scep_prepare(void* heap, const WolfCertScepCaps* caps, * can talk to a peer that requires a particular algorithm (e.g. a wolfSCEP * deployment expecting AES-256). AUTO keeps the RFC 8894 default: the * GetCACaps "AES" keyword advertises AES-128-CBC; otherwise fall back to - * the mandatory-to-implement triple DES-CBC. A wolfSSL built without 3DES - * cannot serve 3DES, so reject that request/fallback with a clear error - * instead of a cryptic encoder failure. */ + * triple DES-CBC. A wolfSSL built without 3DES cannot serve 3DES, so + * reject that request/fallback with a clear error instead of a cryptic + * encoder failure. */ switch (cipher) { case WOLFCERT_SCEP_CIPHER_AES128: -#if !defined(WOLFSSL_AES_128) || !defined(HAVE_AES_CBC) - return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "scep", - "AES-128-CBC content cipher requested but wolfSSL lacks it"); -#else enc_oid = AES128CBCb; break; -#endif case WOLFCERT_SCEP_CIPHER_AES256: #if !defined(WOLFSSL_AES_256) || !defined(HAVE_AES_CBC) return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "scep", @@ -757,21 +752,14 @@ static int scep_prepare(void* heap, const WolfCertScepCaps* caps, #endif case WOLFCERT_SCEP_CIPHER_AUTO: default: - /* The "AES" capability names AES-128-CBC and nothing else, so a - * wolfSSL that cannot do AES-128 has to take the 3DES path even - * against an AES-advertising peer rather than silently substitute - * a cipher the CA never offered. */ -#if defined(WOLFSSL_AES_128) && defined(HAVE_AES_CBC) if (caps != NULL && caps->aes) { enc_oid = AES128CBCb; } - else -#endif - { + else { #ifdef NO_DES3 return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "scep", - "no usable content cipher: AES-128-CBC unavailable or " - "unadvertised, and wolfSSL lacks the 3DES fallback"); + "no usable content cipher: CA does not advertise AES and " + "wolfSSL lacks the 3DES fallback"); #else enc_oid = DES3b; #endif diff --git a/src/scep/scep_server.c b/src/scep/scep_server.c index c7f8204..b1b115d 100644 --- a/src/scep/scep_server.c +++ b/src/scep/scep_server.c @@ -46,17 +46,9 @@ /* Content-encryption cipher for the CertRep and its GetCACaps tokens. RFC 8894 * section 3.5.2: "AES" names AES128-CBC, and "SCEPStandard" implies "AES". */ -#if defined(WOLFSSL_AES_128) && defined(HAVE_AES_CBC) - #define SCEP_SRV_ENC_OID AES128CBCb - #define SCEP_SRV_CIPHER_CAP "AES\r\n" - #define SCEP_SRV_STD_CAP "SCEPStandard\r\n" -#elif !defined(NO_DES3) - #define SCEP_SRV_ENC_OID DES3b - #define SCEP_SRV_CIPHER_CAP "DES3\r\n" - #define SCEP_SRV_STD_CAP "" -#else - #error "wolfCert's SCEP test server needs AES-128-CBC or 3DES-CBC; rebuild wolfSSL with one of them, or configure without the test server" -#endif +#define SCEP_SRV_ENC_OID AES128CBCb +#define SCEP_SRV_CIPHER_CAP "AES\r\n" +#define SCEP_SRV_STD_CAP "SCEPStandard\r\n" typedef struct { /* rawbuf owns the request-line + header bytes read off the wire. It is diff --git a/tests/integration/test_scep_roundtrip.c b/tests/integration/test_scep_roundtrip.c index 26a1dfc..600fbd0 100644 --- a/tests/integration/test_scep_roundtrip.c +++ b/tests/integration/test_scep_roundtrip.c @@ -419,14 +419,6 @@ static int check_bad_csr_sig(const WolfCertServerCfg* cli, return rc; } -/* The content-cipher checks force an AES-CBC cipher, so they only exist when - * wolfSSL can supply one. */ -#if defined(HAVE_AES_CBC) && \ - (defined(WOLFSSL_AES_128) || defined(WOLFSSL_AES_256)) -#define WOLFCERT_TEST_HAVE_CIPHER_OVERRIDE -#endif - -#ifdef WOLFCERT_TEST_HAVE_CIPHER_OVERRIDE /* proto_opts.scep.content_cipher override: enrolling with an explicit * cipher must still issue a cert - the server de-envelops whatever OID the * request carries - proving AES-256 (and explicit AES-128) interoperate. */ @@ -459,7 +451,6 @@ static int check_content_cipher(const WolfCertServerCfg* cli, wolfcert_key_free(key); return rc; } -#endif /* WOLFCERT_TEST_HAVE_CIPHER_OVERRIDE */ /* The listener canned_srv_thread() accepts on and the response it sends. */ struct canned_ctx { @@ -849,7 +840,6 @@ static void* msgtype_srv_thread(void* arg) return NULL; } -#ifdef WOLFCERT_TEST_HAVE_CIPHER_OVERRIDE /* The end-to-end cipher check above only proves the server de-enveloped * whatever arrived, which it does for any OID, so it would pass even if the * override were ignored. Read the algorithm off the wire instead. */ @@ -891,7 +881,6 @@ static int check_content_cipher_wire(const WolfCertScepCaps* caps, REQUIRE(strcmp(mc.cipher, expect) == 0); return 0; } -#endif /* WOLFCERT_TEST_HAVE_CIPHER_OVERRIDE */ /* proto_opts.scep.renewal_msg_type picks the messageType a renewal carries, * while the signer stays the certificate being replaced either way. Default is @@ -1064,7 +1053,6 @@ static int check_getnextca_ca_id(const uint8_t* ca_der_buf, size_t ca_der_len) return 0; } -#if defined(HAVE_AES_CBC) && defined(WOLFSSL_AES_128) /* RFC 8894 section 3.2.1 requires transactionID and a fresh senderNonce in every * pkiMessage; the client always sends both, so POST hand-built ones instead. */ static int check_required_attrs(WolfCertServer* s, const WolfCertKeyCfg* kcfg, @@ -1377,8 +1365,6 @@ static int check_malformed_dispatch(uint16_t port, const WolfCertKeyCfg* kcfg, return rc; } -#endif /* HAVE_AES_CBC && WOLFSSL_AES_128 */ - int main(void) { REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); @@ -1406,13 +1392,8 @@ int main(void) REQUIRE(caps.post_pki_operation); REQUIRE(caps.sha256); -#if defined(WOLFSSL_AES_128) && defined(HAVE_AES_CBC) REQUIRE(caps.aes == 1); REQUIRE(caps.scep_standard == 1); -#else - REQUIRE(caps.aes == 0); - REQUIRE(caps.scep_standard == 0); -#endif REQUIRE(caps.renewal); WolfCertBuffer ca_pem = { 0 }; @@ -1490,18 +1471,16 @@ int main(void) wolfcert_buffer_free(&issued_hash); /* ---- Content-cipher override: explicit AES-256 and AES-128 both enroll. - * Each half needs the cipher wolfSSL was actually built with; scep_prepare - * returns WOLFCERT_ERR_UNSUPPORTED for one the library cannot do. */ -#if defined(WOLFSSL_AES_256) && defined(HAVE_AES_CBC) + * AES-256 needs wolfSSL built with it; scep_prepare returns + * WOLFCERT_ERR_UNSUPPORTED otherwise. */ +#if defined(WOLFSSL_AES_256) REQUIRE(check_content_cipher(&cli, &caps, &kcfg, ca_der->buffer, ca_der->length, WOLFCERT_SCEP_CIPHER_AES256) == WOLFCERT_OK); #endif -#if defined(WOLFSSL_AES_128) && defined(HAVE_AES_CBC) REQUIRE(check_content_cipher(&cli, &caps, &kcfg, ca_der->buffer, ca_der->length, WOLFCERT_SCEP_CIPHER_AES128) == WOLFCERT_OK); -#endif /* ---- Renewal messageType. The signer is the certificate being replaced * in both cases; only the attribute changes, and the in-tree server routes @@ -1567,18 +1546,16 @@ int main(void) /* ...and the same options read off the wire, since the server de-envelops * any OID and so cannot tell an honoured override from an ignored one. */ -#if defined(WOLFSSL_AES_256) && defined(HAVE_AES_CBC) +#if defined(WOLFSSL_AES_256) REQUIRE(check_content_cipher_wire(&caps, &kcfg, ca_der->buffer, ca_der->length, WOLFCERT_SCEP_CIPHER_AES256, "aes256") == 0); #endif -#if defined(WOLFSSL_AES_128) && defined(HAVE_AES_CBC) REQUIRE(check_content_cipher_wire(&caps, &kcfg, ca_der->buffer, ca_der->length, WOLFCERT_SCEP_CIPHER_AES128, "aes128") == 0); -#endif /* The session captures the SCEP options at open, so that path needs its own * check rather than inheriting the one-shot coverage above. */ @@ -1625,17 +1602,12 @@ int main(void) REQUIRE(raw_http_status(wolfcert_server_port(s), "/scep?operation=PKIOperation&message=QUJD", "XYZ") == 400); /* body freed */ -#if defined(HAVE_AES_CBC) && defined(WOLFSSL_AES_128) REQUIRE(check_required_attrs(s, &kcfg, ca_der->buffer, ca_der->length) == WOLFCERT_OK); REQUIRE(check_malformed_dispatch(wolfcert_server_port(s), &kcfg, ca_der->buffer, ca_der->length) == WOLFCERT_OK); -#else - printf("SKIP required-attrs and malformed-dispatch " - "(wolfSSL built without AES-128-CBC)\n"); -#endif #ifdef WOLFCERT_HAVE_ED25519 /* Ed25519 signer must be rejected cleanly (RFC 8894 requires RSA). */ diff --git a/tests/unit/test_scep_msg.c b/tests/unit/test_scep_msg.c index 6b870ce..f2ed30c 100644 --- a/tests/unit/test_scep_msg.c +++ b/tests/unit/test_scep_msg.c @@ -2025,9 +2025,6 @@ static int test_scep_rejects_est_cfg(void) return 0; } -/* Only meaningful where wolfSSL can actually run an AES-CBC content cipher. */ -#if defined(HAVE_AES_CBC) && \ - (defined(WOLFSSL_AES_128) || defined(WOLFSSL_AES_256)) /* The content-cipher choice reaches the wire: enveloping with AES256CBCb / * AES128CBCb yields a message carrying the matching AES-CBC OID. */ static int test_envelop_cipher_oid(void) @@ -2045,7 +2042,7 @@ static int test_envelop_cipher_oid(void) size_t ca_len = 0, key_len = 0; REQUIRE(make_ca(&ca_der, &ca_len, &key_der, &key_len) == 0); -#if defined(WOLFSSL_AES_256) && defined(HAVE_AES_CBC) +#if defined(WOLFSSL_AES_256) WolfCertBuffer env256 = { 0 }; REQUIRE(wolfcert_scep_envelop(ca_der, ca_len, payload, sizeof(payload), AES256CBCb, &env256, NULL) == WOLFCERT_OK); @@ -2054,19 +2051,16 @@ static int test_envelop_cipher_oid(void) wolfcert_buffer_free(&env256); #endif -#if defined(WOLFSSL_AES_128) && defined(HAVE_AES_CBC) WolfCertBuffer env128 = { 0 }; REQUIRE(wolfcert_scep_envelop(ca_der, ca_len, payload, sizeof(payload), AES128CBCb, &env128, NULL) == WOLFCERT_OK); REQUIRE(memmem(env128.data, env128.len, OID_AES128, sizeof(OID_AES128)) != NULL); wolfcert_buffer_free(&env128); -#endif free(ca_der); free(key_der); return 0; } -#endif /* HAVE_AES_CBC && (WOLFSSL_AES_128 || WOLFSSL_AES_256) */ static const byte scep_oid_msg_type[] = { 0x06,0x0A,0x60,0x86,0x48,0x01,0x86,0xF8,0x45,0x01,0x09,0x02 }; @@ -2432,11 +2426,8 @@ int main(void) return 1; if (test_scep_rejects_est_cfg()) return 1; -#if defined(HAVE_AES_CBC) && \ - (defined(WOLFSSL_AES_128) || defined(WOLFSSL_AES_256)) if (test_envelop_cipher_oid()) return 1; -#endif if (test_ca_fingerprint()) return 1; if (test_pki_get_url()) diff --git a/wolfcert/check_config.h b/wolfcert/check_config.h index e5cc370..8ae17ae 100644 --- a/wolfcert/check_config.h +++ b/wolfcert/check_config.h @@ -131,6 +131,11 @@ #ifdef NO_SHA256 #error "wolfSSL was built with NO_SHA256; wolfCert requires SHA-256. Rebuild wolfSSL with --enable-sha256." #endif +/* RFC 8894 section 2.9 makes AES128-CBC mandatory-to-implement for SCEP. */ +#if defined(WOLFCERT_HAVE_SCEP) && (!defined(WOLFSSL_AES_128) || \ + !defined(HAVE_AES_CBC) || !defined(HAVE_AES_DECRYPT)) +#error "wolfCert SCEP requires AES-128-CBC (RFC 8894). Rebuild wolfSSL without NO_AES_128, NO_AES_CBC or NO_AES_DECRYPT, or drop WOLFCERT_HAVE_SCEP." +#endif /* wc_ConstantCompare backs every constant-time comparison of secret material * (SCEP CA fingerprint / challenge password, EST Basic-auth credential). A * wolfSSL built WOLFSSL_NO_CONST_CMP drops the symbol, so catch it here with a diff --git a/zephyr/Kconfig b/zephyr/Kconfig index 9f0c284..c53394b 100644 --- a/zephyr/Kconfig +++ b/zephyr/Kconfig @@ -32,7 +32,9 @@ config WOLFCERT_SCEP bool "SCEP (RFC 8894) support" depends on WOLFCERT_RSA help - Enable the SCEP client. + Enable the SCEP client. RFC 8894 makes AES-128-CBC mandatory, so the + wolfSSL settings file must not define NO_AES_128, NO_AES_CBC or + NO_AES_DECRYPT. config WOLFCERT_BUILTIN_TRANSPORT bool "Built-in BSD-socket transport" From b2426bd533a9e68e4cfe7ceefd6e4fc87255aa3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 30 Sep 2026 14:40:10 +0200 Subject: [PATCH 4/9] scep: reject a non-PrintableString transactionID RFC 8894 types the transactionID attribute as a PrintableString, and the signed-attribute encoder tags it 0x13 without looking at its bytes. The GetCertInitial entry points accepted any caller-supplied byte sequence, so a value holding characters such as '_', control bytes or NUL went on the wire as a malformed PrintableString. The test server likewise copied a request's transactionID into its CertRep unchecked. The IDs the library generates are hex and always conform, so only an inherited or peer-supplied ID can trip this. Check the repertoire in enc_printable_n, which writes every 0x13 attribute (messageType, transactionID, pkiStatus, failInfo), and return WOLFCERT_ERR_BAD_ARG from wolfcert_scep_build_pki_message. That runs before the POST, so the one-shot and both session GetCertInitial paths still fail before any network I/O. The check lives in wolfcert_is_printable_string in internal.c so the CSR subject can reuse it. The test server rejects a non-PrintableString transactionID with HTTP 400 alongside the other required-attribute checks. The existing long-ID poll tests filled their buffers with 0x11 and now use 'A', and the unit and raw-POST tests that used binary transactionIDs now use hex digits. New cases cover '_', '@', 0x11, 0x80 and NUL on the one-shot API, '_' on the blocking session API, and a transactionID made of the allowed punctuation, which reaches the server. Since the encoder can no longer produce one, the roundtrip test signs a PKCSReq with an '@' transactionID through wolfSSL directly and expects the server's 400, with a valid transactionID as the control. Fixes F-12895. --- src/internal.c | 14 ++ src/internal.h | 3 + src/scep/scep_msg.c | 18 ++- src/scep/scep_server.c | 5 +- tests/integration/test_scep_async_roundtrip.c | 10 +- tests/integration/test_scep_poll_roundtrip.c | 34 ++++- tests/integration/test_scep_roundtrip.c | 137 +++++++++++++++++- tests/unit/test_scep_msg.c | 2 +- wolfcert/scep.h | 7 +- 9 files changed, 213 insertions(+), 17 deletions(-) diff --git a/src/internal.c b/src/internal.c index d3ae8b3..3fb08ef 100644 --- a/src/internal.c +++ b/src/internal.c @@ -94,6 +94,20 @@ char* wolfcert_strdup(const char* s, void* heap) return r; } +int wolfcert_is_printable_string(const uint8_t* s, size_t len) +{ + size_t i; + + for (i = 0; i < len; i++) { + uint8_t c = s[i]; + if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && + (c < '0' || c > '9') && + (c == 0 || strchr(" '()+,-./:=?", c) == NULL)) + return 0; + } + return 1; +} + const char* wolfcert_last_error_message(void) { return g_err.message; diff --git a/src/internal.h b/src/internal.h index 85292ef..8ecae99 100644 --- a/src/internal.h +++ b/src/internal.h @@ -522,6 +522,9 @@ WOLFCERT_TEST_VIS int wolfcert_scep_build_next_ca_response( int wolfcert_extract_spki(const uint8_t* der, size_t len, int is_csr, uint8_t** out_spki, size_t* out_len, void* heap); +/* 1 if every byte is in the X.680 PrintableString repertoire, else 0. */ +int wolfcert_is_printable_string(const uint8_t* s, size_t len); + /* RFC 8894: a CertRep must be signed by the CA or its RA. Confirm the response * signer certificate shares a public key with some certificate in the trusted * GetCACert bundle (one or more concatenated DER certs). Returns WOLFCERT_OK on diff --git a/src/scep/scep_msg.c b/src/scep/scep_msg.c index fb464b9..38c9cc3 100644 --- a/src/scep/scep_msg.c +++ b/src/scep/scep_msg.c @@ -114,16 +114,20 @@ static int der_put_len(byte* out, size_t cap, size_t n) * here would land SET { SET { ... } } on the wire, which every other * RFC 8894 implementation rejects. * - * Returns total bytes written, or -1 if `cap` is too small. */ + * Returns total bytes written, WOLFCERT_ERR_BAD_ARG if `v` is not a + * PrintableString, or WOLFCERT_ERR_MEMORY if `cap` is too small. */ static int enc_printable_n(const byte* v, size_t vl, byte* out, size_t cap) { + if (!wolfcert_is_printable_string(v, vl)) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "scep", + "attribute value is not a PrintableString"); if (cap < 1) - return -1; + return WOLFCERT_ERR_MEMORY; out[0] = 0x13; int ll = der_put_len(out + 1, cap - 1, vl); if (ll < 0 || 1 + (size_t)ll + vl > cap) - return -1; + return WOLFCERT_ERR_MEMORY; memcpy(out + 1 + ll, v, vl); return (int)(1 + (size_t)ll + vl); @@ -263,7 +267,7 @@ static int build_signed_attribs(const WolfCertScepAttrs* a, if (a->message_type != NULL) { int vl = enc_printable(a->message_type, scratch + off, scratch_cap - off); if (vl < 0) - return WOLFCERT_ERR_MEMORY; + return vl; attrs[n].oid = OID_MSG_TYPE; attrs[n].oidSz = sizeof(OID_MSG_TYPE); @@ -282,7 +286,7 @@ static int build_signed_attribs(const WolfCertScepAttrs* a, int vl = enc_printable_n(a->transaction_id, a->transaction_id_len, scratch + off, scratch_cap - off); if (vl < 0) - return WOLFCERT_ERR_MEMORY; + return vl; attrs[n].oid = OID_TRANS_ID; attrs[n].oidSz = sizeof(OID_TRANS_ID); @@ -320,7 +324,7 @@ static int build_signed_attribs(const WolfCertScepAttrs* a, if (a->pki_status != NULL) { int vl = enc_printable(a->pki_status, scratch + off, scratch_cap - off); if (vl < 0) - return WOLFCERT_ERR_MEMORY; + return vl; attrs[n].oid = OID_PKI_STATUS; attrs[n].oidSz = sizeof(OID_PKI_STATUS); @@ -332,7 +336,7 @@ static int build_signed_attribs(const WolfCertScepAttrs* a, if (a->fail_info != NULL) { int vl = enc_printable(a->fail_info, scratch + off, scratch_cap - off); if (vl < 0) - return WOLFCERT_ERR_MEMORY; + return vl; attrs[n].oid = OID_FAIL_INFO; attrs[n].oidSz = sizeof(OID_FAIL_INFO); diff --git a/src/scep/scep_server.c b/src/scep/scep_server.c index b1b115d..eb01969 100644 --- a/src/scep/scep_server.c +++ b/src/scep/scep_server.c @@ -1000,8 +1000,9 @@ static int handle_pki_op(WolfCertServer* s, int fd, const ScepRequest* req) /* RFC 8894 section 3.2.1 requires all three in every message, so one that * omits any of them is not a pkiMessage a CertRep could answer. */ - if (tid == NULL || tid_len == 0 || snonce == NULL || snonce_len == 0 || - mt == NULL || mt[0] == '\0') { + if (tid == NULL || tid_len == 0 || + !wolfcert_is_printable_string(tid, tid_len) || + snonce == NULL || snonce_len == 0 || mt == NULL || mt[0] == '\0') { s->keep_alive = 0; send_text(s, fd, 400, "Bad Message", "text/plain", ""); rc = WOLFCERT_ERR_PROTOCOL; diff --git a/tests/integration/test_scep_async_roundtrip.c b/tests/integration/test_scep_async_roundtrip.c index 21b0b51..c2d72e0 100644 --- a/tests/integration/test_scep_async_roundtrip.c +++ b/tests/integration/test_scep_async_roundtrip.c @@ -578,6 +578,7 @@ static int blocking_poll_path(WolfCertServer* s) WolfCertScepResult r1 = { 0 }; WolfCertScepResult r2 = { 0 }; WolfCertScepResult r3 = { 0 }; + WolfCertScepResult r4 = { 0 }; uint8_t long_tid[200]; int ret = 1; @@ -605,7 +606,7 @@ static int blocking_poll_path(WolfCertServer* s) /* A 200-byte transactionID reaches the server intact: the round trip * completes and the server answers FAILURE/badCertId for the unknown * transaction, rather than the client refusing the argument. */ - memset(long_tid, 0x11, sizeof(long_tid)); + memset(long_tid, 'A', sizeof(long_tid)); 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, @@ -613,6 +614,12 @@ static int blocking_poll_path(WolfCertServer* s) REQUIRE_CLEAN(r3.status == WOLFCERT_SCEP_STATUS_FAILURE); REQUIRE_CLEAN(r3.fail_info == 4); + long_tid[1] = '_'; + 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, + long_tid, sizeof(long_tid), &r4) == WOLFCERT_ERR_BAD_ARG); + ret = 0; cleanup: if (sess != NULL) @@ -620,6 +627,7 @@ static int blocking_poll_path(WolfCertServer* s) wolfcert_scep_result_free(&r1); wolfcert_scep_result_free(&r2); wolfcert_scep_result_free(&r3); + wolfcert_scep_result_free(&r4); if (ca_der != NULL) wc_FreeDer(&ca_der); wolfcert_buffer_free(&ca_pem); diff --git a/tests/integration/test_scep_poll_roundtrip.c b/tests/integration/test_scep_poll_roundtrip.c index 0140211..841ddb9 100644 --- a/tests/integration/test_scep_poll_roundtrip.c +++ b/tests/integration/test_scep_poll_roundtrip.c @@ -333,7 +333,7 @@ static int poll_path(WolfCertServer* s) * it, so a value far longer than the generated 32-hex one is sent on the * wire (FAILURE/badCertId again) instead of being rejected up front. */ uint8_t long_tid[200]; - memset(long_tid, 0x11, sizeof(long_tid)); + memset(long_tid, 'A', sizeof(long_tid)); WolfCertScepResult r4 = { 0 }; rc = wolfcert_scep_get_cert_initial(&cli, &caps, ca_der->buffer, ca_der->length, @@ -346,6 +346,38 @@ static int poll_path(WolfCertServer* s) REQUIRE(r4.status == WOLFCERT_SCEP_STATUS_FAILURE); REQUIRE(r4.fail_info == 4); + /* Step 5: '_', '@', control bytes and bytes above 0x7F are outside the + * PrintableString set; its punctuation goes on the wire. */ + static const uint8_t ok_tid[] = { 'A', '-', ':', '.', ' ', '\'', '?' }; + WolfCertScepResult r6 = { 0 }; + rc = wolfcert_scep_get_cert_initial(&cli, &caps, + ca_der->buffer, ca_der->length, + ca_der->buffer, ca_der->length, + NULL, 0, + dk, csr.data, csr.len, + ok_tid, sizeof(ok_tid), &r6); + REQUIRE(rc == WOLFCERT_OK); + REQUIRE(r6.status == WOLFCERT_SCEP_STATUS_FAILURE); + REQUIRE(r6.fail_info == 4); + wolfcert_scep_result_free(&r6); + + static const uint8_t bad_tid[][4] = { + { 'A', 'B', '_', 'C' }, { 'A', 'B', 0x11, 'C' }, { 'A', 'B', 0x00, 'C' }, + { 'A', 'B', '@', 'C' }, { 'A', 'B', 0x80, 'C' } + }; + for (size_t i = 0; i < sizeof(bad_tid) / sizeof(bad_tid[0]); i++) { + WolfCertScepResult r5 = { 0 }; + rc = wolfcert_scep_get_cert_initial(&cli, &caps, + ca_der->buffer, ca_der->length, + ca_der->buffer, ca_der->length, + NULL, 0, + dk, csr.data, csr.len, + bad_tid[i], sizeof(bad_tid[i]), + &r5); + wolfcert_scep_result_free(&r5); + REQUIRE(rc == WOLFCERT_ERR_BAD_ARG); + } + wolfcert_scep_result_free(&r1); wolfcert_scep_result_free(&r2); wolfcert_scep_result_free(&r3); diff --git a/tests/integration/test_scep_roundtrip.c b/tests/integration/test_scep_roundtrip.c index 600fbd0..94db09c 100644 --- a/tests/integration/test_scep_roundtrip.c +++ b/tests/integration/test_scep_roundtrip.c @@ -29,6 +29,7 @@ #include #include /* SHA256h */ #include +#include #include #include @@ -1081,7 +1082,7 @@ static int check_required_attrs(WolfCertServer* s, const WolfCertKeyCfg* kcfg, rc = wolfcert_scep_envelop(ca_der_buf, ca_der_len, csr.data, csr.len, AES128CBCb, &env, NULL); - memset(tid, 0x11, sizeof(tid)); + memset(tid, 'A', sizeof(tid)); memset(snonce, 0x22, sizeof(snonce)); /* Each round omits one required attribute; the last is the control that @@ -1199,6 +1200,134 @@ static int check_required_attrs(WolfCertServer* s, const WolfCertKeyCfg* kcfg, return rc; } +/* Sign a PKCSReq whose transactionID value is `tid` and POST it. The + * transactionID is tagged PrintableString whatever its bytes, which + * wolfcert_scep_build_pki_message refuses to encode. */ +static int post_raw_tid(uint16_t port, const uint8_t* signer, size_t signer_len, + const WolfCertBuffer* kder, const char* tid) +{ + static const byte oid_msg_type[] = + { 0x06,0x0A,0x60,0x86,0x48,0x01,0x86,0xF8,0x45,0x01,0x09,0x02 }; + static const byte oid_snonce[] = + { 0x06,0x0A,0x60,0x86,0x48,0x01,0x86,0xF8,0x45,0x01,0x09,0x05 }; + static const byte oid_tid[] = + { 0x06,0x0A,0x60,0x86,0x48,0x01,0x86,0xF8,0x45,0x01,0x09,0x07 }; + static const byte msg_type[] = { 0x13, 0x02, '1', '9' }; + static const byte junk[] = { 0x04, 0x02, 0xAB, 0xCD }; + byte tid_val[2 + 16]; + byte snonce[2 + 16]; + PKCS7Attrib attribs[3]; + PKCS7* p7 = NULL; + WC_RNG rng; + uint8_t* buf = NULL; + uint8_t* rsp = NULL; + size_t rsp_len = 0; + size_t tid_len = strlen(tid); + int n = 0; + int st = -1; + + if (tid_len > 16 || wc_InitRng(&rng) != 0) + return -1; + + tid_val[0] = 0x13; + tid_val[1] = (byte)tid_len; + memcpy(tid_val + 2, tid, tid_len); + snonce[0] = 0x04; + snonce[1] = 16; + memset(snonce + 2, 0x22, 16); + + attribs[0].oid = oid_msg_type; + attribs[0].oidSz = sizeof(oid_msg_type); + attribs[0].value = msg_type; + attribs[0].valueSz = sizeof(msg_type); + attribs[1].oid = oid_tid; + attribs[1].oidSz = sizeof(oid_tid); + attribs[1].value = tid_val; + attribs[1].valueSz = (word32)(2 + tid_len); + attribs[2].oid = oid_snonce; + attribs[2].oidSz = sizeof(oid_snonce); + attribs[2].value = snonce; + attribs[2].valueSz = sizeof(snonce); + + p7 = wc_PKCS7_New(NULL, INVALID_DEVID); + buf = (uint8_t*)malloc(8192); + if (p7 != NULL && buf != NULL && + wc_PKCS7_InitWithCert(p7, (byte*)signer, (word32)signer_len) == 0) { + p7->rng = &rng; + p7->privateKey = kder->data; + p7->privateKeySz = (word32)kder->len; + p7->encryptOID = RSAk; + p7->hashOID = SHA256h; + p7->content = (byte*)junk; + p7->contentSz = sizeof(junk); + p7->signedAttribs = attribs; + p7->signedAttribsSz = 3; + n = wc_PKCS7_EncodeSignedData(p7, buf, 8192); + } + + if (n > 0) + st = raw_http_req(port, "POST", "/scep?operation=PKIOperation", + "application/x-pki-message", buf, (size_t)n, 0, + &rsp, &rsp_len); + + free(rsp); + free(buf); + if (p7 != NULL) + wc_PKCS7_Free(p7); + wc_FreeRng(&rng); + return st; +} + +/* The server must reject a signed request whose transactionID is not a + * PrintableString; the same message with a valid one is the control. */ +static int check_unprintable_tid(uint16_t port, const WolfCertKeyCfg* kcfg) +{ + WolfCertCertMeta meta = { .subject_dn = "CN=scep-tid" }; + WolfCertKey* key = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertBuffer kder = { 0 }; + uint8_t* signer = NULL; + size_t signer_len = 0; + int st; + int rc; + + rc = wolfcert_key_generate(kcfg, &key); + if (rc == WOLFCERT_OK) + rc = wolfcert_csr_build(key, &meta, &csr); + if (rc == WOLFCERT_OK) + rc = wolfcert_key_to_der(key, &kder); + if (rc == WOLFCERT_OK) + rc = wolfcert_scep_self_signed_rsa((RsaKey*)key->impl, csr.data, + csr.len, &signer, &signer_len, NULL); + + if (rc == WOLFCERT_OK) { + st = post_raw_tid(port, signer, signer_len, &kder, "tid@1"); + if (st != 400) { + fprintf(stderr, "FAIL %s:%d '@' transactionID got status %d\n", + __FILE__, __LINE__, st); + rc = -1; + } + } + + /* The control reaches de-enveloping, which answers the junk content with + * a signed FAILURE. */ + if (rc == WOLFCERT_OK) { + st = post_raw_tid(port, signer, signer_len, &kder, "tid-1"); + if (st != 200) { + fprintf(stderr, "FAIL %s:%d control transactionID got status %d\n", + __FILE__, __LINE__, st); + rc = -1; + } + } + + WOLFCERT_XFREE(signer, NULL); + wolfcert_buffer_free(&kder); + wolfcert_buffer_free(&csr); + wolfcert_key_free(key); + + return rc; +} + /* handle_pki_op's dispatch failures answer with a signed CertRep FAILURE, not * a bare HTTP error. The client cannot produce these messages, so POST * hand-built ones. Owns and frees everything it makes. */ @@ -1605,6 +1734,9 @@ int main(void) REQUIRE(check_required_attrs(s, &kcfg, ca_der->buffer, ca_der->length) == WOLFCERT_OK); + REQUIRE(check_unprintable_tid(wolfcert_server_port(s), &kcfg) + == WOLFCERT_OK); + REQUIRE(check_malformed_dispatch(wolfcert_server_port(s), &kcfg, ca_der->buffer, ca_der->length) == WOLFCERT_OK); @@ -1728,7 +1860,8 @@ int main(void) uint8_t snonce[16], rnonce[16]; memset(snonce, 0x5A, sizeof(snonce)); memset(rnonce, 0xA5, sizeof(rnonce)); - const uint8_t wtid[16] = { 0 }; + const uint8_t wtid[16] = + { '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' }; WolfCertScepAttrs wattrs = { .transaction_id = wtid, .transaction_id_len = sizeof(wtid), .sender_nonce = snonce, .sender_nonce_len = sizeof(snonce), diff --git a/tests/unit/test_scep_msg.c b/tests/unit/test_scep_msg.c index f2ed30c..0d7ec8b 100644 --- a/tests/unit/test_scep_msg.c +++ b/tests/unit/test_scep_msg.c @@ -368,7 +368,7 @@ static int check_no_envelope(const uint8_t* ca_der, size_t ca_len, static const uint8_t ENVELOPED_OID[] = { 0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x07,0x03 }; static const uint8_t tid[16] = - { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 }; + { '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' }; uint8_t sn[16]; uint8_t rn[16]; diff --git a/wolfcert/scep.h b/wolfcert/scep.h index cb7a23c..df0ea57 100644 --- a/wolfcert/scep.h +++ b/wolfcert/scep.h @@ -115,8 +115,8 @@ typedef struct { WolfCertScepStatus status; /* cert_pem is owned and populated iff status == SUCCESS. */ WolfCertBuffer cert_pem; - /* transaction_id (binary) is owned and populated whenever the server - * returns a CertRep; callers echo it back via get_cert_initial. */ + /* transaction_id (PrintableString) is owned and populated whenever the + * server returns a CertRep; callers echo it back via get_cert_initial. */ uint8_t* transaction_id; size_t transaction_id_len; /* RFC 8894 section 3.2.1.4 failInfo; meaningful only when status==FAILURE. @@ -202,7 +202,8 @@ WOLFCERT_API int wolfcert_scep_renewal_req(const WolfCertServerCfg* srv, * * `transaction_id` must be the value returned by the prior request. It is * carried verbatim and the client imposes no length of its own, so whatever - * the server chose is echoed back to it unchanged. + * the server chose is echoed back to it unchanged. A value outside the + * PrintableString character set returns WOLFCERT_ERR_BAD_ARG. * `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). */ WOLFCERT_API int wolfcert_scep_get_cert_initial(const WolfCertServerCfg* srv, From a3aca7f3cd29077f8de46c3f1b71492b41ab7a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 30 Sep 2026 14:46:23 +0200 Subject: [PATCH 5/9] scep: reject a senderNonce that is not 16 bytes The test server checked only that a PKIOperation carried a non-empty senderNonce, and the parser keeps whatever length the request supplied. Every dispatch path hands that nonce to send_cert_rep, which echoes it as the CertRep recipientNonce, so a signed request with a short or long senderNonce was processed, possibly issued, and answered with a malformed recipientNonce. RFC 8894 section 3.2.1 fixes the senderNonce at 16 bytes. Extend the required-attribute check in handle_pki_op to reject any senderNonce whose length is not 16 with HTTP 400, ahead of de-enveloping, issuance and any pending-queue change. check_required_attrs in the SCEP roundtrip test now posts an 8-byte and a 17-byte senderNonce and expects both to be rejected. Fixes F-12896. --- src/internal.h | 2 ++ src/scep/scep_client.c | 2 -- src/scep/scep_server.c | 8 +++++--- tests/integration/test_scep_roundtrip.c | 22 ++++++++++++++++------ 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/internal.h b/src/internal.h index 8ecae99..7458fc1 100644 --- a/src/internal.h +++ b/src/internal.h @@ -410,6 +410,8 @@ WOLFCERT_TEST_VIS size_t wolfcert_oid_to_dotted(const uint8_t* oid, size_t oid_l char* out, size_t out_cap); /* SCEP pkiMessage helpers. */ +#define SCEP_NONCE_SZ 16 + typedef struct { const uint8_t* transaction_id; size_t transaction_id_len; diff --git a/src/scep/scep_client.c b/src/scep/scep_client.c index ad46efb..e47f68d 100644 --- a/src/scep/scep_client.c +++ b/src/scep/scep_client.c @@ -588,8 +588,6 @@ static const char* scep_renewal_msg_type(WolfCertScepRenewalMsgType m) return (m == WOLFCERT_SCEP_RENEWAL_MSG_PKCS_REQ) ? "19" : "17"; } -/* Shared SCEP round-trip sizes. */ -#define SCEP_NONCE_SZ 16 /* A random transactionID is 16 RNG bytes expanded to 32 hex characters. */ #define SCEP_TXID_RAND_SZ 16 diff --git a/src/scep/scep_server.c b/src/scep/scep_server.c index eb01969..559812e 100644 --- a/src/scep/scep_server.c +++ b/src/scep/scep_server.c @@ -998,11 +998,13 @@ static int handle_pki_op(WolfCertServer* s, int fd, const ScepRequest* req) goto out; } - /* RFC 8894 section 3.2.1 requires all three in every message, so one that - * omits any of them is not a pkiMessage a CertRep could answer. */ + /* RFC 8894 section 3.2.1 requires all three in every message, with a + * PrintableString transactionID and a 16-byte senderNonce, or no CertRep + * could answer it. */ if (tid == NULL || tid_len == 0 || !wolfcert_is_printable_string(tid, tid_len) || - snonce == NULL || snonce_len == 0 || mt == NULL || mt[0] == '\0') { + snonce == NULL || snonce_len != SCEP_NONCE_SZ || + mt == NULL || mt[0] == '\0') { s->keep_alive = 0; send_text(s, fd, 400, "Bad Message", "text/plain", ""); rc = WOLFCERT_ERR_PROTOCOL; diff --git a/tests/integration/test_scep_roundtrip.c b/tests/integration/test_scep_roundtrip.c index 94db09c..8f15e98 100644 --- a/tests/integration/test_scep_roundtrip.c +++ b/tests/integration/test_scep_roundtrip.c @@ -1066,7 +1066,7 @@ static int check_required_attrs(WolfCertServer* s, const WolfCertKeyCfg* kcfg, WolfCertBuffer env = { 0 }; uint8_t* signer = NULL; size_t signer_len = 0; - uint8_t tid[16], snonce[16]; + uint8_t tid[16], snonce[16], snonce_long[17]; size_t i; int rc; @@ -1084,10 +1084,11 @@ static int check_required_attrs(WolfCertServer* s, const WolfCertKeyCfg* kcfg, memset(tid, 'A', sizeof(tid)); memset(snonce, 0x22, sizeof(snonce)); + memset(snonce_long, 0x33, sizeof(snonce_long)); - /* Each round omits one required attribute; the last is the control that - * proves this raw-POST harness reaches the issuance path at all. */ - for (i = 0; rc == WOLFCERT_OK && i < 7; ++i) { + /* Each round omits or mis-sizes one required attribute; the last is the + * control that proves this raw-POST harness reaches the issuance path. */ + for (i = 0; rc == WOLFCERT_OK && i < 9; ++i) { WolfCertScepAttrs a = { .message_type = i == 4 ? NULL : i == 5 ? "" : "19" }; WolfCertBuffer msg = { 0 }; @@ -1118,6 +1119,15 @@ static int check_required_attrs(WolfCertServer* s, const WolfCertKeyCfg* kcfg, a.transaction_id = tid; a.transaction_id_len = sizeof(tid); a.sender_nonce = snonce; a.sender_nonce_len = sizeof(snonce); } + else if (i == 6) { /* short senderNonce */ + a.transaction_id = tid; a.transaction_id_len = sizeof(tid); + a.sender_nonce = snonce; a.sender_nonce_len = 8; + } + else if (i == 7) { /* long senderNonce */ + a.transaction_id = tid; a.transaction_id_len = sizeof(tid); + a.sender_nonce = snonce_long; + a.sender_nonce_len = sizeof(snonce_long); + } else { /* control: all three present */ a.transaction_id = tid; a.transaction_id_len = sizeof(tid); a.sender_nonce = snonce; a.sender_nonce_len = sizeof(snonce); @@ -1135,8 +1145,8 @@ static int check_required_attrs(WolfCertServer* s, const WolfCertKeyCfg* kcfg, msg.data, msg.len, 0, &rsp, &rsp_len); wolfcert_buffer_free(&msg); - if (i < 6) { - /* An attribute that is absent or empty is not a pkiMessage. */ + if (i < 8) { + /* An absent, empty or missized attribute is not a pkiMessage. */ ok = (st == 400); } else { From 90de2ea5c11d0c515a2157497fecaeb7c8ace8fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 30 Sep 2026 14:57:12 +0200 Subject: [PATCH 6/9] Reject a bind_host that is not a numeric IPv4 address wolfcert_server_start fell back to INADDR_ANY whenever inet_pton could not parse bind_host, so a hostname such as localhost, an IPv6 literal, an empty string or a mistyped address made the server listen on every IPv4 interface instead of the one the operator asked for. The CLI passes --listen through unchanged, so --listen localhost:8080 exposed the enrollment endpoint externally. Parse bind_host before any allocation and return WOLFCERT_ERR_BAD_ARG when it is not a numeric IPv4 address. Only a NULL bind_host now binds all interfaces. Checking up front also stops a bad address from generating and persisting a CA before the start fails. The wolfcert-server --listen help now says the address must be numeric IPv4, and a failed start prints the library's error detail, so --listen localhost:PORT names the reason instead of only "Bad argument". The unit test starts each bad address against a memory store and checks no CA was written to it, and starts once with a NULL bind_host. cli_proto_scoping.sh checks the refusal reason reaches stderr. Fixes F-9813. --- cli/wolfcert_server.c | 7 +++-- src/internal.h | 1 - src/server.c | 17 ++++++------ tests/integration/cli_proto_scoping.sh | 20 ++++++++++++++ tests/unit/test_server_ca_store.c | 37 ++++++++++++++++++++++++++ wolfcert/server.h | 4 +-- 6 files changed, 73 insertions(+), 13 deletions(-) diff --git a/cli/wolfcert_server.c b/cli/wolfcert_server.c index ee0e162..313ec11 100644 --- a/cli/wolfcert_server.c +++ b/cli/wolfcert_server.c @@ -58,7 +58,7 @@ static void print_usage(FILE* out) { fprintf(out, "wolfcert-server %s\n" - "Usage: wolfcert-server --proto est|scep [--listen HOST:PORT]\n" + "Usage: wolfcert-server --proto est|scep [--listen ADDR:PORT]\n" " [--basic USER:PASS] [--challenge PASS]\n" " [--tls-cert PEM --tls-key PEM [--tls-client-ca PEM]]\n" " [--scep-require-approval] [--scep-enable-next-ca]\n" @@ -66,7 +66,7 @@ static void print_usage(FILE* out) "\n" "Options:\n" " --proto est|scep Protocol to serve (required)\n" - " --listen HOST:PORT Bind address (default 0.0.0.0:8080)\n" + " --listen ADDR:PORT Numeric IPv4 bind address (default 0.0.0.0:8080)\n" " --basic USER:PASS Require HTTP Basic auth (EST enroll); both non-empty\n" " --challenge PASS Require this SCEP challengePassword in the CSR\n" " --tls-cert PEMFILE Terminate TLS with this server certificate (PEM);\n" @@ -394,7 +394,10 @@ int main(int argc, char** argv) int rc = wolfcert_server_start(&cfg, &g_server); if (rc != WOLFCERT_OK) { + const char* m = wolfcert_last_error_message(); fprintf(stderr, "wolfcert-server: start failed (%s)\n", wolfcert_strerror(rc)); + if (m != NULL && *m != '\0') + fprintf(stderr, "wolfcert-server: %s\n", m); goto out; } diff --git a/src/internal.h b/src/internal.h index 7458fc1..b9a36e8 100644 --- a/src/internal.h +++ b/src/internal.h @@ -178,7 +178,6 @@ typedef struct { struct WolfCertServer { WolfCertServerCfgSrv cfg; - char* cfg_bind_host; char* cfg_challenge; char* cfg_basic_user; char* cfg_basic_pass; diff --git a/src/server.c b/src/server.c index 497432e..d46131f 100644 --- a/src/server.c +++ b/src/server.c @@ -301,6 +301,12 @@ int wolfcert_server_start(const WolfCertServerCfgSrv* cfg, WolfCertServer** out) "EST enrollment needs http_basic_user or tls_client_ca_pem, " "or est_allow_anonymous_enroll"); + struct in_addr bind_addr = { .s_addr = htonl(INADDR_ANY) }; + if (cfg->bind_host != NULL && + inet_pton(AF_INET, cfg->bind_host, &bind_addr) != 1) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "server", + "bind_host \"%s\" is not a numeric IPv4 address", cfg->bind_host); + void* heap = cfg->heap ? cfg->heap : wolfcert_default_heap(); WolfCertServer* s = (WolfCertServer*)WOLFCERT_XMALLOC(sizeof(*s), heap); if (s == NULL) @@ -317,8 +323,6 @@ int wolfcert_server_start(const WolfCertServerCfgSrv* cfg, WolfCertServer** out) s->ops = ops; s->heap = heap; - s->cfg_bind_host = wolfcert_strdup(cfg->bind_host ? cfg->bind_host : "0.0.0.0", heap); - if (cfg->challenge_password) s->cfg_challenge = wolfcert_strdup(cfg->challenge_password, heap); @@ -328,8 +332,7 @@ int wolfcert_server_start(const WolfCertServerCfgSrv* cfg, WolfCertServer** out) if (cfg->http_basic_pass) s->cfg_basic_pass = wolfcert_strdup(cfg->http_basic_pass, heap); - if (s->cfg_bind_host == NULL || - (cfg->challenge_password && s->cfg_challenge == NULL) || + if ((cfg->challenge_password && s->cfg_challenge == NULL) || (cfg->http_basic_user && s->cfg_basic_user == NULL) || (cfg->http_basic_pass && s->cfg_basic_pass == NULL)) { wolfcert_server_free(s); @@ -395,9 +398,8 @@ int wolfcert_server_start(const WolfCertServerCfgSrv* cfg, WolfCertServer** out) int yes = 1; setsockopt(s->listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); - struct sockaddr_in sa = { .sin_family = AF_INET, .sin_port = htons(cfg->bind_port) }; - if (inet_pton(AF_INET, s->cfg_bind_host, &sa.sin_addr) != 1) - sa.sin_addr.s_addr = htonl(INADDR_ANY); + struct sockaddr_in sa = { .sin_family = AF_INET, .sin_port = htons(cfg->bind_port), + .sin_addr = bind_addr }; if (bind(s->listen_fd, (struct sockaddr*)&sa, sizeof(sa)) < 0 || listen(s->listen_fd, 8) < 0) { @@ -596,7 +598,6 @@ void wolfcert_server_free(WolfCertServer* srv) close(srv->listen_fd); wolfcert_ca_free(&srv->ca); - WOLFCERT_XFREE(srv->cfg_bind_host, srv->heap); WOLFCERT_XFREE(srv->cfg_challenge, srv->heap); WOLFCERT_XFREE(srv->cfg_basic_user, srv->heap); WOLFCERT_XFREE(srv->cfg_basic_pass, srv->heap); diff --git a/tests/integration/cli_proto_scoping.sh b/tests/integration/cli_proto_scoping.sh index 5620499..89fb428 100755 --- a/tests/integration/cli_proto_scoping.sh +++ b/tests/integration/cli_proto_scoping.sh @@ -347,6 +347,26 @@ else fails=$((fails + 1)) fi fi + + # A hostname --listen is refused, and the reason reaches stderr. + "$SERVER" --proto scep --listen localhost:18090 >"$tmp/listen.log" 2>&1 & + listen_pid=$! + i=0 + while [ "$i" -lt "$poll_tries" ] && kill -0 "$listen_pid" 2>/dev/null; do + sleep "$poll_delay" + i=$((i + 1)) + done + if kill -0 "$listen_pid" 2>/dev/null; then + kill "$listen_pid" 2>/dev/null + echo "FAIL: --listen localhost was accepted" + fails=$((fails + 1)) + elif grep -q "not a numeric IPv4 address" "$tmp/listen.log"; then + echo "ok --listen localhost names the reason it was refused" + else + echo "FAIL: --listen localhost refused without the reason" + cat "$tmp/listen.log" + fails=$((fails + 1)) + fi fi if [ "$fails" -ne 0 ]; then diff --git a/tests/unit/test_server_ca_store.c b/tests/unit/test_server_ca_store.c index 9f2d72d..1a019e2 100644 --- a/tests/unit/test_server_ca_store.c +++ b/tests/unit/test_server_ca_store.c @@ -913,6 +913,41 @@ static int test_leaf_ca_rejected(void) } #endif /* WOLFCERT_HAVE_ECC */ +static int test_bad_bind_host_rejected(void) +{ + static const char* const hosts[] = { + "localhost", "::1", "", "127.0.0.300", "127.0.0.1 " + }; + WolfCertServerCfgSrv cfg; + WolfCertServer* srv = NULL; + WolfCertBuffer left = { 0 }; + size_t i; + + for (i = 0; i < sizeof(hosts) / sizeof(hosts[0]); i++) { + WolfCertStoreOps* mem = wolfcert_store_memory_open(NULL); + REQUIRE(mem != NULL); + + ca_store_cfg(&cfg, mem); + cfg.bind_host = hosts[i]; + REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_ERR_BAD_ARG); + REQUIRE(srv == NULL); + REQUIRE(mem->read(mem->ctx, "ca.cert.der", &left) == + WOLFCERT_ERR_NOT_FOUND); + REQUIRE(mem->read(mem->ctx, "ca.key.der", &left) == + WOLFCERT_ERR_NOT_FOUND); + wolfcert_store_memory_close(mem); + } + + /* NULL binds every interface and must still start. */ + ca_store_cfg(&cfg, NULL); + cfg.bind_host = NULL; + REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); + REQUIRE(wolfcert_server_port(srv) != 0); + wolfcert_server_free(srv); + + return 0; +} + int main(void) { REQUIRE(test_static_mem_init() == 0); @@ -958,6 +993,8 @@ int main(void) #endif if (test_ca_key_usage()) return 1; + if (test_bad_bind_host_rejected()) + return 1; #if CA_STORE_NEEDS_TLS free(srv_cert_pem); diff --git a/wolfcert/server.h b/wolfcert/server.h index 334598f..cb19669 100644 --- a/wolfcert/server.h +++ b/wolfcert/server.h @@ -35,8 +35,8 @@ typedef struct WolfCertServer WolfCertServer; typedef struct { WolfCertProtocol protocol; - const char* bind_host; /* e.g. "0.0.0.0"; ignored when - * serve_fd() is used directly */ + const char* bind_host; /* numeric IPv4, NULL = all + * interfaces */ uint16_t bind_port; WolfCertStoreOps* ca_store; /* optional: persist the local CA across runs; NULL = regen on From 8313b0f2453422a70003af88d84f9ef4d81d0db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 30 Sep 2026 15:32:55 +0200 Subject: [PATCH 7/9] Reject post-handshake auth for non-EST server protocols With tls_post_handshake_auth set, tls_setup drops WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT so the handshake can complete anonymously and the client certificate is requested later. Only the EST enrollment handler ever requests and checks that certificate. A SCEP server started with a client CA and PHA enabled therefore served clients that never presented a certificate, silently turning the configured mutual TLS into optional client auth. wolfcert_server_start now rejects tls_post_handshake_auth with WOLFCERT_ERR_BAD_ARG for any protocol other than EST. The header, ARCHITECTURE.md and the wolfcert-server --tls-post-handshake-auth help document the restriction. Fixes F-11068. --- cli/wolfcert_server.c | 3 ++- docs/ARCHITECTURE.md | 6 ++++-- src/server.c | 5 +++++ tests/integration/test_est_pha_roundtrip.c | 7 +++++++ wolfcert/server.h | 7 ++++--- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/cli/wolfcert_server.c b/cli/wolfcert_server.c index 313ec11..fccde6c 100644 --- a/cli/wolfcert_server.c +++ b/cli/wolfcert_server.c @@ -93,7 +93,8 @@ static void print_usage(FILE* out) " TLS 1.3 post-handshake auth (RFC 8446 section 4.6.2): initial\n" " handshake is anonymous, client cert is requested when\n" " EST /simpleenroll is hit on the kept-alive connection.\n" - " Requires --tls-cert/-key and --tls-client-ca.\n" + " Requires --proto est, --tls-cert/-key and\n" + " --tls-client-ca.\n" " --csrattrs-file PATH Serve this DER-encoded CsrAttrs blob (RFC 7030 section 4.5.2)\n" " from GET /.well-known/est/csrattrs; without this the\n" " server answers 204 No Content.\n" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 05e4bb4..05b5a18 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -164,8 +164,10 @@ what enables the post-handshake-auth bootstrap below. **Test server PHA mode.** `WolfCertServerCfgSrv.tls_post_handshake_auth` turns the in-tree server into the other end of shape 3. It needs -`tls_client_ca_pem`; without it `wolfcert_server_start()` returns -`WOLFCERT_ERR_BAD_ARG`. The CTX gets `WOLFSSL_VERIFY_POST_HANDSHAKE` and not +`tls_client_ca_pem` and the EST protocol; without either +`wolfcert_server_start()` returns `WOLFCERT_ERR_BAD_ARG`, since no other +protocol requests the deferred certificate. The CTX gets +`WOLFSSL_VERIFY_POST_HANDSHAKE` and not `FAIL_IF_NO_PEER_CERT`, so a TLS 1.3 handshake stays anonymous and a TLS 1.2 client can still fetch `/cacerts`. A TLS 1.2 client is asked for its cert during the handshake, since TLS 1.2 has no PHA. On the first `/simpleenroll` diff --git a/src/server.c b/src/server.c index d46131f..37b0641 100644 --- a/src/server.c +++ b/src/server.c @@ -301,6 +301,11 @@ int wolfcert_server_start(const WolfCertServerCfgSrv* cfg, WolfCertServer** out) "EST enrollment needs http_basic_user or tls_client_ca_pem, " "or est_allow_anonymous_enroll"); + /* Only EST requests the certificate that PHA defers past the handshake. */ + if (cfg->protocol != WOLFCERT_PROTO_EST && cfg->tls_post_handshake_auth) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "server", + "post-handshake auth is supported only for EST"); + struct in_addr bind_addr = { .s_addr = htonl(INADDR_ANY) }; if (cfg->bind_host != NULL && inet_pton(AF_INET, cfg->bind_host, &bind_addr) != 1) diff --git a/tests/integration/test_est_pha_roundtrip.c b/tests/integration/test_est_pha_roundtrip.c index c7679a9..46cab08 100644 --- a/tests/integration/test_est_pha_roundtrip.c +++ b/tests/integration/test_est_pha_roundtrip.c @@ -200,6 +200,13 @@ int main(void) bad.est_allow_anonymous_enroll = 1; REQUIRE(wolfcert_server_start(&bad, &srv) == WOLFCERT_ERR_BAD_ARG); REQUIRE(srv == NULL); +#ifdef WOLFCERT_HAVE_SCEP + /* SCEP never requests the certificate, so PHA would drop mTLS there. */ + bad = cfg; + bad.protocol = WOLFCERT_PROTO_SCEP; + REQUIRE(wolfcert_server_start(&bad, &srv) == WOLFCERT_ERR_BAD_ARG); + REQUIRE(srv == NULL); +#endif int start_rc = wolfcert_server_start(&cfg, &srv); if (start_rc == WOLFCERT_ERR_UNSUPPORTED) { diff --git a/wolfcert/server.h b/wolfcert/server.h index cb19669..8045198 100644 --- a/wolfcert/server.h +++ b/wolfcert/server.h @@ -115,9 +115,10 @@ typedef struct { int est_retry_after_sec; /* TLS 1.3 post-handshake auth (RFC 8446 section 4.6.2) for EST enrollment, - * checked against `tls_client_ca_pem`, which it requires. - * wolfcert_server_start() returns WOLFCERT_ERR_UNSUPPORTED without - * KEEP_PEER_CERT and WOLFSSL_HAVE_TLS_UNIQUE; see docs/ARCHITECTURE.md. */ + * checked against `tls_client_ca_pem`, which it requires. Other protocols + * reject it with WOLFCERT_ERR_BAD_ARG. wolfcert_server_start() returns + * WOLFCERT_ERR_UNSUPPORTED without KEEP_PEER_CERT and + * WOLFSSL_HAVE_TLS_UNIQUE; see docs/ARCHITECTURE.md. */ int tls_post_handshake_auth; /* EST /csrattrs body. When set, the EST server returns this From 6fcefc2edba764450f56dd5111e0524003251aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 30 Sep 2026 15:57:10 +0200 Subject: [PATCH 8/9] Scrub server CLI secrets from argv after parsing wolfcert-server took the EST Basic password and the SCEP challenge password on the command line and left both in argv for the life of the process. The challenge was used directly as the optarg pointer, and parse_basic copied the password without touching the original argument, so any local user able to read process arguments through ps or /proc//cmdline could recover them. parse_basic now zeroes the password half of its argument after copying it, and --challenge is copied with strdup before its optarg is zeroed. Both copies are wiped before they are freed. A failed copy of either option exits instead of starting the server without it: a NULL Basic user would have turned Basic auth off, and a NULL password would have accepted "user:". wolfcert_server_free also wipes the library's own copies of the Basic password and the challenge before freeing them. A new group in cli_proto_scoping.sh starts the server with both options and checks that neither secret appears in ps output once it is listening. Fixes F-12911. --- cli/wolfcert_server.c | 39 +++++++++++++++++--- src/server.c | 4 ++ tests/integration/cli_proto_scoping.sh | 51 ++++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/cli/wolfcert_server.c b/cli/wolfcert_server.c index fccde6c..d477d6b 100644 --- a/cli/wolfcert_server.c +++ b/cli/wolfcert_server.c @@ -126,14 +126,27 @@ static int parse_listen(const char* arg, char** host, uint16_t* port) return 0; } -static int parse_basic(const char* arg, char** user, char** pass) +static void free_secret(char* s) { - const char* colon = strchr(arg, ':'); + if (s != NULL) { + wc_ForceZero(s, (word32)strlen(s)); + free(s); + } +} + +static int parse_basic(char* arg, char** user, char** pass) +{ + char* colon = strchr(arg, ':'); if (colon == NULL || colon == arg || colon[1] == '\0') return -1; + free(*user); + free_secret(*pass); *user = strndup(arg, (size_t)(colon - arg)); *pass = strdup(colon + 1); + wc_ForceZero(colon + 1, (word32)strlen(colon + 1)); + if (*user == NULL || *pass == NULL) + return -2; return 0; } @@ -211,6 +224,7 @@ int main(int argc, char** argv) size_t csr_attrs_blob_len = 0; int est_require_csr_attrs = 0; int est_allow_anonymous = 0; + int basic_rc; int c; while ((c = getopt_long(argc, argv, "", opts, NULL)) != -1) { @@ -225,13 +239,24 @@ int main(int argc, char** argv) } break; case 'b': - if (parse_basic(optarg, &user, &pass) != 0) { + basic_rc = parse_basic(optarg, &user, &pass); + if (basic_rc == -2) { + fprintf(stderr, "out of memory copying --basic\n"); + return 1; + } + if (basic_rc != 0) { fprintf(stderr, "invalid --basic (expected non-empty USER:PASS)\n"); return 1; } break; case 'X': - challenge = optarg; + free_secret(challenge); + challenge = strdup(optarg); + wc_ForceZero(optarg, (word32)strlen(optarg)); + if (challenge == NULL) { + fprintf(stderr, "out of memory copying --challenge\n"); + return 1; + } break; case 'C': tls_cert = slurp(optarg, &tls_cert_len); @@ -357,7 +382,8 @@ int main(int argc, char** argv) free(csr_attrs_blob); free(host); free(user); - free(pass); + free_secret(pass); + free_secret(challenge); free(tls_cert); free(tls_key); free(tls_ca); @@ -418,7 +444,8 @@ int main(int argc, char** argv) g_server = NULL; free(host); free(user); - free(pass); + free_secret(pass); + free_secret(challenge); free(tls_cert); free(tls_key); free(tls_ca); diff --git a/src/server.c b/src/server.c index 37b0641..44b7353 100644 --- a/src/server.c +++ b/src/server.c @@ -603,6 +603,10 @@ void wolfcert_server_free(WolfCertServer* srv) close(srv->listen_fd); wolfcert_ca_free(&srv->ca); + if (srv->cfg_challenge != NULL) + wc_ForceZero(srv->cfg_challenge, (word32)strlen(srv->cfg_challenge)); + if (srv->cfg_basic_pass != NULL) + wc_ForceZero(srv->cfg_basic_pass, (word32)strlen(srv->cfg_basic_pass)); WOLFCERT_XFREE(srv->cfg_challenge, srv->heap); WOLFCERT_XFREE(srv->cfg_basic_user, srv->heap); WOLFCERT_XFREE(srv->cfg_basic_pass, srv->heap); diff --git a/tests/integration/cli_proto_scoping.sh b/tests/integration/cli_proto_scoping.sh index 89fb428..4bfee5e 100755 --- a/tests/integration/cli_proto_scoping.sh +++ b/tests/integration/cli_proto_scoping.sh @@ -6,8 +6,8 @@ # either and quietly doing nothing. Both were only ever checked by hand. # # Most cases here fail before any network access. The --ca-fingerprint pinning -# group is the exception: it starts wolfcert-server, and skips itself when that -# binary was not built. +# and server argv groups are the exception: they start wolfcert-server, and skip +# themselves when that binary was not built. set -u @@ -251,7 +251,9 @@ if [ ! -x "$SERVER" ]; then else tmp="$(mktemp -d -t wolfcert-cli.XXXXXX)" srv_pid="" - trap '[ -n "$srv_pid" ] && kill "$srv_pid" 2>/dev/null; rm -rf "$tmp"' EXIT + argv_pid="" + trap '[ -n "$srv_pid" ] && kill "$srv_pid" 2>/dev/null; + [ -n "$argv_pid" ] && kill "$argv_pid" 2>/dev/null; rm -rf "$tmp"' EXIT # Not every sleep(1) takes a fractional delay. Poll in whole seconds where # it does not, keeping the same ten-second budget. @@ -367,6 +369,49 @@ else cat "$tmp/listen.log" fails=$((fails + 1)) fi + + # Secrets passed on the server command line must not stay visible to ps. + listening=0 + for port in 18089 18189 18289 18389; do + "$SERVER" --proto scep --listen "127.0.0.1:$port" \ + --basic argvuser:argv-basic-secret \ + --challenge argv-challenge-secret >"$tmp/argv.log" 2>&1 & + argv_pid=$! + i=0 + while [ "$i" -lt "$poll_tries" ] && kill -0 "$argv_pid" 2>/dev/null; do + if grep -q "listening" "$tmp/argv.log"; then + listening=1 + break + fi + sleep "$poll_delay" + i=$((i + 1)) + done + if [ "$listening" -eq 1 ]; then + break + fi + kill "$argv_pid" 2>/dev/null + done + + if [ "$listening" -ne 1 ]; then + echo "skip server argv scrubbing (no test server would start)" + cat "$tmp/argv.log" + else + args="$(ps -ww -o args= -p "$argv_pid")" + case "$args" in + *argv-basic-secret*|*argv-challenge-secret*) + echo "FAIL: server secrets still visible in ps: $args" + fails=$((fails + 1)) + ;; + *--basic*) + echo "ok server scrubs --basic and --challenge from argv" ;; + *) + echo "FAIL: could not read the server argv from ps: $args" + fails=$((fails + 1)) + ;; + esac + fi + kill "$argv_pid" 2>/dev/null + argv_pid="" fi if [ "$fails" -ne 0 ]; then From a37f325b971cdb7d84a43e918698219eb5e04382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 30 Sep 2026 16:07:19 +0200 Subject: [PATCH 9/9] Scrub client CLI credentials from argv after parsing wolfcert-client kept --pass and --challenge as direct optarg pointers, so the EST Basic password and the SCEP challenge password stayed in argv for the whole run. Enrollment with SCEP pending polling or an EST Retry-After wait can keep the client alive for minutes, during which any local user able to read process arguments through ps or /proc//cmdline could recover them. parse_common now copies both values through a new opt_secret helper, which zeroes the argv original and wipes any earlier copy when the flag is repeated. opts_free wipes and frees the copies. A new group in cli_proto_scoping.sh parks the client on a --trust FIFO after option parsing and checks that neither secret appears in its ps output. Fixes F-12912. --- cli/wolfcert_client.c | 26 +++++++++++++++++--- tests/integration/cli_proto_scoping.sh | 34 ++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/cli/wolfcert_client.c b/cli/wolfcert_client.c index 741b252..760c961 100644 --- a/cli/wolfcert_client.c +++ b/cli/wolfcert_client.c @@ -179,8 +179,8 @@ typedef struct { const char* url; const char* trust_file; const char* user; - const char* pass; - const char* challenge; + char* pass; + char* challenge; const char* client_cert_file; const char* client_key_file; const char* key_type; @@ -227,8 +227,24 @@ static int opt_append(const char*** arr, size_t* len, size_t* cap, return 0; } +/* Copy a secret option value, then wipe it from argv so ps cannot show it. */ +static int opt_secret(char** dst, char* arg) +{ + if (*dst != NULL) + free_secret(*dst, strlen(*dst)); + + *dst = strdup(arg); + wc_ForceZero(arg, (word32)strlen(arg)); + + return *dst == NULL ? -1 : 0; +} + static void opts_free(Opts* opts) { + if (opts->pass != NULL) + free_secret(opts->pass, strlen(opts->pass)); + if (opts->challenge != NULL) + free_secret(opts->challenge, strlen(opts->challenge)); free(opts->san_dns); free(opts->san_ip); free(opts->san_uri); @@ -292,10 +308,12 @@ static int parse_common(int argc, char** argv, Opts* opts) opts->user = optarg; break; case 'P': - opts->pass = optarg; + if (opt_secret(&opts->pass, optarg) != 0) + return -1; break; case 'X': - opts->challenge = optarg; + if (opt_secret(&opts->challenge, optarg) != 0) + return -1; break; case 'M': opts->client_cert_file = optarg; diff --git a/tests/integration/cli_proto_scoping.sh b/tests/integration/cli_proto_scoping.sh index 4bfee5e..6194886 100755 --- a/tests/integration/cli_proto_scoping.sh +++ b/tests/integration/cli_proto_scoping.sh @@ -213,6 +213,40 @@ else esac fi +# Credentials on the client command line must not stay visible to ps. The client +# parks opening a --trust FIFO that nobody writes, well after option parsing. +ctmp="$(mktemp -d -t wolfcert-argv.XXXXXX)" +mkfifo "$ctmp/trust" +"$CLI" enroll --proto est --url "$EST_URL" --trust "$ctmp/trust" \ + --subject CN=argv --user argvuser --pass argv-client-pass \ + --challenge argv-client-challenge >/dev/null 2>&1 & +cargv_pid=$! +trap 'kill "$cargv_pid" 2>/dev/null; rm -rf "$ctmp"' EXIT +i=0 +args="" +while [ "$i" -lt 10 ]; do + args="$(ps -ww -o args= -p "$cargv_pid")" + case "$args" in + *argv-client-pass*|*argv-client-challenge*) ;; + *--trust*) break ;; + esac + sleep 1 + i=$((i + 1)) +done +case "$args" in + *argv-client-pass*|*argv-client-challenge*) + echo "FAIL: client secrets still visible in ps: $args" + fails=$((fails + 1)) + ;; + *--trust*) echo "ok client scrubs --pass and --challenge from argv" ;; + *) + echo "FAIL: client did not stay parked on the --trust FIFO" + fails=$((fails + 1)) + ;; +esac +kill "$cargv_pid" 2>/dev/null +rm -rf "$ctmp" + # The pinning itself, end to end against the in-tree test server. wolfcert-server # is built alongside wolfcert-client whenever the server is enabled; without it # there is nothing to enroll against, so skip just this group.