Commit Graph
31315 Commits
Author SHA1 Message Date
Yosuke Shimizu f6bd188eee Declare QUIC record length from bytes remaining 2026-08-05 14:12:26 +09:00
David Garske d6708600a2 Merge pull request #11027 from Frauschi/fenrir_2
Fixes for OCSP stapling, cert manager, and certificate_status_request_v2 handling
2026-08-04 15:54:59 -07:00
David Garske 684e06df00 Merge pull request #10991 from padelsbach/ccache-init-seed-settings
CI: set ccache path so settings are saved on initial seed
2026-08-04 15:54:14 -07:00
Tobias Frauenschläger 039d689809 Merge pull request #10975 from aidangarske/x509-tiny-ci
Move WOLFSSL_X509_TINY test to the unit test suite and run
2026-08-04 23:28:52 +02:00
Aidan Garske 82cf3d8947 Add x509 tiny certificate test coverage 2026-08-04 12:21:25 -07:00
Paul Adelsbach 1c9ebe1af6 PR feedback: use instead of duplicating path 2026-08-04 12:06:11 -07:00
David Garske e100f72548 Merge pull request #10938 from yosuke-wolfssl/fix/f_6767
Avoid writing into caller ikm buffer in wc_Tls13_HKDF_Extract
2026-08-04 10:43:24 -07:00
David Garske 5d5199b01e Merge pull request #10937 from embhorn/zd22160
Fix PKCS7 SignerIdentifier SKID to implicit [0] tagging
2026-08-04 10:33:15 -07:00
David Garske 3656306dd4 Merge pull request #10930 from julek-wolfssl/fenrir-wolfcrypt-tls-fixes
wolfCrypt and TLS 1.3 correctness fixes (F-1376, F-2653, F-1972, F-4593)
2026-08-04 10:31:44 -07:00
David Garske 7e5610debf Merge pull request #10911 from rizlik/der_import_trusted
wolfssl: expose trusted argument in ed25519/ed448 der export
2026-08-04 10:29:42 -07:00
David Garske 0fb71c572f Merge pull request #10949 from SparkiDev/aes_asm_gcm_tables_fixup
AES asm: Add GCM 8-bit tabl, fixes
2026-08-04 09:48:30 -07:00
David Garske d1a683d862 Merge pull request #11047 from danielinux/mcdc-part5-fixes
Bugfixes from MCDC part 5 campaign
2026-08-04 09:45:50 -07:00
Sean Parkinson 03e9107df4 AES asm: Add GCM 8-bit table, fixes
Added assembly to do 8-bit-table GCM_gmult_len.
Wired it into aes.c and wired small to use C code.
Fixed guards around assembly.
2026-08-04 17:48:14 +10:00
Daniele Lacamera 40f191717c wc_port: bound the dst scan in wc_strlcat and return the attempted length
wc_strlcat measured dst with XSTRLEN(), which keeps reading until it finds
a NUL regardless of dstSize. When dst holds no NUL within dstSize -- the
case strlcat(3) explicitly bounds to prevent security problems in
incorrect code -- that read runs off the end of the buffer. Under
AddressSanitizer an unterminated 16-byte dst with dstSize 8 is a
stack-buffer-overflow READ of size 17.

Scan for the end of dst without passing dstSize. If no NUL is found the
length of dst is taken to be dstSize, nothing is appended and dst is left
un-terminated because there is no room for the NUL, matching strlcat(3).

The return value is now the total length attempted in every case: the
initial length of dst plus the length of src. The dstSize == 0 case
returns the length of src rather than 0, since nothing can be appended and
that is still the length the call tried to create.

Verified against the OpenBSD implementation over a matrix of dst values,
src values and destination sizes, including dst buffers containing no NUL
at all: 196 cases, identical return values and identical destination
buffers throughout.
2026-08-04 07:23:09 +02:00
Daniele Lacamera 24a207826d wc_port: return the source length from wc_strlcpy
wc_strlcpy returned the number of bytes it copied, so a truncating call
reported dstSize - 1 and a caller could not tell a truncated copy from an
exact fit. strlcpy(3) instead returns the length of src -- the length the
copy would have needed -- which is what makes the documented truncation
check (ret >= dstSize) work.

The doxygen comment in doc/dox_comments/header_files/types.h already
specifies "Length of source string", so this brings the implementation in
line with its own documentation rather than changing the contract.

Walking the remainder of src also gives wc_strlcat the length it attempted
in its truncating path, matching strlcat(3). No in-tree caller uses either
return value.

Verified against the OpenBSD implementation over a matrix of source
strings and destination sizes (including dstSize 0): identical return
values and identical destination buffers in all cases.
2026-08-04 07:16:43 +02:00
David Garske 5b22fa901e Merge pull request #10954 from aidangarske/pkcs11-ecc-pubkey-label
Set label and ID on PKCS11 EC public key
2026-08-03 11:36:43 -07:00
David Garske 47cdd2c3aa Merge pull request #11026 from kojo1/sig_min_hash_test
Honor WC_SIG_MIN_HASH_TYPE override in signature decision test
2026-08-03 11:17:12 -07:00
John Safranek 396a160954 Merge pull request #10984 from embhorn/zd22162
Enforce RFC 5746 renegotiation_info in TLS 1.2 client by default
2026-08-03 10:11:25 -07:00
JacobBarthelmeh b93f965a11 Merge pull request #11024 from yosuke-wolfssl/feat/JPdoc
[JA] Add missing Japanese dox_comments for 7 API groups
2026-08-03 10:57:53 -06:00
Daniel Pouzzner b04e07f8a4 Merge pull request #11036 from Frauschi/fix-os-check-run-length
OS check CI fix
2026-08-03 11:35:47 -05:00
David Garske 80631786fc Merge pull request #10958 from Frauschi/fenrir
wolfCrypt security hardening, portability fixes, and negative test coverage
2026-08-03 09:13:36 -07:00
David Garske 74ef67f5b0 Merge pull request #11018 from Frauschi/fenrir_3
Security and correctness fixes, plus a DTLS 1.3 scheduled work API
2026-08-03 09:13:21 -07:00
David Garske bd74881516 Merge pull request #10794 from JacobBarthelmeh/changelog
update description of ML-KEM AVX2 issue
2026-08-03 09:06:41 -07:00
Tobias Frauenschläger 91769d99f6 CI: raise the os-check linux timeout and allow seeding on demand
The ccache that keeps this workflow fast is written only by the weekday
seed job, which runs on a schedule against the default branch. Nothing
had seeded it since 2026-07-24, because the workflow itself was failing
to load for that whole stretch, and the Actions cache evicts entries
untouched for seven days. The first run after the fix therefore reported
"Cache not found for input keys" on all four shards.

