From e5d8ddff89d4ce93dd65f38db7922e9298093cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 1 Oct 2026 09:40:53 +0200 Subject: [PATCH 01/10] ca: issue the CSR's subjectAltName verbatim The test CA rebuilt the issued SAN from wolfSSL's parsed alt-name lists. Those lists are split by type, so the issued SAN did not keep the entry order the CSR asked for and its criticality was dropped. Copy the subjectAltName extension value from the CSR into the issued certificate byte for byte, with its criticality. Like before, a SAN holding an otherName, x400Address, directoryName or ediPartyName is refused: only rfc822Name, dNSName, URI, iPAddress and registeredID are issued, so a client cannot get, say, a UPN otherName signed. find_san() in csr.c, which already pulled that value out of a certificate for a renewal CSR, moves to internal.c as wolfcert_find_san(). It takes the DecodedCert and uses its isCSR to tell a CSR's extensions, which carry no [3] wrapper, from a certificate's. A renewal CSR built from the same names as the original request now carries the same SAN bytes as the certificate it renews. server_ca_store issues a CSR whose SAN holds a dNSName, an rfc822Name and an iPAddress in that order and checks that the issued SAN is identical, and checks that a directoryName SAN is refused. --- src/ca_issue.c | 93 ++++++++++++++++--------------- src/csr.c | 69 +---------------------- src/internal.c | 67 ++++++++++++++++++++++ src/internal.h | 5 ++ tests/unit/test_server_ca_store.c | 84 ++++++++++++++++++++++++++++ 5 files changed, 206 insertions(+), 112 deletions(-) diff --git a/src/ca_issue.c b/src/ca_issue.c index 8894d29..22c3027 100644 --- a/src/ca_issue.c +++ b/src/ca_issue.c @@ -638,52 +638,59 @@ static void free_subject_pubkey(word32 keyOID, void* impl, void* heap) } } -/* Carry the subjectAltName from the parsed CSR into the issued cert. wolfSSL - * only transcribes the subject DN, and it also splits parsed alt names by type - * (rfc822Name lands in altEmailNames, not altNames), so we recombine the - * carriable lists -- DNS / URI / IP / registeredID in altNames and rfc822Name - * in altEmailNames -- into one list and flatten it into the GeneralNames - * SEQUENCE that Cert.altNames expects. - * - * directoryName and otherName cannot be faithfully re-encoded from a parsed - * cert via wc_FlattenAltNames (the parser strips the directoryName SEQUENCE - * wrapper, and wolfSSL's cert generator has no path to restore it). Rather - * than silently issue a cert missing a SAN entry the requester asked for, we - * reject such a CSR. The altDirNames / altOtherNamesRaw lists (and the - * rfc822Name split) only exist when wolfSSL keeps name-constraint state. */ -static int flatten_csr_san(DecodedCert* dc, Cert* nc, void* heap) +/* 1 when every GeneralName in san is an rfc822Name, dNSName, URI, iPAddress or + * registeredID, the forms the test CA issues. */ +static int san_types_issuable(const byte* san, word32 san_len) { - DNS_entry* merged = NULL; - int rc = 0; - -#ifndef IGNORE_NAME_CONSTRAINTS - if (dc->altDirNames != NULL || dc->altOtherNamesRaw != NULL) - return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "ca", - "CSR carries a directoryName/otherName SAN that cannot be issued"); -#endif + word32 idx = 0; + word32 end; + int len = 0; + byte tag = 0; + + if (GetASNTag(san, &idx, &tag, san_len) < 0 || + tag != (ASN_SEQUENCE | ASN_CONSTRUCTED) || + GetLength(san, &idx, &len, san_len) < 0) + return 0; + + end = idx + (word32)len; + while (idx < end) { + if (GetASNTag(san, &idx, &tag, end) < 0 || + GetLength(san, &idx, &len, end) < 0) + return 0; + if (tag != (ASN_CONTEXT_SPECIFIC | ASN_RFC822_TYPE) && + tag != (ASN_CONTEXT_SPECIFIC | ASN_DNS_TYPE) && + tag != (ASN_CONTEXT_SPECIFIC | ASN_URI_TYPE) && + tag != (ASN_CONTEXT_SPECIFIC | ASN_IP_TYPE) && + tag != (ASN_CONTEXT_SPECIFIC | ASN_RID_TYPE)) + return 0; + idx += (word32)len; + } - const DNS_entry* srcs[] = { - dc->altNames, -#ifndef IGNORE_NAME_CONSTRAINTS - dc->altEmailNames, -#endif - }; + return 1; +} - for (size_t i = 0; i < sizeof(srcs) / sizeof(srcs[0]) && rc == 0; ++i) { - for (const DNS_entry* e = srcs[i]; e != NULL && rc == 0; e = e->next) - rc = wc_SetDNSEntry(heap, e->name, e->len, e->type, &merged); - } - if (rc != 0) { - FreeAltNames(merged, heap); - return WOLFCERT_ERR_WC(rc, "ca", "SetDNSEntry(issue SAN)"); - } +/* Copy the CSR's subjectAltName into the issued cert byte for byte. */ +static int copy_csr_san(const DecodedCert* dc, Cert* nc) +{ + const byte* san = NULL; + word32 san_len = 0; - /* Encode straight into nc->altNames / altNamesSz (0 when no SAN). */ - rc = wc_SetAltNamesFromList(nc, merged); + if (wolfcert_find_san(dc, &san, &san_len) != WOLFCERT_OK) + return WOLFCERT_ERR(WOLFCERT_ERR_PARSE, "ca", + "CSR extensions do not parse"); + if (san_len > sizeof(nc->altNames)) + return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "ca", + "CSR SAN is %u bytes, limit %d", (unsigned)san_len, + (int)sizeof(nc->altNames)); + if (san != NULL && !san_types_issuable(san, san_len)) + return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "ca", + "CSR carries an otherName, x400Address, directoryName or " + "ediPartyName SAN"); - FreeAltNames(merged, heap); - if (rc != 0) - return WOLFCERT_ERR_WC(rc, "ca", "SetAltNamesFromList(issue)"); + if (san != NULL) + memcpy(nc->altNames, san, san_len); + nc->altNamesSz = (int)san_len; + nc->altNamesCrit = dc->extSubjAltNameCrit; return WOLFCERT_OK; } @@ -791,9 +798,7 @@ int wolfcert_ca_issue(WolfCertCa* ca, nc->daysValid = 365; nc->isCA = 0; - /* Carry the requested subjectAltName from the CSR into the issued - * cert. */ - rc = flatten_csr_san(&dc, nc, heap); + rc = copy_csr_san(&dc, nc); } if (rc == 0) { diff --git a/src/csr.c b/src/csr.c index c16cd95..fef603a 100644 --- a/src/csr.c +++ b/src/csr.c @@ -271,72 +271,6 @@ static int choose_sig_type(const WolfCertKey* key, const WolfCertKeyAlg* alg, return alg->ctc_sig_default; } -/* Find the subjectAltName extension in a certificate's [3] Extensions and - * return its GeneralNames bytes; *san stays NULL when there is none. */ -static int find_san(const byte* ext, int ext_sz, const byte** san, - word32* san_len) -{ - static const byte san_oid[] = { ASN_OBJECT_ID, 0x03, 0x55, 0x1D, 0x11 }; - word32 idx = 0; - word32 end = 0; - word32 ext_end = 0; - int len = 0; - int rc = WOLFCERT_OK; - byte tag = 0; - - *san = NULL; - *san_len = 0; - if (ext == NULL || ext_sz <= 0) - return WOLFCERT_OK; - - if (GetASNTag(ext, &idx, &tag, (word32)ext_sz) < 0 || - tag != (ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED | 3) || - GetLength(ext, &idx, &len, (word32)ext_sz) < 0 || - GetASNTag(ext, &idx, &tag, (word32)ext_sz) < 0 || - tag != (ASN_SEQUENCE | ASN_CONSTRUCTED) || - GetLength(ext, &idx, &len, (word32)ext_sz) < 0) - rc = WOLFCERT_ERR_PARSE; - else - end = idx + (word32)len; - - /* Extension ::= SEQUENCE { OID, critical BOOLEAN OPTIONAL, OCTET STRING } */ - while (rc == WOLFCERT_OK && *san == NULL && idx < end) { - if (GetASNTag(ext, &idx, &tag, end) < 0 || - tag != (ASN_SEQUENCE | ASN_CONSTRUCTED) || - GetLength(ext, &idx, &len, end) < 0) { - rc = WOLFCERT_ERR_PARSE; - } - else { - ext_end = idx + (word32)len; - if (ext_end - idx < sizeof(san_oid) || - memcmp(ext + idx, san_oid, sizeof(san_oid)) != 0) { - idx = ext_end; - } - else { - idx += (word32)sizeof(san_oid); - if (idx < ext_end && ext[idx] == ASN_BOOLEAN) { - if (GetASNTag(ext, &idx, &tag, ext_end) < 0 || - GetLength(ext, &idx, &len, ext_end) < 0) - rc = WOLFCERT_ERR_PARSE; - else - idx += (word32)len; - } - if (rc == WOLFCERT_OK && - (GetASNTag(ext, &idx, &tag, ext_end) < 0 || - tag != ASN_OCTET_STRING || - GetLength(ext, &idx, &len, ext_end) < 0)) - rc = WOLFCERT_ERR_PARSE; - if (rc == WOLFCERT_OK) { - *san = ext + idx; - *san_len = (word32)len; - } - } - } - } - - return rc; -} - /* Copy renew_cert's Subject and SAN into cert */ static int copy_cert_identity(Cert* cert, const uint8_t* renew_cert, size_t renew_cert_len, void* heap) @@ -394,8 +328,7 @@ static int copy_cert_identity(Cert* cert, const uint8_t* renew_cert, } if (rc == WOLFCERT_OK && - find_san(dc->extensions, dc->extensionsSz, &san, &san_len) != - WOLFCERT_OK) + wolfcert_find_san(dc, &san, &san_len) != WOLFCERT_OK) rc = WOLFCERT_ERR(WOLFCERT_ERR_PARSE, "csr", "certificate extensions do not parse"); if (rc == WOLFCERT_OK && san_len > sizeof(cert->altNames)) diff --git a/src/internal.c b/src/internal.c index 25c9b1f..c8d2c0e 100644 --- a/src/internal.c +++ b/src/internal.c @@ -525,3 +525,70 @@ int wolfcert_pem_cert_to_der(const uint8_t* pem, size_t pem_len, wc_FreeDer(&der); return WOLFCERT_OK; } + +WOLFCERT_TEST_VIS int wolfcert_find_san(const DecodedCert* dc, + const byte** san, word32* san_len) +{ + static const byte san_oid[] = { ASN_OBJECT_ID, 0x03, 0x55, 0x1D, 0x11 }; + const byte* ext = dc->extensions; + int ext_sz = dc->extensionsSz; + word32 idx = 0; + word32 end = 0; + word32 ext_end = 0; + int len = 0; + int rc = WOLFCERT_OK; + byte tag = 0; + + *san = NULL; + *san_len = 0; + if (ext == NULL || ext_sz <= 0) + return WOLFCERT_OK; + + if ((!dc->isCSR && + (GetASNTag(ext, &idx, &tag, (word32)ext_sz) < 0 || + tag != (ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED | 3) || + GetLength(ext, &idx, &len, (word32)ext_sz) < 0)) || + GetASNTag(ext, &idx, &tag, (word32)ext_sz) < 0 || + tag != (ASN_SEQUENCE | ASN_CONSTRUCTED) || + GetLength(ext, &idx, &len, (word32)ext_sz) < 0) + rc = WOLFCERT_ERR_PARSE; + else + end = idx + (word32)len; + + /* Extension ::= SEQUENCE { OID, critical BOOLEAN OPTIONAL, OCTET STRING } */ + while (rc == WOLFCERT_OK && *san == NULL && idx < end) { + if (GetASNTag(ext, &idx, &tag, end) < 0 || + tag != (ASN_SEQUENCE | ASN_CONSTRUCTED) || + GetLength(ext, &idx, &len, end) < 0) { + rc = WOLFCERT_ERR_PARSE; + } + else { + ext_end = idx + (word32)len; + if (ext_end - idx < sizeof(san_oid) || + memcmp(ext + idx, san_oid, sizeof(san_oid)) != 0) { + idx = ext_end; + } + else { + idx += (word32)sizeof(san_oid); + if (idx < ext_end && ext[idx] == ASN_BOOLEAN) { + if (GetASNTag(ext, &idx, &tag, ext_end) < 0 || + GetLength(ext, &idx, &len, ext_end) < 0) + rc = WOLFCERT_ERR_PARSE; + else + idx += (word32)len; + } + if (rc == WOLFCERT_OK && + (GetASNTag(ext, &idx, &tag, ext_end) < 0 || + tag != ASN_OCTET_STRING || + GetLength(ext, &idx, &len, ext_end) < 0)) + rc = WOLFCERT_ERR_PARSE; + if (rc == WOLFCERT_OK) { + *san = ext + idx; + *san_len = (word32)len; + } + } + } + } + + return rc; +} diff --git a/src/internal.h b/src/internal.h index e11fcbb..9b3f5ad 100644 --- a/src/internal.h +++ b/src/internal.h @@ -359,6 +359,11 @@ int wolfcert_transport_fd(const WolfCertTransport* t, void* conn); int wolfcert_pem_cert_to_der(const uint8_t* pem, size_t pem_len, WolfCertBuffer* out_der, void* heap); +/* GeneralNames of the subjectAltName in dc, or *san NULL when there is + * none. Returns WOLFCERT_OK or WOLFCERT_ERR_PARSE. */ +WOLFCERT_TEST_VIS int wolfcert_find_san(const DecodedCert* dc, + const byte** san, word32* san_len); + /* wolfcert_csr_build() for a renewal: with renew_cert (PEM or DER) set, the * CSR carries that certificate's Subject and SAN, and meta may not set them. */ WOLFCERT_TEST_VIS int wolfcert_csr_build_ex(const WolfCertKey* key, diff --git a/tests/unit/test_server_ca_store.c b/tests/unit/test_server_ca_store.c index 7fecb9d..397d7fa 100644 --- a/tests/unit/test_server_ca_store.c +++ b/tests/unit/test_server_ca_store.c @@ -543,6 +543,88 @@ static int generate_ca_into(WolfCertStoreOps* store, WolfCertKeyType type, return 0; } +/* Have the CA in store issue a CSR carrying san; *rc_out is the issue result. */ +static int issue_with_san(WolfCertStoreOps* store, const uint8_t* san, + size_t san_len, int* rc_out, uint8_t** issued, + size_t* issued_len) +{ + WolfCertCa ca; + WC_RNG rng; + test_signkey key; + Cert req; + uint8_t csr[4096]; + int csr_len; + + REQUIRE(wc_InitRng(&rng) == 0); + REQUIRE(test_signkey_make(&key, &rng) == 0); + REQUIRE(wc_InitCert(&req) == 0); + strncpy(req.subject.commonName, "san-leaf", CTC_NAME_SIZE - 1); + memcpy(req.altNames, san, san_len); + req.altNamesSz = (int)san_len; + req.sigType = TEST_CERT_SIGTYPE; + csr_len = test_sign_certreq(&req, csr, (int)sizeof(csr), &key, &rng); + test_signkey_free(&key); + wc_FreeRng(&rng); + REQUIRE(csr_len > 0); + + REQUIRE(wolfcert_ca_load(&ca, store, NULL) == WOLFCERT_OK); + *rc_out = wolfcert_ca_issue(&ca, csr, (size_t)csr_len, issued, issued_len); + wolfcert_ca_free(&ca); + return 0; +} + +/* The CA issues a CSR's SAN unchanged, entries in their order across types, + * and refuses the GeneralName forms it does not issue. */ +static int test_issued_san_verbatim(void) +{ + static const uint8_t san[] = { + 0x30, 0x20, + 0x82, 0x09, 'b', '.', 'e', 'x', 'a', 'm', 'p', 'l', 'e', + 0x81, 0x0d, 'a', '@', 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', + 'm', + 0x87, 0x04, 127, 0, 0, 1 + }; + static const uint8_t dir_san[] = { + 0x30, 0x12, + 0xa4, 0x10, 0x30, 0x0e, 0x31, 0x0c, 0x30, 0x0a, 0x06, 0x03, 0x55, 0x04, + 0x03, 0x0c, 0x03, 'd', 'i', 'r' + }; + WolfCertStoreOps* store = wolfcert_store_memory_open(NULL); + WolfCertBuffer ca_cert = { 0 }; + WolfCertBuffer ca_key = { 0 }; + uint8_t* issued = NULL; + size_t issued_len = 0; + DecodedCert dc; + const byte* got = NULL; + word32 got_len = 0; + int rc = -1; + + REQUIRE(store != NULL); + REQUIRE(generate_ca_into(store, CA_KEY_TYPES[0], &ca_cert, &ca_key) == 0); + wolfcert_buffer_free(&ca_cert); + wolfcert_buffer_free(&ca_key); + + REQUIRE(issue_with_san(store, dir_san, sizeof(dir_san), &rc, &issued, + &issued_len) == 0); + REQUIRE(rc == WOLFCERT_ERR_UNSUPPORTED); + REQUIRE(issue_with_san(store, san, sizeof(san), &rc, &issued, + &issued_len) == 0); + wolfcert_store_memory_close(store); + REQUIRE(rc == WOLFCERT_OK); + + wc_InitDecodedCert(&dc, issued, (word32)issued_len, NULL); + rc = wc_ParseCert(&dc, CERT_TYPE, NO_VERIFY, NULL); + if (rc == 0) + rc = wolfcert_find_san(&dc, &got, &got_len); + if (rc == 0 && (got_len != sizeof(san) || + memcmp(got, san, sizeof(san)) != 0)) + rc = -1; + wc_FreeDecodedCert(&dc); + WOLFCERT_XFREE(issued, NULL); + REQUIRE(rc == 0); + return 0; +} + static int mismatched_ca_rejected(WolfCertKeyType type) { WolfCertStoreOps* src_a = wolfcert_store_memory_open(NULL); @@ -790,6 +872,8 @@ int main(void) return 1; if (test_corrupt_ca_cert_rejected()) return 1; + if (test_issued_san_verbatim()) + return 1; #ifdef WOLFCERT_HAVE_ECC if (test_leaf_ca_rejected()) return 1; From fabd16e8726e2dfdd20b65e675f946ad811bdf6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 29 Sep 2026 15:09:08 +0200 Subject: [PATCH 02/10] est: bind simplereenroll to the certificate being renewed The test server sent /simpleenroll and /simplereenroll down the same handler and issued whatever Subject and SAN the CSR carried. A client authenticated over mutual TLS as one identity could therefore reenroll as another, contrary to RFC 7030 section 4.2.2, which requires the reenroll request to keep the Subject and SubjectAltName of the certificate being renewed. handler_enroll() now knows which operation it serves. For a reenroll it verifies the CSR and compares it against the TLS client certificate: the raw Subject and the raw subjectAltName extension value must both match byte for byte, with the same SAN criticality, otherwise the request gets 400. Comparing the raw value covers every GeneralName type, including those wolfSSL does not parse into its alt-name lists. A body that is not a valid, signed PKCS#10 request also gets 400, and a reenroll with no client certificate to renew gets 401. On a server with a client CA a missing certificate is not the client's fault, for example on a resumed session without SESSION_CERTS, so it gets 500. Reading the peer certificate needs KEEP_PEER_CERT. Without it, a connection that never asked for a client certificate still gets 401, while an mTLS server, whose handshake did demand one, refuses the reenroll rather than issue without the comparison. README.md and CLAUDE.md list the new KEEP_PEER_CERT requirement. The mutual TLS integration test gains cases that reenroll with a changed CN and with an added SAN and expect both to be refused, that post a body which is not a CSR, and that renew a server-issued certificate carrying several SANs, so the comparison is checked against the CA's own encoding. Renewing that certificate with one dNSName changed, or with its SAN marked critical, is refused. On a wolfSSL without KEEP_PEER_CERT the reenroll cases skip, and one case checks that the mTLS server answers 500 instead. est_pha_roundtrip renews the client certificate over post-handshake auth with a raw TLS client; the client API cannot, since only sessions offer PHA and they have no reenroll. The TLS integration test's client reenroll cases, which asked a server with no client CA to renew, now start their own server that trusts the certificate being renewed, so a renewal that keeps the identity is checked end to end against the comparison. Fixes F-8034. --- CLAUDE.md | 6 +- README.md | 6 +- src/est/est_server.c | 104 +++++- tests/integration/test_est_mtls_roundtrip.c | 359 +++++++++++++++++++- tests/integration/test_est_pha_roundtrip.c | 58 ++++ tests/integration/test_est_tls_roundtrip.c | 39 ++- 6 files changed, 555 insertions(+), 17 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ef33260..bc53a02 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,9 +35,7 @@ configure time if the installed wolfSSL lacks any of `HAVE_PKCS7`, 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 -one links them regardless. The test server's post-handshake-auth mode -additionally needs `KEEP_PEER_CERT` and `WOLFSSL_HAVE_TLS_UNIQUE`; without -them it returns `WOLFCERT_ERR_UNSUPPORTED`. The OpenSSL compatibility layer +one links them regardless. The OpenSSL compatibility layer itself is not required. With ML-DSA enabled it additionally needs `WOLFSSL_MLDSA_CHECK_KEY` (`wc_MlDsaKey_CheckKey()`), which reloading an ML-DSA CA from a store calls -- checked when `src/key_algs.c` compiles, @@ -45,6 +43,8 @@ since only `dilithium.h` resolves that macro. `--enable-mldsa` gives it by default; it is lost only if wolfSSL is built with `WOLFSSL_DILITHIUM_NO_CHECK_KEY` or `WOLFSSL_MLDSA_VERIFY_ONLY`. +The in-tree test server needs `KEEP_PEER_CERT` for post-handshake auth and `/simplereenroll`, and `WOLFSSL_HAVE_TLS_UNIQUE` for post-handshake auth; `README.md` has the details below its configure line. + **Key algorithms are gated** by `WOLFCERT_HAVE_` (RSA, ECC, ED25519, ED448, MLDSA). RSA, ECC, Ed25519, Ed448 and ML-DSA are each *optional* (absent => warning, that key type returns diff --git a/README.md b/README.md index 6377bd3..492431a 100644 --- a/README.md +++ b/README.md @@ -72,11 +72,7 @@ optional key types wolfCert picks up when available: -DWOLFSSL_PUBLIC_ASN -DWOLFSSL_HAVE_TLS_UNIQUE" ``` -`-DKEEP_PEER_CERT` and `-DWOLFSSL_HAVE_TLS_UNIQUE` are needed only by the test -server's post-handshake-auth mode (`wolfcert-server --tls-post-handshake-auth`). -Without them that mode returns `WOLFCERT_ERR_UNSUPPORTED`, and -`est_pha_roundtrip` / `est_async_roundtrip` skip. `--enable-opensslextra` -gives `KEEP_PEER_CERT` but not `WOLFSSL_HAVE_TLS_UNIQUE`. +`-DKEEP_PEER_CERT` and `-DWOLFSSL_HAVE_TLS_UNIQUE` are needed only by the test server. Its post-handshake-auth mode (`wolfcert-server --tls-post-handshake-auth`) needs both, and its `/simplereenroll` needs `KEEP_PEER_CERT` to compare the CSR with the certificate being renewed. Without them that mode returns `WOLFCERT_ERR_UNSUPPORTED`, an mTLS server answers `/simplereenroll` with 500, `est_pha_roundtrip` / `est_async_roundtrip` skip, and `est_mtls_roundtrip` skips its reenroll cases. `--enable-opensslextra` gives `KEEP_PEER_CERT` but not `WOLFSSL_HAVE_TLS_UNIQUE`. ### Header-based configuration (no build system) diff --git a/src/est/est_server.c b/src/est/est_server.c index 2fcb6db..c342879 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -33,6 +33,7 @@ #include "../internal.h" #include +#include #include #include #include @@ -932,7 +933,90 @@ static void send_missing_oid(WolfCertServer* s, int fd, send_all(s, fd, body, (size_t)bl); } -static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req) +/* RFC 7030 section 4.2.2: a reenroll CSR must carry the Subject and SAN of + * the TLS client certificate it renews. */ +static int reenroll_identity_check(WolfCertServer* s, + const uint8_t* csr_der, size_t csr_len) +{ +#ifdef KEEP_PEER_CERT + WOLFSSL_X509* peer = NULL; + const unsigned char* peer_der = NULL; + int peer_len = 0; + DecodedCert pc; + DecodedCert cc; + const byte* psan = NULL; + const byte* csan = NULL; + word32 psan_len = 0; + word32 csan_len = 0; + int rc = WOLFCERT_OK; + int wrc; + + if (s->tls_current != NULL) + peer = wolfSSL_get_peer_certificate(s->tls_current); + if (peer != NULL) + peer_der = wolfSSL_X509_get_der(peer, &peer_len); + if (peer_der == NULL || peer_len <= 0) { + if (peer != NULL) + wolfSSL_FreeX509(peer); + /* With a client CA set, a missing cert is not the client's fault. */ + if (s->tls_current != NULL && s->cfg.tls_client_ca_pem != NULL && + s->cfg.tls_client_ca_pem_len > 0) + return WOLFCERT_ERR(WOLFCERT_ERR_TLS, "est", + "reenroll: cannot read the client certificate"); + return WOLFCERT_ERR(WOLFCERT_ERR_AUTH, "est", + "reenroll: no client certificate to renew"); + } + + wc_InitDecodedCert(&pc, peer_der, (word32)peer_len, s->heap); + wc_InitDecodedCert(&cc, (byte*)csr_der, (word32)csr_len, s->heap); + wrc = wc_ParseCert(&pc, CERT_TYPE, NO_VERIFY, NULL); + if (wrc != 0) { + rc = WOLFCERT_ERR(wrc == MEMORY_E ? WOLFCERT_ERR_MEMORY : + WOLFCERT_ERR_CRYPTO, "est", + "reenroll: cannot parse the client certificate (%d)", wrc); + } + else if ((wrc = wc_ParseCert(&cc, CERTREQ_TYPE, VERIFY, NULL)) != 0) { + if (wrc == MEMORY_E) + rc = WOLFCERT_ERR_WC(wrc, "est", "reenroll: ParseCert(CSR)"); + else + rc = WOLFCERT_ERR(WOLFCERT_ERR_PARSE, "est", + "reenroll: CSR does not parse or verify (%d)", wrc); + } + else if (wolfcert_find_san(&pc, &psan, &psan_len) != WOLFCERT_OK || + wolfcert_find_san(&cc, &csan, &csan_len) != WOLFCERT_OK) { + rc = WOLFCERT_ERR(WOLFCERT_ERR_PARSE, "est", + "reenroll: extensions do not parse"); + } + else if (pc.subjectRawLen != cc.subjectRawLen || + (pc.subjectRawLen > 0 && + memcmp(pc.subjectRaw, cc.subjectRaw, + (size_t)pc.subjectRawLen) != 0) || + pc.extSubjAltNameCrit != cc.extSubjAltNameCrit || + (psan == NULL) != (csan == NULL) || psan_len != csan_len || + (psan_len > 0 && memcmp(psan, csan, psan_len) != 0)) { + rc = WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "est", + "reenroll: CSR Subject/SAN differs from the renewed certificate"); + } + + wc_FreeDecodedCert(&cc); + wc_FreeDecodedCert(&pc); + wolfSSL_FreeX509(peer); + return rc; +#else + (void)csr_der; + (void)csr_len; + if (s->tls_current == NULL || s->cfg.tls_client_ca_pem == NULL || + s->cfg.tls_client_ca_pem_len == 0) + return WOLFCERT_ERR(WOLFCERT_ERR_AUTH, "est", + "reenroll: no client certificate to renew"); + /* The handshake demanded a client cert, which this build cannot read. */ + return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "est", + "reenroll: this wolfSSL build keeps no peer certificate"); +#endif +} + +static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, + int reenroll) { int pha = ensure_post_handshake_auth(s); if (pha != WOLFCERT_OK) { @@ -971,6 +1055,21 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req) return rc; } + if (reenroll) { + rc = reenroll_identity_check(s, csr.data, csr.len); + if (rc != WOLFCERT_OK) { + if (rc == WOLFCERT_ERR_AUTH) + send_status(s, fd, 401, "Unauthorized"); + else if (rc == WOLFCERT_ERR_PARSE || rc == WOLFCERT_ERR_PROTOCOL) + send_status(s, fd, 400, "Bad Request"); + else + send_status(s, fd, 500, "Server Error"); + + wolfcert_buffer_free(&csr); + return rc; + } + } + /* RFC 7030 section 3.5 lets an EST client bind the proof-of-possession to * the TLS session by carrying the tls-unique channel binding (RFC 5929) * inside the CSR (typically the PKCS#9 challengePassword), in which case @@ -1081,7 +1180,8 @@ static int handle_request(WolfCertServer* s, int fd) rc = WOLFCERT_ERR_AUTH; } else { - rc = handler_enroll(s, fd, &req); + rc = handler_enroll(s, fd, &req, + strcmp(suffix, "simplereenroll") == 0); } } else { diff --git a/tests/integration/test_est_mtls_roundtrip.c b/tests/integration/test_est_mtls_roundtrip.c index 89221a9..ea5ee34 100644 --- a/tests/integration/test_est_mtls_roundtrip.c +++ b/tests/integration/test_est_mtls_roundtrip.c @@ -24,13 +24,18 @@ * then runs wolfcert-client through wolfcert_est_simple_enroll with * client_cert / client_key set on WolfCertServerCfg. * - * Three assertions: + * Six assertions: * 1. mTLS works: a client that does NOT present a certificate is * rejected by the TLS handshake. * 2. mTLS works: a client that DOES present a cert signed by the * configured trust anchor enrolls successfully. * 3. /simplereenroll presents the cert being renewed even when * client_cert names a different, untrusted identity. + * 4. /simplereenroll refuses a CSR whose Subject or SAN differs from + * the cert being renewed. + * 5. /simplereenroll accepts a renewal of a multi-SAN cert the server's + * own CA issued, but not one that changes a SAN entry or marks it critical. + * 6. /simplereenroll answers a body that is not a PKCS#10 request with 400. * * Exercises the TLS 1.3 negotiation path (wolfTLS_client_method / * wolfTLS_server_method) and the new client_cert plumbing on @@ -71,6 +76,7 @@ static void* server_thread(void* arg) { wolfcert_server_run((WolfCertServer*)arg); return NULL; } +#ifdef KEEP_PEER_CERT /* Re-enroll the trusted cli_cert while cfg carries an identity the server * does not trust; the handshake must present the cert being renewed. */ static int test_reenroll_ignores_cfg_identity(const char* url, @@ -131,6 +137,331 @@ static int test_reenroll_ignores_cfg_identity(const char* url, return 0; } +/* Re-enroll cli_cert with a CSR whose Subject or SAN differs from it; the + * server must refuse to issue. */ +static int test_reenroll_rejects_identity_change(const char* url, + const uint8_t* tls_cert, size_t tls_cert_len, + const uint8_t* cli_cert, size_t cli_cert_len, + const uint8_t* cli_key, size_t cli_key_len) +{ + static const char* const dns[] = { "other.example" }; + const WolfCertCertMeta metas[2] = { + { .subject_dn = "CN=someone-else" }, + { .subject_dn = "CN=factory-bootstrap", .san_dns = dns, .san_dns_len = 1 }, + }; + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertServerCfg cli = { + .protocol = WOLFCERT_PROTO_EST, + .server_url = url, + .trust_anchors = tls_cert, + .trust_anchors_len = tls_cert_len, + .verify_server = 1, + }; + WolfCertKey* cur_key = NULL; + WolfCertKey* dk = NULL; + + REQUIRE(wolfcert_key_from_pem(cli_key, cli_key_len, NULL, &cur_key) == WOLFCERT_OK); + REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); + + for (size_t i = 0; i < sizeof(metas) / sizeof(metas[0]); ++i) { + WolfCertBuffer csr = { 0 }; + WolfCertBuffer issued = { 0 }; + int rc; + + REQUIRE(wolfcert_csr_build(dk, &metas[i], &csr) == WOLFCERT_OK); + rc = wolfcert_est_simple_reenroll(&cli, cli_cert, cli_cert_len, cur_key, + csr.data, csr.len, &issued); + wolfcert_buffer_free(&csr); + if (rc == WOLFCERT_OK) + fprintf(stderr, "reenroll with changed identity %zu was issued\n", i); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(issued.data == NULL); + } + + wolfcert_key_free(dk); + wolfcert_key_free(cur_key); + return 0; +} + +/* A reenroll body that is DER but not PKCS#10 is the client's fault: 400. */ +static int test_reenroll_rejects_non_csr(const char* url, + const uint8_t* tls_cert, size_t tls_cert_len, + const uint8_t* cli_cert, size_t cli_cert_len, + const uint8_t* cli_key, size_t cli_key_len) +{ + static const uint8_t not_csr[] = { 0x30, 0x03, 0x02, 0x01, 0x00 }; + WolfCertServerCfg cli = { + .protocol = WOLFCERT_PROTO_EST, + .server_url = url, + .trust_anchors = tls_cert, + .trust_anchors_len = tls_cert_len, + .verify_server = 1, + }; + WolfCertKey* cur_key = NULL; + WolfCertBuffer issued = { 0 }; + int rc; + + REQUIRE(wolfcert_key_from_pem(cli_key, cli_key_len, NULL, &cur_key) == WOLFCERT_OK); + rc = wolfcert_est_simple_reenroll(&cli, cli_cert, cli_cert_len, cur_key, + not_csr, sizeof(not_csr), &issued); + wolfcert_key_free(cur_key); + if (rc != WOLFCERT_ERR_HTTP) + fprintf(stderr, "non-CSR reenroll rc=%d (%s)\n", rc, + wolfcert_last_error_message()); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(issued.data == NULL); + REQUIRE(strstr(wolfcert_last_error_message(), "HTTP 400") != NULL); + return 0; +} + +#ifdef HAVE_ECC +/* The GeneralNames of a non-critical SAN extension in der, or NULL. */ +static const uint8_t* raw_san(const uint8_t* der, size_t der_len, size_t* len) +{ + static const uint8_t oid[] = { 0x06, 0x03, 0x55, 0x1d, 0x11, 0x04 }; + const uint8_t* p = memmem(der, der_len, oid, sizeof(oid)); + size_t n; + + if (p == NULL || (size_t)(der + der_len - p) < sizeof(oid) + 3) + return NULL; + p += sizeof(oid); + if (p[0] < 0x80) { + *len = p[0]; + n = 1; + } + else if (p[0] == 0x81) { + *len = p[1]; + n = 2; + } + else if (p[0] == 0x82) { + *len = ((size_t)p[1] << 8) | p[2]; + n = 3; + } + else { + return NULL; + } + return (size_t)(der + der_len - p) >= n + *len ? p + n : NULL; +} + +/* A CSR carrying cert's raw Subject and SAN, with the SAN marked critical. */ +static int csr_with_critical_san(const WolfCertBuffer* cert_pem, + uint8_t* out, size_t out_cap) +{ + WC_RNG rng; + ecc_key key; + Cert req; + uint8_t der[4096]; + const uint8_t* san; + size_t san_len = 0; + int der_len; + int len = -1; + + der_len = wc_CertPemToDer(cert_pem->data, (int)cert_pem->len, der, + (int)sizeof(der), CERT_TYPE); + if (der_len <= 0) + return -1; + san = raw_san(der, (size_t)der_len, &san_len); + if (san == NULL || san_len > sizeof(req.altNames) || wc_InitRng(&rng) != 0) + return -1; + if (wc_ecc_init(&key) == 0) { + if (wc_ecc_make_key(&rng, 32, &key) == 0 && wc_InitCert(&req) == 0 && + wc_SetSubjectRaw(&req, der, der_len) == 0) { + memcpy(req.altNames, san, san_len); + req.altNamesSz = (int)san_len; + req.altNamesCrit = 1; + req.version = 0; + req.sigType = CTC_SHA256wECDSA; + len = wc_MakeCertReq_ex(&req, out, (word32)out_cap, ECC_TYPE, &key); + if (len > 0) + len = wc_SignCert_ex(req.bodySz, req.sigType, out, + (word32)out_cap, ECC_TYPE, &key, &rng); + } + wc_ecc_free(&key); + } + wc_FreeRng(&rng); + return len; +} +#endif + +/* Enroll a multi-RDN, multi-SAN identity through the server's own CA, then + * renew it: the CA's re-encoding must pass the reenroll identity check. */ +static int test_reenroll_server_issued(const uint8_t* tls_cert, size_t tls_cert_len, + const uint8_t* tls_key, size_t tls_key_len, + const uint8_t* cli_cert, size_t cli_cert_len, + const uint8_t* cli_key, size_t cli_key_len) +{ + static const char* const dns[] = { "a.example", "b.example", "c.example" }; + static const char* const dns2[] = { "a.example", "b.example", "d.example" }; + static const char* const ip[] = { "127.0.0.1", "10.0.0.7" }; + static const char* const email[] = { "dev@example.com" }; + const WolfCertCertMeta meta = { + .subject_dn = "CN=multi-san,O=wolfSSL,OU=EST", + .san_dns = dns, .san_dns_len = 3, + .san_ip = ip, .san_ip_len = 2, + .san_email = email, .san_email_len = 1, + }; + const WolfCertCertMeta meta2 = { + .subject_dn = "CN=multi-san,O=wolfSSL,OU=EST", + .san_dns = dns2, .san_dns_len = 3, + .san_ip = ip, .san_ip_len = 2, + .san_email = email, .san_email_len = 1, + }; + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertServerCfgSrv cfg = { + .protocol = WOLFCERT_PROTO_EST, + .bind_host = "127.0.0.1", + .bind_port = 0, + .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, + .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .tls_client_ca_pem = cli_cert, .tls_client_ca_pem_len = cli_cert_len, + }; + WolfCertStoreOps* store = wolfcert_store_memory_open(NULL); + WolfCertServer* srv = NULL; + WolfCertBuffer ca_der = { 0 }; + WolfCertBuffer csr = { 0 }; + WolfCertBuffer issued = { 0 }; + WolfCertBuffer renewed = { 0 }; + WolfCertBuffer san_changed = { 0 }; + WolfCertKey* k1 = NULL; + WolfCertKey* k2 = NULL; + uint8_t* bundle = NULL; + size_t bundle_cap; + pthread_t tid; + char url[128]; + int pem_len; + int rc; +#ifdef HAVE_ECC + uint8_t crit_csr[4096]; + WolfCertBuffer refused = { 0 }; + int crit_len; +#endif + + /* The first start mints the CA into the store; the second adopts it. */ + REQUIRE(store != NULL); + cfg.ca_store = store; + REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); + wolfcert_server_free(srv); + srv = NULL; + REQUIRE(store->read(store->ctx, "ca.cert.der", &ca_der) == WOLFCERT_OK); + + bundle_cap = cli_cert_len + ca_der.len * 2 + 256; + bundle = (uint8_t*)malloc(bundle_cap); + REQUIRE(bundle != NULL); + memcpy(bundle, cli_cert, cli_cert_len); + pem_len = wc_DerToPem(ca_der.data, (word32)ca_der.len, bundle + cli_cert_len, + (word32)(bundle_cap - cli_cert_len), CERT_TYPE); + REQUIRE(pem_len > 0); + cfg.tls_client_ca_pem = bundle; + cfg.tls_client_ca_pem_len = cli_cert_len + (size_t)pem_len; + + REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); + REQUIRE(pthread_create(&tid, NULL, server_thread, srv) == 0); + snprintf(url, sizeof(url), "https://127.0.0.1:%u/.well-known/est", + wolfcert_server_port(srv)); + + WolfCertServerCfg cli = { + .protocol = WOLFCERT_PROTO_EST, + .server_url = url, + .trust_anchors = tls_cert, + .trust_anchors_len = tls_cert_len, + .verify_server = 1, + .client_cert = cli_cert, + .client_cert_len = cli_cert_len, + .client_key = cli_key, + .client_key_len = cli_key_len, + }; + + REQUIRE(wolfcert_key_generate(&kcfg, &k1) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build(k1, &meta, &csr) == WOLFCERT_OK); + rc = wolfcert_est_simple_enroll(&cli, csr.data, csr.len, &issued); + wolfcert_buffer_free(&csr); + REQUIRE(rc == WOLFCERT_OK); + + REQUIRE(wolfcert_key_generate(&kcfg, &k2) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build(k2, &meta, &csr) == WOLFCERT_OK); + rc = wolfcert_est_simple_reenroll(&cli, issued.data, issued.len, k1, + csr.data, csr.len, &renewed); + if (rc != WOLFCERT_OK) + fprintf(stderr, "reenroll of a server-issued cert rc=%d (%s)\n", rc, + wolfcert_strerror(rc)); + REQUIRE(rc == WOLFCERT_OK); + REQUIRE(memmem(renewed.data, renewed.len, "BEGIN CERTIFICATE", 17) != NULL); + + /* Same Subject and SAN shape, one dNSName changed. */ + wolfcert_buffer_free(&csr); + REQUIRE(wolfcert_csr_build(k2, &meta2, &csr) == WOLFCERT_OK); + rc = wolfcert_est_simple_reenroll(&cli, issued.data, issued.len, k1, + csr.data, csr.len, &san_changed); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(strstr(wolfcert_last_error_message(), "HTTP 400") != NULL); + REQUIRE(san_changed.data == NULL); + +#ifdef HAVE_ECC + crit_len = csr_with_critical_san(&issued, crit_csr, sizeof(crit_csr)); + REQUIRE(crit_len > 0); + rc = wolfcert_est_simple_reenroll(&cli, issued.data, issued.len, k1, + crit_csr, (size_t)crit_len, &refused); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(strstr(wolfcert_last_error_message(), "HTTP 400") != NULL); + REQUIRE(refused.data == NULL); +#endif + + wolfcert_server_stop(srv); + pthread_join(tid, NULL); + wolfcert_server_free(srv); + wolfcert_store_memory_close(store); + wolfcert_buffer_free(&renewed); + wolfcert_buffer_free(&issued); + wolfcert_buffer_free(&csr); + wolfcert_buffer_free(&ca_der); + wolfcert_key_free(k2); + wolfcert_key_free(k1); + free(bundle); + return 0; +} + +#endif /* KEEP_PEER_CERT */ + +#ifndef KEEP_PEER_CERT +/* The mTLS handshake demanded a client cert this build cannot read: 500. */ +static int test_reenroll_needs_peer_cert(const char* url, + const uint8_t* tls_cert, size_t tls_cert_len, + const uint8_t* cli_cert, size_t cli_cert_len, + const uint8_t* cli_key, size_t cli_key_len) +{ + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertCertMeta meta = { .subject_dn = "CN=factory-bootstrap" }; + WolfCertServerCfg cli = { + .protocol = WOLFCERT_PROTO_EST, + .server_url = url, + .trust_anchors = tls_cert, + .trust_anchors_len = tls_cert_len, + .verify_server = 1, + }; + WolfCertKey* cur_key = NULL; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertBuffer issued = { 0 }; + int rc; + + REQUIRE(wolfcert_key_from_pem(cli_key, cli_key_len, NULL, &cur_key) == WOLFCERT_OK); + REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build(dk, &meta, &csr) == WOLFCERT_OK); + rc = wolfcert_est_simple_reenroll(&cli, cli_cert, cli_cert_len, cur_key, + csr.data, csr.len, &issued); + wolfcert_buffer_free(&csr); + wolfcert_key_free(dk); + wolfcert_key_free(cur_key); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(strstr(wolfcert_last_error_message(), "HTTP 500") != NULL); + REQUIRE(issued.data == NULL); + return 0; +} +#endif + int main(void) { REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); @@ -225,15 +556,41 @@ int main(void) wolfcert_key_free(dk); } +#ifdef KEEP_PEER_CERT /* --- Case 3: reenroll presents the cert being renewed, not cfg's. */ REQUIRE(test_reenroll_ignores_cfg_identity(url, tls_cert, tls_cert_len, cli_cert, cli_cert_len, cli_key, cli_key_len) == 0); + /* --- Case 4: reenroll may not change the Subject or SAN. */ + REQUIRE(test_reenroll_rejects_identity_change(url, tls_cert, tls_cert_len, + cli_cert, cli_cert_len, + cli_key, cli_key_len) == 0); + + /* --- Case 6: a body that is not a CSR is a 400, not a server error. */ + REQUIRE(test_reenroll_rejects_non_csr(url, tls_cert, tls_cert_len, + cli_cert, cli_cert_len, + cli_key, cli_key_len) == 0); +#else + /* --- Without KEEP_PEER_CERT an mTLS reenroll is a 500, not an issue. */ + REQUIRE(test_reenroll_needs_peer_cert(url, tls_cert, tls_cert_len, + cli_cert, cli_cert_len, + cli_key, cli_key_len) == 0); + printf("skip reenroll cases: wolfSSL built without KEEP_PEER_CERT\n"); +#endif + wolfcert_server_stop(srv); pthread_join(tid, NULL); wolfcert_server_free(srv); +#ifdef KEEP_PEER_CERT + /* --- Case 5: renew a server-issued cert carrying several SANs. */ + REQUIRE(test_reenroll_server_issued(tls_cert, tls_cert_len, + tls_key, tls_key_len, + cli_cert, cli_cert_len, + cli_key, cli_key_len) == 0); +#endif + free(tls_cert); free(tls_key); free(cli_cert); diff --git a/tests/integration/test_est_pha_roundtrip.c b/tests/integration/test_est_pha_roundtrip.c index 37fc07c..9e77543 100644 --- a/tests/integration/test_est_pha_roundtrip.c +++ b/tests/integration/test_est_pha_roundtrip.c @@ -28,6 +28,8 @@ * 2. Call /simpleenroll - the server must trigger a CertificateRequest * via wolfSSL_request_certificate(); the client answers from the * pre-loaded identity and the CSR is issued. + * A raw TLS client then renews its cert with /simplereenroll over PHA, which + * the client API cannot do: only sessions offer PHA, and they have no reenroll. * * Negative controls: a session without a client identity, and one with an * identity but no PHA opt-in, must both fail /simpleenroll while /cacerts @@ -45,6 +47,7 @@ #include #include +#include #include #include "tls_test_util.h" @@ -67,6 +70,57 @@ static void* server_thread(void* arg) { wolfcert_server_run((WolfCertServer*)arg); return NULL; } +#ifdef WOLFSSL_POST_HANDSHAKE_AUTH +/* POST a renewal CSR for cli_cert over a PHA connection and expect a 200. */ +static int pha_reenroll(uint16_t port, const uint8_t* tls_cert, size_t tls_cert_len, + const uint8_t* cli_cert, size_t cli_cert_len, + const uint8_t* cli_key, size_t cli_key_len) +{ + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertCertMeta meta = { .subject_dn = "CN=factory-bootstrap" }; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + TestTlsConn c; + char req[4096]; + byte b64[3072]; + word32 b64_len = sizeof(b64); + char resp[64] = { 0 }; + int n; + + REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build(dk, &meta, &csr) == WOLFCERT_OK); + wolfcert_key_free(dk); + REQUIRE(Base64_Encode_NoNl(csr.data, (word32)csr.len, b64, &b64_len) == 0); + wolfcert_buffer_free(&csr); + n = snprintf(req, sizeof(req), + "POST /.well-known/est/simplereenroll HTTP/1.1\r\n" + "Host: 127.0.0.1\r\nContent-Type: application/pkcs10\r\n" + "Content-Length: %u\r\nConnection: close\r\n\r\n%.*s", + (unsigned)b64_len, (int)b64_len, (const char*)b64); + REQUIRE(n > 0 && (size_t)n < sizeof(req)); + + /* On the CTX: wolfSSL unloads an SSL-level identity after the handshake. */ + REQUIRE(test_tls_setup(&c, port, tls_cert, tls_cert_len) == 0); + REQUIRE(wolfSSL_CTX_use_certificate_buffer(c.ctx, cli_cert, (long)cli_cert_len, + WOLFSSL_FILETYPE_PEM) == WOLFSSL_SUCCESS); + REQUIRE(wolfSSL_CTX_use_PrivateKey_buffer(c.ctx, cli_key, (long)cli_key_len, + WOLFSSL_FILETYPE_PEM) == WOLFSSL_SUCCESS); + wolfSSL_free(c.ssl); + c.ssl = wolfSSL_new(c.ctx); + REQUIRE(c.ssl != NULL && wolfSSL_set_fd(c.ssl, c.fd) == WOLFSSL_SUCCESS); + REQUIRE(wolfSSL_allow_post_handshake_auth(c.ssl) == 0); + REQUIRE(wolfSSL_connect(c.ssl) == WOLFSSL_SUCCESS); + REQUIRE(test_tls_write(&c, req, (size_t)n) == 0); + REQUIRE(test_tls_read(&c, resp, sizeof(resp) - 1) > 0); + test_tls_close(&c); + if (strncmp(resp, "HTTP/1.1 200", 12) != 0) + fprintf(stderr, "pha reenroll: %s\n", resp); + REQUIRE(strncmp(resp, "HTTP/1.1 200", 12) == 0); + return 0; +} +#endif + #ifdef WOLFSSL_POST_HANDSHAKE_AUTH static int discard_send(WOLFSSL* ssl, char* buf, int sz, void* ctx) { @@ -248,6 +302,10 @@ int main(void) } #ifdef WOLFSSL_POST_HANDSHAKE_AUTH + /* --- /simplereenroll checks the CSR against the cert sent over PHA. */ + REQUIRE(pha_reenroll(wolfcert_server_port(srv), tls_cert, tls_cert_len, + cli_cert, cli_cert_len, cli_key, cli_key_len) == 0); + /* --- A PHA client that never answers the CertificateRequest gets a 401 * once the server stops waiting, well before the request deadline. */ { diff --git a/tests/integration/test_est_tls_roundtrip.c b/tests/integration/test_est_tls_roundtrip.c index 5771cb1..0546394 100644 --- a/tests/integration/test_est_tls_roundtrip.c +++ b/tests/integration/test_est_tls_roundtrip.c @@ -61,6 +61,7 @@ static void* server_thread(void* arg) { wolfcert_server_run((WolfCertServer*)arg); return NULL; } +#ifdef KEEP_PEER_CERT static int impostor_customize(void* wolfssl_cert, void* ctx) { Cert* c = (Cert*)wolfssl_cert; @@ -124,12 +125,14 @@ static int same_public_key(const uint8_t* a, size_t a_len, const uint8_t* b, wc_FreeDecodedCert(&dc[1]); return ret; } +#endif /* Reenroll with a mismatched meta, with a renaming customize callback, then - * with a fresh key. */ -static int test_client_reenroll_keeps_identity(const char* url, - const uint8_t* tls_cert, - size_t tls_cert_len) + * with a fresh key, against a server that trusts the cert being renewed. */ +static int test_client_reenroll_keeps_identity(const uint8_t* tls_cert, + size_t tls_cert_len, + const uint8_t* tls_key, + size_t tls_key_len) { static const char* const impostor_dns[] = { "impostor.example" }; uint8_t* cur_cert = NULL; @@ -140,6 +143,16 @@ static int test_client_reenroll_keeps_identity(const char* url, WolfCertKey* out_key = NULL; WolfCertBuffer issued = { 0 }; WolfCertCertMeta meta; + WolfCertServerCfgSrv scfg = { + .protocol = WOLFCERT_PROTO_EST, + .bind_host = "127.0.0.1", + .bind_port = 0, + .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, + .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + }; + WolfCertServer* srv = NULL; + pthread_t tid; + char url[128]; WolfCertServerCfg cli = { .protocol = WOLFCERT_PROTO_EST, .server_url = url, @@ -147,16 +160,25 @@ static int test_client_reenroll_keeps_identity(const char* url, .trust_anchors_len = tls_cert_len, .verify_server = 1, }; +#ifdef KEEP_PEER_CERT WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; int called = 0; +#endif REQUIRE(mint_self_id("reenroll-device", 0, &cur_cert, &cur_cert_len, &cur_key_pem, &cur_key_len) == 0); REQUIRE(wolfcert_key_from_pem(cur_key_pem, cur_key_len, NULL, &cur_key) == WOLFCERT_OK); + scfg.tls_client_ca_pem = cur_cert; + scfg.tls_client_ca_pem_len = cur_cert_len; + REQUIRE(wolfcert_server_start(&scfg, &srv) == WOLFCERT_OK); + REQUIRE(pthread_create(&tid, NULL, server_thread, srv) == 0); + snprintf(url, sizeof(url), "https://127.0.0.1:%u/.well-known/est", + wolfcert_server_port(srv)); + memset(&meta, 0, sizeof(meta)); meta.subject_dn = "CN=impostor"; REQUIRE(wolfcert_client_reenroll(NULL, &cli, cur_cert, cur_cert_len, @@ -170,6 +192,7 @@ static int test_client_reenroll_keeps_identity(const char* url, cur_key, NULL, &meta, &out_key, &issued) == WOLFCERT_ERR_BAD_ARG); +#ifdef KEEP_PEER_CERT /* The caller's callback still runs but cannot rename the cert. */ memset(&meta, 0, sizeof(meta)); meta.customize = impostor_customize; @@ -190,7 +213,11 @@ static int test_client_reenroll_keeps_identity(const char* url, REQUIRE(check_renewed_identity(&issued) == 0); REQUIRE(same_public_key(issued.data, issued.len, cur_cert, cur_cert_len) == 0); +#endif + wolfcert_server_stop(srv); + pthread_join(tid, NULL); + wolfcert_server_free(srv); wolfcert_key_free(out_key); wolfcert_buffer_free(&issued); wolfcert_key_free(cur_key); @@ -296,8 +323,8 @@ int main(void) wolfcert_buffer_free(&ca_pem_der); wc_FreeDer(&ta_der); - REQUIRE(test_client_reenroll_keeps_identity(url, tls_cert, tls_cert_len) - == 0); + REQUIRE(test_client_reenroll_keeps_identity(tls_cert, tls_cert_len, + tls_key, tls_key_len) == 0); wolfcert_server_stop(srv); pthread_join(tid, NULL); From afcfd6b226c45987a9aefe0610237f808870d426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 1 Oct 2026 14:13:54 +0200 Subject: [PATCH 03/10] est: send a plaintext reason with every server error response RFC 7030 section 4.2.3 requires an error response that carries no media type to include a plaintext, human-readable explanation of why the request was rejected. The EST test server answered every 4xx and 5xx with an empty body and no Content-Type, except for the missing CSR-attribute case. Add send_error(), which emits Content-Type: text/plain and a one-line reason, and use it at every error site in the request dispatcher and the enroll, cacerts and csrattrs handlers. send_missing_oid() now formats its message and hands it to send_error(). send_status() is kept only for the body-less 204 from /csrattrs. A HEAD request to an unknown path gets its 404 without a body, as RFC 9110 section 9.3.2 requires. Each reenroll failure now names its cause: a missing client certificate stays 401, a CSR that does not parse or does not match gets 400, and a failure on the server side, such as a client certificate it cannot parse or an mTLS server built without KEEP_PEER_CERT, gets 500. Extend the CSR-attribute enforcement test to check that a 404 and an empty-body 400 both come back as text/plain with a non-empty body, and est_chunked_robustness to check that a HEAD for an unknown path gets no body. Fixes F-8035. --- src/est/est_server.c | 84 ++++++++++++------- .../integration/test_est_chunked_robustness.c | 34 +++++++- .../integration/test_est_csr_attrs_enforce.c | 34 ++++++++ 3 files changed, 123 insertions(+), 29 deletions(-) diff --git a/src/est/est_server.c b/src/est/est_server.c index c342879..6320522 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -520,6 +520,21 @@ static void send_status(WolfCertServer* s, int fd, int status, const char* phras send_all(s, fd, line, (size_t)n); } +/* RFC 7030 section 4.2.3: an untyped error body must be a plaintext reason. */ +static void send_error(WolfCertServer* s, int fd, int status, + const char* phrase, const char* why) +{ + char hdr[192]; + size_t wl = strlen(why); + int n = snprintf(hdr, sizeof(hdr), + "HTTP/1.1 %d %s\r\nContent-Type: text/plain\r\n" + "Content-Length: %zu\r\nConnection: %s\r\n\r\n", + status, phrase, wl, conn_hdr(s)); + + send_all(s, fd, hdr, (size_t)n); + send_all(s, fd, why, wl); +} + static void send_pkcs7_b64(WolfCertServer* s, int fd, const uint8_t* b64, size_t b64_len) { char hdr[256]; @@ -611,7 +626,8 @@ static int handler_csr_attrs(WolfCertServer* s, int fd) int rc = wolfcert_base64_encode_mime(s->cfg_csr_attrs, s->cfg_csr_attrs_len, &b64, s->heap); if (rc != WOLFCERT_OK) { - send_status(s, fd, 500, "Server Error"); + send_error(s, fd, 500, "Server Error", + "cannot encode the CSR attributes\n"); return rc; } @@ -639,7 +655,8 @@ static int handler_cacerts(WolfCertServer* s, int fd) int rc = wolfcert_pkcs7_build_certs_only(certs, clen, 1, &p7, s->heap); if (rc != WOLFCERT_OK) { - send_status(s, fd,500, "Server Error"); + send_error(s, fd, 500, "Server Error", + "cannot build the CA certificates response\n"); return rc; } @@ -648,7 +665,8 @@ static int handler_cacerts(WolfCertServer* s, int fd) wolfcert_buffer_free(&p7); if (rc != WOLFCERT_OK) { - send_status(s, fd,500, "Server Error"); + send_error(s, fd, 500, "Server Error", + "cannot build the CA certificates response\n"); return rc; } @@ -921,16 +939,9 @@ static void send_missing_oid(WolfCertServer* s, int fd, wolfcert_oid_to_dotted(oid, oid_len, txt, sizeof(txt)); char body[192]; - int bl = snprintf(body, sizeof(body), - "CSR missing required attribute OID %s\n", txt); - char hdr[192]; - int hl = snprintf(hdr, sizeof(hdr), - "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\n" - "Content-Length: %d\r\nConnection: %s\r\n\r\n", - bl, conn_hdr(s)); - - send_all(s, fd, hdr, (size_t)hl); - send_all(s, fd, body, (size_t)bl); + snprintf(body, sizeof(body), + "CSR missing required attribute OID %s\n", txt); + send_error(s, fd, 400, "Bad Request", body); } /* RFC 7030 section 4.2.2: a reenroll CSR must carry the Subject and SAN of @@ -1020,12 +1031,13 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, { int pha = ensure_post_handshake_auth(s); if (pha != WOLFCERT_OK) { - send_status(s, fd, 401, "Unauthorized"); + send_error(s, fd, 401, "Unauthorized", + "client certificate authentication failed\n"); return pha; } if (req->body == NULL || req->body_len == 0) { - send_status(s, fd,400, "Bad Request"); + send_error(s, fd, 400, "Bad Request", "request body is empty\n"); return WOLFCERT_ERR_HTTP; } @@ -1039,7 +1051,8 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, int idx = pending_find(p, h); if (idx < 0) { if (!pending_add(p, h)) { - send_status(s, fd, 503, "Service Unavailable"); + send_error(s, fd, 503, "Service Unavailable", + "pending enrollment queue is full\n"); return WOLFCERT_ERR_PROTOCOL; } send_accepted_retry_after(s, fd, s->cfg.est_retry_after_sec); @@ -1051,7 +1064,7 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, WolfCertBuffer csr = { 0 }; int rc = wolfcert_base64_decode(req->body, req->body_len, &csr, s->heap); if (rc != WOLFCERT_OK) { - send_status(s, fd,400, "Bad Request"); + send_error(s, fd, 400, "Bad Request", "CSR is not valid base64\n"); return rc; } @@ -1059,11 +1072,17 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, rc = reenroll_identity_check(s, csr.data, csr.len); if (rc != WOLFCERT_OK) { if (rc == WOLFCERT_ERR_AUTH) - send_status(s, fd, 401, "Unauthorized"); - else if (rc == WOLFCERT_ERR_PARSE || rc == WOLFCERT_ERR_PROTOCOL) - send_status(s, fd, 400, "Bad Request"); + send_error(s, fd, 401, "Unauthorized", + "no client certificate to renew\n"); + else if (rc == WOLFCERT_ERR_PARSE) + send_error(s, fd, 400, "Bad Request", + "CSR is not a valid PKCS#10 request\n"); + else if (rc == WOLFCERT_ERR_PROTOCOL) + send_error(s, fd, 400, "Bad Request", + "CSR does not match the certificate being renewed\n"); else - send_status(s, fd, 500, "Server Error"); + send_error(s, fd, 500, "Server Error", + "cannot check the certificate being renewed\n"); wolfcert_buffer_free(&csr); return rc; @@ -1098,7 +1117,8 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, if (erc == WOLFCERT_ERR_PROTOCOL && missing_len > 0) send_missing_oid(s, fd, missing_oid, missing_len); else - send_status(s, fd, 400, "Bad Request"); + send_error(s, fd, 400, "Bad Request", + "CSR does not satisfy the CSR attributes policy\n"); wolfcert_buffer_free(&csr); return erc; @@ -1111,7 +1131,7 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, wolfcert_buffer_free(&csr); if (rc != WOLFCERT_OK) { - send_status(s, fd,400, "Bad CSR"); + send_error(s, fd, 400, "Bad CSR", "CSR rejected by the CA\n"); return rc; } @@ -1122,7 +1142,8 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, WOLFCERT_XFREE(issued, s->heap); if (rc != WOLFCERT_OK) { - send_status(s, fd,500, "Server Error"); + send_error(s, fd, 500, "Server Error", + "cannot build the certificate response\n"); return rc; } @@ -1131,7 +1152,8 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, wolfcert_buffer_free(&p7); if (rc != WOLFCERT_OK) { - send_status(s, fd,500, "Server Error"); + send_error(s, fd, 500, "Server Error", + "cannot build the certificate response\n"); return rc; } @@ -1150,7 +1172,7 @@ static int handle_request(WolfCertServer* s, int fd) /* No parseable request means the client hung up or sent * garbage; either way we're done with this connection. */ s->keep_alive = 0; - send_status(s, fd, 400, "Bad Request"); + send_error(s, fd, 400, "Bad Request", "malformed HTTP request\n"); free_req(&req); return rc; } @@ -1176,7 +1198,8 @@ static int handle_request(WolfCertServer* s, int fd) (strcmp(suffix, "simpleenroll") == 0 || strcmp(suffix, "simplereenroll") == 0)) { if (!check_basic_auth(s, req.auth_header)) { - send_status(s, fd,401, "Unauthorized"); + send_error(s, fd, 401, "Unauthorized", + "HTTP Basic authentication failed\n"); rc = WOLFCERT_ERR_AUTH; } else { @@ -1184,8 +1207,13 @@ static int handle_request(WolfCertServer* s, int fd) strcmp(suffix, "simplereenroll") == 0); } } + else if (strcmp(req.method, "HEAD") == 0) { + /* RFC 9110 section 9.3.2: a response to HEAD carries no content. */ + send_error(s, fd, 404, "Not Found", ""); + rc = WOLFCERT_ERR_NOT_FOUND; + } else { - send_status(s, fd,404, "Not Found"); + send_error(s, fd, 404, "Not Found", "unknown EST operation\n"); rc = WOLFCERT_ERR_NOT_FOUND; } diff --git a/tests/integration/test_est_chunked_robustness.c b/tests/integration/test_est_chunked_robustness.c index 11a1410..efad365 100644 --- a/tests/integration/test_est_chunked_robustness.c +++ b/tests/integration/test_est_chunked_robustness.c @@ -113,6 +113,36 @@ static int send_and_read_status(uint16_t port, return (int)n; } +/* A 404 for HEAD must end at the headers, or its body would be read as the + * next response on a kept-alive connection. */ +static int head_404_has_no_body(uint16_t port) +{ + static const char req[] = + "HEAD /.well-known/est/nope HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Connection: close\r\n" + "\r\n"; + TestTlsConn c; + char resp[512]; + const char* eoh; + size_t n = 0; + int r; + + REQUIRE(test_tls_connect(&c, port, g_tls_cert, g_tls_cert_len) == 0); + REQUIRE(test_tls_write(&c, req, sizeof(req) - 1) == 0); + while (n + 1 < sizeof(resp) && + (r = test_tls_read(&c, resp + n, sizeof(resp) - 1 - n)) > 0) + n += (size_t)r; + test_tls_close(&c); + resp[n] = '\0'; + + REQUIRE(strstr(resp, " 404 ") != NULL); + eoh = strstr(resp, "\r\n\r\n"); + REQUIRE(eoh != NULL); + REQUIRE(eoh[4] == '\0'); + return 0; +} + /* Shape #1: a chunk-size line longer than 8 hex digits. The parser * must reject this rather than letting the shift-accumulate silently * wrap. */ @@ -459,7 +489,9 @@ int main(void) uint16_t port = wolfcert_server_port(srv); - int rc = reject_oversized_chunk_size(port); + int rc = head_404_has_no_body(port); + if (rc == 0) + rc = reject_oversized_chunk_size(port); if (rc == 0) rc = reject_corrupt_chunk_trailer(port); if (rc == 0) diff --git a/tests/integration/test_est_csr_attrs_enforce.c b/tests/integration/test_est_csr_attrs_enforce.c index d6659eb..2289130 100644 --- a/tests/integration/test_est_csr_attrs_enforce.c +++ b/tests/integration/test_est_csr_attrs_enforce.c @@ -270,6 +270,38 @@ static int reject_body_names_missing_oid(uint16_t port) return 0; } +/* RFC 7030 section 4.2.3: an error response without a media type must carry a + * plaintext explanation, so neither a 404 nor an empty-body 400 may be empty. */ +static int reject_bodies_are_plaintext(uint16_t port) +{ + static const char* const reqs[] = { + "GET /.well-known/est/nosuchop HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Connection: close\r\n" + "\r\n", + "POST /.well-known/est/simpleenroll HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Content-Type: application/pkcs10\r\n" + "Content-Length: 0\r\n" + "Connection: close\r\n" + "\r\n" + }; + char resp[1024]; + size_t i; + + for (i = 0; i < sizeof(reqs) / sizeof(reqs[0]); ++i) { + const char* body; + int n = send_and_read_all(port, reqs[i], strlen(reqs[i]), + resp, sizeof(resp)); + REQUIRE(n > 0); + REQUIRE(strstr(resp, i == 0 ? " 404 " : " 400 ") != NULL); + REQUIRE(strstr(resp, "Content-Type: text/plain\r\n") != NULL); + body = strstr(resp, "\r\n\r\n"); + REQUIRE(body != NULL && body[4] != '\0'); + } + return 0; +} + /* Client C - server advertises ONLY an Attribute-with-values item * (no bare OIDs). The CSR doesn't carry anything matching it. * Enforcement is presence-only on bare OIDs, so this must still @@ -394,6 +426,8 @@ int main(void) REQUIRE(pthread_create(&tid_raw, NULL, server_thread, srv_raw) == 0); rc = reject_body_names_missing_oid(wolfcert_server_port(srv_raw)); + if (rc == 0) + rc = reject_bodies_are_plaintext(wolfcert_server_port(srv_raw)); wolfcert_server_stop(srv_raw); pthread_join(tid_raw, NULL); From db109f8309a2a963c6cf46f47aeffb8aa86b8187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 29 Sep 2026 15:26:25 +0200 Subject: [PATCH 04/10] est: fix 204 and 401 response headers in the test server RFC 9110 forbids Content-Length on a 204 and requires every 401 to carry at least one WWW-Authenticate challenge. The EST test server sent "Content-Length: 0" with the 204 from /csrattrs, and none of its 401 responses carried a challenge. Every response now goes through one send_response(), which omits Content-Length for a 204 and sends only the headers for a NULL body, so the headers are formatted in one place. A failed HTTP Basic check now answers 401 with WWW-Authenticate: Basic realm="estrealm". The two client-certificate failures, post-handshake auth that does not complete and a simplereenroll without a peer certificate to renew, run after Basic auth has already passed and cannot be satisfied by any HTTP challenge, so they now answer 403 Forbidden instead of an unchallenged 401. The EST client maps 401 and 403 to the same WOLFCERT_ERR_AUTH, so callers see no change. A HEAD for /cacerts or /csrattrs is now answered by the GET handler without the content, and a HEAD for an unknown path gets the same 404 headers as a GET, including the Content-Length of the plaintext reason, but no body: RFC 9110 section 8.6 forbids a HEAD Content-Length that differs from GET's. Extend the CSR-attribute enforcement test with a Basic-auth server that checks the 204 headers, the 401 challenge and the reenroll 403. est_chunked_robustness compares the HEAD headers with GET's for an unknown path, /cacerts and /csrattrs and checks that nothing follows them. The post-handshake-auth test and ARCHITECTURE.md expect 403. --- docs/ARCHITECTURE.md | 2 +- src/est/est_server.c | 152 +++++++++--------- .../integration/test_est_chunked_robustness.c | 48 ++++-- .../integration/test_est_csr_attrs_enforce.c | 66 ++++++++ tests/integration/test_est_pha_roundtrip.c | 6 +- 5 files changed, 174 insertions(+), 100 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5e69c02..6249b2b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -172,7 +172,7 @@ during the handshake, since TLS 1.2 has no PHA. On the first `/simpleenroll` or `/simplereenroll` without a peer cert, the server calls `wolfSSL_request_certificate()` and waits (5 s, capped at the per-request deadline) for the client's post-handshake Finished. Any TLS error, app data -first, or an empty Certificate gets a 401; if the request deadline ends the +first, or an empty Certificate gets a 403; if the request deadline ends the wait first, the connection is dropped like any request that misses it. The accept loop keeps the connection open across requests, so the anonymous `/cacerts` and the authenticated enroll land on one connection. The mode diff --git a/src/est/est_server.c b/src/est/est_server.c index 6320522..055bf84 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -510,59 +510,52 @@ static const char* conn_hdr(const WolfCertServer* s) return s->keep_alive ? "keep-alive" : "close"; } -static void send_status(WolfCertServer* s, int fd, int status, const char* phrase) +#define EST_TEXT_PLAIN "Content-Type: text/plain\r\n" +#define EST_PKCS7_CERTS \ + "Content-Type: application/pkcs7-mime; smime-type=certs-only\r\n" \ + "Content-Transfer-Encoding: base64\r\n" + +/* Send a response with body_len bytes of body; a NULL body sends only the + * headers, as for HEAD. `headers` holds CRLF-terminated header lines. */ +static void send_response(WolfCertServer* s, int fd, int status, + const char* phrase, const char* headers, + const void* body, size_t body_len) { - char line[128]; - int n = snprintf(line, sizeof(line), - "HTTP/1.1 %d %s\r\nContent-Length: 0\r\nConnection: %s\r\n\r\n", - status, phrase, conn_hdr(s)); + char hdr[320]; + int n; + + /* RFC 9110 section 8.6: a 204 carries no Content-Length. */ + if (status == 204) + n = snprintf(hdr, sizeof(hdr), "HTTP/1.1 204 %s\r\n%s" + "Connection: %s\r\n\r\n", phrase, headers, conn_hdr(s)); + else + n = snprintf(hdr, sizeof(hdr), "HTTP/1.1 %d %s\r\n%s" + "Content-Length: %zu\r\nConnection: %s\r\n\r\n", + status, phrase, headers, body_len, conn_hdr(s)); + if (n < 0 || (size_t)n >= sizeof(hdr)) + return; - send_all(s, fd, line, (size_t)n); + send_all(s, fd, hdr, (size_t)n); + if (body != NULL && body_len > 0) + send_all(s, fd, body, body_len); } -/* RFC 7030 section 4.2.3: an untyped error body must be a plaintext reason. */ +/* RFC 7030 section 4.2.3: an error body is a plaintext reason. */ static void send_error(WolfCertServer* s, int fd, int status, const char* phrase, const char* why) { - char hdr[192]; - size_t wl = strlen(why); - int n = snprintf(hdr, sizeof(hdr), - "HTTP/1.1 %d %s\r\nContent-Type: text/plain\r\n" - "Content-Length: %zu\r\nConnection: %s\r\n\r\n", - status, phrase, wl, conn_hdr(s)); - - send_all(s, fd, hdr, (size_t)n); - send_all(s, fd, why, wl); -} - -static void send_pkcs7_b64(WolfCertServer* s, int fd, const uint8_t* b64, size_t b64_len) -{ - char hdr[256]; - int n = snprintf(hdr, sizeof(hdr), - "HTTP/1.1 200 OK\r\n" - "Content-Type: application/pkcs7-mime; smime-type=certs-only\r\n" - "Content-Transfer-Encoding: base64\r\n" - "Content-Length: %zu\r\n" - "Connection: %s\r\n\r\n", - b64_len, conn_hdr(s)); - - send_all(s, fd, hdr, (size_t)n); - send_all(s, fd, b64, b64_len); + send_response(s, fd, status, phrase, EST_TEXT_PLAIN, why, strlen(why)); } /* RFC 7030 section 4.2.3: 202 Accepted with `Retry-After` tells the client the * enrolment is pending manual approval and can be retried. */ static void send_accepted_retry_after(WolfCertServer* s, int fd, int retry_after_sec) { - char hdr[192]; - int n = snprintf(hdr, sizeof(hdr), - "HTTP/1.1 202 Accepted\r\n" - "Retry-After: %d\r\n" - "Content-Length: 0\r\n" - "Connection: %s\r\n\r\n", - retry_after_sec > 0 ? retry_after_sec : 1, conn_hdr(s)); + char ra[40]; - send_all(s, fd, hdr, (size_t)n); + snprintf(ra, sizeof(ra), "Retry-After: %d\r\n", + retry_after_sec > 0 ? retry_after_sec : 1); + send_response(s, fd, 202, "Accepted", ra, NULL, 0); } /* HTTP Basic authentication scheme token, including its trailing space. */ @@ -615,10 +608,12 @@ static int check_basic_auth(const WolfCertServer* s, const char* auth_header) * - 204 No Content -> server has no attributes to advertise. * - 200 OK -> CsrAttrs DER, base64-encoded, content-type * application/csrattrs. */ -static int handler_csr_attrs(WolfCertServer* s, int fd) +static int handler_csr_attrs(WolfCertServer* s, int fd, int head) { + static const char why[] = "cannot encode the CSR attributes\n"; + if (s->cfg_csr_attrs == NULL || s->cfg_csr_attrs_len == 0) { - send_status(s, fd, 204, "No Content"); + send_response(s, fd, 204, "No Content", "", NULL, 0); return WOLFCERT_OK; } @@ -626,51 +621,40 @@ static int handler_csr_attrs(WolfCertServer* s, int fd) int rc = wolfcert_base64_encode_mime(s->cfg_csr_attrs, s->cfg_csr_attrs_len, &b64, s->heap); if (rc != WOLFCERT_OK) { - send_error(s, fd, 500, "Server Error", - "cannot encode the CSR attributes\n"); + send_response(s, fd, 500, "Server Error", EST_TEXT_PLAIN, + head ? NULL : why, sizeof(why) - 1); return rc; } - char hdr[256]; - int n = snprintf(hdr, sizeof(hdr), - "HTTP/1.1 200 OK\r\n" - "Content-Type: application/csrattrs\r\n" - "Content-Transfer-Encoding: base64\r\n" - "Content-Length: %zu\r\n" - "Connection: %s\r\n\r\n", - b64.len, conn_hdr(s)); - - send_all(s, fd, hdr, (size_t)n); - send_all(s, fd, b64.data, b64.len); + send_response(s, fd, 200, "OK", + "Content-Type: application/csrattrs\r\n" + "Content-Transfer-Encoding: base64\r\n", + head ? NULL : b64.data, b64.len); wolfcert_buffer_free(&b64); return WOLFCERT_OK; } -static int handler_cacerts(WolfCertServer* s, int fd) +static int handler_cacerts(WolfCertServer* s, int fd, int head) { + static const char why[] = "cannot build the CA certificates response\n"; const uint8_t* certs[1] = { s->ca.cert_der }; size_t clen[1] = { s->ca.cert_der_len }; WolfCertBuffer p7 = { 0 }; - - int rc = wolfcert_pkcs7_build_certs_only(certs, clen, 1, &p7, s->heap); - if (rc != WOLFCERT_OK) { - send_error(s, fd, 500, "Server Error", - "cannot build the CA certificates response\n"); - return rc; - } - WolfCertBuffer b64 = { 0 }; - rc = wolfcert_base64_encode_mime(p7.data, p7.len, &b64, s->heap); + int rc = wolfcert_pkcs7_build_certs_only(certs, clen, 1, &p7, s->heap); + if (rc == WOLFCERT_OK) + rc = wolfcert_base64_encode_mime(p7.data, p7.len, &b64, s->heap); wolfcert_buffer_free(&p7); if (rc != WOLFCERT_OK) { - send_error(s, fd, 500, "Server Error", - "cannot build the CA certificates response\n"); + send_response(s, fd, 500, "Server Error", EST_TEXT_PLAIN, + head ? NULL : why, sizeof(why) - 1); return rc; } - send_pkcs7_b64(s, fd,b64.data, b64.len); + send_response(s, fd, 200, "OK", EST_PKCS7_CERTS, + head ? NULL : b64.data, b64.len); wolfcert_buffer_free(&b64); return WOLFCERT_OK; @@ -1031,7 +1015,7 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, { int pha = ensure_post_handshake_auth(s); if (pha != WOLFCERT_OK) { - send_error(s, fd, 401, "Unauthorized", + send_error(s, fd, 403, "Forbidden", "client certificate authentication failed\n"); return pha; } @@ -1072,7 +1056,7 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, rc = reenroll_identity_check(s, csr.data, csr.len); if (rc != WOLFCERT_OK) { if (rc == WOLFCERT_ERR_AUTH) - send_error(s, fd, 401, "Unauthorized", + send_error(s, fd, 403, "Forbidden", "no client certificate to renew\n"); else if (rc == WOLFCERT_ERR_PARSE) send_error(s, fd, 400, "Bad Request", @@ -1157,7 +1141,7 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, return rc; } - send_pkcs7_b64(s, fd,b64.data, b64.len); + send_response(s, fd, 200, "OK", EST_PKCS7_CERTS, b64.data, b64.len); wolfcert_buffer_free(&b64); return WOLFCERT_OK; @@ -1165,6 +1149,7 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, static int handle_request(WolfCertServer* s, int fd) { + static const char unknown_op[] = "unknown EST operation\n"; EstRequest req = { 0 }; int rc = parse_request(s, fd, &req, s->heap); @@ -1188,18 +1173,25 @@ static int handle_request(WolfCertServer* s, int fd) suffix = last ? last + 1 : req.path; } - if (strcmp(req.method, "GET") == 0 && strcmp(suffix, "cacerts") == 0) { - rc = handler_cacerts(s, fd); + /* RFC 9110 section 9.3.2: HEAD gets GET's headers without the content. */ + int head = strcmp(req.method, "HEAD") == 0; + if ((head || strcmp(req.method, "GET") == 0) && + strcmp(suffix, "cacerts") == 0) { + rc = handler_cacerts(s, fd, head); } - else if (strcmp(req.method, "GET") == 0 && strcmp(suffix, "csrattrs") == 0) { - rc = handler_csr_attrs(s, fd); + else if ((head || strcmp(req.method, "GET") == 0) && + strcmp(suffix, "csrattrs") == 0) { + rc = handler_csr_attrs(s, fd, head); } else if (strcmp(req.method, "POST") == 0 && (strcmp(suffix, "simpleenroll") == 0 || strcmp(suffix, "simplereenroll") == 0)) { if (!check_basic_auth(s, req.auth_header)) { - send_error(s, fd, 401, "Unauthorized", - "HTTP Basic authentication failed\n"); + static const char why[] = "HTTP Basic authentication failed\n"; + + send_response(s, fd, 401, "Unauthorized", + "WWW-Authenticate: Basic realm=\"estrealm\"\r\n" + EST_TEXT_PLAIN, why, sizeof(why) - 1); rc = WOLFCERT_ERR_AUTH; } else { @@ -1207,13 +1199,13 @@ static int handle_request(WolfCertServer* s, int fd) strcmp(suffix, "simplereenroll") == 0); } } - else if (strcmp(req.method, "HEAD") == 0) { - /* RFC 9110 section 9.3.2: a response to HEAD carries no content. */ - send_error(s, fd, 404, "Not Found", ""); + else if (head) { + send_response(s, fd, 404, "Not Found", EST_TEXT_PLAIN, NULL, + sizeof(unknown_op) - 1); rc = WOLFCERT_ERR_NOT_FOUND; } else { - send_error(s, fd, 404, "Not Found", "unknown EST operation\n"); + send_error(s, fd, 404, "Not Found", unknown_op); rc = WOLFCERT_ERR_NOT_FOUND; } diff --git a/tests/integration/test_est_chunked_robustness.c b/tests/integration/test_est_chunked_robustness.c index efad365..60ab724 100644 --- a/tests/integration/test_est_chunked_robustness.c +++ b/tests/integration/test_est_chunked_robustness.c @@ -113,33 +113,45 @@ static int send_and_read_status(uint16_t port, return (int)n; } -/* A 404 for HEAD must end at the headers, or its body would be read as the - * next response on a kept-alive connection. */ -static int head_404_has_no_body(uint16_t port) +static int fetch_whole(uint16_t port, const char* method, const char* op, + char* resp, size_t cap) { - static const char req[] = - "HEAD /.well-known/est/nope HTTP/1.1\r\n" - "Host: 127.0.0.1\r\n" - "Connection: close\r\n" - "\r\n"; TestTlsConn c; - char resp[512]; - const char* eoh; + char req[160]; size_t n = 0; + int len; int r; + len = snprintf(req, sizeof(req), "%s /.well-known/est/%s HTTP/1.1\r\n" + "Host: 127.0.0.1\r\nConnection: close\r\n\r\n", + method, op); + REQUIRE(len > 0 && (size_t)len < sizeof(req)); REQUIRE(test_tls_connect(&c, port, g_tls_cert, g_tls_cert_len) == 0); - REQUIRE(test_tls_write(&c, req, sizeof(req) - 1) == 0); - while (n + 1 < sizeof(resp) && - (r = test_tls_read(&c, resp + n, sizeof(resp) - 1 - n)) > 0) + REQUIRE(test_tls_write(&c, req, (size_t)len) == 0); + while (n + 1 < cap && (r = test_tls_read(&c, resp + n, cap - 1 - n)) > 0) n += (size_t)r; test_tls_close(&c); resp[n] = '\0'; + return 0; +} - REQUIRE(strstr(resp, " 404 ") != NULL); - eoh = strstr(resp, "\r\n\r\n"); +/* HEAD must answer with GET's headers and end there, or its body would be read + * as the next response on a kept-alive connection. */ +static int head_matches_get(uint16_t port, const char* op, const char* status) +{ + char get[4096]; + char head[4096]; + const char* eoh; + + REQUIRE(fetch_whole(port, "GET", op, get, sizeof(get)) == 0); + REQUIRE(fetch_whole(port, "HEAD", op, head, sizeof(head)) == 0); + if (strncmp(head, status, strlen(status)) != 0) + fprintf(stderr, "HEAD %s: %.40s\n", op, head); + REQUIRE(strncmp(head, status, strlen(status)) == 0); + eoh = strstr(head, "\r\n\r\n"); REQUIRE(eoh != NULL); REQUIRE(eoh[4] == '\0'); + REQUIRE(strncmp(get, head, (size_t)(eoh + 4 - head)) == 0); return 0; } @@ -489,7 +501,11 @@ int main(void) uint16_t port = wolfcert_server_port(srv); - int rc = head_404_has_no_body(port); + int rc = head_matches_get(port, "nope", "HTTP/1.1 404"); + if (rc == 0) + rc = head_matches_get(port, "cacerts", "HTTP/1.1 200"); + if (rc == 0) + rc = head_matches_get(port, "csrattrs", "HTTP/1.1 204"); if (rc == 0) rc = reject_oversized_chunk_size(port); if (rc == 0) diff --git a/tests/integration/test_est_csr_attrs_enforce.c b/tests/integration/test_est_csr_attrs_enforce.c index 2289130..f6d930a 100644 --- a/tests/integration/test_est_csr_attrs_enforce.c +++ b/tests/integration/test_est_csr_attrs_enforce.c @@ -302,6 +302,51 @@ static int reject_bodies_are_plaintext(uint16_t port) return 0; } +/* RFC 9110: a 204 carries no Content-Length, a 401 carries a challenge, and a + * missing client certificate is a 403 since HTTP auth cannot supply one. */ +static int bare_status_headers(uint16_t port) +{ + static const char get_attrs[] = + "GET /.well-known/est/csrattrs HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Connection: close\r\n" + "\r\n"; + static const char enroll_noauth[] = + "POST /.well-known/est/simpleenroll HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Content-Type: application/pkcs10\r\n" + "Content-Length: 4\r\n" + "Connection: close\r\n" + "\r\n" + "AAAA"; + static const char reenroll_nocert[] = + "POST /.well-known/est/simplereenroll HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Authorization: Basic YWxpY2U6c2VjcmV0\r\n" /* alice:secret */ + "Content-Type: application/pkcs10\r\n" + "Content-Length: 4\r\n" + "Connection: close\r\n" + "\r\n" + "AAAA"; + char resp[1024]; + + REQUIRE(send_and_read_all(port, get_attrs, sizeof(get_attrs) - 1, + resp, sizeof(resp)) > 0); + REQUIRE(strstr(resp, " 204 ") != NULL); + REQUIRE(strstr(resp, "Content-Length") == NULL); + + REQUIRE(send_and_read_all(port, enroll_noauth, sizeof(enroll_noauth) - 1, + resp, sizeof(resp)) > 0); + REQUIRE(strstr(resp, " 401 ") != NULL); + REQUIRE(strstr(resp, "WWW-Authenticate: Basic realm=") != NULL); + + REQUIRE(send_and_read_all(port, reenroll_nocert, + sizeof(reenroll_nocert) - 1, + resp, sizeof(resp)) > 0); + REQUIRE(strstr(resp, " 403 ") != NULL); + return 0; +} + /* Client C - server advertises ONLY an Attribute-with-values item * (no bare OIDs). The CSR doesn't carry anything matching it. * Enforcement is presence-only on bare OIDs, so this must still @@ -433,6 +478,27 @@ int main(void) pthread_join(tid_raw, NULL); wolfcert_server_free(srv_raw); wolfcert_buffer_free(&policy_raw); + if (rc != 0) + return rc; + + /* Fourth server with no policy and Basic auth on, for bodiless replies. */ + WolfCertServerCfgSrv cfg_bare = { + .protocol = WOLFCERT_PROTO_EST, + .bind_host = "127.0.0.1", .bind_port = 0, + .http_basic_user = "alice", .http_basic_pass = "secret", + .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, + .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + }; + WolfCertServer* srv_bare = NULL; + REQUIRE(wolfcert_server_start(&cfg_bare, &srv_bare) == WOLFCERT_OK); + pthread_t tid_bare; + REQUIRE(pthread_create(&tid_bare, NULL, server_thread, srv_bare) == 0); + + rc = bare_status_headers(wolfcert_server_port(srv_bare)); + + wolfcert_server_stop(srv_bare); + pthread_join(tid_bare, NULL); + wolfcert_server_free(srv_bare); free(tls_cert); free(tls_key); if (rc != 0) diff --git a/tests/integration/test_est_pha_roundtrip.c b/tests/integration/test_est_pha_roundtrip.c index 9e77543..6287c5e 100644 --- a/tests/integration/test_est_pha_roundtrip.c +++ b/tests/integration/test_est_pha_roundtrip.c @@ -34,7 +34,7 @@ * Negative controls: a session without a client identity, and one with an * identity but no PHA opt-in, must both fail /simpleenroll while /cacerts * still succeeds, as it must for an anonymous TLS 1.2 client. A client that - * never answers the CertificateRequest gets a 401 once the wait runs out. + * never answers the CertificateRequest gets a 403 once the wait runs out. */ #define _POSIX_C_SOURCE 200809L @@ -306,7 +306,7 @@ int main(void) REQUIRE(pha_reenroll(wolfcert_server_port(srv), tls_cert, tls_cert_len, cli_cert, cli_cert_len, cli_key, cli_key_len) == 0); - /* --- A PHA client that never answers the CertificateRequest gets a 401 + /* --- A PHA client that never answers the CertificateRequest gets a 403 * once the server stops waiting, well before the request deadline. */ { static const char req[] = @@ -331,7 +331,7 @@ int main(void) /* Drop the reply wolfSSL_read() sends to the CertificateRequest. */ wolfSSL_SSLSetIOSend(c.ssl, discard_send); REQUIRE(test_tls_read(&c, resp, sizeof(resp) - 1) > 0); - REQUIRE(strncmp(resp, "HTTP/1.1 401", 12) == 0); + REQUIRE(strncmp(resp, "HTTP/1.1 403", 12) == 0); test_tls_close(&c); } #endif From 02ff3244b9b8192dc6e1d49cce7d3093af41a118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 29 Sep 2026 15:41:54 +0200 Subject: [PATCH 05/10] est: refuse to start an EST server with no client authentication The EST test server issued a certificate for any CSR posted to /simpleenroll when it had no HTTP Basic credentials and no client CA configured, because check_basic_auth() passes every request when no username is set and TLS never asks for a client certificate without a client CA. The README quick start ran the server in exactly that mode. wolfcert_server_start() now returns WOLFCERT_ERR_BAD_ARG for EST unless http_basic_user or tls_client_ca_pem is set, or the caller opts in to open enrollment with the new est_allow_anonymous_enroll field, added at the end of WolfCertServerCfgSrv so the existing members keep their offsets. The check runs before the CA is minted, so a refused start leaves no CA behind. An empty Basic user or password is refused as well, since the header it produces is one any client can send. A failed copy of any configured string now fails the start with WOLFCERT_ERR_MEMORY: a Basic user lost that way was read as no authentication at all. wolfcert_server_serve_fd() runs no TLS, so a server started on a client CA alone would still issue to anyone there. handler_enroll() now answers 403 when the connection has no TLS and neither Basic credentials nor the anonymous opt-in are configured. /simplereenroll already refuses a request without a TLS client certificate and is unchanged. wolfcert-server gains --est-allow-anonymous, fails early with a clear message when none of the three is given, and rejects an empty --basic. Tests, interop scripts and the quick start that relied on open enrollment now opt in explicitly. est_tls_roundtrip covers the refused start, the empty credentials, a Basic user with an empty password, and enrollment over serve_fd: refused with only a client CA, let through to the CA with Basic credentials or the anonymous opt-in. The tls_client_ca_pem comment in server.h states what /simplereenroll needs, and ARCHITECTURE.md points to it. cli_proto_scoping.sh covers the CLI checks. server_ca_store fails each allocation of a start in turn and checks that any start which still succeeds kept its credentials. Fixes F-8036. --- CLAUDE.md | 2 +- README.md | 5 +- cli/wolfcert_server.c | 25 +++++- docs/ARCHITECTURE.md | 2 +- examples/certs/README.md | 3 +- examples/certs/gen-certs.sh | 2 +- src/est/est_server.c | 10 +++ src/server.c | 22 ++++++ tests/integration/cli_proto_scoping.sh | 29 +++++++ .../integration/test_est_chunked_robustness.c | 2 + .../test_est_csr_attrs_apply_roundtrip.c | 1 + .../integration/test_est_csr_attrs_enforce.c | 3 + .../test_est_csr_attrs_roundtrip.c | 2 + .../integration/test_est_pending_roundtrip.c | 1 + tests/integration/test_est_pha_roundtrip.c | 1 + tests/integration/test_est_tls_roundtrip.c | 78 +++++++++++++++++++ tests/integration/test_server_stop_idle.c | 2 + tests/interop/est_globalsign.sh | 2 +- tests/interop/est_libest.sh | 2 +- tests/interop/openssl_pkcs7_xcheck.sh | 4 +- tests/unit/test_server_ca_store.c | 78 +++++++++++++++++++ wolfcert/server.h | 10 ++- 22 files changed, 271 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bc53a02..5fac90d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -107,7 +107,7 @@ After a build with `-DWOLFCERT_ENABLE_CLI=ON` (the default): ```sh build/wolfcert-server --proto est --listen 127.0.0.1:8443 \ - --tls-cert server.crt --tls-key server.key + --tls-cert server.crt --tls-key server.key --est-allow-anonymous build/wolfcert-client enroll --proto est \ --url https://127.0.0.1:8443/.well-known/est --trust server.crt \ --key-type ecc:256 --subject "CN=dev" \ diff --git a/README.md b/README.md index 492431a..23ba912 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,11 @@ Start the bundled test server (issues from an auto-generated local CA): ```sh # EST is TLS-only (RFC 7030), so it needs a server identity for the # listen address; SCEP authenticates at the pkiMessage layer instead. +# EST also needs --basic USER:PASS, --tls-client-ca PEM, or +# --est-allow-anonymous to issue to any client; /simplereenroll +# also needs --tls-client-ca and the KEEP_PEER_CERT build above. ./wolfcert-server --proto est --listen 127.0.0.1:8443 \ - --tls-cert server.crt --tls-key server.key + --tls-cert server.crt --tls-key server.key --est-allow-anonymous ./wolfcert-server --proto scep --listen 127.0.0.1:8088 ``` diff --git a/cli/wolfcert_server.c b/cli/wolfcert_server.c index 96ba0b5..a6055e7 100644 --- a/cli/wolfcert_server.c +++ b/cli/wolfcert_server.c @@ -62,17 +62,21 @@ static void print_usage(FILE* out) " [--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" - " [--scep-enable-get-cert]\n" + " [--scep-enable-get-cert] [--est-allow-anonymous]\n" "\n" "Options:\n" " --proto est|scep Protocol to serve (required)\n" " --listen HOST:PORT Bind address (default 0.0.0.0:8080)\n" - " --basic USER:PASS Require HTTP Basic auth (EST enroll)\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" " required for --proto est (RFC 7030)\n" " --tls-key PEMFILE Private key for --tls-cert (PEM)\n" " --tls-client-ca PEMFILE Require mutual TLS; verify clients against this CA\n" + " --est-allow-anonymous Issue EST certificates to any client; --proto est\n" + " needs this, --basic or --tls-client-ca.\n" + " /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" " --scep-enable-next-ca Advertise + answer GetNextCACert (RFC 8894 section 4.7),\n" @@ -123,7 +127,7 @@ static int parse_listen(const char* arg, char** host, uint16_t* port) static int parse_basic(const char* arg, char** user, char** pass) { const char* colon = strchr(arg, ':'); - if (colon == NULL) + if (colon == NULL || colon == arg || colon[1] == '\0') return -1; *user = strndup(arg, (size_t)(colon - arg)); @@ -177,6 +181,7 @@ int main(int argc, char** argv) { "tls-post-handshake-auth", no_argument, NULL, 'H' }, { "csrattrs-file", required_argument, NULL, 'F' }, { "est-require-csrattrs", no_argument, NULL, 'Q' }, + { "est-allow-anonymous", no_argument, NULL, 'Y' }, { "help", no_argument, NULL, 'h' }, { "version", no_argument, NULL, 'V' }, { 0 } @@ -203,6 +208,7 @@ int main(int argc, char** argv) uint8_t* csr_attrs_blob = NULL; size_t csr_attrs_blob_len = 0; int est_require_csr_attrs = 0; + int est_allow_anonymous = 0; int c; while ((c = getopt_long(argc, argv, "", opts, NULL)) != -1) { @@ -218,7 +224,7 @@ int main(int argc, char** argv) break; case 'b': if (parse_basic(optarg, &user, &pass) != 0) { - fprintf(stderr, "invalid --basic (expected USER:PASS)\n"); + fprintf(stderr, "invalid --basic (expected non-empty USER:PASS)\n"); return 1; } break; @@ -274,6 +280,9 @@ int main(int argc, char** argv) case 'Q': est_require_csr_attrs = 1; break; + case 'Y': + est_allow_anonymous = 1; + break; case 'V': printf("wolfcert-server %s\n", wolfcert_version_string()); return 0; @@ -310,6 +319,13 @@ int main(int argc, char** argv) return 1; } + if (sel == WOLFCERT_PROTO_EST && user == NULL && tls_ca == NULL && + !est_allow_anonymous) { + fprintf(stderr, "wolfcert-server: --proto est requires --basic, " + "--tls-client-ca or --est-allow-anonymous\n"); + return 1; + } + if (host == NULL) host = strdup("0.0.0.0"); @@ -372,6 +388,7 @@ int main(int argc, char** argv) .csr_attributes_der = csr_attrs_blob, .csr_attributes_len = csr_attrs_blob_len, .est_require_csr_attributes = est_require_csr_attrs, + .est_allow_anonymous_enroll = est_allow_anonymous, }; int rc = wolfcert_server_start(&cfg, &g_server); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6249b2b..05e4bb4 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -138,7 +138,7 @@ four endpoints a typical device needs: | `GET /cacerts` | `wolfcert_est_get_cacerts` | CA chain as degenerate PKCS#7; decoded to PEM for you. | | `GET /csrattrs` | `wolfcert_est_get_csr_attrs` | Raw body (empty on HTTP 204); decode with `wolfcert_est_parse_csr_attrs`. | | `POST /simpleenroll` | `wolfcert_est_simple_enroll` | Body is base64-wrapped CSR DER; 200 returns the issued cert as PKCS#7. | -| `POST /simplereenroll` | `wolfcert_est_simple_reenroll` | Same, with the cert being renewed used as the implicit client identity. | +| `POST /simplereenroll` | `wolfcert_est_simple_reenroll` | Same, with the cert being renewed used as the implicit client identity. The test server's requirements for it are at `WolfCertServerCfgSrv.tls_client_ca_pem` in `wolfcert/server.h`. | A typical flow is: `wolfcert_key_generate` → `wolfcert_csr_build` → `wolfcert_est_simple_enroll` → persist the returned PEM. The keep-alive diff --git a/examples/certs/README.md b/examples/certs/README.md index eab9636..9b6411b 100644 --- a/examples/certs/README.md +++ b/examples/certs/README.md @@ -20,7 +20,8 @@ Two algorithm families, each a self-signed CA plus a server and a client leaf: ## Use with the CLIs -Server-side TLS (optionally mutual TLS with `--tls-client-ca`): +Server-side TLS with mutual TLS via `--tls-client-ca` (swap it for +`--est-allow-anonymous` to issue to any client): ```sh build/wolfcert-server --proto est --listen 127.0.0.1:8443 \ diff --git a/examples/certs/gen-certs.sh b/examples/certs/gen-certs.sh index ad4f49d..be9c2d9 100755 --- a/examples/certs/gen-certs.sh +++ b/examples/certs/gen-certs.sh @@ -16,7 +16,7 @@ # # Mapping onto the CLIs: # wolfcert-server --tls-cert /server-cert.pem --tls-key /server-key.pem \ -# [--tls-client-ca /ca-cert.pem] +# --tls-client-ca /ca-cert.pem | --est-allow-anonymous # wolfcert-client ... --trust /ca-cert.pem \ # [--client-cert /client-cert.pem --client-key /client-key.pem] # diff --git a/src/est/est_server.c b/src/est/est_server.c index 055bf84..3a6f645 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -1020,6 +1020,16 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, return pha; } + /* No TLS on this connection: only Basic or the anonymous opt-in admits + * an enroll. */ + if (s->cfg_basic_user == NULL && !s->cfg.est_allow_anonymous_enroll && + s->tls_current == NULL) { + send_error(s, fd, 403, "Forbidden", + "client authentication required\n"); + return WOLFCERT_ERR(WOLFCERT_ERR_AUTH, "est", + "enroll: no Basic credentials or TLS client on this connection"); + } + if (req->body == NULL || req->body_len == 0) { send_error(s, fd, 400, "Bad Request", "request body is empty\n"); return WOLFCERT_ERR_HTTP; diff --git a/src/server.c b/src/server.c index 4be66b1..497432e 100644 --- a/src/server.c +++ b/src/server.c @@ -287,6 +287,20 @@ int wolfcert_server_start(const WolfCertServerCfgSrv* cfg, WolfCertServer** out) return WOLFCERT_ERR(WOLFCERT_ERR_TLS, "server", "EST requires TLS: set tls_cert_pem and tls_key_pem (RFC 7030)"); + if (cfg->http_basic_user != NULL && + (cfg->http_basic_user[0] == '\0' || + cfg->http_basic_pass == NULL || cfg->http_basic_pass[0] == '\0')) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "server", + "http_basic_user and http_basic_pass must both be non-empty"); + + if (cfg->protocol == WOLFCERT_PROTO_EST && cfg->http_basic_user == NULL && + (cfg->tls_client_ca_pem == NULL || + cfg->tls_client_ca_pem_len == 0) && + !cfg->est_allow_anonymous_enroll) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "server", + "EST enrollment needs http_basic_user or tls_client_ca_pem, " + "or est_allow_anonymous_enroll"); + void* heap = cfg->heap ? cfg->heap : wolfcert_default_heap(); WolfCertServer* s = (WolfCertServer*)WOLFCERT_XMALLOC(sizeof(*s), heap); if (s == NULL) @@ -314,6 +328,14 @@ 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) || + (cfg->http_basic_user && s->cfg_basic_user == NULL) || + (cfg->http_basic_pass && s->cfg_basic_pass == NULL)) { + wolfcert_server_free(s); + return WOLFCERT_ERR_MEMORY; + } + if (cfg->csr_attributes_der != NULL && cfg->csr_attributes_len > 0) { s->cfg_csr_attrs = (uint8_t*)WOLFCERT_XMALLOC(cfg->csr_attributes_len, heap); if (s->cfg_csr_attrs == NULL) { diff --git a/tests/integration/cli_proto_scoping.sh b/tests/integration/cli_proto_scoping.sh index f3ac0c6..5620499 100755 --- a/tests/integration/cli_proto_scoping.sh +++ b/tests/integration/cli_proto_scoping.sh @@ -217,6 +217,35 @@ fi # is built alongside wolfcert-client whenever the server is enabled; without it # there is nothing to enroll against, so skip just this group. SERVER="$(dirname "$CLI")/wolfcert-server" + +# wolfcert-server refuses an EST listener that could not authenticate anyone. +# Any readable file passes as --tls-cert: these checks run before it is parsed. +expect_server_reject() { + local saved="$CLI" + CLI="$SERVER" + expect_reject "$@" + CLI="$saved" +} +if [ -x "$SERVER" ]; then + expect_server_reject "est server with no client auth" "--est-allow-anonymous" \ + --proto est --tls-cert "$0" --tls-key "$0" + for b in ":pw" "alice:" "alice"; do + expect_server_reject "--basic $b" "non-empty USER:PASS" \ + --proto est --tls-cert "$0" --tls-key "$0" --basic "$b" + done + # $0 is no PEM, so start fails in TLS setup, before anything binds. + out="$("$SERVER" --proto est --listen 127.0.0.1:18099 --tls-cert "$0" \ + --tls-key "$0" --est-allow-anonymous 2>&1)" + case "$out" in + *"start failed"*) + echo "ok --est-allow-anonymous passes the auth check" ;; + *) + echo "FAIL: --est-allow-anonymous did not reach server start" + echo " got: $out" + fails=$((fails + 1)) + ;; + esac +fi if [ ! -x "$SERVER" ]; then echo "skip --ca-fingerprint pinning (wolfcert-server not built)" else diff --git a/tests/integration/test_est_chunked_robustness.c b/tests/integration/test_est_chunked_robustness.c index 60ab724..b89a88a 100644 --- a/tests/integration/test_est_chunked_robustness.c +++ b/tests/integration/test_est_chunked_robustness.c @@ -428,6 +428,7 @@ static int no_sigpipe_on_response(void) WolfCertServerCfgSrv cfg = { .protocol = WOLFCERT_PROTO_EST, .bind_host = "127.0.0.1", .bind_port = 0, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv = NULL; struct sigaction sa, old; @@ -493,6 +494,7 @@ int main(void) .bind_host = "127.0.0.1", .bind_port = 0, .tls_cert_pem = g_tls_cert, .tls_cert_pem_len = g_tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv = NULL; REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); diff --git a/tests/integration/test_est_csr_attrs_apply_roundtrip.c b/tests/integration/test_est_csr_attrs_apply_roundtrip.c index 131b2a3..fa23563 100644 --- a/tests/integration/test_est_csr_attrs_apply_roundtrip.c +++ b/tests/integration/test_est_csr_attrs_apply_roundtrip.c @@ -278,6 +278,7 @@ int main(void) .csr_attributes_len = policy.len, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv = NULL; REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); diff --git a/tests/integration/test_est_csr_attrs_enforce.c b/tests/integration/test_est_csr_attrs_enforce.c index f6d930a..0120ac6 100644 --- a/tests/integration/test_est_csr_attrs_enforce.c +++ b/tests/integration/test_est_csr_attrs_enforce.c @@ -406,6 +406,7 @@ int main(void) .est_require_csr_attributes = 1, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv = NULL; REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); @@ -434,6 +435,7 @@ int main(void) .est_require_csr_attributes = 1, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv2 = NULL; REQUIRE(wolfcert_server_start(&cfg2, &srv2) == WOLFCERT_OK); @@ -464,6 +466,7 @@ int main(void) .est_require_csr_attributes = 1, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv_raw = NULL; REQUIRE(wolfcert_server_start(&cfg_raw, &srv_raw) == WOLFCERT_OK); diff --git a/tests/integration/test_est_csr_attrs_roundtrip.c b/tests/integration/test_est_csr_attrs_roundtrip.c index ce8ed30..70601dd 100644 --- a/tests/integration/test_est_csr_attrs_roundtrip.c +++ b/tests/integration/test_est_csr_attrs_roundtrip.c @@ -114,6 +114,7 @@ int main(void) .csr_attributes_len = blob.len, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv = NULL; REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); @@ -174,6 +175,7 @@ int main(void) .bind_host = "127.0.0.1", .bind_port = 0, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv2 = NULL; REQUIRE(wolfcert_server_start(&cfg2, &srv2) == WOLFCERT_OK); diff --git a/tests/integration/test_est_pending_roundtrip.c b/tests/integration/test_est_pending_roundtrip.c index 691780a..594ad23 100644 --- a/tests/integration/test_est_pending_roundtrip.c +++ b/tests/integration/test_est_pending_roundtrip.c @@ -483,6 +483,7 @@ int main(void) .est_retry_after_sec = 1, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* s = NULL; REQUIRE(wolfcert_server_start(&cfg, &s) == WOLFCERT_OK); diff --git a/tests/integration/test_est_pha_roundtrip.c b/tests/integration/test_est_pha_roundtrip.c index 6287c5e..c7679a9 100644 --- a/tests/integration/test_est_pha_roundtrip.c +++ b/tests/integration/test_est_pha_roundtrip.c @@ -197,6 +197,7 @@ int main(void) WolfCertServerCfgSrv bad = cfg; bad.tls_client_ca_pem = NULL; bad.tls_client_ca_pem_len = 0; + bad.est_allow_anonymous_enroll = 1; REQUIRE(wolfcert_server_start(&bad, &srv) == WOLFCERT_ERR_BAD_ARG); REQUIRE(srv == NULL); diff --git a/tests/integration/test_est_tls_roundtrip.c b/tests/integration/test_est_tls_roundtrip.c index 0546394..94b56b9 100644 --- a/tests/integration/test_est_tls_roundtrip.c +++ b/tests/integration/test_est_tls_roundtrip.c @@ -49,6 +49,8 @@ #include #include #include +#include +#include #define REQUIRE(cond) \ do { \ @@ -226,6 +228,43 @@ static int test_client_reenroll_keeps_identity(const uint8_t* tls_cert, return 0; } +/* POST a junk CSR over serve_fd(), which carries no TLS, and expect `want`: + * " 403 " where the guard refuses it, " 400 " where the CA does. */ +static int serve_fd_enroll(const WolfCertServerCfgSrv* cfg, const char* auth, + const char* want) +{ + WolfCertServer* srv = NULL; + char req[512]; + char resp[512]; + ssize_t n; + int len; + int sv[2]; + int rc; + + len = snprintf(req, sizeof(req), + "POST /.well-known/est/simpleenroll HTTP/1.1\r\n" + "Host: 127.0.0.1\r\nContent-Type: application/pkcs10\r\n%s" + "Content-Length: 4\r\nConnection: close\r\n\r\nAAAA", auth); + REQUIRE(len > 0 && (size_t)len < sizeof(req)); + REQUIRE(wolfcert_server_start(cfg, &srv) == WOLFCERT_OK); + REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); + REQUIRE(write(sv[1], req, (size_t)len) == (ssize_t)len); + + rc = wolfcert_server_serve_fd(srv, sv[0]); + n = read(sv[1], resp, sizeof(resp) - 1); + close(sv[0]); + close(sv[1]); + wolfcert_server_free(srv); + + REQUIRE(n > 0); + resp[n] = '\0'; + if (strstr(resp, want) == NULL) + fprintf(stderr, "serve_fd enroll: wanted%s, got %.40s\n", want, resp); + REQUIRE(strstr(resp, want) != NULL); + REQUIRE(strcmp(want, " 403 ") != 0 || rc == WOLFCERT_ERR_AUTH); + return 0; +} + int main(void) { REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); @@ -258,12 +297,51 @@ int main(void) == WOLFCERT_ERR_NOT_FOUND); wolfcert_store_memory_close(plain_store); + /* Nor may it start with no way to authenticate an enrolling client. */ + WolfCertServerCfgSrv open_cfg = { + .protocol = WOLFCERT_PROTO_EST, + .bind_host = "127.0.0.1", + .bind_port = 0, + .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, + .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + }; + WolfCertServer* open_srv = NULL; + REQUIRE(wolfcert_server_start(&open_cfg, &open_srv) == WOLFCERT_ERR_BAD_ARG); + REQUIRE(open_srv == NULL); + + /* Empty Basic credentials would admit the public header "Basic Og==". */ + open_cfg.http_basic_user = ""; + open_cfg.http_basic_pass = ""; + REQUIRE(wolfcert_server_start(&open_cfg, &open_srv) == WOLFCERT_ERR_BAD_ARG); + open_cfg.http_basic_user = "alice"; + open_cfg.http_basic_pass = NULL; + REQUIRE(wolfcert_server_start(&open_cfg, &open_srv) == WOLFCERT_ERR_BAD_ARG); + open_cfg.http_basic_pass = ""; + REQUIRE(wolfcert_server_start(&open_cfg, &open_srv) == WOLFCERT_ERR_BAD_ARG); + REQUIRE(open_srv == NULL); + + /* serve_fd() has no TLS: a client CA alone admits nobody, Basic or the + * anonymous opt-in let the request through to the CA. */ + open_cfg.http_basic_pass = "secret"; + REQUIRE(serve_fd_enroll(&open_cfg, + "Authorization: Basic YWxpY2U6c2VjcmV0\r\n", + " 400 ") == 0); + open_cfg.http_basic_user = NULL; + open_cfg.http_basic_pass = NULL; + open_cfg.est_allow_anonymous_enroll = 1; + REQUIRE(serve_fd_enroll(&open_cfg, "", " 400 ") == 0); + open_cfg.est_allow_anonymous_enroll = 0; + open_cfg.tls_client_ca_pem = tls_cert; + open_cfg.tls_client_ca_pem_len = tls_cert_len; + REQUIRE(serve_fd_enroll(&open_cfg, "", " 403 ") == 0); + WolfCertServerCfgSrv cfg = { .protocol = WOLFCERT_PROTO_EST, .bind_host = "127.0.0.1", .bind_port = 0, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv = NULL; REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); diff --git a/tests/integration/test_server_stop_idle.c b/tests/integration/test_server_stop_idle.c index 0a466bd..6a8fc6c 100644 --- a/tests/integration/test_server_stop_idle.c +++ b/tests/integration/test_server_stop_idle.c @@ -294,6 +294,7 @@ int main(void) cfg.tls_cert_pem_len = tls_cert_len; cfg.tls_key_pem = tls_key; cfg.tls_key_pem_len = tls_key_len; + cfg.est_allow_anonymous_enroll = 1; /* 1. Parked in wolfSSL_accept(): TCP is up, no ClientHello follows. */ memset(&ctx, 0, sizeof(ctx)); @@ -414,6 +415,7 @@ int main(void) cfg.tls_cert_pem_len = tls_cert_len; cfg.tls_key_pem = tls_key; cfg.tls_key_pem_len = tls_key_len; + cfg.est_allow_anonymous_enroll = 1; memset(&ctx, 0, sizeof(ctx)); REQUIRE(wolfcert_server_start(&cfg, &ctx.srv) == WOLFCERT_OK); diff --git a/tests/interop/est_globalsign.sh b/tests/interop/est_globalsign.sh index 4875378..93a916e 100755 --- a/tests/interop/est_globalsign.sh +++ b/tests/interop/est_globalsign.sh @@ -129,7 +129,7 @@ openssl req -x509 -newkey rsa:2048 -nodes -days 1 \ -batch >/dev/null 2>&1 "$WC_SERVER" --proto est --listen "127.0.0.1:$WC_PORT" \ - --tls-cert srv/wc.crt --tls-key srv/wc.key \ + --tls-cert srv/wc.crt --tls-key srv/wc.key --est-allow-anonymous \ >wc-server.log 2>&1 & WC_PID=$! trap 'kill_if "$WC_PID"' EXIT diff --git a/tests/interop/est_libest.sh b/tests/interop/est_libest.sh index eed8dbd..539fdb2 100755 --- a/tests/interop/est_libest.sh +++ b/tests/interop/est_libest.sh @@ -124,7 +124,7 @@ openssl req -x509 -newkey rsa:2048 -nodes -days 1 \ -addext "subjectAltName=IP:127.0.0.1" >/dev/null 2>&1 "$WC_SERVER" --proto est --listen "127.0.0.1:$EST_PORT" \ - --tls-cert wc-srv.crt --tls-key wc-srv.key \ + --tls-cert wc-srv.crt --tls-key wc-srv.key --est-allow-anonymous \ >wc-server.log 2>&1 & WC_PID=$! trap 'kill_if "$WC_PID"' EXIT diff --git a/tests/interop/openssl_pkcs7_xcheck.sh b/tests/interop/openssl_pkcs7_xcheck.sh index 8b0675c..6feaa59 100755 --- a/tests/interop/openssl_pkcs7_xcheck.sh +++ b/tests/interop/openssl_pkcs7_xcheck.sh @@ -49,7 +49,7 @@ echo "[1] EST /cacerts -> OpenSSL pkcs7 parse" PORT=$(free_port) "$WC_SERVER" --proto est --listen "127.0.0.1:$PORT" \ --tls-cert tls-server.crt --tls-key tls-server.key \ - >wc-server.log 2>&1 & + --est-allow-anonymous >wc-server.log 2>&1 & WC_PID=$! trap 'kill_if "$WC_PID"' EXIT wait_port 127.0.0.1 "$PORT" @@ -122,7 +122,7 @@ echo "[3] OpenSSL-generated CSR -> wolfcert-server EST enroll" PORT=$(free_port) "$WC_SERVER" --proto est --listen "127.0.0.1:$PORT" \ --tls-cert tls-server.crt --tls-key tls-server.key \ - >wc-server3.log 2>&1 & + --est-allow-anonymous >wc-server3.log 2>&1 & WC_PID=$! trap 'kill_if "$WC_PID"' EXIT wait_port 127.0.0.1 "$PORT" diff --git a/tests/unit/test_server_ca_store.c b/tests/unit/test_server_ca_store.c index 397d7fa..9f2d72d 100644 --- a/tests/unit/test_server_ca_store.c +++ b/tests/unit/test_server_ca_store.c @@ -72,6 +72,7 @@ static void ca_store_cfg(WolfCertServerCfgSrv* cfg, WolfCertStoreOps* store) cfg->tls_cert_pem_len = srv_cert_pem_len; cfg->tls_key_pem = srv_key_pem; cfg->tls_key_pem_len = srv_key_pem_len; + cfg->est_allow_anonymous_enroll = 1; #endif cfg->bind_host = "127.0.0.1"; cfg->ca_store = store; @@ -415,6 +416,78 @@ static int test_ca_persists_across_starts(void) return 0; } +#if defined(USE_WOLFSSL_MEMORY) && !defined(WOLFSSL_STATIC_MEMORY) && \ + !defined(WOLFSSL_DEBUG_MEMORY) +/* Index of the one allocation fail_one_malloc() refuses; -1 refuses none. */ +static int g_fail_at = -1; +static int g_allocs; + +static void* fail_one_malloc(size_t sz) +{ + return g_allocs++ == g_fail_at ? NULL : malloc(sz); +} + +static void fail_one_free(void* ptr) +{ + free(ptr); +} + +static void* fail_one_realloc(void* ptr, size_t sz) +{ + return g_allocs++ == g_fail_at ? NULL : realloc(ptr, sz); +} + +/* A start that survives any single failed allocation keeps every credential. */ +static int test_start_oom_keeps_credentials(void) +{ + WolfCertStoreOps* store = wolfcert_store_memory_open(NULL); + WolfCertServerCfgSrv cfg; + WolfCertServer* srv = NULL; + wolfSSL_Malloc_cb mf; + wolfSSL_Free_cb ff; + wolfSSL_Realloc_cb rf; + int fail_at; + int rc; + + REQUIRE(store != NULL); + ca_store_cfg(&cfg, store); + cfg.http_basic_user = "alice"; + cfg.http_basic_pass = "secret"; + cfg.challenge_password = "otp"; + REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); + wolfcert_server_free(srv); + + REQUIRE(wolfSSL_GetAllocators(&mf, &ff, &rf) == 0); + for (fail_at = 0; ; fail_at++) { + srv = NULL; + g_fail_at = fail_at; + g_allocs = 0; + REQUIRE(wolfSSL_SetAllocators(fail_one_malloc, fail_one_free, + fail_one_realloc) == 0); + rc = wolfcert_server_start(&cfg, &srv); + REQUIRE(wolfSSL_SetAllocators(mf, ff, rf) == 0); + if (rc != WOLFCERT_OK) { + REQUIRE(srv == NULL); + continue; + } + REQUIRE(srv->cfg_basic_user != NULL && + strcmp(srv->cfg_basic_user, "alice") == 0); + REQUIRE(srv->cfg_basic_pass != NULL && + strcmp(srv->cfg_basic_pass, "secret") == 0); + REQUIRE(srv->cfg_challenge != NULL && + strcmp(srv->cfg_challenge, "otp") == 0); + wolfcert_server_free(srv); + if (g_allocs <= fail_at) + break; + } + g_fail_at = -1; + REQUIRE(fail_at > 4); + + wolfcert_store_memory_close(store); + return 0; +} +#endif + /* Fill `store` with a freshly generated CA of `type` by letting a server start * against it, then hand back copies of the stored pair. */ /* Every compiled key type, so each algorithm's certificate-to-key check is @@ -866,6 +939,11 @@ int main(void) return 1; if (test_ca_persists_across_starts()) return 1; +#if defined(USE_WOLFSSL_MEMORY) && !defined(WOLFSSL_STATIC_MEMORY) && \ + !defined(WOLFSSL_DEBUG_MEMORY) + if (test_start_oom_keeps_credentials()) + return 1; +#endif if (test_every_alg_reloads()) return 1; if (test_mismatched_ca_rejected()) diff --git a/wolfcert/server.h b/wolfcert/server.h index 7fee4d4..571cb6a 100644 --- a/wolfcert/server.h +++ b/wolfcert/server.h @@ -46,7 +46,7 @@ typedef struct { to an ephemeral CA. */ const char* challenge_password; /* SCEP challengePassword to accept; NULL disables */ const char* http_basic_user; /* EST HTTP Basic credentials to accept; NULL disables */ - const char* http_basic_pass; + const char* http_basic_pass; /* must be non-empty when http_basic_user is set */ /* CA configuration. NULL/zero values fall back to library defaults. */ WolfCertKeyType ca_key_type; /* WOLFCERT_KEY_RSA default */ @@ -56,6 +56,8 @@ typedef struct { * terminates TLS on every accepted connection before dispatching to * the protocol handler. tls_client_ca_pem, when set, enables mutual * TLS (WOLFSSL_VERIFY_PEER) against the supplied client-CA bundle. + * EST /simplereenroll needs it (else 403) and a wolfSSL built with + * KEEP_PEER_CERT (else 500). * * Mandatory for WOLFCERT_PROTO_EST, which RFC 7030 section 3.1 defines * over TLS only: wolfcert_server_start() returns WOLFCERT_ERR_TLS @@ -145,6 +147,9 @@ typedef struct { /* Heap hint for server-internal allocations. */ void* heap; + + /* Lets EST start with neither Basic nor tls_client_ca_pem. */ + int est_allow_anonymous_enroll; } WolfCertServerCfgSrv; WOLFCERT_API int wolfcert_server_start(const WolfCertServerCfgSrv* cfg, WolfCertServer** out); @@ -160,7 +165,8 @@ WOLFCERT_API uint16_t wolfcert_server_port(const WolfCertServer* srv); /* Embed wolfCert's protocol handling in an existing event loop: hand the * library an already-accepted connection; it services exactly one request * and returns, leaving the caller to close the fd. - * WOLFCERT_SERVER_REQUEST_TIMEOUT_MS does not apply. */ + * WOLFCERT_SERVER_REQUEST_TIMEOUT_MS does not apply. The fd carries no TLS, so + * EST enrollment on it needs http_basic_user or est_allow_anonymous_enroll. */ WOLFCERT_API int wolfcert_server_serve_fd(WolfCertServer* srv, int fd); #ifdef __cplusplus From d74aa0d21c43910f15054e07246aabe2d680d880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 29 Sep 2026 16:00:30 +0200 Subject: [PATCH 06/10] Validate the CSR before parking it in the EST approval queue With est_require_approval set, handler_enroll hashed the raw request body and answered 202 Accepted before decoding the base64 or looking at the PKCS#10 request. Arbitrary bytes, a CSR with a forged signature, or a reenroll whose identity did not match the TLS peer all got a pending response instead of an error, and eight such posts filled the fixed-size queue so legitimate clients got 503 until an entry was approved. Move the gate after base64 decoding, the reenroll identity check and the CSR attributes policy, and verify the CSR self-signature before queueing it; a reenroll CSR was already verified by the identity check. A CSR that does not parse or verify gets 400; only running out of memory while checking it gets 500. Key the pending entry on the decoded DER so a retry with different base64 line wrapping still matches. est_pending_roundtrip posts a body that is not a CSR and a CSR with a broken signature and expects both to be refused, and est_mtls_roundtrip checks that a mismatched reenroll on an approval server gets 400 without being parked. est_pending_roundtrip also re-POSTs a parked CSR with different base64 line wrapping and expects it to be issued. Fixes F-8038. --- src/est/est_server.c | 79 +++++++++++++------ tests/integration/test_est_mtls_roundtrip.c | 72 ++++++++++++++++- .../integration/test_est_pending_roundtrip.c | 60 ++++++++++++++ wolfcert/server.h | 10 +-- 4 files changed, 191 insertions(+), 30 deletions(-) diff --git a/src/est/est_server.c b/src/est/est_server.c index 3a6f645..a6f6839 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -53,10 +53,10 @@ * * The EST RFC has no explicit transaction identifier for async * enrollment - the client is expected to re-POST the identical CSR. We - * key pending entries off the SHA-256 of the base64-encoded request - * body so the second POST produces the same digest as the first, even - * across reconnects. The queue is capped and intentionally shallow; - * real deployments use a proper approval workflow. */ + * key pending entries off the SHA-256 of the decoded, signature-checked + * CSR DER so the second POST produces the same digest as the first, even + * across reconnects and base64 re-wrapping. The queue is capped and + * intentionally shallow; real deployments use a proper approval workflow. */ #define EST_PENDING_CAP 8 /* How long a client gets to answer a post-handshake CertificateRequest. */ @@ -1010,6 +1010,24 @@ 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) { @@ -1035,26 +1053,6 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, return WOLFCERT_ERR_HTTP; } - /* Manual-approval gate. First POST for a given CSR: park it and - * return 202 + Retry-After. Second (matching) POST: drop the - * pending entry and fall through to issuance. */ - if (s->cfg.est_require_approval && s->priv != NULL) { - EstPriv* p = (EstPriv*)s->priv; - uint8_t h[32]; - sha256_bytes(req->body, req->body_len, h); - int idx = pending_find(p, h); - if (idx < 0) { - if (!pending_add(p, h)) { - send_error(s, fd, 503, "Service Unavailable", - "pending enrollment queue is full\n"); - return WOLFCERT_ERR_PROTOCOL; - } - send_accepted_retry_after(s, fd, s->cfg.est_retry_after_sec); - return WOLFCERT_OK; - } - pending_remove(p, idx); - } - WolfCertBuffer csr = { 0 }; int rc = wolfcert_base64_decode(req->body, req->body_len, &csr, s->heap); if (rc != WOLFCERT_OK) { @@ -1119,6 +1117,39 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, } } + /* Manual-approval gate. First POST for a given CSR: park it and + * return 202 + Retry-After. Second (matching) POST: drop the + * pending entry and fall through to issuance. */ + if (s->cfg.est_require_approval && s->priv != NULL) { + 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); + if (rc == WOLFCERT_ERR_MEMORY) { + send_error(s, fd, 500, "Server Error", "cannot check the CSR\n"); + wolfcert_buffer_free(&csr); + return rc; + } + if (rc != WOLFCERT_OK) { + send_error(s, fd, 400, "Bad CSR", "CSR rejected by the CA\n"); + wolfcert_buffer_free(&csr); + return rc; + } + sha256_bytes(csr.data, csr.len, h); + int idx = pending_find(p, h); + if (idx < 0) { + wolfcert_buffer_free(&csr); + if (!pending_add(p, h)) { + send_error(s, fd, 503, "Service Unavailable", + "pending enrollment queue is full\n"); + return WOLFCERT_ERR_PROTOCOL; + } + send_accepted_retry_after(s, fd, s->cfg.est_retry_after_sec); + return WOLFCERT_OK; + } + pending_remove(p, idx); + } + uint8_t* issued = NULL; size_t issued_len = 0; rc = wolfcert_ca_issue(&s->ca, csr.data, csr.len, &issued, &issued_len); diff --git a/tests/integration/test_est_mtls_roundtrip.c b/tests/integration/test_est_mtls_roundtrip.c index ea5ee34..157ee9c 100644 --- a/tests/integration/test_est_mtls_roundtrip.c +++ b/tests/integration/test_est_mtls_roundtrip.c @@ -24,7 +24,7 @@ * then runs wolfcert-client through wolfcert_est_simple_enroll with * client_cert / client_key set on WolfCertServerCfg. * - * Six assertions: + * Seven assertions: * 1. mTLS works: a client that does NOT present a certificate is * rejected by the TLS handshake. * 2. mTLS works: a client that DOES present a cert signed by the @@ -36,6 +36,8 @@ * 5. /simplereenroll accepts a renewal of a multi-SAN cert the server's * own CA issued, but not one that changes a SAN entry or marks it critical. * 6. /simplereenroll answers a body that is not a PKCS#10 request with 400. + * 7. With manual approval on, a mismatched reenroll is refused at once + * instead of being parked. * * Exercises the TLS 1.3 negotiation path (wolfTLS_client_method / * wolfTLS_server_method) and the new client_cert plumbing on @@ -422,6 +424,68 @@ static int test_reenroll_server_issued(const uint8_t* tls_cert, size_t tls_cert_ return 0; } +/* The approval gate runs after the identity check, so a mismatched reenroll + * gets its 400 on the first POST rather than a 202. */ +static int test_reenroll_mismatch_not_parked(const uint8_t* tls_cert, size_t tls_cert_len, + const uint8_t* tls_key, size_t tls_key_len, + const uint8_t* cli_cert, size_t cli_cert_len, + const uint8_t* cli_key, size_t cli_key_len) +{ + const WolfCertCertMeta meta = { .subject_dn = "CN=someone-else" }; + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertServerCfgSrv cfg = { + .protocol = WOLFCERT_PROTO_EST, + .bind_host = "127.0.0.1", + .bind_port = 0, + .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, + .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .tls_client_ca_pem = cli_cert, .tls_client_ca_pem_len = cli_cert_len, + .est_require_approval = 1, + }; + WolfCertServer* srv = NULL; + WolfCertKey* cur_key = NULL; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertBuffer issued = { 0 }; + pthread_t tid; + char url[128]; + int rc; + + REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); + REQUIRE(pthread_create(&tid, NULL, server_thread, srv) == 0); + snprintf(url, sizeof(url), "https://127.0.0.1:%u/.well-known/est", + wolfcert_server_port(srv)); + + WolfCertServerCfg cli = { + .protocol = WOLFCERT_PROTO_EST, + .server_url = url, + .trust_anchors = tls_cert, + .trust_anchors_len = tls_cert_len, + .verify_server = 1, + }; + + REQUIRE(wolfcert_key_from_pem(cli_key, cli_key_len, NULL, &cur_key) == WOLFCERT_OK); + REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build(dk, &meta, &csr) == WOLFCERT_OK); + rc = wolfcert_est_simple_reenroll(&cli, cli_cert, cli_cert_len, cur_key, + csr.data, csr.len, &issued); + if (rc != WOLFCERT_ERR_HTTP) + fprintf(stderr, "mismatched reenroll under approval rc=%d (%s)\n", rc, + wolfcert_last_error_message()); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(issued.data == NULL); + REQUIRE(strstr(wolfcert_last_error_message(), "HTTP 400") != NULL); + + wolfcert_server_stop(srv); + pthread_join(tid, NULL); + wolfcert_server_free(srv); + wolfcert_buffer_free(&csr); + wolfcert_key_free(dk); + wolfcert_key_free(cur_key); + return 0; +} + #endif /* KEEP_PEER_CERT */ #ifndef KEEP_PEER_CERT @@ -589,6 +653,12 @@ int main(void) tls_key, tls_key_len, cli_cert, cli_cert_len, cli_key, cli_key_len) == 0); + + /* --- Case 7: under manual approval a mismatch is refused, not parked. */ + REQUIRE(test_reenroll_mismatch_not_parked(tls_cert, tls_cert_len, + tls_key, tls_key_len, + cli_cert, cli_cert_len, + cli_key, cli_key_len) == 0); #endif free(tls_cert); diff --git a/tests/integration/test_est_pending_roundtrip.c b/tests/integration/test_est_pending_roundtrip.c index 594ad23..f1bc050 100644 --- a/tests/integration/test_est_pending_roundtrip.c +++ b/tests/integration/test_est_pending_roundtrip.c @@ -48,6 +48,7 @@ #include #include +#include #include "tls_test_util.h" @@ -136,6 +137,32 @@ static int make_csr(const char* subject, WolfCertKey** out_key, return wolfcert_csr_build(*out_key, &meta, out_csr) == WOLFCERT_OK ? 0 : 1; } +/* POST a base64 CSR body as given and return the response status code. */ +static int post_enroll_raw(uint16_t port, const byte* b64, word32 b64_len) +{ + TestTlsConn c; + char hdr[256]; + char resp[64] = { 0 }; + int n; + + n = snprintf(hdr, sizeof(hdr), + "POST /.well-known/est/simpleenroll HTTP/1.1\r\n" + "Host: 127.0.0.1\r\nContent-Type: application/pkcs10\r\n" + "Content-Length: %u\r\nConnection: close\r\n\r\n", + (unsigned)b64_len); + if (n <= 0 || (size_t)n >= sizeof(hdr) || + test_tls_connect(&c, port, g_ca, g_ca_len) != 0) + return -1; + if (test_tls_write(&c, hdr, (size_t)n) != 0 || + test_tls_write(&c, b64, b64_len) != 0 || + test_tls_read(&c, resp, sizeof(resp) - 1) < 12) { + test_tls_close(&c); + return -1; + } + test_tls_close(&c); + return atoi(resp + 9); +} + static int pending_path(WolfCertServer* s) { char url[128]; @@ -188,6 +215,39 @@ static int pending_path(WolfCertServer* s) wolfSSL_CertManagerFree(cm); wolfcert_est_result_free(&r2); + + /* ---- A malformed or forged CSR is rejected, never parked ---- */ + static const uint8_t not_a_csr[] = "this is not a PKCS#10 request"; + WolfCertEstResult bad = { 0 }; + rc = wolfcert_est_simple_enroll_ex(&cli, not_a_csr, sizeof(not_a_csr), + &bad); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(bad.status == WOLFCERT_EST_STATUS_FAILURE); + wolfcert_est_result_free(&bad); + + csr_ex.data[csr_ex.len - 1] ^= 0x01; + rc = wolfcert_est_simple_enroll_ex(&cli, csr_ex.data, csr_ex.len, &bad); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(bad.status == WOLFCERT_EST_STATUS_FAILURE); + wolfcert_est_result_free(&bad); + + /* The queue matches the decoded CSR, whatever the base64 line wrapping. */ + WolfCertKey* dk_wrap = NULL; + WolfCertBuffer csr_wrap = { 0 }; + byte b64[4096]; + word32 b64_len = sizeof(b64); + REQUIRE(make_csr("CN=device-est-pending-rewrap", &dk_wrap, &csr_wrap) == 0); + REQUIRE(Base64_Encode(csr_wrap.data, (word32)csr_wrap.len, b64, + &b64_len) == 0); + REQUIRE(memchr(b64, '\n', b64_len - 1) != NULL); + REQUIRE(post_enroll_raw(wolfcert_server_port(s), b64, b64_len) == 202); + b64_len = sizeof(b64); + REQUIRE(Base64_Encode_NoNl(csr_wrap.data, (word32)csr_wrap.len, b64, + &b64_len) == 0); + REQUIRE(post_enroll_raw(wolfcert_server_port(s), b64, b64_len) == 200); + wolfcert_buffer_free(&csr_wrap); + wolfcert_key_free(dk_wrap); + wolfcert_buffer_free(&csr_ex); wolfcert_key_free(dk_ex); diff --git a/wolfcert/server.h b/wolfcert/server.h index 571cb6a..1a2e6e2 100644 --- a/wolfcert/server.h +++ b/wolfcert/server.h @@ -102,11 +102,11 @@ typedef struct { /* EST manual-approval mode (RFC 7030 section 4.2.3). When set, the first * /simpleenroll or /simplereenroll POST for a given CSR returns * `202 Accepted` with a `Retry-After: ` header; - * the next POST with the same CSR body issues the certificate - * normally. Server-side state is keyed on the SHA-256 of the CSR - * body so the client must re-POST an identical request - which is - * what `wolfcert_est_simple_enroll_ex` does when a caller loops on - * the PENDING status. + * the next POST with the same CSR issues the certificate normally, so + * the client must re-POST the same CSR - which is what + * `wolfcert_est_simple_enroll_ex` does when a caller loops on the + * PENDING status. A CSR that does not decode or whose signature does + * not verify is answered with 400 and never parked. * * `est_retry_after_sec` is the value emitted in the `Retry-After` * header; defaults to 1 when zero. This is a test-server From cba067b9ae4d2b43cccb0955574f338ef8749c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 29 Sep 2026 17:30:05 +0200 Subject: [PATCH 07/10] Parse the HTTP-date form of Retry-After RFC 9110 section 10.2.3 allows Retry-After to carry either delay-seconds or an HTTP-date. Both the blocking and the non-blocking response parsers only recognised a leading digit, so a date left retry_after_sec at 0 and an EST 202 Accepted told the caller to re-POST at once, against the RFC 7030 section 4.2.3 requirement to wait at least that long. A delay above one day was also discarded as 0 instead of being bounded. Both parsers now share parse_retry_after(), which accepts all three HTTP-date layouts (IMF-fixdate, RFC 850 and asctime), converts the date to a delay against wc_Time() so a port's clock override applies, and caps the result at 86400 seconds. A date in the past still yields 0, as does any date when wolfSSL is built with NO_ASN_TIME and has no clock, or while wc_Time() reads earlier than 2026. That is a device clock not yet set (no RTC, before SNTP), against which every date is decades ahead and would clamp to a full day. The two-digit year of an RFC 850 date resolves to the most recent year no more than 50 calendar years ahead, per RFC 9110 section 5.6.7. Delay-seconds must be digits alone apart from trailing whitespace; the strtol() parse it replaces read 120junk as 120. The transport unit test covers all three layouts, the RFC 850 century window on both sides of exactly 50 years, the cap, a set of malformed dates, malformed delay-seconds and a clock still near 1970. Fixes F-8039. --- Makefile.am | 1 + src/http.c | 205 ++++++++++++++++++++++++++++++++---- tests/CMakeLists.txt | 1 + tests/unit/test_transport.c | 200 +++++++++++++++++++++++++++++++++++ wolfcert/est.h | 14 +-- wolfcert/http.h | 6 +- 6 files changed, 394 insertions(+), 33 deletions(-) diff --git a/Makefile.am b/Makefile.am index 7db25c1..295d8de 100644 --- a/Makefile.am +++ b/Makefile.am @@ -130,6 +130,7 @@ test_csr_LDADD = libwolfcert.la $(WOLFSSL_LIBS) test_store_SOURCES = tests/unit/test_store.c test_store_LDADD = libwolfcert.la $(WOLFSSL_LIBS) test_transport_SOURCES = tests/unit/test_transport.c +test_transport_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/src test_transport_LDADD = libwolfcert.la $(WOLFSSL_LIBS) test_net_SOURCES = tests/unit/test_net.c diff --git a/src/http.c b/src/http.c index 5c13eaf..9431c5b 100644 --- a/src/http.c +++ b/src/http.c @@ -446,7 +446,7 @@ struct WolfCertHttpSession { long sm_content_length; /* -1 if unknown */ char* sm_content_type; /* taken from headers */ int sm_status; - int sm_retry_after_sec; /* delta-seconds; 0 if absent */ + int sm_retry_after_sec; /* seconds; 0 if absent */ int sm_head_request; int sm_interim; /* interim 1xx blocks dropped so far */ WolfCertHttpResponse* sm_resp; /* caller's resp; written to on DONE */ @@ -641,6 +641,186 @@ static char* find_header(const char* headers, const char* name, void* heap) return NULL; } +#define WOLFCERT_HTTP_MAX_RETRY_AFTER 86400 +/* 2026-01-01 00:00:00 UTC; an earlier wc_Time() is a clock not yet set. */ +#define WOLFCERT_HTTP_CLOCK_FLOOR 1767225600 + +static int only_ows(const char* p) +{ + while (*p == ' ' || *p == '\t') + ++p; + + return *p == '\0'; +} + +#ifndef NO_ASN_TIME +static int take_digits(const char** p, int min, int max, int* out) +{ + int n = 0; + int v = 0; + + while (n < max && **p >= '0' && **p <= '9') { + v = v * 10 + (**p - '0'); + ++*p; + ++n; + } + if (n < min) + return -1; + + *out = v; + return 0; +} + +static int take_month(const char** p) +{ + static const char months[] = "JanFebMarAprMayJunJulAugSepOctNovDec"; + int i; + + for (i = 0; i < 12; i++) { + if (strncmp(*p, months + i * 3, 3) == 0) { + *p += 3; + return i + 1; + } + } + + return 0; +} + +static int take_time_of_day(const char** p, int* out) +{ + int h; + int m; + int sec; + + if (take_digits(p, 2, 2, &h) != 0 || *(*p)++ != ':' || + take_digits(p, 2, 2, &m) != 0 || *(*p)++ != ':' || + take_digits(p, 2, 2, &sec) != 0 || h > 23 || m > 59 || sec > 60) + return -1; + + *out = h * 3600 + m * 60 + sec; + return 0; +} + +/* Seconds since 1970-01-01 00:00:00 UTC for a UTC date and time of day. */ +static int64_t date_to_unix_time(int year, int mon, int day, int tod) +{ + int64_t y = year - (mon <= 2); + int64_t era = (y >= 0 ? y : y - 399) / 400; + int64_t yoe = y - era * 400; + int64_t doy = (153 * (mon + (mon > 2 ? -3 : 9)) + 2) / 5 + day - 1; + + return (era * 146097 + yoe * 365 + yoe / 4 - yoe / 100 + doy - 719468) + * 86400 + tod; +} + +/* Seconds since 1970-01-01 00:00:00 UTC for an HTTP-date (RFC 9110 + * section 5.6.7), or -1. */ +static int64_t http_date_to_unix_time(const char* s, int64_t now) +{ + static const unsigned char mdays[12] = { + 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 + }; + const char* p = s; + int day = 0; + int mon = 0; + int year = 0; + int tod = 0; + int two_digit = 0; + + while ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z')) + ++p; + + if (p[0] == ',' && p[1] == ' ') { + p += 2; + if (take_digits(&p, 2, 2, &day) != 0) + return -1; + + if (*p == ' ') { + ++p; + mon = take_month(&p); + if (mon == 0 || *p++ != ' ' || take_digits(&p, 4, 4, &year) != 0) + return -1; + } + else if (*p == '-') { + ++p; + mon = take_month(&p); + if (mon == 0 || *p++ != '-' || take_digits(&p, 2, 2, &year) != 0) + return -1; + two_digit = 1; + } + else { + return -1; + } + + if (*p++ != ' ' || take_time_of_day(&p, &tod) != 0 || + strncmp(p, " GMT", 4) != 0 || !only_ows(p + 4)) + return -1; + } + else if (p[0] == ' ') { + ++p; + mon = take_month(&p); + if (mon == 0 || *p++ != ' ') + return -1; + if (*p == ' ') + ++p; + if (take_digits(&p, 1, 2, &day) != 0 || *p++ != ' ' || + take_time_of_day(&p, &tod) != 0 || *p++ != ' ' || + take_digits(&p, 4, 4, &year) != 0 || !only_ows(p)) + return -1; + } + else { + return -1; + } + + /* A two-digit year more than 50 years ahead is the most recent past one. */ + if (two_digit) { + year += 1900; + while (date_to_unix_time(year + 50, mon, day, tod) <= now) + year += 100; + } + + if (day < 1 || day > mdays[mon - 1] || + (mon == 2 && day == 29 && + !((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))) + return -1; + + return date_to_unix_time(year, mon, day, tod); +} +#endif + +/* Retry-After as seconds to wait; see WolfCertHttpResponse.retry_after_sec. */ +static int parse_retry_after(const char* v) +{ + int64_t delay = 0; + + if (*v >= '0' && *v <= '9') { + while (*v >= '0' && *v <= '9') { + if (delay <= WOLFCERT_HTTP_MAX_RETRY_AFTER) + delay = delay * 10 + (*v - '0'); + ++v; + } + if (!only_ows(v)) + delay = 0; + } +#ifndef NO_ASN_TIME + else { + int64_t now = (int64_t)wc_Time(NULL); + int64_t when = -1; + + if (now >= WOLFCERT_HTTP_CLOCK_FLOOR) + when = http_date_to_unix_time(v, now); + if (when >= 0) + delay = when - now; + } +#endif + + if (delay <= 0) + return 0; + + return (delay > WOLFCERT_HTTP_MAX_RETRY_AFTER) + ? WOLFCERT_HTTP_MAX_RETRY_AFTER : (int)delay; +} + static int parse_status_line(const char* line, int* out_status) { if (strncmp(line, "HTTP/", 5) != 0) @@ -1342,19 +1522,9 @@ static int http_read_response(WolfCertConn* c, if (rc == WOLFCERT_OK) { ct = find_header(headers_nt, "Content-Type", heap); - /* RFC 7231 section 7.1.3: `Retry-After` carries either delta-seconds - * or an HTTP-date. wolfCert parses delta-seconds only. */ ra = find_header(headers_nt, "Retry-After", heap); if (ra != NULL) { - const char* p = ra; - while (*p == ' ' || *p == '\t') - ++p; - - if (*p >= '0' && *p <= '9') { - long v = strtol(p, NULL, 10); - if (v > 0 && v <= 86400) - retry_after = (int)v; - } + retry_after = parse_retry_after(ra); WOLFCERT_XFREE(ra, heap); } @@ -1932,16 +2102,7 @@ static int inspect_headers(WolfCertHttpSession* s) s->sm_retry_after_sec = 0; char* ra = find_header(hdrs, "Retry-After", s->heap); if (ra != NULL) { - const char* p = ra; - while (*p == ' ' || *p == '\t') - ++p; - - if (*p >= '0' && *p <= '9') { - long v = strtol(p, NULL, 10); - if (v > 0 && v <= 86400) - s->sm_retry_after_sec = (int)v; - } - + s->sm_retry_after_sec = parse_retry_after(ra); WOLFCERT_XFREE(ra, s->heap); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9a55a0a..bf823ce 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,6 +26,7 @@ endif() find_package(Threads REQUIRED) add_executable(test_transport unit/test_transport.c) +target_include_directories(test_transport PRIVATE ${CMAKE_SOURCE_DIR}/src) target_link_libraries(test_transport PRIVATE wolfcert) add_test(NAME transport COMMAND test_transport) # A lost never-0 guard spins the read-until-close loop, so bound the run. diff --git a/tests/unit/test_transport.c b/tests/unit/test_transport.c index 612cc55..f190e2a 100644 --- a/tests/unit/test_transport.c +++ b/tests/unit/test_transport.c @@ -31,9 +31,11 @@ #include #include #include "../test_static_mem.h" +#include "internal.h" #include #include +#include #define REQUIRE(cond) \ do { \ @@ -662,6 +664,196 @@ static int test_nb_204_keeps_retry_after(void) return 0; } +static int retry_after_of(const char* value, int nb, int* out) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + char raw[256]; + int rc; + + snprintf(raw, sizeof(raw), + "HTTP/1.1 503 Service Unavailable\r\nRetry-After: %s\r\n" + "Content-Length: 0\r\nConnection: keep-alive\r\n\r\n", value); + p.open_rc = WOLFCERT_ERR_IO; + rc = nb ? nb_fetch(&p, raw, &resp) : fetch(&p, raw, sizeof(raw), &resp); + *out = resp.retry_after_sec; + wolfcert_http_response_free(&resp); + + return rc; +} + +/* delay-seconds is digits alone, apart from trailing whitespace. */ +static int test_retry_after_delay_seconds(void) +{ + static const char* const bad[] = { "120junk", "12 0", "120s", "1.5" }; + size_t i; + int nb; + int sec; + + for (nb = 0; nb <= 1; nb++) { + REQUIRE(retry_after_of("120 \t", nb, &sec) == WOLFCERT_OK); + REQUIRE(sec == 120); + REQUIRE(retry_after_of("99999999999999999999999", nb, &sec) + == WOLFCERT_OK); + REQUIRE(sec == 86400); + for (i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { + REQUIRE(retry_after_of(bad[i], nb, &sec) == WOLFCERT_OK); + if (sec != 0) + fprintf(stderr, "accepted malformed delay \"%s\"\n", bad[i]); + REQUIRE(sec == 0); + } + } + + return 0; +} + +#ifndef NO_ASN_TIME +/* RFC 9110 section 5.6.7: IMF-fixdate, obsolete RFC 850 and asctime. */ +static void retry_after_date(char* out, size_t sz, time_t t, int form) +{ + static const char* const wd[] = { + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" + }; + static const char* const wdl[] = { + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", + "Saturday" + }; + static const char* const mon[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + }; + struct tm* g = gmtime(&t); + + if (form == 0) { + snprintf(out, sz, "%s, %02d %s %04d %02d:%02d:%02d GMT", + wd[g->tm_wday], g->tm_mday, mon[g->tm_mon], + g->tm_year + 1900, g->tm_hour, g->tm_min, g->tm_sec); + } + else if (form == 1) { + snprintf(out, sz, "%s, %02d-%s-%02d %02d:%02d:%02d GMT", + wdl[g->tm_wday], g->tm_mday, mon[g->tm_mon], + g->tm_year % 100, g->tm_hour, g->tm_min, g->tm_sec); + } + else { + snprintf(out, sz, "%s %s %2d %02d:%02d:%02d %04d", + wd[g->tm_wday], mon[g->tm_mon], g->tm_mday, + g->tm_hour, g->tm_min, g->tm_sec, g->tm_year + 1900); + } +} + +static int test_retry_after_http_date(void) +{ + static const char* const bad[] = { + "Sun, 06 Nov 2095 08:49", + "Sun, 06 Nov 2095 08:49:37", + "Sun, 06 Nov 2095 08:49:37 UTC", + "Sun, 06 Nov 2095 08:49:37 GMTx", + "Sun, 06 Nov 2095 08:49:37 GMT extra", + "Sun, 06 Nov 2095 24:00:00 GMT", + "Sun, 06 Foo 2095 08:49:37 GMT", + "Sun, 6 Nov 2095 08:49:37 GMT", + "Tue, 29 Feb 2095 08:49:37 GMT", + "Mon, 29 Feb 2100 08:49:37 GMT", + "Sunday, 06-Nov-95 08:49", + "Sun Nov 6 08:49:37", + "Sunday", + "", + }; + char date[64]; + size_t i; + time_t t = time(NULL); + int cy = gmtime(&t)->tm_year + 1900; + int form; + int nb; + int sec; + + for (nb = 0; nb <= 1; nb++) { + for (form = 0; form <= 2; form++) { + retry_after_date(date, sizeof(date), time(NULL) + 120, form); + REQUIRE(retry_after_of(date, nb, &sec) == WOLFCERT_OK); + REQUIRE(sec >= 110 && sec <= 120); + + retry_after_date(date, sizeof(date), time(NULL) - 120, form); + REQUIRE(retry_after_of(date, nb, &sec) == WOLFCERT_OK); + REQUIRE(sec == 0); + } + + /* RFC 850 years resolve against now, not a fixed 1970 pivot. */ + snprintf(date, sizeof(date), "Monday, 01-Jan-%02d 00:00:00 GMT", + (cy + 45) % 100); + REQUIRE(retry_after_of(date, nb, &sec) == WOLFCERT_OK); + REQUIRE(sec == 86400); + snprintf(date, sizeof(date), "Monday, 01-Jan-%02d 00:00:00 GMT", + (cy + 55) % 100); + REQUIRE(retry_after_of(date, nb, &sec) == WOLFCERT_OK); + REQUIRE(sec == 0); + + /* Exactly 50 calendar years ahead is still the future. */ + for (i = 0; i < 2; i++) { + time_t at = time(NULL) + (i == 0 ? -60 : 120); + struct tm g = *gmtime(&at); + + if (g.tm_mon == 1 && g.tm_mday == 29) + break; + strftime(date, sizeof(date), "Monday, %d-%b-", &g); + snprintf(date + strlen(date), sizeof(date) - strlen(date), + "%02d %02d:%02d:%02d GMT", (g.tm_year + 1900 + 50) % 100, + g.tm_hour, g.tm_min, g.tm_sec); + REQUIRE(retry_after_of(date, nb, &sec) == WOLFCERT_OK); + REQUIRE(sec == (i == 0 ? 86400 : 0)); + } + + /* Future years throughout, so 0 can only mean the value was rejected. */ + for (i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { + REQUIRE(retry_after_of(bad[i], nb, &sec) == WOLFCERT_OK); + if (sec != 0) + fprintf(stderr, "accepted malformed date \"%s\"\n", bad[i]); + REQUIRE(sec == 0); + } + REQUIRE(retry_after_of("Thu, 29 Feb 2096 08:49:37 GMT", nb, &sec) + == WOLFCERT_OK); + REQUIRE(sec == 86400); + + REQUIRE(retry_after_of("Sun, 06 Nov 2094 08:49:37 GMT", nb, &sec) + == WOLFCERT_OK); + REQUIRE(sec == 86400); + REQUIRE(retry_after_of("172800", nb, &sec) == WOLFCERT_OK); + REQUIRE(sec == 86400); + REQUIRE(retry_after_of("Sun, 31 Nov 2094 08:49:37 GMT", nb, &sec) + == WOLFCERT_OK); + REQUIRE(sec == 0); + } + + return 0; +} + +static time_t unset_clock(time_t* t) +{ + if (t != NULL) + *t = 1000; + return 1000; +} + +/* A device clock still near 1970 must not turn every date into a day's wait. */ +static int test_retry_after_unset_clock(void) +{ + int sec = -1; + int rc; + + REQUIRE(wc_SetTimeCb(unset_clock) == 0); + rc = retry_after_of("Sun, 06 Nov 2094 08:49:37 GMT", 0, &sec); + wc_SetTimeCb(NULL); + REQUIRE(rc == WOLFCERT_OK); + REQUIRE(sec == 0); + REQUIRE(wc_SetTimeCb(unset_clock) == 0); + rc = retry_after_of("120", 1, &sec); + wc_SetTimeCb(NULL); + REQUIRE(rc == WOLFCERT_OK); + REQUIRE(sec == 120); + return 0; +} +#endif + static int test_nb_interim_then_final(void) { WolfCertHttpResponse resp = { 0 }; @@ -965,6 +1157,14 @@ int main(void) return 1; if (test_nb_204_keeps_retry_after()) return 1; + if (test_retry_after_delay_seconds()) + return 1; +#ifndef NO_ASN_TIME + if (test_retry_after_http_date()) + return 1; + if (test_retry_after_unset_clock()) + return 1; +#endif if (test_nb_interim_then_final()) return 1; if (test_eof_empty_body_is_null()) diff --git a/wolfcert/est.h b/wolfcert/est.h index c130fa2..90df444 100644 --- a/wolfcert/est.h +++ b/wolfcert/est.h @@ -145,7 +145,7 @@ WOLFCERT_API int wolfcert_csr_attrs_build(const WolfCertCsrAttrItem* items, /* ---- EST enrollment result (RFC 7030 section 4.2) ----------------------------- * * RFC 7030 section 4.2.3 lets a server respond to /simpleenroll (or - * /simplereenroll) with `202 Accepted` + `Retry-After: ` when the + * /simplereenroll) with `202 Accepted` + `Retry-After` when the * request has been accepted but the certificate is not yet ready - * typically because the deployment requires manual approval. This is the * EST analogue of SCEP's `pkiStatus=PENDING`. The client is expected to @@ -158,9 +158,9 @@ WOLFCERT_API int wolfcert_csr_attrs_build(const WolfCertCsrAttrItem* items, * the issued cert (PEM). * status == PENDING -> the call returns `WOLFCERT_OK`; `cert_pem` is * empty; `retry_after_sec` carries - * the server's hint (0 when the server did not - * send Retry-After, or sent it in the HTTP-date - * form which wolfCert does not yet parse). + * the server's hint in seconds (0 when the + * server did not send a usable Retry-After; see + * `WolfCertHttpResponse.retry_after_sec`). * status == FAILURE -> any other HTTP status came back; the call returns * `WOLFCERT_ERR_AUTH` for 401/403, else * `WOLFCERT_ERR_HTTP`. @@ -187,9 +187,9 @@ typedef struct { /* Populated and owned iff status == SUCCESS. */ WolfCertBuffer cert_pem; /* Server's suggested wait before the client should re-POST. Only - * meaningful when status == PENDING. 0 when the server did not send - * `Retry-After`; callers may apply their own backoff policy in that - * case. */ + * meaningful when status == PENDING. 0 when the server sent no usable + * `Retry-After` (see `WolfCertHttpResponse.retry_after_sec`); callers may + * apply their own backoff policy in that case. */ int retry_after_sec; void* heap; } WolfCertEstResult; diff --git a/wolfcert/http.h b/wolfcert/http.h index b926551..c5731f7 100644 --- a/wolfcert/http.h +++ b/wolfcert/http.h @@ -76,10 +76,8 @@ typedef struct { /* May be NULL when body_len is 0; check body_len, not body. */ uint8_t* body; size_t body_len; - /* `Retry-After` header parsed as delta-seconds (RFC 7231 section 7.1.3). - * Populated for any response that carries the header; 0 means the - * header was absent or wasn't in the delta-seconds form. The HTTP - * date form is not supported. */ + /* `Retry-After` in seconds, capped at 86400 (RFC 9110 10.2.3); 0 if absent, + * malformed, past, or a date under NO_ASN_TIME or an unset clock. */ int retry_after_sec; void* heap; } WolfCertHttpResponse; From 5e6ac36ed602bfd7a481888787f10b50a37ba415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 29 Sep 2026 17:34:29 +0200 Subject: [PATCH 08/10] Reject EST requests carrying more than one Authorization header The EST test server's request parser allocated a fresh copy of every Authorization header line into the same field, so each repeat orphaned the previous allocation. An unauthenticated client could leak close to a full request-header buffer per connection by sending a long Authorization header followed by a short one. Authorization is a singleton field (RFC 9110), so a second occurrence now fails the parse and the client gets 400. The existing error path frees the first value. Header names are now matched exactly, up to their colon, with the ASCII-only wolfcert_ascii_ncasecmp() that the HTTP client already uses: the old prefix match read an Authorization-Foo field as Authorization, so it and a real Authorization header together would be refused as a duplicate, and a Content-Length-Foo field could set the body length. A field with whitespace before its colon now gets 400, as RFC 9112 section 5.1 requires; ignoring it would leave its body to be read as the next request on a kept-alive connection. A regression case in est_chunked_robustness sends the duplicate and expects the 400, checks that an Authorization-Foo field next to Authorization is accepted, and that "Content-Length : 0" gets 400. Fixes F-8047. --- src/est/est_server.c | 84 ++++++++++--------- .../integration/test_est_chunked_robustness.c | 39 +++++++++ 2 files changed, 84 insertions(+), 39 deletions(-) diff --git a/src/est/est_server.c b/src/est/est_server.c index a6f6839..efe4a61 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -259,6 +259,15 @@ static int chunked_body_complete(const uint8_t* raw, size_t raw_len) return 0; } +/* 1 when the line's field name is `name`, followed directly by ':'. */ +static int hdr_is(const char* line, size_t llen, const char* name) +{ + size_t n = strlen(name); + + return llen > n && line[n] == ':' && + wolfcert_ascii_ncasecmp(line, name, n) == 0; +} + static int parse_request(WolfCertServer* s, int fd, EstRequest* out, void* heap) { memset(out, 0, sizeof(*out)); @@ -307,52 +316,49 @@ static int parse_request(WolfCertServer* s, int fd, EstRequest* out, void* heap) int chunked = 0; while (read_line(&p, end, &line, &llen) == 0 && llen > 0) { - if (llen > 14 && strncasecmp(line, "Content-Length", 14) == 0) { - char* colon = memchr(line, ':', llen); - if (colon != NULL) - out->content_length = (size_t)strtoul(colon + 1, NULL, 10); - } - else if (llen > 17 && strncasecmp(line, "Transfer-Encoding", 17) == 0) { - char* colon = memchr(line, ':', llen); - if (colon != NULL) { - const char* v = colon + 1; - while (v < line + llen && (*v == ' ' || *v == '\t')) { - ++v; - } + const char* hc = memchr(line, ':', llen); - size_t vlen = (size_t)(line + llen - v); - if (vlen >= 7 && strncasecmp(v, "chunked", 7) == 0) - chunked = 1; + /* RFC 9112 section 5.1: whitespace before the colon is a 400. */ + if (hc != NULL && hc > line && (hc[-1] == ' ' || hc[-1] == '\t')) + return WOLFCERT_ERR_PROTOCOL; + if (hdr_is(line, llen, "Content-Length")) { + out->content_length = (size_t)strtoul(hc + 1, NULL, 10); + } + else if (hdr_is(line, llen, "Transfer-Encoding")) { + const char* v = hc + 1; + while (v < line + llen && (*v == ' ' || *v == '\t')) { + ++v; } + + size_t vlen = (size_t)(line + llen - v); + if (vlen >= 7 && wolfcert_ascii_ncasecmp(v, "chunked", 7) == 0) + chunked = 1; } - else if (llen > 13 && strncasecmp(line, "Authorization", 13) == 0) { - char* colon = memchr(line, ':', llen); - if (colon != NULL) { - char* val = colon + 1; - while (*val == ' ' || *val == '\t') { - ++val; - } + else if (hdr_is(line, llen, "Authorization")) { + if (out->auth_header != NULL) + return WOLFCERT_ERR_PROTOCOL; - size_t vlen = llen - (size_t)(val - line); - out->auth_header = (char*)WOLFCERT_XMALLOC(vlen + 1, heap); - if (out->auth_header) { - memcpy(out->auth_header, val, vlen); - out->auth_header[vlen] = '\0'; - } + const char* val = hc + 1; + while (val < line + llen && (*val == ' ' || *val == '\t')) { + ++val; } - } - else if (llen > 10 && strncasecmp(line, "Connection", 10) == 0) { - char* colon = memchr(line, ':', llen); - if (colon != NULL) { - const char* v = colon + 1; - while (v < line + llen && (*v == ' ' || *v == '\t')) { - ++v; - } - size_t vlen = (size_t)(line + llen - v); - if (vlen >= 5 && strncasecmp(v, "close", 5) == 0) - out->connection_close = 1; + size_t vlen = (size_t)(line + llen - val); + out->auth_header = (char*)WOLFCERT_XMALLOC(vlen + 1, heap); + if (out->auth_header) { + memcpy(out->auth_header, val, vlen); + out->auth_header[vlen] = '\0'; + } + } + else if (hdr_is(line, llen, "Connection")) { + const char* v = hc + 1; + while (v < line + llen && (*v == ' ' || *v == '\t')) { + ++v; } + + size_t vlen = (size_t)(line + llen - v); + if (vlen >= 5 && wolfcert_ascii_ncasecmp(v, "close", 5) == 0) + out->connection_close = 1; } } diff --git a/tests/integration/test_est_chunked_robustness.c b/tests/integration/test_est_chunked_robustness.c index b89a88a..20e8a17 100644 --- a/tests/integration/test_est_chunked_robustness.c +++ b/tests/integration/test_est_chunked_robustness.c @@ -272,6 +272,43 @@ static int accept_multisegment_chunked_body(uint16_t port) return 0; } +/* Authorization is a singleton field, so a second one is a malformed request. */ +static int reject_duplicate_authorization(uint16_t port) +{ + const char* req = + "GET /.well-known/est/cacerts HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Authorization: Basic AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n" + "Authorization: Basic AA==\r\n" + "Connection: close\r\n" + "\r\n"; + /* A field that only starts with "Authorization" is a different field. */ + const char* other = + "GET /.well-known/est/cacerts HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Authorization-Foo: x\r\n" + "Authorization: Basic AA==\r\n" + "Connection: close\r\n" + "\r\n"; + /* RFC 9112 section 5.1: whitespace before a colon is a 400. */ + const char* spaced = + "GET /.well-known/est/cacerts HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Content-Length : 0\r\n" + "Connection: close\r\n" + "\r\n"; + char status[128] = { 0 }; + send_and_read_status(port, req, strlen(req), status, sizeof(status)); + REQUIRE(strstr(status, "400") != NULL); + memset(status, 0, sizeof(status)); + send_and_read_status(port, other, strlen(other), status, sizeof(status)); + REQUIRE(strstr(status, "200") != NULL); + memset(status, 0, sizeof(status)); + send_and_read_status(port, spaced, strlen(spaced), status, sizeof(status)); + REQUIRE(strstr(status, "400") != NULL); + return 0; +} + /* Build a chunked simpleenroll request whose body carries a real, * base64-encoded CSR in a single chunk, but split so the last-chunk line * ("0\r\n") is delivered separately from its terminating trailer CRLF. @@ -518,6 +555,8 @@ int main(void) rc = accept_multisegment_chunked_body(port); if (rc == 0) rc = keepalive_after_split_trailer(port); + if (rc == 0) + rc = reject_duplicate_authorization(port); if (rc == 0) rc = no_sigpipe_on_response(); From 70bbe01fcc0a71798f3f9283b17d4a6898ad0a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 29 Sep 2026 22:38:58 +0200 Subject: [PATCH 09/10] scep: heap-allocate DecodedCert on the client paths A DecodedCert is about 2.3 KiB, and the SCEP client runs on MCU task stacks. derive_txid_pubkey() already allocated its DecodedCert with WOLFCERT_XMALLOC; the other client-side parses kept theirs on the stack, wolfcert_scep_issuer_and_subject() two at once. Move them to the heap hint: - wolfcert_scep_pem_has_cert() allocates one DecodedCert and reuses it for every PEM entry. Running out of memory, for the DecodedCert, the DER copy or inside wc_ParseCert(), returns WOLFCERT_ERR_MEMORY, which GetCert passes on instead of reporting a response without the certificate. Only an entry that does not parse is skipped. For that, wolfcert_pem_cert_to_der() now reports a MEMORY_E from wc_PemToDer() as WOLFCERT_ERR_MEMORY rather than a parse error. - wolfcert_scep_issuer_and_subject() and wolfcert_scep_issuer_and_serial() allocate in a thin wrapper and pass the structs to a static helper holding the old body, so every early return keeps its cleanup. - wolfcert_scep_self_signed_rsa() and wolfcert_extract_spki() allocate inline. wolfcert_extract_spki() is shared with the server, but the client reaches it through wolfcert_scep_verify_rep_signer(). It now reports a MEMORY_E from wc_ParseCert() as WOLFCERT_ERR_MEMORY, and wolfcert_scep_verify_rep_signer() passes that on, so the client reports running out of memory instead of a CertRep that is not signed by the CA. test_scep_msg fails each allocation of a GetCert bundle search, and of a CertRep signer check, in turn, where the wolfSSL allocator can be swapped, and expects WOLFCERT_ERR_MEMORY every time until the call succeeds. Server-only code (the EST and SCEP servers, ca_issue.c and wolfcert_scep_issuer_name_matches()) keeps its DecodedCerts on the stack; the server runs on a host. --- src/internal.c | 2 +- src/internal.h | 5 +- src/scep/scep_client.c | 81 +++++++------ src/scep/scep_msg.c | 232 +++++++++++++++++++++++-------------- tests/unit/test_scep_msg.c | 103 ++++++++++++++++ wolfcert/scep.h | 3 +- 6 files changed, 297 insertions(+), 129 deletions(-) diff --git a/src/internal.c b/src/internal.c index c8d2c0e..d3ae8b3 100644 --- a/src/internal.c +++ b/src/internal.c @@ -508,7 +508,7 @@ int wolfcert_pem_cert_to_der(const uint8_t* pem, size_t pem_len, if (rc != 0 || der == NULL) { if (der != NULL) wc_FreeDer(&der); - return WOLFCERT_ERR_PARSE; + return rc == MEMORY_E ? WOLFCERT_ERR_MEMORY : WOLFCERT_ERR_PARSE; } uint8_t* buf = (uint8_t*)WOLFCERT_XMALLOC(der->length, heap); diff --git a/src/internal.h b/src/internal.h index 9b3f5ad..c1328c7 100644 --- a/src/internal.h +++ b/src/internal.h @@ -448,7 +448,8 @@ WOLFCERT_TEST_VIS int wolfcert_scep_parse_issuer_and_serial( /* Does any certificate in `pem` carry `issuer` and `serial`? A CertRep is only * checked for signer and nonce, so without this a CA could answer a GetCert with * some other certificate and the caller would write it out as the one it named. - * An entry that will not parse is skipped, not treated as the end of the list. */ + * An entry that will not parse is skipped, not treated as the end of the list. + * Returns 1 if found, 0 if not, or WOLFCERT_ERR_MEMORY. */ WOLFCERT_TEST_VIS int wolfcert_scep_pem_has_cert(const uint8_t* pem, size_t pem_len, const uint8_t* issuer, size_t issuer_len, const uint8_t* serial, size_t serial_len, @@ -515,7 +516,7 @@ int wolfcert_extract_spki(const uint8_t* der, size_t len, int is_csr, /* 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 - * match, WOLFCERT_ERR_AUTH otherwise. */ + * match, WOLFCERT_ERR_MEMORY on OOM, WOLFCERT_ERR_AUTH otherwise. */ WOLFCERT_TEST_VIS int wolfcert_scep_verify_rep_signer( const uint8_t* signer_cert, size_t signer_cert_len, const uint8_t* ca_bundle, size_t ca_bundle_len, void* heap); diff --git a/src/scep/scep_client.c b/src/scep/scep_client.c index db625b4..b28c238 100644 --- a/src/scep/scep_client.c +++ b/src/scep/scep_client.c @@ -893,7 +893,9 @@ static int scep_finish(void* heap, if (rc == WOLFCERT_OK) { rc = wolfcert_scep_verify_rep_signer(rx_signer, rx_signer_len, ca_bundle, ca_bundle_len, heap); - if (rc != WOLFCERT_OK) + if (rc == WOLFCERT_ERR_MEMORY) + rc = WOLFCERT_ERR(rc, "scep", "out of memory checking the CertRep signer"); + else if (rc != WOLFCERT_OK) rc = WOLFCERT_ERR(WOLFCERT_ERR_AUTH, "scep", "CertRep is not signed by the CA/RA certificate"); } @@ -1323,20 +1325,26 @@ WOLFCERT_TEST_VIS int wolfcert_scep_pem_has_cert(const uint8_t* pem, size_t pem_ { static const char BEGIN[] = "-----BEGIN CERTIFICATE-----"; const size_t blen = sizeof(BEGIN) - 1; + DecodedCert* dc; const char* p; const char* end; + int match = 0; if (pem == NULL || issuer == NULL || serial == NULL || pem_len < blen) return 0; + dc = (DecodedCert*)WOLFCERT_XMALLOC(sizeof(*dc), heap); + if (dc == NULL) + return WOLFCERT_ERR(WOLFCERT_ERR_MEMORY, "scep", + "GetCert: cannot allocate a DecodedCert"); + p = (const char*)pem; end = p + pem_len; /* Compare the remaining length: p + blen would run past the buffer. */ - while ((size_t)(end - p) >= blen) { + while (match == 0 && (size_t)(end - p) >= blen) { WolfCertBuffer der = { 0 }; - DecodedCert dc; - int match; + int rc; if (memcmp(p, BEGIN, blen) != 0) { p++; @@ -1344,36 +1352,36 @@ WOLFCERT_TEST_VIS int wolfcert_scep_pem_has_cert(const uint8_t* pem, size_t pem_ } /* A bad entry must not end the search: the target may sit behind it. */ - if (wolfcert_pem_cert_to_der((const uint8_t*)p, (size_t)(end - p), - &der, heap) != WOLFCERT_OK) { - p += blen; - continue; + rc = wolfcert_pem_cert_to_der((const uint8_t*)p, (size_t)(end - p), + &der, heap); + if (rc == WOLFCERT_ERR_MEMORY) { + match = WOLFCERT_ERR(WOLFCERT_ERR_MEMORY, "scep", + "GetCert: out of memory decoding a certificate"); } - - wc_InitDecodedCert(&dc, der.data, (word32)der.len, heap); - if (wc_ParseCert(&dc, CERT_TYPE, NO_VERIFY, NULL) != 0) { - wc_FreeDecodedCert(&dc); - wolfcert_buffer_free(&der); - p += blen; - continue; + else if (rc == WOLFCERT_OK) { + wc_InitDecodedCert(dc, der.data, (word32)der.len, heap); + rc = wc_ParseCert(dc, CERT_TYPE, NO_VERIFY, NULL); + if (rc == MEMORY_E) { + match = WOLFCERT_ERR(WOLFCERT_ERR_MEMORY, "scep", + "GetCert: out of memory parsing a certificate"); + } + else if (rc == 0) { + match = dc->serialSz > 0 && + (size_t)dc->serialSz == serial_len && + memcmp(dc->serial, serial, serial_len) == 0 && + dc->issuerRaw != NULL && dc->issuerRawLen > 0 && + (size_t)dc->issuerRawLen == issuer_len && + memcmp(dc->issuerRaw, issuer, issuer_len) == 0; + } + wc_FreeDecodedCert(dc); } - match = dc.serialSz > 0 && (size_t)dc.serialSz == serial_len && - memcmp(dc.serial, serial, serial_len) == 0 && - dc.issuerRaw != NULL && dc.issuerRawLen > 0 && - (size_t)dc.issuerRawLen == issuer_len && - memcmp(dc.issuerRaw, issuer, issuer_len) == 0; - - wc_FreeDecodedCert(&dc); wolfcert_buffer_free(&der); - - if (match) - return 1; - p += blen; } - return 0; + WOLFCERT_XFREE(dc, heap); + return match; } int wolfcert_scep_get_cert(const WolfCertServerCfg* srv, @@ -1435,14 +1443,17 @@ int wolfcert_scep_get_cert(const WolfCertServerCfg* srv, key_der, key_der_len, "21", ias.data, ias.len, NULL, 0, out); - if (rc == WOLFCERT_OK && out->status == WOLFCERT_SCEP_STATUS_SUCCESS && - !wolfcert_scep_pem_has_cert(out->cert_pem.data, out->cert_pem.len, - want_issuer, want_issuer_len, - want_serial, want_serial_len, heap)) { - wolfcert_buffer_free(&out->cert_pem); - out->status = WOLFCERT_SCEP_STATUS_UNSET; - rc = WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "scep", - "GetCert returned no certificate with the requested issuer and serial"); + if (rc == WOLFCERT_OK && out->status == WOLFCERT_SCEP_STATUS_SUCCESS) { + int has = wolfcert_scep_pem_has_cert(out->cert_pem.data, + out->cert_pem.len, + want_issuer, want_issuer_len, + want_serial, want_serial_len, heap); + if (has != 1) { + wolfcert_buffer_free(&out->cert_pem); + out->status = WOLFCERT_SCEP_STATUS_UNSET; + rc = (has < 0) ? has : WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "scep", + "GetCert returned no certificate with the requested issuer and serial"); + } } wolfcert_buffer_free(&ias); diff --git a/src/scep/scep_msg.c b/src/scep/scep_msg.c index 6752993..fb464b9 100644 --- a/src/scep/scep_msg.c +++ b/src/scep/scep_msg.c @@ -462,7 +462,7 @@ WOLFCERT_TEST_VIS int wolfcert_scep_self_signed_rsa(RsaKey* key, const uint8_t* csr_der, size_t csr_len, uint8_t** out_der, size_t* out_len, void* heap) { - DecodedCert dc; + DecodedCert* dc; Cert* cert; WC_RNG rng; uint8_t* der; @@ -486,6 +486,12 @@ WOLFCERT_TEST_VIS int wolfcert_scep_self_signed_rsa(RsaKey* key, wc_InitCert_ex(cert, heap, WOLFCERT_DEVID_SOFTWARE); + dc = (DecodedCert*)WOLFCERT_XMALLOC(sizeof(*dc), heap); + if (dc == NULL) { + wc_CertFree(cert); + return WOLFCERT_ERR_MEMORY; + } + /* RFC 8894 section 2.3: the signer certificate SHOULD carry the same * subject name as the enclosed PKCS#10 request. The certificate is * self-signed, so its issuer name is the same DN. @@ -496,33 +502,35 @@ WOLFCERT_TEST_VIS int wolfcert_scep_self_signed_rsa(RsaKey* key, * for a NUL-free DN that leaves room for a terminator, and fall back to the * request's common name otherwise. The signer subject is not security * relevant: issuance binds on the public key, not this name. */ - wc_InitDecodedCert(&dc, (const byte*)csr_der, (word32)csr_len, heap); - rc = wc_ParseCert(&dc, CERTREQ_TYPE, NO_VERIFY, NULL); + wc_InitDecodedCert(dc, (const byte*)csr_der, (word32)csr_len, heap); + rc = wc_ParseCert(dc, CERTREQ_TYPE, NO_VERIFY, NULL); if (rc != 0) { - wc_FreeDecodedCert(&dc); + wc_FreeDecodedCert(dc); + WOLFCERT_XFREE(dc, heap); wc_CertFree(cert); return WOLFCERT_ERR_PARSE; } - if (dc.subjectRaw != NULL && dc.subjectRawLen > 0 && - dc.subjectRawLen < (int)sizeof(cert->sbjRaw) && - memchr(dc.subjectRaw, 0x00, (size_t)dc.subjectRawLen) == NULL) { - memcpy(cert->sbjRaw, dc.subjectRaw, (size_t)dc.subjectRawLen); - cert->sbjRaw[dc.subjectRawLen] = '\0'; - memcpy(cert->issRaw, dc.subjectRaw, (size_t)dc.subjectRawLen); - cert->issRaw[dc.subjectRawLen] = '\0'; + if (dc->subjectRaw != NULL && dc->subjectRawLen > 0 && + dc->subjectRawLen < (int)sizeof(cert->sbjRaw) && + memchr(dc->subjectRaw, 0x00, (size_t)dc->subjectRawLen) == NULL) { + memcpy(cert->sbjRaw, dc->subjectRaw, (size_t)dc->subjectRawLen); + cert->sbjRaw[dc->subjectRawLen] = '\0'; + memcpy(cert->issRaw, dc->subjectRaw, (size_t)dc->subjectRawLen); + cert->issRaw[dc->subjectRawLen] = '\0'; } - else if (dc.subjectCN != NULL && dc.subjectCNLen > 0) { - int cn = dc.subjectCNLen < CTC_NAME_SIZE - 1 - ? dc.subjectCNLen : CTC_NAME_SIZE - 1; - memcpy(cert->subject.commonName, dc.subjectCN, (size_t)cn); + else if (dc->subjectCN != NULL && dc->subjectCNLen > 0) { + int cn = dc->subjectCNLen < CTC_NAME_SIZE - 1 + ? dc->subjectCNLen : CTC_NAME_SIZE - 1; + memcpy(cert->subject.commonName, dc->subjectCN, (size_t)cn); cert->subject.commonName[cn] = '\0'; } else { strncpy(cert->subject.commonName, "SCEP Enrollee", CTC_NAME_SIZE - 1); cert->subject.commonName[CTC_NAME_SIZE - 1] = '\0'; } - wc_FreeDecodedCert(&dc); + wc_FreeDecodedCert(dc); + WOLFCERT_XFREE(dc, heap); cert->selfSigned = 1; cert->sigType = CTC_SHA256wRSA; @@ -1037,57 +1045,52 @@ static void issuing_ca_name(const DecodedCert* dc, const uint8_t** out_name, } } -WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_subject( - const uint8_t* ra_cert_der, size_t ra_cert_len, - const uint8_t* csr_der, size_t csr_len, - WolfCertBuffer* out_der, void* heap) +static int issuer_and_subject_der(DecodedCert* ic, DecodedCert* sc, + const uint8_t* ra_cert_der, size_t ra_cert_len, + const uint8_t* csr_der, size_t csr_len, + WolfCertBuffer* out_der, void* heap) { const uint8_t* issuer_name; int issuer_name_len; - if (ra_cert_der == NULL || csr_der == NULL || out_der == NULL) - return WOLFCERT_ERR_BAD_ARG; - - DecodedCert ic; - wc_InitDecodedCert(&ic, (byte*)ra_cert_der, + wc_InitDecodedCert(ic, (byte*)ra_cert_der, (word32)ra_cert_len, heap); - int rc = wc_ParseCert(&ic, CERT_TYPE, NO_VERIFY, NULL); + int rc = wc_ParseCert(ic, CERT_TYPE, NO_VERIFY, NULL); if (rc != 0) { - wc_FreeDecodedCert(&ic); + wc_FreeDecodedCert(ic); return WOLFCERT_ERR_PARSE; } - DecodedCert sc; - wc_InitDecodedCert(&sc, (byte*)csr_der, (word32)csr_len, heap); + wc_InitDecodedCert(sc, (byte*)csr_der, (word32)csr_len, heap); - rc = wc_ParseCert(&sc, CERTREQ_TYPE, NO_VERIFY, NULL); + rc = wc_ParseCert(sc, CERTREQ_TYPE, NO_VERIFY, NULL); if (rc != 0) { - wc_FreeDecodedCert(&ic); - wc_FreeDecodedCert(&sc); + wc_FreeDecodedCert(ic); + wc_FreeDecodedCert(sc); return WOLFCERT_ERR_PARSE; } - issuing_ca_name(&ic, &issuer_name, &issuer_name_len); + issuing_ca_name(ic, &issuer_name, &issuer_name_len); if (issuer_name == NULL || issuer_name_len <= 0 || - sc.subjectRaw == NULL || sc.subjectRawLen <= 0) { - wc_FreeDecodedCert(&ic); - wc_FreeDecodedCert(&sc); + sc->subjectRaw == NULL || sc->subjectRawLen <= 0) { + wc_FreeDecodedCert(ic); + wc_FreeDecodedCert(sc); return WOLFCERT_ERR_PARSE; } /* Give each Name its own SEQUENCE, so the result decodes as * IssuerAndSubject ::= SEQUENCE { issuer Name, subject Name }. */ size_t issuer_tlv = enc_tlv_len((size_t)issuer_name_len); - size_t subject_tlv = enc_tlv_len((size_t)sc.subjectRawLen); + size_t subject_tlv = enc_tlv_len((size_t)sc->subjectRawLen); size_t inner = issuer_tlv + subject_tlv; size_t cap = inner + 8; uint8_t* buf = (uint8_t*)WOLFCERT_XMALLOC(cap, heap); if (buf == NULL) { - wc_FreeDecodedCert(&ic); - wc_FreeDecodedCert(&sc); + wc_FreeDecodedCert(ic); + wc_FreeDecodedCert(sc); return WOLFCERT_ERR_MEMORY; } @@ -1095,8 +1098,8 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_subject( int ll = der_put_len(buf + 1, cap - 1, inner); if (ll < 0) { WOLFCERT_XFREE(buf, heap); - wc_FreeDecodedCert(&ic); - wc_FreeDecodedCert(&sc); + wc_FreeDecodedCert(ic); + wc_FreeDecodedCert(sc); return WOLFCERT_ERR_MEMORY; } @@ -1105,21 +1108,21 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_subject( buf + off, cap - off); if (n > 0) { off += (size_t)n; - n = enc_seq(sc.subjectRaw, (size_t)sc.subjectRawLen, + n = enc_seq(sc->subjectRaw, (size_t)sc->subjectRawLen, buf + off, cap - off); } if (n < 0) { WOLFCERT_XFREE(buf, heap); - wc_FreeDecodedCert(&ic); - wc_FreeDecodedCert(&sc); + wc_FreeDecodedCert(ic); + wc_FreeDecodedCert(sc); return WOLFCERT_ERR_MEMORY; } off += (size_t)n; - wc_FreeDecodedCert(&ic); - wc_FreeDecodedCert(&sc); + wc_FreeDecodedCert(ic); + wc_FreeDecodedCert(sc); out_der->data = buf; out_der->len = off; out_der->heap = heap; @@ -1127,31 +1130,47 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_subject( return WOLFCERT_OK; } -WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_serial( +WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_subject( const uint8_t* ra_cert_der, size_t ra_cert_len, - const uint8_t* serial, size_t serial_len, + const uint8_t* csr_der, size_t csr_len, WolfCertBuffer* out_der, void* heap) { - const uint8_t* issuer_name; - int issuer_name_len; + DecodedCert* dc; + int rc; - if (ra_cert_der == NULL || serial == NULL || serial_len == 0 || - out_der == NULL) + if (ra_cert_der == NULL || csr_der == NULL || out_der == NULL) return WOLFCERT_ERR_BAD_ARG; - DecodedCert ic; - wc_InitDecodedCert(&ic, (byte*)ra_cert_der, (word32)ra_cert_len, heap); + dc = (DecodedCert*)WOLFCERT_XMALLOC(2 * sizeof(*dc), heap); + if (dc == NULL) + return WOLFCERT_ERR_MEMORY; + + rc = issuer_and_subject_der(&dc[0], &dc[1], ra_cert_der, ra_cert_len, + csr_der, csr_len, out_der, heap); + WOLFCERT_XFREE(dc, heap); + return rc; +} + +static int issuer_and_serial_der(DecodedCert* ic, + const uint8_t* ra_cert_der, size_t ra_cert_len, + const uint8_t* serial, size_t serial_len, + WolfCertBuffer* out_der, void* heap) +{ + const uint8_t* issuer_name; + int issuer_name_len; - int rc = wc_ParseCert(&ic, CERT_TYPE, NO_VERIFY, NULL); + wc_InitDecodedCert(ic, (byte*)ra_cert_der, (word32)ra_cert_len, heap); + + int rc = wc_ParseCert(ic, CERT_TYPE, NO_VERIFY, NULL); if (rc != 0) { - wc_FreeDecodedCert(&ic); + wc_FreeDecodedCert(ic); return WOLFCERT_ERR_PARSE; } - issuing_ca_name(&ic, &issuer_name, &issuer_name_len); + issuing_ca_name(ic, &issuer_name, &issuer_name_len); if (issuer_name == NULL || issuer_name_len <= 0) { - wc_FreeDecodedCert(&ic); + wc_FreeDecodedCert(ic); return WOLFCERT_ERR_PARSE; } @@ -1164,7 +1183,7 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_serial( uint8_t* buf = (uint8_t*)WOLFCERT_XMALLOC(cap, heap); if (buf == NULL) { - wc_FreeDecodedCert(&ic); + wc_FreeDecodedCert(ic); return WOLFCERT_ERR_MEMORY; } @@ -1172,7 +1191,7 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_serial( int ll = der_put_len(buf + 1, cap - 1, inner); if (ll < 0) { WOLFCERT_XFREE(buf, heap); - wc_FreeDecodedCert(&ic); + wc_FreeDecodedCert(ic); return WOLFCERT_ERR_MEMORY; } @@ -1184,7 +1203,7 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_serial( n = enc_integer(serial, serial_len, buf + off, cap - off); } - wc_FreeDecodedCert(&ic); + wc_FreeDecodedCert(ic); if (n < 0) { WOLFCERT_XFREE(buf, heap); @@ -1199,6 +1218,28 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_serial( return WOLFCERT_OK; } +WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_serial( + const uint8_t* ra_cert_der, size_t ra_cert_len, + const uint8_t* serial, size_t serial_len, + WolfCertBuffer* out_der, void* heap) +{ + DecodedCert* ic; + int rc; + + if (ra_cert_der == NULL || serial == NULL || serial_len == 0 || + out_der == NULL) + return WOLFCERT_ERR_BAD_ARG; + + ic = (DecodedCert*)WOLFCERT_XMALLOC(sizeof(*ic), heap); + if (ic == NULL) + return WOLFCERT_ERR_MEMORY; + + rc = issuer_and_serial_der(ic, ra_cert_der, ra_cert_len, serial, serial_len, + out_der, heap); + WOLFCERT_XFREE(ic, heap); + return rc; +} + WOLFCERT_TEST_VIS int wolfcert_scep_parse_issuer_and_serial( const uint8_t* der, size_t der_len, const uint8_t** out_issuer, @@ -1279,32 +1320,36 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_name_matches( int wolfcert_extract_spki(const uint8_t* der, size_t len, int is_csr, uint8_t** out_spki, size_t* out_len, void* heap) { - DecodedCert dc; - wc_InitDecodedCert(&dc, (byte*)der, (word32)len, heap); + DecodedCert* dc = (DecodedCert*)WOLFCERT_XMALLOC(sizeof(*dc), heap); + uint8_t* buf = NULL; + int rc = WOLFCERT_OK; + int wrc; - int rc = wc_ParseCert(&dc, is_csr ? CERTREQ_TYPE : CERT_TYPE, NO_VERIFY, NULL); - if (rc != 0) { - wc_FreeDecodedCert(&dc); - return WOLFCERT_ERR_PARSE; - } + if (dc == NULL) + return WOLFCERT_ERR_MEMORY; - if (dc.publicKey == NULL || dc.pubKeySize == 0) { - wc_FreeDecodedCert(&dc); - return WOLFCERT_ERR_PARSE; - } + wc_InitDecodedCert(dc, (byte*)der, (word32)len, heap); + wrc = wc_ParseCert(dc, is_csr ? CERTREQ_TYPE : CERT_TYPE, NO_VERIFY, NULL); + if (wrc == MEMORY_E) + rc = WOLFCERT_ERR_MEMORY; + else if (wrc != 0 || dc->publicKey == NULL || dc->pubKeySize == 0) + rc = WOLFCERT_ERR_PARSE; - uint8_t* buf = (uint8_t*)WOLFCERT_XMALLOC(dc.pubKeySize, heap); - if (buf == NULL) { - wc_FreeDecodedCert(&dc); - return WOLFCERT_ERR_MEMORY; + if (rc == WOLFCERT_OK) { + buf = (uint8_t*)WOLFCERT_XMALLOC(dc->pubKeySize, heap); + if (buf == NULL) + rc = WOLFCERT_ERR_MEMORY; } - memcpy(buf, dc.publicKey, dc.pubKeySize); - *out_spki = buf; - *out_len = dc.pubKeySize; - wc_FreeDecodedCert(&dc); + if (rc == WOLFCERT_OK) { + memcpy(buf, dc->publicKey, dc->pubKeySize); + *out_spki = buf; + *out_len = dc->pubKeySize; + } - return WOLFCERT_OK; + wc_FreeDecodedCert(dc); + WOLFCERT_XFREE(dc, heap); + return rc; } /* Total length (tag + length octets + value) of the DER SEQUENCE at `p`, or 0 @@ -1333,28 +1378,35 @@ WOLFCERT_TEST_VIS int wolfcert_scep_verify_rep_signer(const uint8_t* signer_cert size_t ca_spki_len = 0; size_t off = 0; size_t clen; - int matched = 0; + int erc; + int rc; if (signer_cert == NULL || ca_bundle == NULL) return WOLFCERT_ERR_AUTH; - if (wolfcert_extract_spki(signer_cert, signer_cert_len, 0, - &signer_spki, &signer_spki_len, heap) != WOLFCERT_OK) - return WOLFCERT_ERR_AUTH; + rc = wolfcert_extract_spki(signer_cert, signer_cert_len, 0, + &signer_spki, &signer_spki_len, heap); + if (rc != WOLFCERT_OK) + return rc == WOLFCERT_ERR_MEMORY ? rc : WOLFCERT_ERR_AUTH; /* RFC 8894: the CertRep is signed by the CA or its RA. Accept the signer * if it shares a public key with any certificate in the trusted GetCACert * bundle (one or more concatenated DER certs). */ - while (off < ca_bundle_len && !matched) { + rc = WOLFCERT_ERR_AUTH; + while (off < ca_bundle_len && rc == WOLFCERT_ERR_AUTH) { clen = der_seq_len(ca_bundle + off, ca_bundle_len - off); if (clen == 0) break; - if (wolfcert_extract_spki(ca_bundle + off, clen, 0, - &ca_spki, &ca_spki_len, heap) == WOLFCERT_OK) { + erc = wolfcert_extract_spki(ca_bundle + off, clen, 0, + &ca_spki, &ca_spki_len, heap); + if (erc == WOLFCERT_ERR_MEMORY) { + rc = erc; + } + else if (erc == WOLFCERT_OK) { if (ca_spki_len == signer_spki_len && memcmp(ca_spki, signer_spki, signer_spki_len) == 0) - matched = 1; + rc = WOLFCERT_OK; WOLFCERT_XFREE(ca_spki, heap); ca_spki = NULL; } @@ -1363,7 +1415,7 @@ WOLFCERT_TEST_VIS int wolfcert_scep_verify_rep_signer(const uint8_t* signer_cert } WOLFCERT_XFREE(signer_spki, heap); - return matched ? WOLFCERT_OK : WOLFCERT_ERR_AUTH; + return rc; } WOLFCERT_TEST_VIS int wolfcert_scep_check_cert_rep(const char* msg_type, diff --git a/tests/unit/test_scep_msg.c b/tests/unit/test_scep_msg.c index b7a16da..6b870ce 100644 --- a/tests/unit/test_scep_msg.c +++ b/tests/unit/test_scep_msg.c @@ -789,6 +789,103 @@ static int check_result_defined(const char* what, int rc, const WolfCertScepResu return 0; } +/* OPENSSL_EXTRA's GetCertName reports a failed X509_NAME allocation as + * ASN_PARSE_E, indistinguishable from a bad certificate. */ +#if defined(USE_WOLFSSL_MEMORY) && !defined(WOLFSSL_STATIC_MEMORY) && \ + !defined(WOLFSSL_DEBUG_MEMORY) && !defined(OPENSSL_EXTRA) && \ + !defined(OPENSSL_EXTRA_X509_SMALL) +#define TEST_ALLOC_FAILURES +#endif + +#ifdef TEST_ALLOC_FAILURES +/* Allocations left before failing_malloc() returns NULL; -1 never fails. */ +static int g_allocs_left = -1; + +static void* failing_malloc(size_t sz) +{ + if (g_allocs_left == 0) + return NULL; + if (g_allocs_left > 0) + g_allocs_left--; + return malloc(sz); +} + +static void failing_free(void* ptr) +{ + free(ptr); +} + +static void* failing_realloc(void* ptr, size_t sz) +{ + if (g_allocs_left == 0) + return NULL; + if (g_allocs_left > 0) + g_allocs_left--; + return realloc(ptr, sz); +} + +/* Failing each allocation in turn must give WOLFCERT_ERR_MEMORY, never a + * "not found" that GetCert would blame on the CA. */ +static int pem_has_cert_reports_oom(const char* pem, size_t pem_len, + const DecodedCert* lc) +{ + wolfSSL_Malloc_cb mf; + wolfSSL_Free_cb ff; + wolfSSL_Realloc_cb rf; + int fails; + int rc = 0; + + REQUIRE(wolfSSL_GetAllocators(&mf, &ff, &rf) == 0); + for (fails = 0; rc != 1; fails++) { + g_allocs_left = fails; + REQUIRE(wolfSSL_SetAllocators(failing_malloc, failing_free, + failing_realloc) == 0); + rc = wolfcert_scep_pem_has_cert((const uint8_t*)pem, pem_len, + lc->issuerRaw, + (size_t)lc->issuerRawLen, lc->serial, + (size_t)lc->serialSz, NULL); + REQUIRE(wolfSSL_SetAllocators(mf, ff, rf) == 0); + g_allocs_left = -1; + if (rc != 1 && rc != WOLFCERT_ERR_MEMORY) { + fprintf(stderr, "FAIL pem_has_cert with allocation %d failing: " + "%d\n", fails, rc); + return 1; + } + } + REQUIRE(fails > 1); + return 0; +} + +/* Running out of memory while checking a CertRep signer is not a forgery. */ +static int rep_signer_reports_oom(const uint8_t* signer, size_t signer_len, + const uint8_t* bundle, size_t bundle_len) +{ + wolfSSL_Malloc_cb mf; + wolfSSL_Free_cb ff; + wolfSSL_Realloc_cb rf; + int fails; + int rc = WOLFCERT_ERR_MEMORY; + + REQUIRE(wolfSSL_GetAllocators(&mf, &ff, &rf) == 0); + for (fails = 0; rc != WOLFCERT_OK; fails++) { + g_allocs_left = fails; + REQUIRE(wolfSSL_SetAllocators(failing_malloc, failing_free, + failing_realloc) == 0); + rc = wolfcert_scep_verify_rep_signer(signer, signer_len, bundle, + bundle_len, NULL); + REQUIRE(wolfSSL_SetAllocators(mf, ff, rf) == 0); + g_allocs_left = -1; + if (rc != WOLFCERT_OK && rc != WOLFCERT_ERR_MEMORY) { + fprintf(stderr, "FAIL verify_rep_signer with allocation %d " + "failing: %d\n", fails, rc); + return 1; + } + } + REQUIRE(fails > 1); + return 0; +} +#endif + /* The GetCert response check walks a PEM bundle: a certificate that will not * parse is skipped, so one ahead of the target cannot hide it. */ static int test_pem_has_cert(void) @@ -821,6 +918,9 @@ static int test_pem_has_cert(void) REQUIRE(wolfcert_scep_pem_has_cert((const uint8_t*)pem, (size_t)n, lc.issuerRaw, (size_t)lc.issuerRawLen, lc.serial, (size_t)lc.serialSz, NULL) == 1); +#ifdef TEST_ALLOC_FAILURES + REQUIRE(pem_has_cert_reports_oom(pem, (size_t)n, &lc) == 0); +#endif /* Behind an unparseable entry it must still be found. */ REQUIRE((size_t)n + sizeof(JUNK) < sizeof(bundle)); @@ -1592,6 +1692,9 @@ static int test_signer_matches_any_bundle_cert(void) REQUIRE(wolfcert_scep_verify_rep_signer(u_der, u_len, bundle, bundle_len, NULL) != WOLFCERT_OK); +#ifdef TEST_ALLOC_FAILURES + REQUIRE(rep_signer_reports_oom(b_der, b_len, bundle, bundle_len) == 0); +#endif free(bundle); free(a_der); diff --git a/wolfcert/scep.h b/wolfcert/scep.h index 231fbf5..cb7a23c 100644 --- a/wolfcert/scep.h +++ b/wolfcert/scep.h @@ -237,7 +237,8 @@ WOLFCERT_API int wolfcert_scep_get_cert_initial(const WolfCertServerCfg* srv, * trip that verifies but answers with no certificate carrying the requested * issuer and serial is the one outcome that is neither: it returns * WOLFCERT_ERR_PROTOCOL and leaves out->status UNSET, so a substituted bundle - * is never mistaken for the certificate that was asked for. + * is never mistaken for the certificate that was asked for. Running out of + * memory while searching the reply returns WOLFCERT_ERR_MEMORY instead. * * Despite the name, wolfcert_scep_get_cert_initial is not an "initial" variant * of this call: it is messageType 20, polling a pending enrollment. There is no From ccf37a35c4e2294068890e8b6114d1c7e8746ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Wed, 30 Sep 2026 09:46:59 +0200 Subject: [PATCH 10/10] client, csr: refuse a renewal's identity meta before the rekey wolfcert_client_reenroll() generated the new key before wolfcert_csr_build_ex() refused a meta that sets subject_dn or a SAN. A caller reusing its enroll meta for a rekeying renewal therefore paid for a full key generation, seconds for RSA-4096 or ML-DSA on an MCU or a key created on a CryptoCb device, only to get WOLFCERT_ERR_BAD_ARG. The check now runs with the other argument checks, through a helper that wolfcert_csr_build_ex() shares. The TLS integration test proves the order with a rekey config whose generation would fail UNSUPPORTED. test_csr also gains boundary cases for the two size refusals in copy_cert_identity(), which no test reached and which also guard a memcpy: a Subject whose contents are sizeof(CertName) - 1 bytes is copied byte for byte and one byte more is refused, and a SAN of sizeof(altNames) bytes is copied and one byte more is refused. The certificates are built by hand, since wc_MakeCert() cannot produce the oversized ones. --- src/client.c | 3 + src/csr.c | 12 +- src/internal.h | 3 + tests/integration/test_est_tls_roundtrip.c | 8 + tests/unit/test_csr.c | 221 +++++++++++++++++++++ 5 files changed, 243 insertions(+), 4 deletions(-) diff --git a/src/client.c b/src/client.c index a358a87..f898d8d 100644 --- a/src/client.c +++ b/src/client.c @@ -232,6 +232,9 @@ int wolfcert_client_reenroll(WolfCertClient* client, const WolfCertServerCfg* sr if (srv == NULL || current_cert == NULL || current_key == NULL || meta == NULL || out_key == NULL || out_cert_pem == NULL) return WOLFCERT_ERR_BAD_ARG; + if (wolfcert_csr_meta_sets_identity(meta)) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "client", + "a renewal keeps the certificate's subject and SAN"); WolfCertKey* nk = NULL; int rc; diff --git a/src/csr.c b/src/csr.c index fef603a..fb3816e 100644 --- a/src/csr.c +++ b/src/csr.c @@ -350,6 +350,13 @@ static int copy_cert_identity(Cert* cert, const uint8_t* renew_cert, return rc; } +int wolfcert_csr_meta_sets_identity(const WolfCertCertMeta* meta) +{ + return meta->subject_dn != NULL || meta->san_dns_len != 0 || + meta->san_ip_len != 0 || meta->san_uri_len != 0 || + meta->san_email_len != 0; +} + int wolfcert_csr_build(const WolfCertKey* key, const WolfCertCertMeta* meta, WolfCertBuffer* out_der) { @@ -363,10 +370,7 @@ int wolfcert_csr_build_ex(const WolfCertKey* key, const WolfCertCertMeta* meta, if (key == NULL || meta == NULL || out_der == NULL) return WOLFCERT_ERR_BAD_ARG; - if (renew_cert != NULL && - (meta->subject_dn != NULL || meta->san_dns_len != 0 || - meta->san_ip_len != 0 || meta->san_uri_len != 0 || - meta->san_email_len != 0)) + if (renew_cert != NULL && wolfcert_csr_meta_sets_identity(meta)) return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "csr", "a renewal keeps the certificate's subject and SAN"); diff --git a/src/internal.h b/src/internal.h index c1328c7..17098af 100644 --- a/src/internal.h +++ b/src/internal.h @@ -359,6 +359,9 @@ int wolfcert_transport_fd(const WolfCertTransport* t, void* conn); int wolfcert_pem_cert_to_der(const uint8_t* pem, size_t pem_len, WolfCertBuffer* out_der, void* heap); +/* 1 when meta sets a Subject or SAN. */ +int wolfcert_csr_meta_sets_identity(const WolfCertCertMeta* meta); + /* GeneralNames of the subjectAltName in dc, or *san NULL when there is * none. Returns WOLFCERT_OK or WOLFCERT_ERR_PARSE. */ WOLFCERT_TEST_VIS int wolfcert_find_san(const DecodedCert* dc, diff --git a/tests/integration/test_est_tls_roundtrip.c b/tests/integration/test_est_tls_roundtrip.c index 94b56b9..5434175 100644 --- a/tests/integration/test_est_tls_roundtrip.c +++ b/tests/integration/test_est_tls_roundtrip.c @@ -152,6 +152,7 @@ static int test_client_reenroll_keeps_identity(const uint8_t* tls_cert, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, }; + WolfCertKeyCfg no_kcfg = { .type = (WolfCertKeyType)0x7f }; WolfCertServer* srv = NULL; pthread_t tid; char url[128]; @@ -194,6 +195,13 @@ static int test_client_reenroll_keeps_identity(const uint8_t* tls_cert, cur_key, NULL, &meta, &out_key, &issued) == WOLFCERT_ERR_BAD_ARG); + /* Refused before key generation, which would fail UNSUPPORTED here. */ + memset(&meta, 0, sizeof(meta)); + meta.subject_dn = "CN=impostor"; + REQUIRE(wolfcert_client_reenroll(NULL, &cli, cur_cert, cur_cert_len, + cur_key, &no_kcfg, &meta, &out_key, + &issued) == WOLFCERT_ERR_BAD_ARG); + #ifdef KEEP_PEER_CERT /* The caller's callback still runs but cannot rename the cert. */ memset(&meta, 0, sizeof(meta)); diff --git a/tests/unit/test_csr.c b/tests/unit/test_csr.c index 01007a1..c771e6c 100644 --- a/tests/unit/test_csr.c +++ b/tests/unit/test_csr.c @@ -30,6 +30,7 @@ #include #include +#include #include #define REQUIRE(cond) \ @@ -632,6 +633,224 @@ static int csr_carries_large_san(void) wolfcert_key_free(key); return 0; } + +/* Write a DER tag and length; returns the header size. */ +static word32 der_hdr(byte* out, byte tag, word32 len) +{ + word32 n = 0; + + out[n++] = tag; + if (len >= 0x100) { + out[n++] = 0x82; + out[n++] = (byte)(len >> 8); + } + else if (len >= 0x80) { + out[n++] = 0x81; + } + out[n++] = (byte)len; + return n; +} + +/* Wrap the len bytes at buf in a tag and length; returns the new length. */ +static word32 der_wrap(byte* buf, word32 len, byte tag) +{ + byte hdr[4]; + word32 n = der_hdr(hdr, tag, len); + + memmove(buf + n, buf, len); + memcpy(buf, hdr, n); + return n + len; +} + +#define OU_VALUE_MAX 250 + +/* Encoded size of one OU RDN holding v value bytes. */ +static word32 ou_rdn_len(word32 v) +{ + byte hdr[4]; + word32 atv = 5 + der_hdr(hdr, ASN_UTF8STRING, v) + v; + word32 seq = der_hdr(hdr, ASN_SEQUENCE | ASN_CONSTRUCTED, atv) + atv; + + return der_hdr(hdr, ASN_SET | ASN_CONSTRUCTED, seq) + seq; +} + +/* The value size of the one OU RDN that encodes to exactly len bytes, or 0. */ +static word32 ou_value_for(word32 len) +{ + word32 v; + + for (v = 1; v <= OU_VALUE_MAX; v++) { + if (ou_rdn_len(v) == len) + return v; + } + return 0; +} + +/* OU RDNs whose encoding is exactly len (>= 12) bytes, in few enough RDNs for + * the 16 an OPENSSL_EXTRA wolfSSL parses. */ +static word32 put_ou_rdns(byte* out, word32 len) +{ + static const byte ou_oid[] = { 0x06, 0x03, 0x55, 0x04, 0x0b }; + word32 p = 0; + word32 start; + word32 rest; + word32 v; + word32 w; + + while (p < len) { + v = ou_value_for(len - p); + for (w = OU_VALUE_MAX; v == 0 && w > 0; w--) { + if (ou_rdn_len(w) + 12 <= len - p) { + rest = len - p - ou_rdn_len(w); + if (rest > ou_rdn_len(OU_VALUE_MAX) || ou_value_for(rest) != 0) + v = w; + } + } + start = p; + memcpy(out + p, ou_oid, sizeof(ou_oid)); + p += (word32)sizeof(ou_oid); + p += der_hdr(out + p, ASN_UTF8STRING, v); + memset(out + p, 'a', v); + p += v; + p = start + der_wrap(out + start, p - start, + ASN_SEQUENCE | ASN_CONSTRUCTED); + p = start + der_wrap(out + start, p - start, ASN_SET | ASN_CONSTRUCTED); + } + return p; +} + +/* A GeneralNames of dNSNames that is exactly len (>= 260) bytes; returns the + * number of names. */ +static int put_dns_names(byte* out, word32 len) +{ + word32 p = der_hdr(out, ASN_SEQUENCE | ASN_CONSTRUCTED, len - 4); + word32 v; + int n = 0; + + while (p < len) { + v = (len - p >= 105) ? 100 : len - p - 2; + out[p++] = ASN_CONTEXT_SPECIFIC | ASN_DNS_TYPE; + out[p++] = (byte)v; + memset(out + p, 'a', v); + p += v; + n++; + } + return n; +} + +/* An unsigned cert whose Subject contents are subj_len bytes and whose SAN, + * if san_len is not 0, is san_len bytes; *names gets the SAN's name count. */ +static int make_sized_cert(const WolfCertKey* key, word32 subj_len, + word32 san_len, byte* out, int* names) +{ + static const byte tbs_head[] = { + 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x01, 0x01, + 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, + 0x30, 0x0c, 0x31, 0x0a, 0x30, 0x08, 0x06, 0x03, 0x55, 0x04, 0x03, + 0x0c, 0x01, 'i', + 0x30, 0x1e, + 0x17, 0x0d, '2', '6', '0', '1', '0', '1', '0', '0', '0', '0', '0', '0', + 'Z', + 0x17, 0x0d, '3', '6', '0', '1', '0', '1', '0', '0', '0', '0', '0', '0', + 'Z' + }; + static const byte sig_tail[] = { + 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, + 0x03, 0x09, 0x00, 0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01 + }; + static const byte san_oid[] = { 0x06, 0x03, 0x55, 0x1d, 0x11 }; + word32 p = (word32)sizeof(tbs_head); + word32 start; + word32 ext; + int spki; + + memcpy(out, tbs_head, sizeof(tbs_head)); + start = p; + p += put_ou_rdns(out + p, subj_len); + p = start + der_wrap(out + start, p - start, + ASN_SEQUENCE | ASN_CONSTRUCTED); + spki = wc_EccPublicKeyToDer((ecc_key*)key->impl, out + p, 512, 1); + if (spki <= 0) + return -1; + p += (word32)spki; + + if (san_len != 0) { + ext = p; + memcpy(out + p, san_oid, sizeof(san_oid)); + p += (word32)sizeof(san_oid); + *names = put_dns_names(out + p, san_len); + p += der_wrap(out + p, san_len, ASN_OCTET_STRING); + p = ext + der_wrap(out + ext, p - ext, ASN_SEQUENCE | ASN_CONSTRUCTED); + p = ext + der_wrap(out + ext, p - ext, ASN_SEQUENCE | ASN_CONSTRUCTED); + p = ext + der_wrap(out + ext, p - ext, + ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED | 3); + } + + p = der_wrap(out, p, ASN_SEQUENCE | ASN_CONSTRUCTED); + memcpy(out + p, sig_tail, sizeof(sig_tail)); + p += (word32)sizeof(sig_tail); + return (int)der_wrap(out, p, ASN_SEQUENCE | ASN_CONSTRUCTED); +} + +/* The largest Subject and SAN a renewal carries, and one byte past each. */ +static int renewal_size_limits(void) +{ + const word32 name_max = (word32)sizeof(((Cert*)NULL)->sbjRaw) - 1; + const word32 san_max = (word32)sizeof(((Cert*)NULL)->altNames); + WolfCertKeyCfg cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertKey* key = NULL; + WolfCertCertMeta meta = { 0 }; + WolfCertBuffer csr = { 0 }; + DecodedCert cc; + DecodedCert dc; + byte* cert; + int cert_len; + int names = 0; + int same; + + REQUIRE(wolfcert_key_generate(&cfg, &key) == WOLFCERT_OK); + REQUIRE((cert = (byte*)malloc(name_max + san_max + 1024)) != NULL); + + cert_len = make_sized_cert(key, name_max, 0, cert, &names); + REQUIRE(cert_len > 0); + REQUIRE(wolfcert_csr_build_ex(key, &meta, cert, (size_t)cert_len, &csr) + == WOLFCERT_OK); + wc_InitDecodedCert(&cc, cert, (word32)cert_len, NULL); + wc_InitDecodedCert(&dc, csr.data, (word32)csr.len, NULL); + same = wc_ParseCert(&cc, CERT_TYPE, NO_VERIFY, NULL) == 0 && + wc_ParseCert(&dc, CERTREQ_TYPE, NO_VERIFY, NULL) == 0 && + cc.subjectRawLen == (int)name_max && + dc.subjectRawLen == (int)name_max && + memcmp(cc.subjectRaw, dc.subjectRaw, name_max) == 0; + wc_FreeDecodedCert(&dc); + wc_FreeDecodedCert(&cc); + REQUIRE(same); + wolfcert_buffer_free(&csr); + + cert_len = make_sized_cert(key, name_max + 1, 0, cert, &names); + REQUIRE(cert_len > 0); + REQUIRE(wolfcert_csr_build_ex(key, &meta, cert, (size_t)cert_len, &csr) + == WOLFCERT_ERR_UNSUPPORTED); + REQUIRE(csr.data == NULL); + + cert_len = make_sized_cert(key, 12, san_max, cert, &names); + REQUIRE(cert_len > 0); + REQUIRE(wolfcert_csr_build_ex(key, &meta, cert, (size_t)cert_len, &csr) + == WOLFCERT_OK); + REQUIRE(count_csr_sans(&csr) == names); + wolfcert_buffer_free(&csr); + + cert_len = make_sized_cert(key, 12, san_max + 1, cert, &names); + REQUIRE(cert_len > 0); + REQUIRE(wolfcert_csr_build_ex(key, &meta, cert, (size_t)cert_len, &csr) + == WOLFCERT_ERR_UNSUPPORTED); + REQUIRE(csr.data == NULL); + + free(cert); + wolfcert_key_free(key); + return 0; +} #endif int main(void) @@ -662,6 +881,8 @@ int main(void) return 1; if (csr_carries_large_san()) return 1; + if (renewal_size_limits()) + return 1; if (build_and_reparse(WOLFCERT_KEY_ECC, 256)) return 1; #endif