Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unchecked credential-copy failures can bypass EST authentication, and Retry-After and SAN validation contain correctness gaps.
Review effort: Balanced
Findings: 1
Open (4)
What changed in this PR
Hardens the EST test server and HTTP/SCEP clients against authentication, validation, resource, and protocol-handling issues.
Changes:
- Enforces EST authentication and reenrollment identity checks.
- Validates CSRs before approval and improves HTTP error/Retry-After handling.
- Reduces SCEP stack usage and expands regression coverage.
| File | Description |
|---|---|
wolfcert/server.h |
Documents anonymous EST enrollment controls. |
wolfcert/http.h |
Documents HTTP-date Retry-After support. |
wolfcert/est.h |
Updates pending-response documentation. |
tests/unit/test_transport.c |
Tests Retry-After date parsing. |
tests/unit/test_server_ca_store.c |
Opts test server into anonymous enrollment. |
tests/unit/test_scep_msg.c |
Tests SCEP allocation failures. |
tests/unit/test_csr.c |
Tests renewal identity size limits. |
tests/interop/openssl_pkcs7_xcheck.sh |
Enables anonymous EST interop. |
tests/interop/est_libest.sh |
Enables anonymous libest interop. |
tests/interop/est_globalsign.sh |
Enables anonymous GlobalSign interop. |
tests/integration/test_server_stop_idle.c |
Updates EST server configuration. |
tests/integration/test_est_tls_roundtrip.c |
Tests authentication and early metadata rejection. |
tests/integration/test_est_pha_roundtrip.c |
Updates PHA failure expectations. |
tests/integration/test_est_pending_roundtrip.c |
Tests invalid CSR rejection before queuing. |
tests/integration/test_est_mtls_roundtrip.c |
Tests reenrollment identity enforcement. |
tests/integration/test_est_csr_attrs_roundtrip.c |
Updates anonymous test configuration. |
tests/integration/test_est_csr_attrs_enforce.c |
Tests error bodies and status headers. |
tests/integration/test_est_csr_attrs_apply_roundtrip.c |
Updates anonymous test configuration. |
tests/integration/test_est_chunked_robustness.c |
Tests HEAD and duplicate authorization handling. |
tests/integration/cli_proto_scoping.sh |
Tests new server CLI validation. |
tests/CMakeLists.txt |
Exposes internal headers to transport tests. |
src/server.c |
Rejects unsafe EST server configurations. |
src/scep/scep_msg.c |
Heap-allocates large certificate parsers. |
src/scep/scep_client.c |
Propagates GetCert allocation failures. |
src/internal.h |
Adds internal identity and result contracts. |
src/internal.c |
Preserves PEM conversion OOM errors. |
src/http.c |
Parses HTTP-date Retry-After values. |
src/est/est_server.c |
Hardens enrollment, errors, and approval handling. |
src/csr.c |
Shares renewal identity validation. |
src/client.c |
Rejects identity metadata before rekeying. |
README.md |
Documents secure EST server startup. |
Makefile.am |
Adds internal include path for tests. |
examples/certs/README.md |
Updates EST authentication examples. |
examples/certs/gen-certs.sh |
Updates generated certificate guidance. |
docs/ARCHITECTURE.md |
Documents reenrollment and 403 behavior. |
cli/wolfcert_server.c |
Adds --est-allow-anonymous. |
CLAUDE.md |
Updates build and server guidance. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
@wolfSSL-Fenrir-bot review balanced |
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #45
Scan targets checked: wolfcert-src, wolfcert-bugs
Coverage: 15 of 20 in-scope changed file(s) opened by the reviewer; not opened: tests/integration/test_est_csr_attrs_apply_roundtrip.c, tests/integration/test_server_stop_idle.c, tests/unit/test_csr.c, wolfcert/est.h, wolfcert/http.h
Fenrir result: Approved ✅
No new issues found in the changed files.
Advisory only — this automated result does not count as a GitHub approval.
Review tier: Balanced
yosuke-wolfssl
left a comment
There was a problem hiding this comment.
The fixes look right to me. The branch builds clean with -Wall -Wextra -Werror, and ctest passes 30/30 against a KEEP_PEER_CERT wolfSSL, so the reenroll cases ran.
Main points, inline below:
- Reenroll SAN check: no test reaches a SAN-content mismatch, and the parsed-list comparison misses some GeneralName types.
- Retry-After HTTP-date: a device whose clock is not set yet waits a full day.
est_allow_anonymous_enrollis inserted in the middle ofWolfCertServerCfgSrv.send_*helpers: the three new ones can become one.- Wording: "epoch" and
delta-seconds. - Duplicated contracts: a few comments restate a contract the header already owns. The copies of the Retry-After rules have already drifted apart.
Not on diff lines:
wolfcert/est.h: the section header still showsRetry-After: <delta>, and theWolfCertEstResult.retry_after_secfield comment still says "0 when the server did not sendRetry-After". Both predate the HTTP-date parsing.wolfcert/scep.h: thewolfcert_scep_get_cert()doc namesWOLFCERT_ERR_PROTOCOLas the only error after a verified round trip.WOLFCERT_ERR_MEMORYfromwolfcert_scep_pem_has_cert()can now arrive there too.- When
wolfcert_extract_spki()runs out of memory, the SCEP client still reports "CertRep is not signed by the CA/RA certificate". That is the same misreport this PR fixes inwolfcert_scep_pem_has_cert(). parse_request()matches header names by prefix, so a request carrying anAuthorization-Fooheader and anAuthorizationheader now gets 400. The prefix matching is old; the 400 is new.- The requirement that
/simplereenrollneeds a client CA and aKEEP_PEER_CERTwolfSSL is now written in six places:wolfcert/server.h:51, thewolfcert-serverhelp text,README.md(twice, at lines 75 and 93-94),docs/ARCHITECTURE.md:141andCLAUDE.md:46. The copies have already drifted:README.md:94says reenroll "works only with --tls-client-ca" and leaves outKEEP_PEER_CERT.ARCHITECTURE.mdgives only the 403 for a server with no client CA, not the 500 for a wolfSSL withoutKEEP_PEER_CERT.CLAUDE.md:46nearly repeatsREADME.md:75and could point to it instead.
- Test cases worth adding:
- a non-empty Basic user with an empty password
- an enroll over
serve_fdthat the guard should let through (Basic or the anonymous opt-in) /simplereenrollon a post-handshake-auth server- an approval re-POST with different base64 line wrapping, which the DER-keyed queue now promises to match
|
Thanks! All addressed, including the off-diff points:
All four suggested tests are added, the PHA one with a raw TLS client. |
yosuke-wolfssl
left a comment
There was a problem hiding this comment.
Thanks, this addresses everything from the last round. The branch builds clean with -Wall -Wextra -Werror and ctest passes 30/30. With the SAN byte comparison in reenroll_identity_check() disabled, the new "one dNSName changed" case fails, so the comparison is covered now.
A few small things inline, none blocking:
wolfcert_find_san()could take theDecodedCertdirectly.- A missing client certificate on a resumed TLS session is reported as out of memory.
hdr_is()could reusewolfcert_ascii_ncasecmp().- The
/simplereenrollrequirements sit on an unrelated field inserver.h.
Nits:
- The comment at
src/est/est_server.c:1059is 89 columns, which came from my wording last round. Could you wrap it? copy_csr_san()returnsWOLFCERT_ERR_BAD_ARGfor a SAN larger thanCert.altNames, whilecopy_cert_identity()returnsWOLFCERT_ERR_UNSUPPORTEDfor the same limit.
|
|
||
| /* subjectAltName value in DecodedCert.extensions, which carry a [3] wrapper | ||
| * unless is_csr; *san stays NULL when there is none. */ | ||
| WOLFCERT_TEST_VIS int wolfcert_find_san(const byte* ext, int ext_sz, |
There was a problem hiding this comment.
Every caller passes dc->extensions, dc->extensionsSz from a DecodedCert it has already parsed, and wolfSSL sets dc->isCSR when it parses a CSR (that field is under WOLFSSL_CERT_REQ, which wolfCert already requires). Taking the DecodedCert removes two arguments and the bare 0/1 at the call sites:
/* 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);Call sites then read wolfcert_find_san(&cc, &csan, &csan_len).
The suggested comment also fixes two gaps in the current one: it doesn't name the return codes, and "value" doesn't say that it means the OCTET STRING contents, i.e. the GeneralNames SEQUENCE.
Optional: the test CA and the EST server now depend on a helper that lives in csr.c, the client's CSR builder. src/internal.c would be a more neutral home.
There was a problem hiding this comment.
Done: it takes the DecodedCert and reads isCSR, with your comment, and moved to internal.c.
| if (peer_der == NULL || peer_len <= 0) { | ||
| if (peer != NULL) | ||
| wolfSSL_FreeX509(peer); | ||
| /* A client CA means the handshake or PHA already required a cert. */ |
There was a problem hiding this comment.
The premise of this comment doesn't hold on a resumed TLS session. A resumed handshake exchanges no client certificate, and wolfSSL_get_peer_certificate() copies ssl->peerCert only when the handshake filled it in. It falls back to the session's chain only when wolfSSL is built with SESSION_CERTS. So a third-party client that resumes on reconnect, for example after a 202, gets a 500 logged as out of memory.
The request is still refused, so this is about the diagnostic. A code that doesn't claim out of memory would be accurate, and WOLFCERT_ERR_TLS still maps to 500 in handler_enroll():
/* 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");There was a problem hiding this comment.
Good catch. It's WOLFCERT_ERR_TLS now, with your wording.
| return 0; | ||
| } | ||
|
|
||
| /* 1 when the header line is field `name`, which a colon ends with no space. */ |
There was a problem hiding this comment.
strncasecmp() here is locale-dependent, and it is POSIX rather than C89 (MSVC lacks it). wolfCert already has wolfcert_ascii_ncasecmp() in src/http.c for exactly this case, and find_header() there applies the same rule: name, then ':' immediately after it.
Two related points:
parse_request()computeshcandhdr_is()guaranteesline[n] == ':', yet each branch below still callsmemchr(line, ':', llen)again.- The comment could be plainer:
/* 1 when the line's field name is `name`, followed directly by ':'. */
Possible follow-up rather than this PR: src/scep/scep_server.c:234 and :239 still prefix-match (llen > 14 && strncasecmp(line, "Content-Length", 14)), so Content-Length-Foo: 99999 sets the body length there. One shared internal helper could serve both servers.
There was a problem hiding this comment.
Switched to wolfcert_ascii_ncasecmp(), and so are the chunked/close value checks next to it. The branches use hc instead of a second memchr(), and I took your comment. I'll track the SCEP server's prefix match as a follow-up.
| /* Heap hint for server-internal allocations. */ | ||
| void* heap; | ||
|
|
||
| /* Lets EST start with neither Basic nor tls_client_ca_pem. /simplereenroll |
There was a problem hiding this comment.
"/simplereenroll needs tls_client_ca_pem (else 403) and KEEP_PEER_CERT (else 500)" doesn't relate to anonymous enrollment. A caller setting up mTLS wouldn't look for it on this field, and docs/ARCHITECTURE.md now points readers here for the reenroll requirements. The tls_client_ca_pem paragraph at the top of the TLS block (around line 57) seems like the natural home, and this comment can then stay with what the field does:
/* Lets EST start with neither Basic nor tls_client_ca_pem. */There was a problem hiding this comment.
Moved to the tls_client_ca_pem paragraph; ARCHITECTURE.md points there now.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
|
Thanks! Everything inline is addressed, plus both nits: the comment at |


Problem
A batch of Fenrir findings against the in-tree EST test server and the HTTP client as well as some issues found along:
/simplereenrollissued whatever Subject and SAN the CSR carried, so a client authenticated over mTLS as one identity could renew as another, contrary to RFC 7030 §4.2.2./simpleenrollissued a certificate for any CSR, and the README quick start ran the server in exactly that mode.est_require_approval, the server answered 202 before decoding or verifying the CSR. Arbitrary bytes got a pending response, and eight of them filled the queue so real clients got 503./csrattrscarriedContent-Length: 0, and no 401 carriedWWW-Authenticate.Authorizationheader orphaned the previous copy, so an unauthenticated client could leak close to a header buffer per connection.retry_after_secat 0, so a 202 told the caller to re-POST at once.wolfcert_client_reenroll()generated a rekey's new key before refusing ametathat sets the Subject or SAN, and no test reached the two size limits on the Subject and SAN it copies, which also guard amemcpy.DecodedCert(about 2.3 KiB) on the stack, one of them two at once, while the client runs on MCU task stacks.Fix
ca: issue the CSR's subjectAltName verbatim: the test CA rebuilt the issued SAN from wolfSSL's per-type alt-name lists, which lost the CSR's entry order and criticality. It now copies the CSR's SAN extension value byte for byte with its criticality, and, as before, refuses an otherName, x400Address, directoryName or ediPartyName, throughwolfcert_find_san(): the renewal CSR'sfind_san(), moved fromcsr.ctointernal.c, now takes theDecodedCertand uses itsisCSRto read a CSR's unwrapped extensions.est: bind simplereenroll to the certificate being renewed: for a reenroll,handler_enroll()compares the CSR with the TLS client cert. The CSR must verify, and its raw Subject and raw SAN extension value must both match byte for byte, with the same SAN criticality, otherwise 400. The raw comparison covers every GeneralName type, including those wolfSSL does not parse into its alt-name lists. With no client cert to renew, the server answers 403; on a client-CA server a missing cert is not the client's fault (a resumed session withoutSESSION_CERTShas none), so it is a 500 reported as a TLS error. A wolfSSL withoutKEEP_PEER_CERTcan't read the peer cert: an mTLS server then answers 500, and one with no client CA answers 403.est: send a plaintext reason with every server error response:send_error()emitstext/plainand a one-line reason at every error site.est: fix 204 and 401 response headers: every response goes through onesend_response(), which dropsContent-Lengthfor a 204 and sends only the headers for a NULL body.HEAD /cacertsandHEAD /csrattrsget the GET handler's headers without the content, and a HEAD 404 carries GET'sContent-Lengthwithout the body. A failed Basic check answers 401 withWWW-Authenticate: Basic realm="estrealm". Client-cert failures (PHA not completed, reenroll without a peer cert) now answer 403, since no HTTP challenge can satisfy them. The EST client maps 401 and 403 to the sameWOLFCERT_ERR_AUTH.est: refuse to start an EST server with no client authentication:wolfcert_server_start()returnsWOLFCERT_ERR_BAD_ARGfor EST unlesshttp_basic_user,tls_client_ca_pemor the newest_allow_anonymous_enroll(added at the end ofWolfCertServerCfgSrv, so existing members keep their offsets) is set. Empty Basic credentials are rejected too, and a failed copy of any configured string fails the start withWOLFCERT_ERR_MEMORYinstead of leaving the Basic user NULL (read as no authentication).wolfcert_server_serve_fd()runs no TLS, so enrollment there refuses with 403 unless Basic or the anonymous opt-in is configured.wolfcert-servergains--est-allow-anonymousand rejects an empty--basic.Validate the CSR before parking it in the EST approval queue: the approval gate now runs after base64 decoding, the reenroll identity check, the CSR-attributes policy and a self-signature check (for a reenroll, the identity check already verified it). The pending key is the SHA-256 of the decoded DER, so a retry with different line wrapping still matches.Parse the HTTP-date form of Retry-After: both parsers shareparse_retry_after(), which accepts IMF-fixdate, RFC 850 and asctime, and caps the delay at 86400 s. It measures againstwc_Time(), so a port's clock override applies. RFC 850 two-digit years resolve with the RFC 9110 §5.6.7 50-year rule, counted in calendar years. Delay-seconds must be digits alone apart from trailing whitespace; the oldstrtol()read120junkas 120. UnderNO_ASN_TIME, or whilewc_Time()reads before 2026 (a device clock not yet set), the date form yields 0 rather than a full day's wait.Reject EST requests carrying more than one Authorization header: a second occurrence fails the parse with 400. Header names are now matched exactly up to their colon, soAuthorization-Foois not mistaken forAuthorization, norContent-Length-FooforContent-Length, and a field with whitespace before its colon gets 400 (RFC 9112 §5.1) instead of being skipped.scep: heap-allocate DecodedCert on the client paths:pem_has_cert,issuer_and_subject,issuer_and_serial,self_signed_rsaandextract_spkiallocate from the heap hint. Running out of memory anywhere in the GetCert bundle search, or in the CertRep signer check, returnsWOLFCERT_ERR_MEMORYinstead of reporting the certificate as missing or the CertRep as unsigned. Server-only code keeps its stackDecodedCerts.client, csr: refuse a renewal's identity meta before the rekey: the check runs with the other argument checks, before any key generation, through a helperwolfcert_csr_build_ex()shares.test_csrpins both size limits from each side.Closes F-8034, F-8035, F-8036, F-8038, F-8039, F-8047.
Behaviour changes for callers
est_allow_anonymous_enroll(CLI--est-allow-anonymous) to keep open enrollment. The tests, interop scripts and README quick start now opt in explicitly./simplereenrollon the test server needs a client CA (else 403) and aKEEP_PEER_CERTwolfSSL (else 500), as thetls_client_ca_pemcomment inserver.hstates.HEAD /cacertsandHEAD /csrattrsnow answer 200 and 204 instead of 404.wolfcert_client_reenroll()does that since client, csr: copy the renewed cert's subject and SAN into reenroll CSRs #44; callers ofwolfcert_est_simple_reenroll_exbuild the CSR themselves and must match it byte for byte, which a CSR rebuilt from the same names does for a cert the test CA issued.Tests
est_mtls_roundtrip: a reenroll with a changed CN or an added SAN is refused. A server-issued multi-SAN cert renews, but not with one dNSName changed or its SAN marked critical. A non-CSR body gets 400. A mismatched reenroll in approval mode gets 400 and is never parked. The reenroll cases skip on a wolfSSL withoutKEEP_PEER_CERT, where one case checks the mTLS server answers 500.est_csr_attrs_enforce:text/plainerror bodies, the 204 headers, the 401 challenge and the reenroll 403.est_pha_roundtrip: a raw TLS client renews its cert with/simplereenrollover post-handshake auth.est_tls_roundtrip: refused start with no client auth, empty Basic credentials and a Basic user with an empty password. Onserve_fd, an enroll is refused with only a client CA and let through to the CA with Basic credentials or the anonymous opt-in. The client reenroll cases from client, csr: copy the renewed cert's subject and SAN into reenroll CSRs #44 now run against their own server that trusts the cert being renewed, so the client's copied identity passes the server's comparison end to end, and a rekey with an identity-settingmetais refused before key generation.csrunit: a Subject ofsizeof(CertName) - 1bytes and a SAN ofsizeof(altNames)bytes renew, one byte more of either is refused.est_pending_roundtrip: a non-CSR body and a CSR with a broken signature are rejected instead of parked, and a re-POST of a parked CSR with different base64 line wrapping is issued.est_chunked_robustness: duplicateAuthorizationgets 400 whileAuthorization-Foonext toAuthorizationdoes not,Content-Length : 0gets 400, and HEAD to an unknown path,/cacertsand/csrattrsgets exactly GET's headers and no body.transportunit: all three HTTP-date layouts, the RFC 850 century window on both sides of exactly 50 years, the 86400 cap, 14 malformed dates, malformed delay-seconds, and a date against a clock still near 1970.cli_proto_scoping.sh:wolfcert-server --proto estrefuses to start without client auth, gets past that check with--est-allow-anonymous, and rejects an empty--basic.scep_msgunit: failing each allocation of a GetCert bundle search, or of a CertRep signer check, in turn givesWOLFCERT_ERR_MEMORY, never "not found" or "not signed". Skipped underOPENSSL_EXTRA, whoseGetCertNamereports a failedX509_NAMEallocation asASN_PARSE_E.server_ca_storeunit: a CSR whose SAN holds a dNSName, an rfc822Name and an iPAddress is issued with an identical SAN, and a directoryName SAN is refused. Failing each allocation ofwolfcert_server_start()once; any start that still succeeds kept its Basic credentials and challenge password.Verification
Against wolfSSL master (5.9.4):
-Wall -Wextra -Wshadow -Werroron every target.ctest30/30, also on thefull-opensslextraandfull-allCI configs, under ASan+UBSan, and through the autotoolsmake checkwith no warnings.-DKEEP_PEER_CERT: 30/30, warning-free, with the reenroll cases skipped except the 500 check.WOLFSSL_NO_MALLOCconfig: 11/11, including the 16 KB SAN renewal.leaks --atExit: 0 leaks ontest_scep_msg,test_server_ca_store,test_transport,test_est_chunked_robustness,test_est_pending_roundtrip,test_est_pha_roundtrip,test_est_mtls_roundtripandtest_est_tls_roundtrip.serve_fdguard, HEAD without a body, exact header names, the signer-check OOM mapping and the pending queue keyed on the decoded CSR.tests/interop/.