Skip to content

Save digest operation state as plain hash fields - #300

Merged
LinuxJedi merged 1 commit into
wolfSSL:masterfrom
Frauschi:wolfssl-master-compat
Nov 13, 2026
Merged

LinuxJedi merged 1 commit into
wolfSSL:masterfrom
Frauschi:wolfssl-master-compat

Conversation

@Frauschi

Copy link
Copy Markdown
Member

C_GetOperationState saved a digest by calling wc_*Copy() into the caller's buffer at a misaligned offset, so wolfSSL master's new alignment check (#12035) broke it and CI went red. The saved blob was also the raw wolfCrypt object, leaking heap pointers, stale AES key schedule bytes on v5.6.6, and letting a forged state steer pointers on restore.

Changes

  • Save only the resumable hash state (digest words, buffLen buffer bytes, lengths) after the mechanism, with no pointers or handles
  • Verify each saved state by loading it into a fresh hash and comparing wc_*GetHash(); a mismatch returns CKR_STATE_UNSAVEABLE
  • Validate restored states (exact length, mechanism, buffLen, length width, updated byte) before touching the running operation
  • Zero the hash in WP11_Digest_Init so a digest never inherits data of an earlier operation
  • Add WOLFPKCS11_NO_DIGEST_STATE, auto-defined for hash hardware ports, WOLFSSL_NO_HASH_RAW and WOLFSSL_HASH_KEEP, and documented in the README
  • States saved by earlier versions are rejected with CKR_SAVED_STATE_INVALID

Testing

  • single_part_op_state_test: odd-offset save/restore for every MD5/SHA-1/SHA-2 mechanism, forged states, stale params-union data, empty-state restore
  • NO_DIGEST_STATE build checks that saves are refused and restores rejected
  • Existing op-state tests gated on WOLFPKCS11_NO_DIGEST_STATE
  • New no_digest_state CI job

C_GetOperationState saved a digest by calling wc_*Copy() straight into
the caller's buffer, after the mechanism type, and C_SetOperationState
copied that buffer back into the session the same way. The saved state
was therefore the raw wolfCrypt hash object, which is not plain data:

- wolfSSL #12035 made wc_Sha256Copy() and wc_Sha224Copy() return
  BAD_ALIGN_E for a destination below the type's alignment on x86_64.
  The return value was ignored, so the saved state was garbage and
  restoring it failed or produced the wrong digest.
- Depending on the build the object holds pointers to heap memory the
  copy allocated (the W cache, or with WOLFSSL_HASH_KEEP the whole
  message), device and async handles, a heap hint and flags. Every
  save leaked those allocations and handed their addresses to the
  application.
- Copying into the caller's buffer first freed whatever pointers the
  buffer happened to contain, since newer wc_*Copy() versions free the
  destination before overwriting it.
- On restore, wolfCrypt dereferenced pointers taken from the
  application's bytes: a forged state could make the copy read from an
  arbitrary address, pick a heap hint or select a crypto callback
  device.
- The whole hash buffer was saved, including the bytes after buffLen.
  The digest shares the session's params union with the other
  operations, wc_HashInit() does not clear the buffer, and single-part
  AES operations end without zeroing params. Against wolfSSL v5.6.6,
  whose wc_AesFree() keeps the key schedule, C_Encrypt with a sensitive
  key followed by C_DigestInit and C_GetOperationState handed parts of
  the AES key schedule to the application.

The resumable state of MD5, SHA-1 and SHA-2 is small and fully defined:
the chaining value, the input not yet processed and the total length.
The saved state is now just that, after the mechanism type: loLen,
hiLen, buffLen, the digest words and the first buffLen bytes of the
buffer, zero padded, followed by the existing optional updated byte.
No pointer, handle or flag is saved. C_DigestInit and
C_SetOperationState also zero the hash before wc_HashInit(), so a new
digest never starts with data of an earlier operation.
The layout uses host byte order and the size of CK_MECHANISM_TYPE, so
a state still only restores in a build with the same ABI.

C_GetOperationState reads the fields from the session's hash, loads
them into a freshly initialized hash and compares wc_*GetHash() of both.
If they differ, the state is not held in those fields and it returns
CKR_STATE_UNSAVEABLE instead of a state that would restore wrongly.

The check cannot see how a fresh hash continues an update, so builds
whose hash state is not all in those fields are excluded at compile
time instead: wolfpkcs11/internal.h defines WOLFPKCS11_NO_DIGEST_STATE
for WOLFSSL_NO_HASH_RAW, which wolfSSL sets for ports without raw hash
access, for WOLFSSL_HASH_KEEP, whose backends may read the kept
message, and for the ports wolfSSL itself excludes from its SHA-256
hash block code because their state is outside sha256->digest, such as
the ESP32 engine that restarts from the IV. With the define,
C_GetOperationState always returns CKR_STATE_UNSAVEABLE, which PKCS#11
allows, and C_SetOperationState returns CKR_SAVED_STATE_INVALID. A
port missing from the list fails to build the digest state code and
can define it itself; the README documents it.

C_SetOperationState requires the exact length, with or without the
updated byte, a supported mechanism, a buffLen smaller than the block
size, lengths that fit the hash's length fields and an updated byte of 0
or 1 that is set when any data has been hashed. A state with hashed data
but no updated flag would let a following C_Digest start over and
silently drop the restored data. loLen is not checked against buffLen:
the wolfSSL v5.6.6 ARMv8 SHA-256 code counts only whole blocks in loLen
until the final block. The checks run before the current operation is
touched, so an invalid state leaves it intact. The digest is then
started through WP11_Digest_Init(), as C_DigestInit does, and the fields
are written into the fresh hash, so a forged state can only choose a
hash state. A failed hash initialization now returns CKR_HOST_MEMORY or
CKR_FUNCTION_FAILED instead of the wolfCrypt error code; the previous
operation has already ended at that point. C_GetOperationState maps hash
failures in its check the same way.

States saved by earlier versions are rejected with
CKR_SAVED_STATE_INVALID; they were only usable by the same build.

The test saves and restores every enabled MD5, SHA-1 and SHA-2 mechanism
through buffers at odd offsets, after more than one block of input so
the chaining value is exercised. It checks that a trailing byte, a
truncated state, a full buffer, a SHA-256 length beyond 32 bits and a
SHA-3 or non-digest mechanism, an updated byte of 2 and a cleared
updated byte after hashed data are rejected, and that the running digest
still completes correctly after those rejections. A state saved before
any update restores and still allows C_Digest. It checks that a state
saved right after a single-part AES-CBC encrypt holds no buffer bytes;
against wolfSSL v5.6.6 that fails only when both the buffLen-only save
and the zeroing in WP11_Digest_Init are removed. In a
WOLFPKCS11_NO_DIGEST_STATE build it checks that a state save is refused,
a state is rejected and the digest still completes. The save and restore
tests, here and in the existing tests, are compiled out under
WOLFPKCS11_NO_DIGEST_STATE, so make check passes in those builds while a
normal build that cannot save a state fails them. A new CI job builds
with WOLFPKCS11_NO_DIGEST_STATE.
Copilot AI balanced review requested due to automatic review settings November 12, 2026 15:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Security-sensitive serialization relies on version- and hardware-specific wolfCrypt internals, so missing dependency sources and runtime validation leave compatibility and hardware-port exclusions requiring human verification.

No open issues 🎉

What changed in this PR

This PR replaces raw wolfCrypt digest snapshots with plain hash fields, avoiding alignment failures and preventing saved operation states from exposing internal pointers or stale operation data.

Changes:

  • Serialize resumable digest fields, verify exports, validate restores, and clear hash storage before initialization.
  • Add and document WOLFPKCS11_NO_DIGEST_STATE, with automatic detection for unsupported hash configurations.
  • Expand state regression tests and add CI coverage for disabled digest-state support.
File Description
wolfpkcs11/​internal.h Detects configurations that disable digest-state support.
tests/​single_part_op_state_test.c Tests misaligned buffers, forged states, stale data, and disabled support.
tests/​session_op_lifecycle_test.c Gates save/restore lifecycle tests on digest-state support.
tests/​pkcs11test.c Gates successful operation-state tests on feature availability.
tests/​digest_length_test.c Skips successful digest-state saves when support is disabled.
src/​internal.c Implements field-based serialization, validation, and hash-storage clearing.
README.md Documents the digest-state disable macro and automatic detection.
.github/​workflows/​unit-test.yml Adds a CI job with digest-state support disabled.

Review effort: Balanced

@Frauschi

Copy link
Copy Markdown
Member Author

@claude review

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #300

Scan targets checked: wolfpkcs11-src, wolfpkcs11-bugs
Not reviewed (no scan target covers them): .github/workflows/unit-test.yml, README.md
Coverage: 6 of 6 in-scope changed file(s) opened by the reviewer

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: Max

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. It rewrites how C_GetOperationState and C_SetOperationState serialize and validate digest state from untrusted application bytes, so a maintainer familiar with wolfCrypt hash internals should also take a look.

What was reviewed:

  • The save, check and load helpers in src/internal.c, including the compile-time size assert, zero-padding of the buffer past buffLen, and the 32-bit length check for MD5, SHA-1, SHA-224 and SHA-256.
  • Export verification: the saved fields are loaded into a fresh wc_HashAlg, both GetHash outputs are compared, and the temporaries are wiped with ForceZero.
  • The restore path runs every validation before it aborts the running operation. Also covered: WOLFPKCS11_NO_DIGEST_STATE gating in internal.h and the tests, and the new CI job.
Extended reasoning...

The change replaces raw wc_*Copy() serialization of wolfCrypt hash objects with a pointer-free WP11_DigestState struct in src/internal.c, roughly 430 changed lines. It also adds an auto-defined opt-out macro in wolfpkcs11/internal.h, plus tests and a CI job. The security-sensitive surface is crypto state handling: application-supplied bytes are parsed and loaded into live hash contexts, and saved state could expose leftover params-union data. The bug hunt ran until it found nothing more (dry_streak) and reported no findings. I deferred because this is a sizable crypto and PKCS#11 API change with compatibility effects: states saved by earlier versions are now rejected, and hardware ports are opted out.

@LinuxJedi
LinuxJedi merged commit 179d396 into wolfSSL:master Nov 13, 2026
111 checks passed
@Frauschi
Frauschi deleted the wolfssl-master-compat branch November 13, 2026 12:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants