Build / portability:
- Drop #pragma message in fwtpm_crypto.c (MSVC-incompatible)
- Replace non-ASCII section sign with Sec. across all sources/docs
Configure:
- Add --enable-pqc alias for --enable-v185 (same WOLFTPM_V185 macro)
- Auto-detect: when --enable-fwtpm + wolfCrypt has dilithium.h+mlkem.h
and neither flag is set, configure auto-enables PQC; --disable-pqc
opts out
- Both flags probe the wolfSSL PQC headers and fail at configure time
with a clear hint when missing
Spec / security hardening:
- VerifySequenceComplete now emits TPM_ST_DIGEST_VERIFIED (with hashAlg
metadata) for Hash-ML-DSA tickets, MESSAGE_VERIFIED for Pure ML-DSA
(was mis-tagging digests as messages, breaking PolicyTicket consumers)
- Sign/VerifySequenceComplete: free the slot on TPM_RC_SIGN_CONTEXT_KEY
too, so wrong-key Complete cannot exhaust FWTPM_MAX_SIGN_SEQ slots
(CWE-772 DoS)
- TestParms PQC arms return TPM_RC_PARMS (spec-correct) instead of
TPM_RC_VALUE; reject MLDSA/MLKEM parameter sets not actually compiled
in; parse TPMS_MLKEM_PARMS.symmetric via TPM2_Packet_ParseSymmetric
- GetCapability TPMA_ML_PARAMETER_SETS gates each MLDSA/MLKEM bit on the
per-set wolfCrypt availability macro (subset builds advertise truth)
- TPM2_VerifySignature client parser now defensive: only consume the
v1.85 metaAlg when tag==DIGEST_VERIFIED && hierarchy!=RH_NULL
- VerifyDigestSignature: hard-fail on keyName overflow instead of
silently emitting a ticket missing the name binding
- TPM_GENERATED_VALUE prefix check guarded with rc==0
- Drop dead (void)cmdSize casts in Sign/VerifySequenceStart
- wolfTPM2_EncryptSecret_MLKEM: track wc_InitRng_ex/wc_MlKemKey_Init
success flags so Free is only called on initialized state
- UBSan-v185 sanitizer cflags: explicitly disable signed-integer-overflow
and shift checks (matches the comment about wolfSSL Hash_df 440<<24)
Embedded RAM:
- FWTPM_NV_PUBAREA_EST derives from FWTPM_MAX_MLDSA_PUB_SIZE /
FWTPM_MAX_MLKEM_PUB_SIZE auto-shrink macros (subset builds save NV)
- tpm2_types.h MAX_MLDSA_*/MAX_MLKEM_* stay at worst-case (ABI floor
for TPM2B wire buffers) with comment
Tests:
- Negative test for Hash-MLDSA VerifySeqComplete ticket tag
- Negative test exposing sign-seq slot leak on TPM_RC_SIGN_CONTEXT_KEY
- Roundtrip test for wolfTPM2_SignDigest + VerifyDigestSignature
Documentation:
- README, FWTPM.md, fwtpm/README.md, examples/pqc/README.md mention
both --enable-pqc and --enable-v185 + auto-detect
- README wolfSSL line: --enable-pkcallbacks + WC_RSA_NO_PADDING
- fwtpm/README.md: drop FWTPM_SPEC_* labels (macros never existed),
remove v1.85 Additions table (all 8 commands implemented), update
coverage table to 137/113/24 (82%); note remaining gaps are
inherited v1.59/v1.84 commands, not PQC
- fwtpm_nv.h:52: clarify 2592 vs 2720 math (PQC pub key + header slack)
Fixes 5 v1.85 PR CI/build issues:
1. src/tpm2_wrap.c: add #include <wolfssl/wolfcrypt/mlkem.h> inside the
v185 MLKEM guard. Builds with --disable-fwtpm against wolfSSL with
--enable-mlkem failed because the MLKEM symbol declarations were
only pulled in transitively by src/fwtpm/fwtpm_crypto.c.
2. src/fwtpm/fwtpm_command.c: switch FWTPM_ALLOC_BUF(privKeyDer) to
FWTPM_CALLOC_BUF in 4 sites (Create, Load, LoadExternal, Import,
CreateLoaded). MSan-v185 flagged uninit-value reads in SocketSend
originating from FwCmd_Create's keyedHash branch — when caller
supplies undersized inSensitive material, FwComputeUniqueHash hashed
beyond what was written. Zero-initialising the buffer eliminates the
class of issue.
3. examples/keygen/keygen.c: pass allowExternalMu=NO for MLDSA. The
v1.85 EXT_MU enforcement now correctly rejects allowExternalMu=YES
at object creation per Part 2 §12.2.3.6.
4. .github/workflows/make-test-swtpm.yml: convert v185-pqc-swtpm lane
to build-only. swtpm has no v1.85 PQC, so unit.test PQC blocks fail
on TPM_RC_SIZE; runtime PQC coverage stays in the fwtpm-v185 lane.
5. .github/workflows/sanitizer.yml: UBSan-v185 now uses the same
sanitizer flags as the classical UBSan lane (drops ).
Pre-existing wolfSSL UB at misc.c:117 (440<<24 in Hash_df) only
surfaces under -fsanitize=integer.
Closes 13 findings across two reviews of the v1.85 PQC paths.
Tickets (TPMT_TK_VERIFIED / TPMT_TK_HASHCHECK / TPMT_TK_CREATION):
- FwAppendTicket binds tag (always) and metadata (DIGEST_VERIFIED only)
into the HMAC per Part 2 §10.6.5 Eq (5). Streamed via chunked
wc_HmacUpdate, no temp buffer. All 5 callers updated; the hand-rolled
VerifyDigestSignature path collapses into FwAppendTicket.
- FWTPM_Object gains a hierarchy field, captured at every load/create
site (CreatePrimary, Load, LoadExternal, CreateLoaded). Replaces
hardcoded TPM_RH_OWNER in VerifySignature, VerifySequenceComplete,
VerifyDigestSignature, ContextSave, and Create's creation ticket.
- VerifySequenceComplete snapshots the verified digest before
wc_HashFinal so Hash-ML-DSA tickets bind (digest || keyName) rather
than just keyName — pre-fix, two distinct verified digests on the
same key produced byte-identical tickets (universal reuse).
Authorization:
- Sign/VerifySequenceStart split TPM_RC_KEY (non-signing key, e.g.
ML-KEM) from TPM_RC_SCHEME (signing key, scheme unsupported) using
TPMA_OBJECT_sign per Part 3 §17.5.1 / §17.6.1.
- SignDigest restricted-key path validates TPMT_TK_HASHCHECK HMAC
per Part 3 §20.7.1 instead of blanket-rejecting; x509sign keeps
the TPM_RC_ATTRIBUTES short-circuit.
- Decapsulate, SignDigest, SignSequenceComplete reject NO_SESSIONS
with TPM_RC_AUTH_MISSING (Auth Role: USER, Tables 62/124/126).
Restricted-key TPM_GENERATED_VALUE check:
- FWTPM_SignSeq.firstBytes[4] populated by SequenceUpdate covers the
Hash-ML-DSA path where bytes are otherwise consumed by hashCtx;
topped-up from the Complete trailing buffer for Pure-MLDSA one-shot.
Closes the Update-then-empty-Complete bypass.
Client-side (src/tpm2.c):
- TPM2_VerifySequenceComplete defensively dispatches on validation.tag
for TPMU_TK_VERIFIED_META, mirroring TPM2_VerifyDigestSignature.
Other:
- TPM2_Packet_AppendSensitive caps mldsa/mlkem .size to buffer length.
- pqc_mssim_e2e.c zeroizes ss1/ss2 on cleanup.
- Untrack examples/pqc/pqc_mssim_e2e (libtool wrapper with hardcoded
/home/aidangarske path; .gitignore already covered it).
- #pragma message at WOLFTPM_V185 build-time flagging that the PQC
primary-key KDFa labels are interpretation pending TCG Part 4 v1.85;
suppressible via -DWOLFTPM_V185_LABELS_ACK.
Tests: 11 new fixtures in tests/fwtpm_unit_tests.c, 4 existing tests
updated to assert new spec-mandated RCs. fwtpm_unit.test reports 105
passing, zero failures.
Main README:
- New ## Post-Quantum Cryptography (v1.85) section between fwTPM and
TPM 2.0 Overview. Covers supported algorithms (ML-DSA-44/65/87,
Hash-ML-DSA, ML-KEM-512/768/1024), exact wolfSSL + wolfTPM build
config (--enable-dilithium --enable-mlkem --enable-experimental ...
for wolfSSL; --enable-fwtpm --enable-v185 for wolfTPM), and a
make check pointer.
- Existing feature-list bullet at line 41 now points to the new
section instead of directly to docs/FWTPM.md.
examples/pqc/README.md:
- Rewrite around three audience splits: (1) build steps, (2) run
everything with make check, (3) per-example details.
- New sections for mlkem_encap and the -mldsa/-hash_mldsa/-mlkem
options on examples/keygen/keygen.
- Drop stale --enable-swtpm reference (wrong flag; caused reviewer
confusion).
- Point users at the existing tests/fwtpm_check.sh and
tests/pqc_mssim_e2e.sh for targeted reruns without the full classical
suite.
Documentation split (no duplication):
- Top-level README - build + I just want to run it
- examples/pqc/README.md per-example usage
- docs/FWTPM.md#tpm-20-v185-post-quantum-support -> server internals
(commands, primary-key derivation, buffer constants, spec
interpretation decisions)
examples/keygen/keygen:
- New -mldsa[=44|65|87], -hash_mldsa[=44|65|87], -mlkem[=512|768|1024]
options alongside existing -rsa/-ecc/-sym/-keyedhash. Dispatches to
wolfTPM2_GetKeyTemplate_{MLDSA,HASH_MLDSA,MLKEM}, then CreateKey under
the SRK parent. AIK template path correctly rejects PQC (AIKs are
RSA/ECC only per TCG).
- Param-set parser defaults: MLDSA-65, MLKEM-768, SHA-256 pre-hash for
Hash-ML-DSA.
examples/pqc/mlkem_encap (new):
- CreatePrimary MLKEM (512/768/1024) then Encapsulate + Decapsulate,
asserting the two shared secrets match byte-for-byte. Companion to
pqc_mssim_e2e but focused on the KEM wrappers alone.
examples/run_examples.sh:
- Detects WOLFTPM_V185 from config.h, runs keygen+keyload round-trip
for all 9 PQC variants (same pattern used by RSA/ECC blocks above).
All 9 pass against fwtpm_server.
Cross-process PQC validation: new examples/pqc/pqc_mssim_e2e exercises
wolfTPM2_* client wrappers against a running fwtpm_server over the
mssim (SWTPM) socket transport. Two round-trips in one binary:
- MLKEM-768 Encap/Decap: asserts ciphertext = 1088 bytes and the two
derived shared secrets are byte-identical.
- HashMLDSA-65 SignDigest/Verify: asserts signature = 3309 bytes and
the validation ticket carries TPM_ST_DIGEST_VERIFIED.
tests/pqc_mssim_e2e.sh spawns fwtpm_server, waits for TCP readiness,
runs the client, and cleans up. Proves client marshaling + mssim
framing + fwtpm_server unmarshaling + PQC handler dispatch agree over
a real socket between two separately-compiled processes — orthogonal
to the in-process fwtpm_unit.test suite.
Infrastructure:
- configure.ac: new AM_CONDITIONAL BUILD_V185 so the example only
builds when --enable-v185 is passed (matches the pattern of
BUILD_SWTPM, BUILD_DEVTPM, etc.).
- examples/include.am, examples/pqc/include.am: register the example.
Output polish: replaced 85 inconsistent calls with a
fwtpm_pass(name, is_pqc) helper that produces aligned
columns. PQC tests are tagged [PQC] so they're visually distinct
from the classical suite at a glance. All 86 existing tests still pass.
MAX_PEM_SIZE aliased to MAX_CONTEXT_SIZE (2 KB), which is enough
for an RSA-2048 self-signed cert but overflows at RSA-4096 where
the signature alone is 512 B plus the cert body, ASN.1, and base64
overhead. wc_MakeCertReq / wc_MakeSelfSignedCert then returned
BUFFER_E (0xffffff7c / 0xffffff53).
Gate on MAX_RSA_KEY_BITS so 2048-bit builds keep the exact same
buffer size; 4096-bit builds (Nations NS350, Infineon SLB967x, or
anyone overriding) get 4 KB. Mirrors the fix already applied to
examples/pkcs7/pkcs7.c.
keygen defaulted srkAlg = TPM_ALG_ECC and only switched to RSA for
RSA keys, so SYMCIPHER and KEYEDHASH blobs were created under the
ECC SRK. After the prior keyload fix made keyload default to RSA
(matching seal.c), a sym/keyedhash blob produced by keygen could
not round-trip through keyload without a parent mismatch.
Invert the default to RSA (matching seal.c and keyload.c) and only
switch to the ECC SRK when the stored key itself is ECC. Keeps all
three tools aligned so any blob round-trips without callers having
to specify a parent.
keyload defaulted srkAlg = TPM_ALG_ECC and only switched to RSA
for TPM_ALG_RSA keys. A sealed KEYEDHASH blob (produced by
examples/seal, which always uses the RSA SRK) therefore tried to
load under the ECC SRK and failed with TPM_RC_INTEGRITY. SYMCIPHER
blobs had the same issue.
Invert the default to RSA (matching seal.c) and only switch to the
ECC SRK when the stored key itself is ECC.
nvram/read hardcoded the RSA SRK, so an ECC child key retrieved
from NV would be loaded under the RSA parent and fail with
TPM_RC_INTEGRITY. Switch the SRK selection to mirror the approach
already used in keygen/keyload: inspect keyBlob.pub.publicArea.type
and load the matching RSA or ECC SRK.
MAX_PKCS7_SIZE aliased to MAX_CONTEXT_SIZE (2 KB), which is enough
for an RSA-2048 signed blob but overflows at RSA-4096 where the
signature alone is 512 B plus a ~1-1.5 KB cert and ASN.1 overhead.
wc_PKCS7_EncodeSignedData then returned BUFFER_E (0xffffff7c).
Gate on MAX_RSA_KEY_BITS so 2048-bit builds keep the exact same
buffer size; 4096-bit builds (Nations NS350, Infineon SLB967x,
or anyone overriding) get 4 KB.
The RSA encrypt/decrypt test with TPM_ALG_NULL padding hardcoded
message.size = 256, which is only valid for 2048-bit RSA keys.
With TPM_ALG_NULL padding the TPM returns a full modulus-sized
plaintext on decrypt, so with a 4096-bit key the 256-byte message
never equals the 512-byte plaintext and the test reports
TPM_RC_TESTING.
Derive the message size from the key's own keyBits so the test
passes for any modulus size.
Reported against wolfTPM 4.0.0 on Nations NS350 hardware (NSING).
- configure.ac: switch wolfCrypt link probe to AC_LINK_IFELSE with real
headers so CC=g++ builds don't fail on name-mangling; gate fwTPM/swTPM
autodetect on ENABLED_WOLFCRYPT=yes (fwTPM requires wolfCrypt)
- wolftpm/fwtpm/fwtpm.h: hoist FWTPM_NV_HAL_S and FWTPM_CLOCK_HAL_S out
of the FWTPM_CTX nested scope so C++ consumers can reference them
- src/fwtpm/fwtpm_command.c, fwtpm_crypto.c: add explicit TPMI_ALG_HASH
and enum wc_HashType casts to silence strict C++ warnings; drop stale
KEYEDHASH hashAlg initializer
- src/tpm2_wrap.c: move rc = TPM_RC_SUCCESS into the correct #else branch
so Linux/winapi paths propagate TPM2_Startup errors
- examples/endorsement/get_ek_certs.c: drop redundant indexType default
- examples/endorsement/verify_ek_cert.c: initialize curveName on the
WOLFTPM2_NO_WOLFCRYPT/no-ECC path
- examples/native/native_test.c: stop masking TPM2_ZGen_2Phase and
TPM2_EC_Ephemeral 'command unavailable' as success
- tests/fwtpm_unit_tests.c: use cmdSz (not hardcoded 14) on FlushContext
- CI: add no-examples and st33ktpm2-i2c compile-only matrix entries to
make-test-swtpm; add release-checks workflow (CC=g++ + scan-build)
NS350's Nations_IdentityKeySet returns TPM_RC_VALUE when asked to set
the identity key to its current value. The nations test assumed the
chip always started at identity-key=1, but self-hosted CI runners
carry persistent NV state across runs — any prior failure left the
chip in a state the next run could not recover from. GPIO reset
clears volatile state but does not reset NV-persistent identity-key
or PSK provisioning.
Add normalize_nations_chip(): GPIO reset + idempotent --psk-clear +
idempotent --identity-key-set. Call at entry of both nations and
nations-psk blocks, and wire to trap EXIT so the chip is always
cleaned up on success, failure, or set -e early exit.
Validated on Pi hardware across five scenarios (identity-key=1,
identity-key=0, PSK-provisioned, nations-psk clean, nations-psk
PSK-stuck): all runs now pass and leave the chip at canonical
identity-key=1.
Reviewer previously requested always using mp_to_unsigned_bin_len_ct,
but CI builds with older or minimal wolfSSL configurations do not
expose that symbol and fail with implicit declaration. Switching
back to mp_to_unsigned_bin_len still fixes the actual security bug
(data-dependent wire offset leaking the leading-zero count of ECDH
shared secrets and ECC signature components) since it writes exactly
the requested number of bytes with left-zero padding. The constant-
time property of the _ct variant is a secondary concern that can be
addressed separately once wolfSSL exposes it universally.
Reviewer feedback on F-2508/F-2512: restore the removed TPM2_PrintBin
debug lines that printed auth values, session keys, bind keys, HMAC
keys, hierarchy auth, and encryption secrets, but gate them behind a
new WOLFTPM_DEBUG_SECRETS macro that is never enabled by a configure
option and must be defined manually. Emit a compile-time #warning
whenever the macro is defined. Document the flag in README.md and in
the new banner in wolftpm/tpm2_types.h.
Reviewer feedback on F-2973/F-2974: always use mp_to_unsigned_bin_len_ct
(not gated on WOLFSSL_HAVE_SP_ECC) for the ECDH shared-secret export in
wolfTPM2_EncryptSecret_ECC and for the r/s export in the policy_sign
example. The _ct variant is available on all wolfSSL math backends via
macro fallback in integer.h.
- Restore userWithAuth attribute in seal example (intentional for
password-based unsealing)
- Restore chained if (rc == TPM_RC_SUCCESS) pattern and the
release-build authSz mismatch check in TPM2_CommandProcess
- Restore TPM2_Packet_AppendSymmetric call in TPM2_Duplicate
- Update ParamEncDec_Dispatch test to use fwtpm raw-pointer
TPM2_ParamEnc_AESCFB signature
Add portable firmware TPM 2.0 implementation (fwTPM) built on wolfCrypt.
Implements 105/113 TPM 2.0 v1.38 commands (93%) as a standalone server
with socket and TIS transports, NV storage, and full CI/fuzz coverage.
- NATIONS_PSK mode check (tpm2_spdm.c) - Only affects PSK mode which was already broken (raw path instead of VENDOR_DEFINED)
- END_SESSION (tpm2_wrap.c) - New behavior but only adds an END_SESSION before the existing cleanup. If it fails, cleanup still proceeds.
- Nations auto-connect (tpm2_wrap.c:279) - Only fires when spdmOnlyDetected is true (TPM locked in SPDM-only mode and TPM2_Startup returned
TPM_RC_DISABLED). Normal operation never hits this path.
- VdCode validation (spdm_tcg.c) - This one could theoretically break something if a response VdCode doesn't match. But all callers like
wolfSPDM_TCG_GetPubKey() already validate VdCode independently (line 316-321), so the existing code was already checking this for specific commands.
- VCA skip in PSK (spdm_psk.c) - Changes the transcript hash. Needs firmware 0.1.0.16 + NS350 to also skip VCA. Vision confirmed this works.
- TPM_CMD_Lx defines / comments - No behavioral change.
- Renamed spdm_demo to spdm_ctrl: file, binary, internal demo_* functions to ctrl_*, all references in
include.am, spdm_test.sh, .gitignore, CLAUDE.md, both READMEs
- README mentions Nations: title updated to Nuvoton NPCT75x and Nations NS350 TPMs, added Nations build section
- README section header renamed from Demo Commands to Setup/Control Commands
- README added reset pin control section: documents GPIO reset requirement, Pi-specific example, custom hardware
design guidance
- Moved spdm_tcg.c to common build section, no longer conditional on Nuvoton/Nations in src/spdm/include.am
- Removed redundant wolfSSL options include from spdm_internal.h since tpm2_types.h handles this
- Added WOLFTPM_SPDM_TCG generic guard as auto-define in spdm_types.h, replaced ~30 occurrences of #if defined(WOLFSPDM_NUVOTON) || defined(WOLFSPDM_NATIONS) across all files
2. examples/seal/seal_policy_auth.c — Clarify header comment: no pre-existing key needed, but authkey.bin must be retained for unseal.
3. examples/seal/seal_test.sh
- Add || return 1 to setup_pcr/change_pcr extend calls
- Use grep -F -q -- for fixed-string secret matching
- Add 6 new param enc tests (3.4a-c XOR, 3.5a-c AES) for seal_nv
4. examples/nvram/seal_nv.c — Implement real parameter encryption:
- Add paramEncSession (separate from tpmSession to avoid conflict)
- Start unsalted HMAC session with XOR/AES-CFB
- Place on session slot 2 (slot 1 is used internally by NVWriteData for NV handle auth)
- Clean up session in exit path
5. examples/run_examples.sh — Add seal_nv XOR param encryption integration test
New examples:
- seal_pcr: PCR-only policy seal/unseal. Binds secrets to specific PCR
values without password or signing key. Supports split seal/unseal,
configurable PCR index, custom blob filenames, XOR/AES param encryption.
- seal_policy_auth: PolicyAuthorize with TPM-resident signing key (ECC/RSA).
Signing key can re-authorize PCR policy, allowing secrets to survive
authorized PCR changes (e.g., OS updates). Supports split seal/unseal,
XOR/AES param encryption.
- seal_nv: NV storage with PCR policy. Stores secrets directly in TPM
non-volatile memory with store/read/delete lifecycle and configurable
NV index. No external blob files needed.
Build system:
- Autotools: Updated include.am for seal and nvram with new build targets
- CMake: Added add_tpm_example() entries for all three examples
- Headers: Updated seal.h and nvram.h with new prototypes, removed old
TPM2_PCR_Seal_With_Policy_Auth_* declarations
- .gitignore: Added built binaries
Testing:
- run_examples.sh: Added ~110 lines of integration tests for seal_pcr,
seal_policy_auth (ECC + RSA), and seal_nv (store/read/delete lifecycle)
- seal_test.sh: Standalone test script with 28 tests across 3 groups
including positive, negative, param encryption, and custom path tests
- seal-test.yml: Dedicated CI workflow with SWTPM, path-filtered to
seal-related files, follows make-test-swtpm.yml pattern
Documentation:
- README.md: Usage examples and policy comparison table for all seal examples
Resolves#457. When the SPI device could not be opened (e.g., kernel TPM
driver owns it), wolfTPM failed silently with no error output. Users had
no indication of what went wrong or how to fix it.
Changes:
- Make TPM2_Init failed message in wolfTPM2_Init_ex always print
(previously required --enable-debug)
- Add SPI/I2C open failure logging in hal/tpm_io_linux.c: permission
denied prints unconditionally, other errors print with DEBUG_WOLFTPM
- Add autodetect scan exhaustion message (DEBUG_WOLFTPM)
- Fix examples (caps, wrap_test, bench, pkcs7) that silently returned
on init failure to print error code and RC string
- Update 13 other examples to include error code in init failure message