That cold run measured ~102 thread-minutes per shard, or 24-27 minutes
of wall including checkout, deps and autogen, against a 30 minute
timeout. Three minutes of headroom on the slowest shard is not enough,
and a shard killed by the timeout presents as a test failure rather than
as a cold cache. Raise it to 40. The comment above it claimed ~68
thread-minutes and ~20 minutes of wall, which the measurement above
contradicts, so replace it with the measured figures. macOS and Windows
are left alone: they came in at 11.6 minutes against 45 and 2.0 against
6.

Add workflow_dispatch so the seed can be run on demand rather than
waiting up to a day for the next cron, which matters exactly in the
situation above, where every PR run stays cold until something refills
the cache.

Adding the trigger alone would not have been enough. The seed behaviour
hangs off `github.event_name == 'schedule'` in five places (CCACHE_RECACHE,
--build-only on linux and macOS, the cache save, and skipping the Windows
job), so a manual run would have gone through the full test path and
saved nothing. All five now treat a dispatch as a seed as well.

The condition is written against `github.event.inputs.seed` rather than
`inputs.seed`, because the `inputs` context is only documented as
available on workflow_dispatch and workflow_call, whereas `github.event`
always exists. That yields strings, so it is compared explicitly rather
than for truthiness, where the string 'false' would read as true. It
tests `!= 'false'` and not `== 'true'` so that a dispatch which sends no
input at all still seeds: a declared default is not reliably reflected
into `github.event.inputs`, and keying on the positive would have made
`gh workflow run` quietly skip the seeding it was invoked to do.

Comments are brought in line with all of this, including one that was
already wrong before the trigger existed: the macOS ccache step is
read-only purely on pull_request, so every non-PR run writes that cache,
where the note claimed only the seed did. The two platforms seed
differently in a second way as well - linux sets CCACHE_RECACHE and so
rebuilds from scratch, macOS never does and only accumulates deltas.
Neither behaviour is changed here, but both are now written down at the
top of the file rather than left to be rediscovered from a surprising
cache.
2026-08-03 17:05:29 +02:00
Tobias Frauenschläger 641c39dbf3 CI: catch workflows that GitHub silently fails to load
A workflow file GitHub cannot load does not fail loudly. Its runs end
within 0s with zero jobs, no logs, no annotations and no check runs, and
the workflow re-registers under its bare path instead of its `name:`
field. Among the few hundred checks on a PR that reads as unrelated
flake, so the coverage just disappears: os-check.yml was in this state on
master for ten days in July 2026 before anyone noticed, and no open PR
reported a problem the whole time.

Add two guards.

Pre-merge, check-workflows.py measures every `run:` step against
GitHub's 21000 character cap and fails the build past it, with a warning
from 18000 so a growing step is noticed while there is still runway.
Sizes come from the parsed YAML, which is what the Actions service
evaluates, so block-scalar indentation needs no guessing. It runs from
check-source-text.yml over every workflow and composite action rather
than only PR-changed files: the cap applies per file, the whole sweep
takes well under a second, and a file can be pushed over the line by a
change elsewhere in the PR. Note that this cap is enforced by the
service and not by the workflow schema, so neither a YAML validator nor
actionlint reports it.

Post-merge, workflow-health.yml runs check-workflow-health.py daily and
looks for the symptom rather than any particular cause, so a workflow
that stops loading for a reason nobody anticipated is still caught. Two
signals: an active workflow whose registered name equals its path, and a
completed run that failed with zero jobs (prefiltered on
created_at == updated_at, so only a handful need a jobs lookup). Against
the live repository the first signal flags os-check.yml and nothing else
across 107 workflows, and reports clean on wolfTPM and wolfMQTT. It
exits 1 on a finding and 2 when the check could not be carried out at
all, because a missing token and a broken workflow call for different
responses.

Findings go into a single reused issue rather than another red check
that would blend into the noise: the body is rewritten on each run, a
comment is posted only when the set of affected workflows changes, and
the issue closes itself once everything loads again.

Finding that issue reliably turned out to be the fiddly part, and the
approach here is the one that survived testing against a live
repository. The issue is identified by both a dedicated label and its
title, and looked up through the REST issues endpoint. Both halves of
that identity matter: the label alone is a normal repository label that
anyone can apply, and an adopted issue has its body overwritten and is
then closed, so matching on the label alone would destroy a mislabelled
issue. Searching by title instead is unusable, because search ignores
--state and returns closed issues, which had the monitor re-closing an
already closed issue on every clean run. `gh issue list` reads a GraphQL
replica that can lag. The REST endpoint lags too, by about 2.4s for a
newly created issue, so the lookup re-checks a few times before
concluding nothing is open - without that, consecutive runs each open a
duplicate, and a clean run right after an outage fails to close the
issue it just opened.

