Fix truncation and noDegenerate bypasses in wc_PKCS7_VerifySignedData - #11017
Fix truncation and noDegenerate bypasses in wc_PKCS7_VerifySignedData#11017miyazakh wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request fixes two PKCS#7 SignedData verification bypasses in wc_PKCS7_VerifySignedData() that could otherwise return success and populate pkcs7->content from unauthenticated data, and adds regression tests covering both bypass scenarios (streaming and non-streaming builds).
Changes:
- Enforce
noDegeneratewhensignerInfosis truly degenerate (empty) even ifdigestAlgorithmsis non-empty. - Remove the streaming stage-3 “< 6 bytes remaining” success early-exit by bounding lookahead to the bytes actually remaining.
- Add new API tests for minimal valid degenerate bundles and for truncated/missing
signerInfoscases; broaden an existing truncation test to run in both build modes.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| wolfcrypt/src/pkcs7.c | Tightens degenerate/noDegenerate enforcement and adjusts streaming footer lookahead to prevent truncation being treated as successful completion. |
| tests/api/test_pkcs7.h | Adds declarations/registration for new SignedData verification regression tests. |
| tests/api/test_pkcs7.c | Adds new regression tests for minimal degenerate success, truncation rejection, and non-empty digestAlgorithms + empty signerInfos rejection; updates an existing truncation test gating. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pkcs7->stream->expected = (ASN_TAG_SZ + MAX_LENGTH_SZ) * 2; | ||
| if (pkcs7->stream->totalRd < pkcs7->stream->maxLen && | ||
| pkcs7->stream->expected > | ||
| pkcs7->stream->maxLen - pkcs7->stream->totalRd) { | ||
| pkcs7->stream->expected = | ||
| pkcs7->stream->maxLen - pkcs7->stream->totalRd; | ||
| } |
There was a problem hiding this comment.
Confirmed. Reproduced with NoSignerInfosTag (bundle truncated exactly at the outer SignedData length, no signerInfos at all): both single-shot and streaming calls returned WC_PKCS7_WANT_READ_E (-270) instead of a deterministic error, and streaming consumed all available bytes without resolving.
Fixed by capping pkcs7->stream->expected to 0 when totalRd >= maxLen, instead of skipping the cap in that case, so stage 4's existing idx >= pkiMsg2Sz bounds check now fires immediately and returns BUFFER_E.
Also strengthened test_wc_PKCS7_VerifySignedData_NoSignerInfosTag to assert the return code is not WC_PKCS7_WANT_READ_E (the prior ExpectIntNE(ret, 0) check passed even on the stall), and added a streaming byte-at-a-time variant of the same case.
Fixed in 8777c2b.
|
Hoist ret/idx declarations to the top of the HAVE_PKCS7 block instead of
a standalone { ... } scope, per wolfSSL C coding style.
Stage 3's cap on stream->expected only applied while totalRd < maxLen. When a bundle is truncated exactly at the outer SignedData length (0 bytes remain, totalRd == maxLen), the cap did not fire, leaving expected at a fixed non-zero value. Stage 4 then requested bytes that would never arrive, returning WC_PKCS7_WANT_READ_E instead of a deterministic parse error, in both single-shot and streaming calls. Cap expected to 0 when totalRd >= maxLen so stage 4's existing bounds check fires immediately with BUFFER_E. Strengthen test_wc_PKCS7_VerifySignedData_NoSignerInfosTag to assert the return code is not WC_PKCS7_WANT_READ_E, and add a streaming byte-at-a-time variant, since the previous ExpectIntNE(ret, 0) check did not catch the stall.
The totalRd >= maxLen zero-cap added in 8777c2b also fired for BER indefinite-length bundles, where maxLen is only a running estimate set from the first available bytes and grows as parsing progresses, not a hard bound on the message size. That made totalRd catch up to maxLen mid-stream on legitimate encode/verify round trips, turning a normal WC_PKCS7_WANT_READ_E into a premature BUFFER_E/ASN_PARSE_E and breaking --enable-all make check (test_wc_PKCS7_EncodeSignedData, test_wc_PKCS7_VerifySignedData_RSA, test_wc_PKCS7_BER). Only apply the zero-cap when pkcs7->stream->indefLen is not set, so it's limited to definite-length bundles where maxLen is authoritative.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #11017
Scan targets checked: wolfcrypt-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
| ExpectNotNull(pkcs7 = wc_PKCS7_New(HEAP_HINT, testDevId)); | ||
| ExpectIntEQ(wc_PKCS7_Init(pkcs7, HEAP_HINT, INVALID_DEVID), 0); | ||
| ExpectIntEQ(wc_PKCS7_InitWithCert(pkcs7, NULL, 0), 0); | ||
| ExpectIntNE(wc_PKCS7_VerifySignedData(pkcs7, der, derSz), 0); |
There was a problem hiding this comment.
🟠 [Medium] TruncSignerInfosTag test does not catch WC_PKCS7_WANT_READ_E stall · Weak or missing assertions
The 31 01 signerInfos header (declared length 1, 0 bytes available) makes the default streaming build's single-shot call return WC_PKCS7_WANT_READ_E (a stall, not a rejection), yet ExpectIntNE(ret, 0) passes since -270 != 0.
Fix: Add ExpectIntNE(ret, WC_NO_ERR_TRACE(WC_PKCS7_WANT_READ_E)) as done in the sibling NoSignerInfosTag test.
Summary
wc_PKCS7_VerifySignedData()had two bugs that let signature verification be silently bypassed (F-7302). Both returned0(success) withpkcs7->contentpopulated from unauthenticated data.NO_PKCS7_STREAMundefined): a bundle truncated 0-5 bytes short of a completesignerInfosSET was accepted as a valid degenerate (certs-only) end.(f7302)digestAlgorithmsand emptysignerInfosbypassedwc_PKCS7_AllowDegenerate(pkcs7, 0)entirely.Bug 1: truncation accepted as a valid degenerate end
WC_PKCS7_VERIFY_STAGE3treated any input with fewer than 6 bytes remaining after the content as a finished, successful parse, unable to distinguish a genuine minimal degenerate end (31 00) from a truncated bundle. Dates back to3cad0bfe5, which left the gap with an acknowledging comment.Fix: stage 3 no longer returns early. It caps
pkcs7->stream->expectedto the bytes actually remaining, then lets stage 4/5/6's existing bounds checks run normally, so truncated input fails there instead of being silently accepted.Bug 2: noDegenerate bypass via non-empty digestAlgorithms
wc_PKCS7_ParseSignerInfo()only enforcednoDegeneratewhen the whole remaining buffer was empty, never when the accurately-computeddegenerateflag (from the realsignerInfosSET) was true. No truncation needed to trigger this.Fix: one line, broadened the guard to also check
degenerate == 1:Tests added
test_wc_PKCS7_VerifySignedData_DegenerateMinimal-- minimal legit degenerate bundle still succeeds.test_wc_PKCS7_VerifySignedData_TruncSignerInfosTag/NoSignerInfosTag-- truncated/missingsignerInfosrejected.test_wc_PKCS7_VerifySignedData_DegenerateNonEmptyDigestAlgos-- bug 2 case returnsPKCS7_NO_SIGNER_E.NO_PKCS7_STREAM-only gate ontest_wc_PKCS7_VerifySignedData_TruncCertSetTag; it now passes in streaming mode too.Testing
Verified in isolated
git worktreebuilds (--enable-pkcs7 --enable-opensslextra, plusCFLAGS=-DNO_PKCS7_STREAMfor the non-streaming build), since the primary tree doesn't enable PKCS7. Each bug: negative test failed pre-fix, passed post-fix. No regressions inpkcs7_sd,wolfcrypt/test/testwolfcrypt, or the fulltests/unit.testsuite, in both build modes.