Harden chain depth bounds and parser input validation#10209
Harden chain depth bounds and parser input validation#10209ColtonWilley wants to merge 6 commits into
Conversation
9a2f68d to
3304644
Compare
|
089287c to
d84c824
Compare
|
retest this please |
Enforce MAX_CHAIN_DEPTH limits in OCSP chain processing (SendCertificateStatus, ProcessChainOCSPRequest), certificate loading (ProcessUserChain), and TLS 1.3 certificate sending (SendTls13Certificate). Add idx bounds checks to chain accessors in ssl.c. Harden SNI extension parser (TLSX_SNI_GetFromBuffer) with length checks preventing buffer overreads on malformed ClientHello. Fix off-by-one in TLSX_CSR_Free where <= should be < since csr->requests is a count, not a max index. Add remaining-buffer bounds checks to PKCS7 decoders: DecodeEnvelopedData, DecodeAuthEnvelopedData (encryptedContentSz and authTagSz), DecodeEncryptedData, SignedData null signature tag, and PwriKek_KeyUnWrap cekLen validation.
test_dtls13_oversized_cert_chain (added upstream in 1653ecd) loaded a >9-cert chain to exercise SendTls13Certificate's word16 overflow guard. This PR now rejects over-MAX_CHAIN_DEPTH chains at load in ProcessUserChain, so the chain never reaches the send path; the load-time rejection is covered by test_wolfSSL_CTX_use_certificate_chain_buffer_max_depth. The word16 send guard remains as defense-in-depth (now only reachable via large PQC chains).
d84c824 to
359fb7e
Compare
There was a problem hiding this comment.
Pull request overview
This PR hardens certificate-chain depth handling and improves parser input validation to prevent out-of-bounds access and buffer overreads, with accompanying API/regression tests.
Changes:
- Enforces
MAX_CHAIN_DEPTHin certificate chain loading/sending paths and adds index bounds checks to chain accessors. - Hardens SNI extension parsing with stricter length validation and fixes an off-by-one in OCSP CSR cleanup.
- Adds PKCS7 AuthEnvelopedData truncation regression coverage and updates API tests; removes a DTLS oversized-chain test.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
wolfcrypt/src/pkcs7.c |
Adds remaining-buffer bounds checks during AuthEnvelopedData parsing. |
src/tls.c |
Tightens SNI extension length validation; fixes OCSP CSR free-loop off-by-one. |
src/ssl.c |
Adds idx bounds checks for peer-chain accessors (length/cert/X509). |
src/ssl_load.c |
Enforces MAX_CHAIN_DEPTH during user chain parsing/loading. |
src/internal.c |
Bounds OCSP chain processing loops by MAX_CHAIN_DEPTH. |
src/tls13.c |
Rejects TLS 1.3 certificate sends when chain count exceeds MAX_CHAIN_DEPTH. |
tests/api/test_pkcs7.h |
Declares/registers new PKCS7 truncated AuthEnvelopedData test. |
tests/api/test_pkcs7.c |
Adds new truncation regression test for AuthEnvelopedData decode. |
tests/api/test_dtls.h |
Removes declaration/registration of oversized-chain DTLS 1.3 test. |
tests/api/test_dtls.c |
Removes implementation of oversized-chain DTLS 1.3 test. |
tests/api.c |
Adds tests for chain accessor idx bounds and max-depth chain buffer rejection. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
julek-wolfssl
left a comment
There was a problem hiding this comment.
Copilot has some valid points
- pkcs7.c: wrap the AuthEnvelopedData encryptedContentSz bounds check entirely in #ifdef NO_PKCS7_STREAM so the default streaming build carries no empty/no-op branch (behavior unchanged). - test_pkcs7.c: assert the encoded size is >32 so the encSz-32 and encSz-1 truncations can't underflow.
Master's test reorg (c674cec) moved test_dtls13_oversized_cert_chain into the new tests/api/test_dtls13.c. This branch removes that test, so resolve by taking master's test_dtls.{c,h} and re-applying the removal in test_dtls13.{c,h}.
Agreed, I have pushed updates to address the issues. |
|
Jenkins retest this please |
dgarske
left a comment
There was a problem hiding this comment.
Skoll Code Review
Scan type: reviewOverall recommendation: COMMENT
Findings: 4 total — 4 posted, 0 skipped
4 finding(s) posted as inline comments (see file-level comments below)
Posted findings
- [Medium] MAX_CHAIN_DEPTH check placement can reject a valid maximum-depth chain that has trailing data —
src/ssl_load.c:328-333 - [Low] Max-depth load test overshoots the limit and omits the exact-boundary success case —
tests/api.c:3866-3915 - [Info] PR description lists pkcs7.c changes that are not present in the diff —
wolfcrypt/src/pkcs7.c:14971-14975,15239-15243 - [Info] SendTls13Certificate depth guard is now defense-in-depth only and its direct test path was removed —
src/tls13.c:9523-9526
Review generated by Skoll
| while ((ret == 0) && (consumed < sz)) { | ||
| DerBuffer* part = NULL; | ||
|
|
||
| /* Enforce maximum chain depth. */ |
There was a problem hiding this comment.
🟠 [Medium] MAX_CHAIN_DEPTH check placement can reject a valid maximum-depth chain that has trailing data
The new depth check if (cnt >= MAX_CHAIN_DEPTH) is placed at the TOP of the parse loop, before DataToDerBuffer. The loop is commonly entered one extra time after the last real certificate to consume trailing data (trailing newline/whitespace), and that extra entry is normally handled gracefully by the ret == ASN_NO_PEM_HEADER && gotOne branch further down. For a chain of EXACTLY MAX_CHAIN_DEPTH (default 9) valid certificates followed by any unconsumed trailing bytes, cnt equals MAX_CHAIN_DEPTH when the loop re-enters, so the new check fires FIRST and returns MAX_CHAIN_ERROR instead of letting the existing ASN_NO_PEM_HEADER path terminate cleanly with ret = 0. This regresses a valid maximum-length chain to a load failure. Note certs/server-cert.pem in this repo is itself a 2-cert PEM, so multi-cert PEM with trailing bytes is the norm; the boundary is real, just narrow (requires hitting exactly the max depth). The PR's own new test uses MAX_CHAIN_DEPTH + 1 copies (overshoots) and never exercises the exact boundary.
Fix: Move the cnt >= MAX_CHAIN_DEPTH check so it only rejects when an additional real certificate is actually present (i.e., after DataToDerBuffer and after the ASN_NO_PEM_HEADER && gotOne trailing-data handling). Add a test that loads exactly MAX_CHAIN_DEPTH certificates (with a trailing newline) and asserts success, in addition to the over-limit case.
| { | ||
| WOLFSSL_X509* x = NULL; | ||
| ExpectNull(x = wolfSSL_get_chain_X509(chain, -1)); | ||
| if (x != NULL) { wolfSSL_X509_free(x); x = NULL; } |
There was a problem hiding this comment.
🔵 [Low] Max-depth load test overshoots the limit and omits the exact-boundary success case
The test sets nCerts = MAX_CHAIN_DEPTH + 1 copies of svrCertFile, but server-cert.pem contains 2 certificates, so the buffer actually holds ~2*(MAX_CHAIN_DEPTH+1) certs. It therefore only proves that a grossly-oversized chain is rejected and does not pin the boundary. It is missing the complementary positive case: a chain of exactly MAX_CHAIN_DEPTH certificates must still load successfully. That gap is exactly what would let the placement issue in ProcessUserChain (separate finding) slip through. The const int nCerts = MAX_CHAIN_DEPTH + 1; naming/comment is also misleading given each file is multi-cert.
Fix: Add a positive boundary test (exactly MAX_CHAIN_DEPTH certs -> success) and clarify the per-file cert count in the comment.
| } | ||
| } | ||
|
|
||
| #ifdef NO_PKCS7_STREAM |
There was a problem hiding this comment.
⚪ [Info] PR description lists pkcs7.c changes that are not present in the diff
The PR body claims bounds checks were added to DecodeEnvelopedData, DecodeEncryptedData (two sites), SignedData null signature tag, and PwriKek_KeyUnWrap (cekLen off-by-4). The actual pkcs7.c diff only contains the two wc_PKCS7_DecodeAuthEnvelopedData checks (encryptedContentSz and authTagSz, both under #ifdef NO_PKCS7_STREAM). Either the commit is incomplete or the description is stale. The two checks that ARE present are correct: idx is advanced only past validated length bytes so pkiMsgSz - idx cannot underflow, and the authTagSz check is a genuine fix (the existing authTagSz > sizeof(authTag) check only guards the destination, not the pkiMsg source read at the subsequent XMEMCPY).
Fix: Confirm whether the other PKCS7 decoder hardening described in the PR body was intended for this commit. If not, trim the description to match the diff so reviewers/changelog stay accurate.
| } | ||
| /* Certificate Data */ | ||
| certSz = ssl->buffers.certificate->length; | ||
| if (ssl->buffers.certChainCnt > MAX_CHAIN_DEPTH) { |
There was a problem hiding this comment.
⚪ [Info] SendTls13Certificate depth guard is now defense-in-depth only and its direct test path was removed
With the new ProcessUserChain limit, an oversized chain can no longer be loaded (it fails with MAX_CHAIN_ERROR at load time), so the new ssl->buffers.certChainCnt > MAX_CHAIN_DEPTH guard inside SendTls13Certificate is effectively unreachable via the normal load path -- it is a reasonable belt-and-suspenders check, but untested. The PR simultaneously deletes test_dtls13_oversized_cert_chain, which was the only test that drove an oversized chain through the actual TLS1.3 certificate-send path. The deletion itself is justified (that test expected WOLFSSL_SUCCESS on load, which is no longer possible), and the test_dtls13.h macro list trailing-comma update is correct. Net effect is reduced direct coverage of the send-side guard.
Fix: Optional: keep the guard (cheap and defensive) but note in a comment that it is unreachable under normal loading, or add a unit test that sets ssl->buffers.certChainCnt directly to exercise the MAX_CHAIN_ERROR return.
Summary
MAX_CHAIN_DEPTHlimits in OCSP chain processing (SendCertificateStatus,ProcessChainOCSPRequest), certificate loading (ProcessUserChain), and TLS 1.3 certificate sending (SendTls13Certificate). Addidxbounds checks to chain accessors inssl.c.TLSX_SNI_GetFromBuffer) with length validation preventing buffer overreads on malformed ClientHello. Require exactlistLen == extLen - OPAQUE16_LENmatch to prevent extension boundary misalignment.TLSX_CSR_Free(<=to<) —csr->requestsis a count, and a full-depth chain with leaf OCSP could push the free loop one past therequest.ocsp[]array.DecodeEnvelopedDataandDecodeAuthEnvelopedData(encryptedContentSz,authTagSz),DecodeEncryptedData(two sites),SignedDatanull signature tag, andPwriKek_KeyUnWrapcekLenoff-by-4.