Verified against the commit that caused the outage: check-workflows.py
fails on acff4d62a (21813 characters) and passes on its parent
f5ace71dd, which it flags at 20662 - already inside the warning band,
338 characters short of breaking. The full issue lifecycle (open,
repeat with no comment, comment on change, close, stay closed, reopen a
fresh issue for a new outage) was exercised end to end against a live
repository.
2026-08-03 17:05:29 +02:00
Tobias Frauenschläger a6bd8c00e4 CI: move parallel-make-check config lists into .github/configs
GitHub caps a single `run:` step at 21000 characters. os-check.yml
embedded its 109-entry Linux config list as a heredoc inside that step,
and commit c00e7260b ("Add AES-GCM DEM, CryptoCb support, and devId
threading to ECIES") pushed it from 20662 to 21813 characters. Since
that merge on 2026-07-24 GitHub has refused to load the file at all:
every run of the workflow ends in failure within 0s with zero jobs, on
master and on every PR branch.

The failure is easy to miss. The run registers under the literal path
`.github/workflows/os-check.yml` rather than its `name:` field, its
check suite carries no check runs so there are no logs or annotations,
and because GitHub cannot read the file it cannot apply the `on:`
filters either - hence the master push runs for a workflow whose push
trigger is restricted to release/**. Meanwhile `gh pr checks` still
reports hundreds of green checks from the other workflows.

Move the config lists to checked-in JSON under .github/configs/;
parallel-make-check.py already accepts the JSON path as its positional
argument. Splitting the step in two would not have been enough: the
heredoc alone was 21387 characters once de-indented.

  os-check-linux.json   109 configs
  os-check-macos.json     7 configs
  pq-all.json            31 configs
  multi-arch.json        23 configs
  smoke-test.json        10 configs

The os-check lists are the fix; the other three are preventive - pq-all
and multi-arch were the next largest run steps at 14076 and 11011
characters. The largest remaining run step is now 6285 characters.

Each list was compared object-for-object against the version it
replaces, so this is a pure relocation with no coverage change.
2026-08-03 17:05:29 +02:00
Daniel Pouzzner c3aebb970d Merge pull request #11037 from Frauschi/fix
Fix for check-source-text false-positive
2026-08-03 09:57:37 -05:00
Tobias Frauenschläger 6690e94562 Fix for check-source-text false-positive.
Reword a comment to not trigger a failure in check-source-text CI job
due to an apparently missing WC_NO_ERR_TRACE in a comment.
2026-08-03 16:28:42 +02:00
Daniele Lacamera 948b4d09c1 pkcs7: fix AuthEnvelopedData bufferPt leak in stream teardown
wc_PKCS7_ResetStream()/wc_PKCS7_FreeStream() freed stream->aad, tag,
nonce, buffer, and key but never stream->bufferPt, the encryptedContent
buffer wc_PKCS7_DecodeAuthEnvelopedData() stashes across WANT_READ
re-entries. When the decode is abandoned mid-stream (e.g. malformed
input drives the parser into a pending WANT_READ state and the caller
then calls wc_PKCS7_Free()) that allocation was never reclaimed.

Found under LeakSanitizer by feeding wc_PKCS7_DecodeAuthEnvelopedData
single-byte-corrupted and truncated copies of a valid AES128GCM
AuthEnvelopedData message built with wc_PKCS7_EncodeAuthEnvelopedData;
a corrupted length field reliably drove it into the leaking state.

Fix: free/null stream->bufferPt in wc_PKCS7_ResetStream() alongside the
other stream buffers. This also required nulling stream->bufferPt right
after wc_PKCS7_DecodeAuthEnvelopedData's own successful-decrypt path
frees the same pointer, since that path used to leave the stream
pointer dangling and rely on the caller not resetting the stream again
before it was overwritten - otherwise ResetStream would double-free it.

Verified: the same corruption/truncation sweep under
-fsanitize=address,undefined,leak now reports no leaks and no
double-frees. ./wolfcrypt/test/testwolfcrypt and
./tests/unit.test --group pkcs7 --group pkcs7_sd --group pkcs7_ed
--group pkcs7_sed --group pkcs7_cd pass on the ASan build. A full
./configure --enable-all && make && ./wolfcrypt/test/testwolfcrypt &&
./tests/unit.test --api build also passes (0 failed, 1758 passed,
338 skipped).
2026-08-03 15:36:12 +02:00
Tobias Frauenschläger c9b027d054 Merge pull request #11028 from danielinux/mcdc-eliminate-dead-code
Remove dead/unreachable code
2026-08-03 14:48:20 +02:00
Daniele Lacamera e16466fd1d tests: retarget the ed448 check_key Y-range stanzas
wc_ed448_check_key()'s Y-range check is a walk over the bytes above the 0xFE
position followed by a single compare of that byte, so the stanza aimed at the
low-byte scan and its final byte compare no longer reaches a decision.

The three stanzas now cover both surviving decisions and both of their operands:
  - every byte above the 0xFE position 0xff, that byte 0xfe: the loop runs to
    exhaustion, so its index operand goes false and the compare below is
    reached and taken.
  - the same value with one byte inside the walked range cleared: the loop
    breaks with ret == 0, covering the byte compare's true side and the
    following (ret == PUBLIC_KEY_E) guard's false side.
  - every byte 0xff, including the 0xFE position: no small-order table row
    matches, the walk finds no byte below 0xff and the 0xFE compare is false,
    so the key is rejected as out of range -- the compare's false side, and the
    one stanza whose result is exact rather than curve-decode dependent.
2026-08-03 12:33:14 +02:00
Daniele Lacamera 1ea6e9e9ba pwdbased: remove unreachable NULL check in DoPKCS12Hash
Premise:  DoPKCS12Hash is static with exactly two call sites, in the two
          mutually exclusive WC_PKCS12_PBKDF_USING_MP_API variants of
          wc_PKCS12_PBKDF_ex -- pwdbased.c:555 (buffer, Ai) and :771 (buffer, B).
Claim:    pwdbased.c:376 `if ((buffer == NULL) || (Ai == NULL))
            return BAD_FUNC_ARG;`
Proof:    four paths reach the function (2 call sites x WOLFSSL_SMALL_STACK on
          and off) and none can pass a NULL:
            Ai / B -- SMALL_STACK: XMALLOC at :479 / :741, each immediately
              followed by `if (... == NULL) return MEMORY_E;`. Otherwise a
              stack array (`byte Ai[WC_MAX_DIGEST_SIZE]` :440,
              `ALIGN8 byte B[WC_MAX_BLOCK_SIZE]` :685).
            buffer -- initialised to staticBuffer (:431 / :687) and only
              replaced by the XMALLOC at :513 / :748, each immediately followed
              by `if (buffer == NULL) { ...free...; return MEMORY_E; }`.
          Both operands are therefore invariantly false and neither
          independence pair is reachable.
Scope:    covers both WOLFSSL_SMALL_STACK settings and both
          WC_PKCS12_PBKDF_USING_MP_API arms; the whole area is inside
          HAVE_PKCS12.
Evidence: llvm-cov MC/DC records both conditions' pairs as uncovered
          (reports/pwdbased/GAPS.md rows 358:9:358:41:0 and :1, pre-drift
          numbering).

The caller obligation the guard used to restate is recorded in the function's
header comment instead.

pwdbased.c is inside the FIPS module boundary for v6 and v7+. Released FIPS
flavours pin pwdbased.c to a tag and are unaffected; fips-dev/fips-ready build
from master and recompute the in-core hash.

Compiler cross-check: gcc -O2 emits byte-identical code for this file before
and after this commit -- the optimiser had already folded the removed
condition, independently confirming it was dead.
2026-08-03 12:33:14 +02:00
Daniele Lacamera 52b79db0a7 kdf: remove tautological ret operand in the KDA-KDF CMAC loop
Premise:  kdf.c:1609 `int ret = 0;` and kdf.c:1666 `if (ret != 0) { break; }`,
          an unconditional statement on every path through the loop body.
Claim:    kdf.c:1636 `while (ret == 0 && len_rem >= WC_AES_BLOCK_SIZE)` -- the
          `ret == 0` operand.
Proof:    first evaluation: ret is its initialiser 0. Nothing between :1609 and
          :1636 writes ret -- the four argument-check paths (:1612-1627) and the
          WOLFSSL_SMALL_STACK allocation failure (:1630) all `return` instead.
          Re-evaluations: the body contains no `continue` and no `goto`, so
          every iteration reaches :1666, which breaks out whenever ret != 0.
          Control therefore returns to the loop condition only with ret == 0:
          the operand is invariantly true and its independence pair is
          unreachable.
Scope:    the only #if inside the body is WOLFSSL_DEBUG_KDF (:1639-1642),
          containing a WOLFSSL_MSG_EX and nothing else; WOLFSSL_SMALL_STACK
          only chooses heap vs stack for cmac, above the loop.
Evidence: llvm-cov MC/DC records this condition as never false
          (reports/kdf/GAPS.md row 1616:12:1616:52:0, pre-drift numbering).

The `break` is what terminates the loop on error and is kept; ret is still
tested at :1672 and returned.

kdf.c is inside the FIPS module boundary (v5, v5-RC12, v6, v7+). Released FIPS
flavours pin kdf.c to a tag and are unaffected; fips-dev/fips-ready build from
master and recompute the in-core hash.

Compiler cross-check: gcc -O2 emits byte-identical code for this file before
and after this commit -- the optimiser had already folded the removed
condition, independently confirming it was dead.
2026-08-03 12:33:14 +02:00
Daniele Lacamera a17cc4ee4e ed448: remove unreachable Y == p case in wc_ed448_check_key
Premise:  ed448.c:1511 `if ((ret == 0) && ed448_is_small_order(key->p)) {
            ...; ret = PUBLIC_KEY_E; }`, and ed448_is_small_order (:245-312),
          which zeroes byte 56 of a copy of the input and memcmps it against a
          table that explicitly includes the non-canonical y = p encoding
          (:283-291: 0xff x28, 0xfe, 0xff x27, 0x00).
Claim:    ed448.c:1554 `if ((ret == PUBLIC_KEY_E) && (key->p[0] < 0xff))` --
          the `key->p[0] < 0xff` operand, whose false half is the Y == p case.
Proof:    reaching :1554 requires the preceding range walk to have found
          p[56..29] all 0xff (loop at :1533), p[28] == 0xfe (:1543) and
          p[27..1] all 0xff (loop at :1546). If p[0] were also 0xff the input
          would be exactly the y = p encoding modulo byte 56 -- which
          ed448_is_small_order masks -- so :1511 would have set
          ret = PUBLIC_KEY_E and the else-if arm containing :1554 would never
          have been entered. The operand can only ever evaluate true, so its
          independence pair is unreachable.
          With that operand fixed true, the remaining `ret == PUBLIC_KEY_E`
          operand no longer decides anything: when it is false the bottom loop
          has already set ret = 0. Both paths end with ret == 0, so the whole
          conditional collapses to `ret = 0;`.
Scope:    neither wc_ed448_check_key nor ed448_is_small_order is conditionally
          compiled, so this holds in every configuration that builds ed448.c.
Evidence: llvm-cov MC/DC records no covered pair for this condition in any
          ed448 variant.

Rejection semantics are unchanged: y = p is still refused, by
ed448_is_small_order rather than by this byte walk, and every other encoding
takes the same path as before. The table's y = p row is therefore what enforces
the upper end of the Y range; ed448_is_small_order carries a note saying so, so
the dependency is stated where the invariant is produced as well as where it is
consumed.

ed448.c is inside the FIPS module boundary. Released FIPS flavours pin ed448.c
to a tag and are unaffected; fips-dev/fips-ready build from master and
recompute the in-core hash.

With the last-byte test gone, the loop over p[27..1] can no longer affect the
outcome -- its only effects were setting ret = 0 and breaking -- so it goes too,
and the two surviving arms both yield ret = 0, collapsing to a single
`p[ED448_PUB_KEY_SIZE/2] <= 0xfe` test. The only input for which this differs
from the original is exactly the y == p encoding, which the proof below shows
never reaches here.

Proof check: gcc does not fold this (it cannot reason about the small-order
table). Machine-checked exhaustively instead: the byte walk pins 56 of the 57
public-key bytes before the final test, leaving p[0] as the only free variable,
so all 256 values were enumerated against the real ed448_is_small_order(). Two
values (p[0] = 0xff, y = p; and p[0] = 0xfe, y = p-1) are rejected early by the
table; the other 254 reach the final test and all satisfy p[0] < 0xff. Zero
counterexamples over the complete state space.

Also checked with clang's deadcode.DeadStores analyzer (the checker behind the
clang-tidy CI legs): clean on this file, with the two pre-existing sp_int.c
core.* findings unchanged from master.
2026-08-03 12:33:14 +02:00
Daniele Lacamera 7ecbd7385a ed448: remove unreachable privKeySet operand in wc_ed448_check_key
Premise:  ed448.c:1517 `if ((ret == 0) && key->privKeySet) { ... }` -- the
          sibling arm of the if/else this condition belongs to.
Claim:    ed448.c:1524 `else if ((ret == 0) && (!key->privKeySet))` -- the
          `!key->privKeySet` operand; and, as a direct consequence, the nested
          `if (ret == 0)` that opened the arm's body.
Proof:    entering the else arm means the sibling condition was false, i.e.
          (ret != 0) || (!privKeySet). && short-circuits, so the else arm's own
          `key->privKeySet` read happens only after `ret == 0` evaluated true --
          and with ret == 0 the sibling's falsity forces !privKeySet. The
          operand is therefore invariantly true. privKeySet is a plain bitfield,
          not volatile, and no statement runs between the two conditions.
          The nested `if (ret == 0)` follows from the same short-circuit: the
          arm is entered only with ret == 0 and nothing precedes the nested test.
Scope:    wc_ed448_check_key contains no #if/#ifdef, so this holds in every
          configuration that compiles ed448.c.
Evidence: llvm-cov MC/DC records this condition's pair as uncovered
          (reports/ed448/GAPS.md row 1503:14:1503:46:1, pre-drift numbering).

Reviewed with `git diff -w`: apart from the two removed lines the change is
pure de-indentation.

ed448.c is inside the FIPS module boundary. Released FIPS flavours pin ed448.c
to a tag and are unaffected; fips-dev/fips-ready build from master and
recompute the in-core hash.

Compiler cross-check: gcc -O2 emits byte-identical code for this file before
and after this commit -- the optimiser had already folded the removed
condition, independently confirming it was dead.
2026-08-03 12:33:14 +02:00
Daniele Lacamera 611cebde8b aes: remove unreachable bit lower-bound in wc_AesFeedbackCFB1
Premise:  aes.c:16444 `int bit = 7;` and the single update site inside the loop,
          aes.c:16492-16498 `bit--; if (bit < 0) { ...; bit = 7U; ... }`.
Claim:    aes.c:16507 `if (bit >= 0 && bit < 7)` -- the `bit >= 0` operand.
Proof:    bit is written nowhere else in the function, and the decrement is
          immediately normalised back to 7 whenever it would go negative, so
          0 <= bit <= 7 holds at every loop-iteration boundary. The loop's only
          other exit is the `break` taken when the block transform fails, which
          happens before the `bit--` and leaves bit unchanged (and sets ret != 0,
          so :16506 short-circuits anyway). sz == 0 is rejected at entry, so the
          zero-trip case still has bit == 7. `bit >= 0` is therefore invariantly
          true and its independence pair is unreachable.
Scope:    there is exactly one definition of wc_AesFeedbackCFB1 in the tree
          (:16438, called only from wc_AesCfb1Encrypt/Decrypt), all of it inside
          #ifndef WOLFSSL_NO_AES_CFB_1_8. The only #ifdef in the function,
          WC_AES_HAVE_PREFETCH_ARG at :16446, declares did_prefetches and
          touches neither bit nor the control flow.
Evidence: llvm-cov MC/DC records this condition's pair as uncovered
          (reports/aes/GAPS.md row 16469:13:16469:32:0, pre-drift numbering).

`bit < 7` stays and is load-bearing: when sz is a whole number of bits the loop
exits with bit == 7 and the trailing partial byte must not be written.

aes.c is inside the FIPS module boundary. Released FIPS flavours pin aes.c to a
tag and are unaffected; fips-dev/fips-ready build from master and recompute the
in-core hash.

Compiler cross-check: gcc -O2 emits byte-identical code for this file before
and after this commit -- the optimiser had already folded the removed
condition, independently confirming it was dead.
2026-08-03 12:33:14 +02:00
Daniele Lacamera 8d8b255ab1 integer: remove unreachable mp_init failure handling in mp_init_multi
Premise:  mp_init (integer.c:145-166) has exactly one non-MP_OKAY return,
          `if (a == NULL) return MP_VAL;` at :150. Everything after it is
          unconditional field initialisation ending in `return MP_OKAY;`.
Claim:    the six guards at integer.c:114, 117, 122, 127, 132, 137 of the shape
            if (x && ((res = mp_init(x)) != MP_OKAY)) { ...mp_clear...; return res; }
          -- specifically the `!= MP_OKAY` operand of each, and the mp_clear
          cascades they guard.
Proof:    the left operand of each && establishes x != NULL, which excludes
          mp_init's only failure path, so mp_init(x) == MP_OKAY and the second
          operand is invariantly false. None of the six bodies can execute, so
          the mp_clear cascades and the early `return res` are unreachable and
          the function's only possible return value is MP_OKAY.
Scope:    the sole preprocessor construct in mp_init is HAVE_WOLF_BIGINT
          (:161-163), which adds the void call wc_bigint_init. The
          `#define mp_init sp_init` in sp_int.h:1359 does not apply -- integer.c
          is compiled only when integer.h is the math backend, and integer.h
          declares mp_init as a plain prototype (:315).
Evidence: llvm-cov MC/DC records the `!= MP_OKAY` operand of all six as
          uncovered (reports/bigint-integer/GAPS.md rows 114/117/122/127/132/137,
          index 1).

The other two math backends already implement this API exactly this way, so the
resulting shape is the tree's majority convention rather than a new one:
sp_int.c's sp_init_multi (mp_init_multi under the SP backend, sp_int.h:1361)
NULL-guards each operand, calls the void _sp_init_size and returns MP_OKAY
unconditionally, and tfm.c:4390 does the same with fp_init. The call sites that
test the result -- integer.c:1089, :1233 and the rest -- are therefore already
testing a constant in every default and fastmath build.

Caveat, since this is the one proof here that rests on the callee's body rather
than a guard in the same function: mp_init is public API whose contract permits
failure, so mp_init_multi now depends on integer.c's mp_init staying
failure-free. Kept as the last commit of the non-FIPS group so it can be dropped
on its own. Note that sp_init has the same shape today -- MP_VAL for a NULL
argument, MP_OKAY otherwise -- so no backend in the tree has an init that can
fail on a non-NULL operand.

The mp_init calls themselves are kept although the XMEMSET above them already
produces the state mp_init writes: dp = NULL, used = 0, alloc = 0,
sign = MP_ZPOS (0), and under HAVE_WOLF_BIGINT wc_bigint_init only zeroes raw.

Compiler cross-check: gcc -O2 emits byte-identical code for this file before
and after this commit -- the optimiser had already folded the removed
condition, independently confirming it was dead.
2026-08-03 12:33:14 +02:00
Daniele Lacamera a2d8cf1fa6 lms: remove unreachable inited operand in wc_hss_reload_key
Premise:  wc_lms_impl.c:3593 `priv_key->inited = 0;`, sitting under the SAME
          #ifndef WOLFSSL_WC_LMS_SMALL gate as the claimed condition -- wherever
          the check is compiled, so is the reset.
Claim:    wc_lms_impl.c:3603 `if ((ret == 0) && (!priv_key->inited))` -- the
          `!priv_key->inited` operand.
Proof:    the only call between the reset and the check is
          wc_hss_expand_private_key(state, priv_key->priv, priv_raw, 0), which
          receives the priv buffer, not the key struct, and so cannot write the
          `word8 inited:1` bitfield (wc_lms.h:741). inited is therefore still 0
          and the operand is invariantly true; its independence pair is
          unreachable.
Scope:    the WOLFSSL_WC_LMS_SERIALIZE_STATE `if (pub_root != NULL)` at :3596
          only decides whether the block runs at all; it does not touch inited.
Evidence: llvm-cov MC/DC records both of this decision's conditions as
          uncovered (reports/lms/GAPS.md rows 3592:13:3592:46:0 and :1, recorded
          against wc_lms.c before the impl-file split).

The `priv_key->inited = 0;` at :3593 goes with it: it was only ever observed by
this operand, and :3620 assigns inited unconditionally on every path out.

Proof check: gcc does not fold this -- it must assume the call may
alias the bitfield. It cannot: HssPrivKey declares 'byte* priv' as a POINTER
member (wc_lms.h:726) alongside 'word8 inited:1' (wc_lms.h:741), and
wc_hss_expand_private_key receives the pointer value, not the struct address.
Writes through it target the pointed-to buffer, a distinct object; reaching
inited that way would be an out-of-bounds write, i.e. undefined behaviour.
2026-08-03 12:23:09 +02:00
Daniele Lacamera da46dcfdca mldsa: remove tautological ret operand in two break-dominated inner loops
Premise:  each of the two inner loops is entered only from an outer loop whose
          own condition already established ret == 0 --
            wc_mldsa.c:8097 `for (r = 0; (ret == 0) && (r < params->k); r++)`
            wc_mldsa.c:8873 `for (; (ret == 0) && valid && (r < params->k); r++)`
          -- and nothing between the outer condition and the inner one assigns
          ret. (At :8884 the WC_MLDSA_FAULT_HARDEN check does write ret, but it
          `break`s out of the outer loop, so it never reaches the inner one.)
Claim:    the `ret == 0` operand of
            wc_mldsa.c:8103 `for (s = 0; (ret == 0) && (s < params->l); s++)`
            wc_mldsa.c:8895 `for (s = 0; (ret == 0) && (s < params->l); s++)`
Proof:    first evaluation: ret == 0 by the premise.
          Re-evaluations: every write to ret inside either body is immediately
          followed by an unconditional `break` --
            :8107 `ret = mldsa_rej_ntt_poly_ex(...)` / :8108 `if (ret != 0) break;`
            :8899 the same, and the WC_MLDSA_FAULT_HARDEN write at :8907, also
            followed by `break`.
          A scan of both bodies finds no other assignment to ret. So the loop
          condition is never re-evaluated with ret != 0, the operand is
          invariantly true, and its independence pair is unreachable.
Scope:    checked with and without WC_MLDSA_FAULT_HARDEN,
          WOLFSSL_MLDSA_SMALL_MEM_POLY64, WOLFSSL_MLDSA_SMALL and
          WOLFSSL_MLDSA_SIGN_SMALL_MEM_PRECALC_A -- every one of those either
          adds a break-terminated write or none at all.
Evidence: llvm-cov MC/DC records both conditions as never false
          (reports/mldsa/GAPS.md rows for 8103 and 8850, pre-drift numbering).

The two enclosing outer loops keep their `ret == 0` operand: they DO re-evaluate
after the inner `break`, so their false half is reachable.

Compiler cross-check: gcc -O2 emits byte-identical code for this file before
and after this commit -- the optimiser had already folded the removed
condition, independently confirming it was dead.
2026-08-03 12:23:09 +02:00
Daniele Lacamera 7daf4c3a1d mldsa: remove tautological ret operand in the small-mem sign entry
Premise:  wc_mldsa.c:8684 `int ret = 0;` in the WOLFSSL_MLDSA_SIGN_SMALL_MEM
          arm of the sign path.
Claim:    wc_mldsa.c:8730 `if ((ret == 0) && (*sigLen < params->sigSz))` -- the
          `ret == 0` operand.
Proof:    ret is not assigned anywhere between its initialiser and :8730 -- a
          grep of the span for `ret` yields only the declaration and this
          condition. The intervening code is the WOLFSSL_CHECK_MEM_ZERO
          bookkeeping at :8724-8728 (an XMEMSET and a wc_MemZero_Add). This is
          the operand's only evaluation, and it is invariantly true, so its
          independence pair is unreachable.
Scope:    the site is inside the #else arm selected by
          WOLFSSL_MLDSA_SIGN_SMALL_MEM; the WOLFSSL_CHECK_MEM_ZERO block in the
          span writes neither ret nor sigLen.
Evidence: llvm-cov MC/DC records this condition as never false
          (reports/mldsa/GAPS.md row 8685:9:8685:48:0, pre-drift numbering).

Compiler cross-check: gcc -O2 emits byte-identical code for this file before
and after this commit -- the optimiser had already folded the removed
condition, independently confirming it was dead.
2026-08-03 12:23:09 +02:00
Daniele Lacamera 964cc72c38 mldsa: remove tautological ret operand in wc_MlDsaKey_CheckKey
Premise:  wc_mldsa.c:11544 `if (ret == 0) {` opens the block containing the
          claimed condition.
Claim:    wc_mldsa.c:11589 `if ((ret == 0) && (x != 0))` -- the `ret == 0`
          operand.
Proof:    :11544 dominates :11589 and nothing in between assigns ret: the block
          decodes, NTTs, matrix-multiplies and XOR-accumulates into x through
          mldsa_vec_decode_*, mldsa_vec_ntt_small_full, mldsa_matrix_mul,
          mldsa_vec_red, mldsa_vec_invntt_full, mldsa_vec_add/sub/make_pos --
          all void-returning. A grep of the span for `ret` finds only the two
          conditions themselves. The operand is invariantly true and its
          independence pair is unreachable.
Scope:    the only preprocessor construct in the span is WOLFSSL_MLDSA_SMALL at
          :11566, which adds another void call.
Evidence: llvm-cov MC/DC records this condition as never false
          (reports/mldsa/GAPS.md row 11479:13:11479:35:0, pre-drift numbering).

The nearby `ret == 0` guards at :11485 and :11488 stay: prvKeySet / pubKeySet
are user-controlled and those decisions are live.

Compiler cross-check: gcc -O2 emits byte-identical code for this file before
and after this commit -- the optimiser had already folded the removed
condition, independently confirming it was dead.
2026-08-03 12:23:09 +02:00
Daniele Lacamera d1ced3d134 mldsa: remove tautological loop operand in mldsa_vec_check_low_c
Premise:  `int ret = 1;` at wc_mldsa.c:5062, and mldsa_check_low (:5028-5046)
          returns only 0 or 1.
Claim:    wc_mldsa.c:5066 `for (i = 0; (ret == 1) && (i < l); i++)` -- the
          `ret == 1` operand.
Proof:    first evaluation: ret is its initialiser 1.
          Re-evaluations: the body's only write is `ret = mldsa_check_low(...)`,
          immediately followed by `if (ret == 0) { break; }` -- so the loop
          condition is re-evaluated only on paths where ret is not 0, and
          mldsa_check_low returns nothing but 0 or 1. Hence ret == 1 whenever
          the operand is evaluated; its false half is unreachable and the
          `break` alone terminates the loop.
Scope:    the function is compiled under
          !WOLFSSL_MLDSA_NO_VERIFY || (!WOLFSSL_MLDSA_NO_SIGN &&
          !WOLFSSL_MLDSA_SIGN_SMALL_MEM); there is no #if inside it. The AVX2
          alternative at :5089 is a separate function and is untouched.
Evidence: llvm-cov MC/DC records this condition as never false
          (reports/mldsa/GAPS.md row 5048:17:5048:38:0, pre-drift numbering).

The guard is dropped rather than the `break`: keeping both would leave the same
dead operand.

Compiler cross-check: gcc -O2 emits byte-identical code for this file before
and after this commit -- the optimiser had already folded the removed
condition, independently confirming it was dead.
2026-08-03 12:23:09 +02:00
Daniele Lacamera d95bef3d39 mlkem: remove unreachable BAD_STATE_E re-check in wc_mlkemkey_check_h
Premise:  wc_MlKemKey_EncodePublicKey ends with
            if (ret == 0) { key->flags |= MLKEM_FLAG_H_SET; }
          (wc_mlkem.c:2796-2799). It is the only definition of that function in
          the tree and is unconditional inside WOLFSSL_HAVE_MLKEM.
Claim:    wc_mlkem.c:1428-1431,
            if ((ret == 0) && ((key->flags & MLKEM_FLAG_H_SET) == 0))
                ret = BAD_STATE_E;
Proof:    two cases at :1428.
          (a) MLKEM_FLAG_H_SET was already set on entry: the block at :1398 is
              skipped entirely and nothing clears the flag, so the second
              operand is false.
          (b) The flag was clear: reaching :1428 with ret == 0 requires
              wc_MlKemKey_PublicKeySize to have succeeded, the (malloc build)
              allocation to have succeeded, and wc_MlKemKey_EncodePublicKey to
              have returned 0 -- which sets the flag. Again the second operand
              is false.
          {ret == 0, H unset} is therefore unreachable, the condition can never
          be true, and BAD_STATE_E can never be returned from here.
Scope:    both the WOLFSSL_NO_MALLOC and !WOLFSSL_NO_MALLOC arms of the block
          funnel through the same EncodePublicKey call; the size-failure and
          allocation-failure paths both leave ret != 0. The enclosing
          !WOLFSSL_MLKEM_NO_ENCAPSULATE || !WOLFSSL_MLKEM_NO_DECAPSULATE gate
          selects whether the function exists at all.
Evidence: llvm-cov MC/DC records both of this decision's conditions as
          uncovered (reports/mlkem/GAPS.md rows 1373:9:1373:61:0 and :1, at the
          pre-drift line numbers) -- they are the only two residuals holding
          wc_mlkem.c at 66/68.

The comment called it an "implementation issue" check, i.e. an assertion
against a future encoder that forgets the flag; it is being removed rather than
retained because no call path can reach it.

Proof check: gcc does not fold this (the encoder call is not inlined).
Verified instead by enumerating every write to key->flags in the translation
unit -- all nine, at wc_mlkem.c:453, 621, 852, 1001, 1003, 2283, 2327, 2430 and
2794, living in Init, Free, MakeKeyWithRandom, DecodePrivateKey,
DecodePublicKey and EncodePublicKey. wc_mlkemkey_check_h calls only
wc_MlKemKey_PublicKeySize, XMALLOC, wc_MlKemKey_EncodePublicKey and XFREE, so
the sole flag write reachable from it is :2794, which SETS MLKEM_FLAG_H_SET on
ret == 0. No reachable path clears it.
2026-08-03 12:23:09 +02:00
Takashi Kojo d06579277d Move WC_SIG_MIN_HASH_TYPE default into signature.h 2026-08-02 20:56:01 +09:00
Tobias Frauenschläger 7a8aae3e40 Merge pull request #10986 from dgarske/ti_port_fenrir_fixes
Fix TI port AES-CTR streaming and hashCopy correctness bugs
2026-08-01 14:21:14 +02:00
Tobias Frauenschläger 097ddc19d8 Use alignment-safe writes in ML-KEM mlkem_vec_compress_10_c
The little-endian large-code path of mlkem_vec_compress_10_c cast the
output byte buffer to word32* and issued five 32-bit stores through it.
That buffer is the caller-supplied ML-KEM ciphertext, which has no
alignment guarantee, so on strict-alignment targets the store bus-faults
and the cast violates strict aliasing. Write each word with
writeUnalignedWord32, which does an alignment-safe byte copy, matching
mldsa_encode_w1_88_c and the neighboring ML-KEM sampling code.

Fixes F-6782.
2026-08-01 13:13:00 +02:00
Tobias Frauenschläger ca449b1263 Use alignment-safe reads in ML-KEM mlkem_cbd_eta3
The little-endian large-code path of mlkem_cbd_eta3 cast the
caller-supplied byte buffer to word32* and read through it. The buffer
has no alignment guarantee, so on strict-alignment targets such as
Cortex-M3 and M4 with unaligned-access trapping enabled the read faults
or returns wrong values, and the cast violates strict aliasing. Read
each word with readUnalignedWord32, which does an alignment-safe byte
copy, matching the neighboring mlkem_cbd_eta2.

Fixes F-6781.
2026-08-01 13:13:00 +02:00
Tobias Frauenschläger af0385cb8d Avoid unaligned word32 read of RSA key id under SE050
wc_InitRsaKey_Id cast the byte array key->id to word32* and dereferenced
it to recover the SE050 key id. key->id is a byte array with no alignment
guarantee inside RsaKey, so the dereference is an unaligned 32-bit read
that faults or mis-reads on strict-alignment targets, and it violates
strict aliasing. Read the value with readUnalignedWord32 instead, which
does an alignment-safe byte copy, matching wc_ecc_init_id.

Fixes F-6624.
2026-08-01 13:13:00 +02:00
Tobias Frauenschläger 1673777231 Fix transposed register and mask in ESP32-C3 RSA unlock
The ESP32-C3 deactivation path in esp_mp_hw_unlock passed its arguments
to DPORT_REG_CLR_BIT and DPORT_REG_SET_BIT in the wrong order and added
a stray DR_REG_RSA_BASE offset. The macros take (register address, bit
mask), but the code used the bit mask as part of the register address
and the register as the bit mask. As a result the RSA clock-enable and
memory power-down bits were never updated at deactivation, and reads and
writes landed at a bogus peripheral address. The accelerator was left
powered up after every bignum and RSA operation.

Pass the register address first and the bit mask second and drop the
offset, mirroring the activation path and the other targets.

Fixes F-6779.
2026-08-01 13:13:00 +02:00
Tobias Frauenschläger c196546762 Surface hardware failures in Intel QAT sync cipher
IntelQaSymCipher ran cpaCySymPerformOp synchronously but never
translated its completion status into the return code. Only the AES-GCM
decrypt auth check, driven by verifyResult, could set an error. For
AES-CBC, AES-GCM encrypt, and 3DES-CBC a non-success status left ret at
zero, so the exit path copied the working buffer to the output and
returned success. On encrypt that buffer still holds the plaintext copy,
so a hardware failure returned success with plaintext written to the
ciphertext output.

Translate a non-success perform-op status into ASYNC_OP_E, matching the
asynchronous port, so hardware failures are surfaced instead of
returning zero with unprocessed output.

Fixes F-6623.
2026-08-01 13:13:00 +02:00