diff --git a/.github/workflows/pqc-build-matrix.yml b/.github/workflows/pqc-build-matrix.yml index 6ffabb70..adf8ee7d 100644 --- a/.github/workflows/pqc-build-matrix.yml +++ b/.github/workflows/pqc-build-matrix.yml @@ -28,6 +28,8 @@ jobs: fail-fast: false matrix: include: + - name: sealsq-qvault-vendor + wolftpm_config: --enable-pqc --enable-sealsq --disable-fwtpm --disable-examples - name: cli-mldsa_all-mlkem_all wolftpm_config: --enable-pqc --enable-mldsa=all --enable-mlkem=all --disable-fwtpm --disable-examples - name: cli-mldsa_all-mlkem_enc diff --git a/.github/workflows/pqc-examples.yml b/.github/workflows/pqc-examples.yml index 4759399f..14dded6a 100644 --- a/.github/workflows/pqc-examples.yml +++ b/.github/workflows/pqc-examples.yml @@ -95,6 +95,13 @@ jobs: - name: ML-KEM encap + decap example (standalone) run: ./examples/pqc/mlkem_encap + - name: PQC control tool (pqc_ctrl.sh suite + argument validation) + # pqc_ctrl.sh runs the full command set and the argument-validation + # negatives, exiting non-zero on any failure. PQC_CTRL_CLEAR is left off + # so the shared Tier-2 fwtpm_server is not wiped mid-sequence; the + # destructive --clear / --pcrextend paths are exercised on hardware. + run: ./examples/pqc/pqc_ctrl.sh + - name: Stop Tier 2 fwtpm_server (free port 2321 for E2E) run: | if [ -f /tmp/fwtpm_server.pid ]; then diff --git a/.gitignore b/.gitignore index 072105e4..a248c34b 100644 --- a/.gitignore +++ b/.gitignore @@ -79,6 +79,9 @@ examples/pqc/mldsa_sign examples/pqc/mlkem_encap examples/pqc/pqc_mssim_e2e examples/pqc/gen_pqc_certs +examples/pqc/pqc_ctrl +examples/pqc/mldsa_verify_neg +examples/pqc/mlkem_decap_neg examples/nvram/extend examples/nvram/store examples/nvram/read diff --git a/README.md b/README.md index 5ec2dca9..c12f6c03 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Portable TPM 2.0 project designed for embedded use. * This implementation provides all TPM 2.0 API's in compliance with the specification. * Wrappers provided to simplify Key Generation/Loading, RSA encrypt/decrypt, ECC sign/verify, ECDH, NV, Hashing/HACM, AES, Sealing/Unsealing, Attestation, PCR Extend/Quote and Secure Root of Trust. -* Any TPM 2.0 compliant module is supported. Tested modules include Infineon SLB9670, SLB9672, SLB9673, STMicroelectronics ST33KTPM2XSPI, ST33KTPM2I, ST33TPHF2XSPI, ST33TPHF2XI2C, Microchip ATTPM20, Nations Technologies/NSING Z32H330, NS350, and Nuvoton NPCT650, NPCT750. +* Any TPM 2.0 compliant module is supported. Tested modules include Infineon SLB9670, SLB9672, SLB9673, STMicroelectronics ST33KTPM2XSPI, ST33KTPM2I, ST33TPHF2XSPI, ST33TPHF2XI2C, Microchip ATTPM20, Nations Technologies/NSING Z32H330, NS350, Nuvoton NPCT650, NPCT750, and SealSQ QVault TPM (first TPM with post-quantum ML-DSA/ML-KEM in silicon). * wolfTPM uses the TPM Interface Specification (TIS) to communicate either over SPI, or using a memory mapped I/O range. * On Linux, wolfTPM auto-detects between the kernel TPM driver (`/dev/tpmX`) and direct SPI access at runtime - a simple `./configure && make` works with either interface. * wolfTPM can also use the Linux TPM kernel interface (`/dev/tpmX`) to talk with any physical TPM on SPI, I2C and even LPC bus. @@ -78,9 +78,12 @@ Supported algorithms: | Hash-ML-DSA (pre-hash signing) | FIPS 204 | ML-DSA-44 / 65 / 87 with caller hash | | ML-KEM (key encapsulation) | FIPS 203 | ML-KEM-512 / 768 / 1024 | -The examples run against the in-tree fwTPM server. No shipping hardware -TPM firmware implements v1.85 PQC yet; upgrade paths for discrete chips -are forward-compatible — the same wrapper API targets both. +wolfTPM **officially supports the SealSQ QVault TPM**, the first shipping TPM 2.0 +with these v1.85 PQC algorithms in silicon. Build for it with `--enable-sealsq +--enable-pqc`. The same examples and wrapper API also run against the in-tree +fwTPM server for CI or when no hardware is present. See the +[TPM2 Benchmarks](#tpm2-benchmarks) section for measured ML-DSA / ML-KEM +performance on the QVault TPM. ### Building @@ -144,9 +147,10 @@ make check ``` See [examples/pqc/README.md](examples/pqc/README.md) for per-example -details (`pqc_mssim_e2e`, `mlkem_encap`) and PQC options on the -general-purpose `keygen`/`keyload` tools (`-mldsa`, `-hash_mldsa`, -`-mlkem`). +details — the `pqc_ctrl` control center (every PQC operation plus board +control in one CLI, with `pqc_ctrl.sh` running the full command set), +`pqc_mssim_e2e`, `mlkem_encap`, and PQC options on the general-purpose +`keygen`/`keyload` tools (`-mldsa`, `-hash_mldsa`, `-mlkem`). For the fwTPM server's PQC internals — the eight v1.85 commands, primary-key derivation, buffer constants, and spec-interpretation @@ -238,6 +242,7 @@ Tested with: * Microchip ATTPM20 module * Nuvoton NPCT65X or NPCT75x TPM2.0 modules * Nations Technologies Z32H330 or NS350 TPM 2.0 modules +* SealSQ QVault TPM 2.0 module (SPI, post-quantum ML-DSA / ML-KEM) #### Device Identification @@ -283,6 +288,10 @@ Nuvoton NPCT750 TPM2.0 TPM2: Caps 0x30000697, Did 0x00fc, Vid 0x1050, Rid 0x 1 Mfg NTC (0), Vendor NPCT75x"!!4rls, Fw 7.2 (131072), FIPS 140-2 1, CC-EAL4 0 +SealSQ QVault TPM 2.0 +TPM2: Caps 0x30000797, Did 0x0083, Vid 0x2406, Rid 0x 3 +Mfg SEAL (6), Vendor QVault TPM, Fw 2.1 (0x3010303), FIPS 140-3, CC-EAL4 0 + ## Building ### Building wolfSSL @@ -346,6 +355,7 @@ make install --enable-microchip Enable Microchip ATTPM20 Support (default: disabled) - WOLFTPM_MICROCHIP --enable-nuvoton Enable Nuvoton NPCT65x/NPCT75x Support (default: disabled) - WOLFTPM_NUVOTON --enable-nations Enable Nations Technology NS350 Support (default: disabled) - WOLFTPM_NATIONS +--enable-sealsq Enable SealSQ QVault post-quantum TPM Support (default: disabled) - WOLFTPM_SEALSQ --enable-devtpm Enable using Linux kernel driver for /dev/tpmX (default: disabled) - WOLFTPM_LINUX_DEV Note: With autodetect (default) this is no longer required on Linux; @@ -594,6 +604,32 @@ ECDSA 256 verify 9 ops took 1.022 sec, avg 113.539 ms, 8.808 ops/se ECDHE 256 agree 5 ops took 1.161 sec, avg 232.144 ms, 4.308 ops/sec ``` +Run on the SealSQ QVault post-quantum TPM (ML-DSA / ML-KEM) on a Raspberry Pi 5 +over SPI. These are the first post-quantum TPM benchmarks measured on +shipping-class silicon: + +``` +./examples/bench/bench +TPM2 Benchmark using Wrapper API's +RNG 10 KB took 1.061 seconds, 9.428 KB/s +AES-256-CBC-enc 57 KB took 1.000 seconds, 56.994 KB/s +SHA256 43 KB took 1.012 seconds, 42.470 KB/s +SHA384 43 KB took 1.024 seconds, 41.984 KB/s +RSA 2048 key gen 3 ops took 20.536 sec, avg 6845.188 ms, 0.146 ops/sec +RSA 2048 Public 71 ops took 1.015 sec, avg 14.289 ms, 69.985 ops/sec +RSA 2048 Private 7 ops took 1.154 sec, avg 164.827 ms, 6.067 ops/sec +ECC 256 key gen 4 ops took 1.170 sec, avg 292.538 ms, 3.418 ops/sec +ECDSA 256 sign 14 ops took 1.019 sec, avg 72.781 ms, 13.740 ops/sec +ECDSA 256 verify 17 ops took 1.031 sec, avg 60.661 ms, 16.485 ops/sec +ECDHE 256 agree 5 ops took 1.030 sec, avg 206.022 ms, 4.854 ops/sec +ML-DSA 65 key gen 8 ops took 16.357 sec, avg 2044.679 ms, 0.489 ops/sec +ML-DSA 65 sign 2 ops took 1.162 sec, avg 581.025 ms, 1.721 ops/sec +ML-DSA 65 verify 7 ops took 1.142 sec, avg 163.118 ms, 6.131 ops/sec +ML-KEM 768 key gen 19 ops took 15.216 sec, avg 800.819 ms, 1.249 ops/sec +ML-KEM 768 encap 5 ops took 1.059 sec, avg 211.777 ms, 4.722 ops/sec +ML-KEM 768 decap 3 ops took 1.276 sec, avg 425.471 ms, 2.350 ops/sec +``` + Run on Infineon OPTIGA SLB9672 at 43MHz: ``` diff --git a/configure.ac b/configure.ac index 642e2e04..409d8a2c 100644 --- a/configure.ac +++ b/configure.ac @@ -253,7 +253,7 @@ fi WOLFTPM_HW_SELECTED=no for _wt_v in "$enable_infineon" "$enable_st" "$enable_st33" \ "$enable_microchip" "$enable_mchp" \ - "$enable_nuvoton" "$enable_nations" \ + "$enable_nuvoton" "$enable_nations" "$enable_sealsq" \ "$enable_spi" "$enable_i2c" "$enable_mmio" \ "$enable_devtpm" "$enable_autodetect" \ "$enable_winapi" "$enable_wintbs"; do @@ -540,6 +540,17 @@ then AM_CFLAGS="$AM_CFLAGS -DWOLFTPM_NATIONS" fi +# SealSQ QVault TPM (post-quantum ML-DSA/ML-KEM) +AC_ARG_ENABLE([sealsq], + [AS_HELP_STRING([--enable-sealsq],[Enable SealSQ QVault TPM Support (default: disabled)])], + [ ENABLED_SEALSQ=$enableval ], + [ ENABLED_SEALSQ=no ] + ) +if test "x$ENABLED_SEALSQ" = "xyes" +then + AM_CFLAGS="$AM_CFLAGS -DWOLFTPM_SEALSQ" +fi + # Infineon SLB9670/SLB9672/SLB9673 AC_ARG_ENABLE([infineon], [AS_HELP_STRING([--enable-infineon],[Enable Infineon SLB9670/SLB9672 TPM Support (default: disabled)])], @@ -621,7 +632,7 @@ then # If a module hasn't been selected then enable auto-detection if test "x$ENABLED_INFINEON" = "xno" && test "x$ENABLED_MCHP" = "xno" && test "x$ENABLED_MICROCHIP" = "xno" && \ test "x$ENABLED_ST" = "xno" && test "x$ENABLED_ST33" = "xno" && test "x$ENABLED_NUVOTON" = "xno" && \ - test "x$ENABLED_NATIONS" = "xno" + test "x$ENABLED_NATIONS" = "xno" && test "x$ENABLED_SEALSQ" = "xno" then ENABLED_AUTODETECT=yes fi @@ -1192,6 +1203,7 @@ echo " * STM ST33: $ENABLED_ST" echo " * Microchip ATTPM20: $ENABLED_MICROCHIP" echo " * Nuvoton NPCT75x: $ENABLED_NUVOTON" echo " * Nations Tech NS350: $ENABLED_NATIONS" +echo " * SealSQ QVault TPM: $ENABLED_SEALSQ" echo " * fwTPM Server: $ENABLED_FWTPM" echo " * fwTPM Only (no client): $ENABLED_FWTPM_ONLY" diff --git a/examples/pqc/README.md b/examples/pqc/README.md index 2e6e228b..e9c5546e 100644 --- a/examples/pqc/README.md +++ b/examples/pqc/README.md @@ -3,10 +3,12 @@ Examples exercising the ML-DSA / ML-KEM post-quantum additions from TCG TPM 2.0 Library Specification v1.85, wrapped by `wolfTPM2_*` API calls. -The examples run against the in-tree fwTPM server. No shipping hardware -TPM firmware implements v1.85 PQC yet. See -[docs/FWTPM.md](../../docs/FWTPM.md#tpm-20-v185-post-quantum-support) for -the full fwTPM PQC reference. +These examples run on the SealSQ QVault TPM — the first shipping TPM 2.0 with +v1.85 post-quantum (ML-DSA / ML-KEM) algorithms in silicon — over SPI, and on +the in-tree fwTPM server for CI or when no hardware is present. Build for the +SealSQ part with `--enable-sealsq --enable-pqc`; see +[docs/FWTPM.md](../../docs/FWTPM.md#tpm-20-v185-post-quantum-support) for the +fwTPM PQC reference. ## Building @@ -67,6 +69,68 @@ All examples expect a running `fwtpm_server` on `127.0.0.1:2321`: ./src/fwtpm/fwtpm_server --clear & ``` +### `pqc_ctrl` — PQC control center + +One CLI to drive and validate a PQC TPM (SealSQ QVault TPM, or the fwTPM), +modeled on `examples/spdm/spdm_ctrl`: each command runs an operation and +controls the board. Every key operation flushes the transient object table +first, so a TPM with a small object memory (e.g. SealSQ QVault TPM) does not +hit `TPM_RC_OBJECT_MEMORY` when commands are chained. + +``` +./examples/pqc/pqc_ctrl # --all (default) +./examples/pqc/pqc_ctrl --caps --algs # identify + list supported algorithms +./examples/pqc/pqc_ctrl --mldsa=87 # ML-DSA-87 sign/verify +./examples/pqc/pqc_ctrl --mlkem=1024 # ML-KEM-1024 encap/decap +./examples/pqc/pqc_ctrl --selftest --getrandom=32 --pcrread=0 +``` + +| Command | Description | +|---|---| +| `--caps` | Manufacturer, vendor string, firmware, FIPS mode | +| `--algs` | List the algorithms the TPM reports as supported | +| `--selftest` | `TPM2_SelfTest` | +| `--getrandom[=N]` | N random bytes (default 16) | +| `--pcrread[=idx]` | Read a PCR (SHA-256 bank, falling back to SHA-384) | +| `--pcrextend=idx` | Extend a PCR with a test digest (explicit index required) | +| `--flush` | Flush transient objects (board reset between ops) | +| `--clear` | `TPM2_Clear` — wipes the owner hierarchy | +| `--mldsa[=44/65/87]` | Pure ML-DSA sign/verify (default 65) | +| `--hash-mldsa[=44/65/87]` | Hash-ML-DSA (SHA-256 pre-hash) sign/verify | +| `--mlkem[=512/768/1024]` | ML-KEM encapsulate/decapsulate | +| `--all` | caps + algs + selftest + getrandom + pcrread + every PQC set | + +Commands run left-to-right, so they can be chained. Requires `--enable-v185` +(or `--enable-pqc`). Point it at the SealSQ part with `--enable-sealsq`, or at +the fwTPM with `--enable-fwtpm --enable-swtpm`. + +Run the whole command set as a pass/fail suite (mirrors +`examples/spdm/spdm_test.sh`). The destructive `--clear` is opt-in via +`PQC_CTRL_CLEAR=1` so the suite never wipes a TPM unexpectedly: + +``` +./examples/pqc/pqc_ctrl.sh +PQC_CTRL_CLEAR=1 ./examples/pqc/pqc_ctrl.sh # also exercise TPM2_Clear +``` + +### Benchmarks on SealSQ QVault TPM silicon + +Measured with `examples/bench/bench` on a Raspberry Pi 5 driving the QVault TPM +over SPI (the first post-quantum TPM benchmarks on shipping-class silicon): + +| Operation | Avg latency | Throughput | +|---|---|---| +| ML-DSA-65 key gen | 2044.7 ms | 0.49 ops/s | +| ML-DSA-65 sign | 581.0 ms | 1.72 ops/s | +| ML-DSA-65 verify | 163.1 ms | 6.13 ops/s | +| ML-KEM-768 key gen | 800.8 ms | 1.25 ops/s | +| ML-KEM-768 encapsulate | 211.8 ms | 4.72 ops/s | +| ML-KEM-768 decapsulate | 425.5 ms | 2.35 ops/s | + +Verification is fast (comparable to ECDSA); key generation is a one-off +provisioning cost. See the top-level `README.md` TPM2 Benchmarks section for the +full classical + PQC run. + ### `pqc_mssim_e2e` End-to-end client test over the mssim socket. Two round-trips: diff --git a/examples/pqc/include.am b/examples/pqc/include.am index 47be3dd9..8f2b9ea5 100644 --- a/examples/pqc/include.am +++ b/examples/pqc/include.am @@ -34,7 +34,13 @@ examples_pqc_gen_pqc_certs_SOURCES = examples/pqc/gen_pqc_certs.c examples_pqc_gen_pqc_certs_LDADD = src/libwolftpm.la $(LIB_STATIC_ADD) examples_pqc_gen_pqc_certs_DEPENDENCIES = src/libwolftpm.la +noinst_PROGRAMS += examples/pqc/pqc_ctrl +examples_pqc_pqc_ctrl_SOURCES = examples/pqc/pqc_ctrl.c +examples_pqc_pqc_ctrl_LDADD = src/libwolftpm.la $(LIB_STATIC_ADD) +examples_pqc_pqc_ctrl_DEPENDENCIES = src/libwolftpm.la + EXTRA_DIST += examples/pqc/README.md +EXTRA_DIST += examples/pqc/pqc_ctrl.sh endif endif diff --git a/examples/pqc/pqc_ctrl.c b/examples/pqc/pqc_ctrl.c new file mode 100644 index 00000000..2e41c1bd --- /dev/null +++ b/examples/pqc/pqc_ctrl.c @@ -0,0 +1,808 @@ +/* pqc_ctrl.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfTPM. + * + * wolfTPM is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfTPM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* Example: single control/validation tool for the TPM 2.0 v1.85 post-quantum + * algorithms (ML-DSA, Hash-ML-DSA, ML-KEM), modeled on examples/spdm/spdm_ctrl. + * One CLI runs each operation and controls the board. Each key operation + * reclaims loaded transient objects first, so a TPM with a small transient- + * object table (e.g. SealSQ QVault TPM) does not hit TPM_RC_OBJECT_MEMORY + * when key operations are chained. */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#include +#include +#include + +#include +#include + +#if !defined(WOLFTPM2_NO_WRAPPER) && \ + defined(WOLFTPM_MLDSA_SIGN) && defined(WOLFTPM_MLDSA_VERIFY) && \ + defined(WOLFTPM_MLKEM_ENCAP) && defined(WOLFTPM_MLKEM_DECAP) && \ + defined(WOLFTPM_HASH_MLDSA) + +static const byte gPqcMsg[] = "wolfTPM PQC control example message"; + +static void usage(void) +{ + printf("Expected usage:\n"); + printf("./examples/pqc/pqc_ctrl [command(s)]\n"); + printf("Commands run in the given order; default (no args) is --all:\n"); + printf("* --caps Print TPM caps (vendor, FW, FIPS)\n"); + printf("* --algs List supported algorithms\n"); + printf("* --selftest Run TPM2_SelfTest\n"); + printf("* --getrandom[=N] Get N random bytes (default 16)\n"); + printf("* --pcrread[=idx] Read PCR index (default 0)\n"); + printf("* --pcrextend=idx Extend PCR (explicit index required)\n"); + printf("* --flush Flush transient objects (board reset)\n"); + printf("* --clear TPM2_Clear (wipes owner hierarchy!)\n"); + printf("* --mldsa[=44|65|87] Pure ML-DSA sign/verify (default 65)\n"); + printf("* --hash-mldsa[=44|65|87] Hash-ML-DSA (SHA-256) sign/verify\n"); + printf("* --mlkem[=512|768|1024] ML-KEM encapsulate/decapsulate\n"); + printf("* --all caps + every parameter set of each op\n"); +} + +/* Empty value means "bare flag, use default set"; any other non-matching + * value is rejected rather than silently defaulted. A NULL ps validates only. */ +static int parseMldsaSet(const char* val, TPMI_MLDSA_PARAMETER_SET* ps) +{ + TPMI_MLDSA_PARAMETER_SET set; + + if (val[0] == '\0' || XSTRCMP(val, "65") == 0) { + set = TPM_MLDSA_65; + } + else if (XSTRCMP(val, "44") == 0) { + set = TPM_MLDSA_44; + } + else if (XSTRCMP(val, "87") == 0) { + set = TPM_MLDSA_87; + } + else { + return BAD_FUNC_ARG; + } + if (ps != NULL) { + *ps = set; + } + return 0; +} + +static int parseMlkemSet(const char* val, TPMI_MLKEM_PARAMETER_SET* ps) +{ + TPMI_MLKEM_PARAMETER_SET set; + + if (val[0] == '\0' || XSTRCMP(val, "768") == 0) { + set = TPM_MLKEM_768; + } + else if (XSTRCMP(val, "512") == 0) { + set = TPM_MLKEM_512; + } + else if (XSTRCMP(val, "1024") == 0) { + set = TPM_MLKEM_1024; + } + else { + return BAD_FUNC_ARG; + } + if (ps != NULL) { + *ps = set; + } + return 0; +} + +static const char* mldsaName(TPMI_MLDSA_PARAMETER_SET ps) +{ + return (ps == TPM_MLDSA_44) ? "44" : (ps == TPM_MLDSA_87) ? "87" : "65"; +} + +static const char* mlkemName(TPMI_MLKEM_PARAMETER_SET ps) +{ + return (ps == TPM_MLKEM_512) ? "512" : (ps == TPM_MLKEM_1024) ? "1024" : + "768"; +} + +/* Reclaim the transient object table by flushing exactly the handles the TPM + * reports as loaded. Parts with a small object table (e.g. SealSQ QVault TPM) + * hit TPM_RC_OBJECT_MEMORY when a key plus a sign/verify sequence object are + * live at once. wolfTPM2_UnloadHandles_AllTransient() only covers + * MAX_HANDLE_NUM (3) fixed handles, so query the actual loaded set instead. */ +static int pqc_flush_transient(WOLFTPM2_DEV* dev) +{ + int rc; + word32 i; + GetCapability_In in; + GetCapability_Out out; + FlushContext_In flushCtx; + + (void)dev; /* operates on the active TPM context */ + XMEMSET(&in, 0, sizeof(in)); + XMEMSET(&out, 0, sizeof(out)); + XMEMSET(&flushCtx, 0, sizeof(flushCtx)); + in.capability = TPM_CAP_HANDLES; + in.property = TRANSIENT_FIRST; + in.propertyCount = MAX_CAP_HANDLES; + rc = TPM2_GetCapability(&in, &out); + if (rc != TPM_RC_SUCCESS) { + return rc; + } + if (out.capabilityData.capability != TPM_CAP_HANDLES) { + return TPM_RC_FAILURE; + } + for (i = 0; i < out.capabilityData.data.handles.count; i++) { + flushCtx.flushHandle = out.capabilityData.data.handles.handle[i]; + (void)TPM2_FlushContext(&flushCtx); + } + return TPM_RC_SUCCESS; +} + +static int do_caps(WOLFTPM2_DEV* dev) +{ + int rc; + WOLFTPM2_CAPS caps; + + XMEMSET(&caps, 0, sizeof(caps)); + + rc = wolfTPM2_GetCapabilities(dev, &caps); + if (rc != TPM_RC_SUCCESS) { + printf("GetCapabilities failed 0x%x: %s\n", + rc, wolfTPM2_GetRCString(rc)); + return rc; + } + printf("Caps: Mfg %s, Vendor %s, Fw %u.%u, FIPS %s\n", + caps.mfgStr, caps.vendorStr, caps.fwVerMajor, caps.fwVerMinor, + TPM2_GetCapsFipsStr(caps.fips140_3, caps.fips140_2)); + return rc; +} + +static const char* algName(TPM_ALG_ID alg) +{ + switch (alg) { + case TPM_ALG_RSA: return "RSA"; + case TPM_ALG_SHA1: return "SHA1"; + case TPM_ALG_SHA256: return "SHA256"; + case TPM_ALG_SHA384: return "SHA384"; + case TPM_ALG_SHA512: return "SHA512"; + case TPM_ALG_AES: return "AES"; + case TPM_ALG_ECC: return "ECC"; + case TPM_ALG_HMAC: return "HMAC"; + case TPM_ALG_KEYEDHASH: return "KEYEDHASH"; + #ifdef WOLFTPM_PQC + case TPM_ALG_MLKEM: return "ML-KEM"; + case TPM_ALG_MLDSA: return "ML-DSA"; + case TPM_ALG_HASH_MLDSA: return "Hash-ML-DSA"; + #endif + default: return NULL; + } +} + +static int do_algs(WOLFTPM2_DEV* dev) +{ + int rc; + word32 i; + word32 count; + const char* name; + TPM_ALG_ID alg; + GetCapability_In in; + GetCapability_Out out; + + (void)dev; + XMEMSET(&in, 0, sizeof(in)); + XMEMSET(&out, 0, sizeof(out)); + in.capability = TPM_CAP_ALGS; + in.property = TPM_ALG_FIRST; /* spec-conformant start; 0 is TPM_RC_VALUE */ + /* MAX_CAP_ALGS covers the whole algorithm range in one request. Paging is + * avoided deliberately: TPM_CAP_ALGS advances by a property cursor that + * assumes ascending IDs, but some parts return the list unordered. */ + in.propertyCount = MAX_CAP_ALGS; + + rc = TPM2_GetCapability(&in, &out); + if (rc != TPM_RC_SUCCESS) { + printf("GetCapability(ALGS) failed 0x%x: %s\n", + rc, wolfTPM2_GetRCString(rc)); + return rc; + } + if (out.capabilityData.capability != TPM_CAP_ALGS) { + printf("GetCapability(ALGS) returned unexpected capability 0x%x\n", + (unsigned)out.capabilityData.capability); + return TPM_RC_FAILURE; + } + count = out.capabilityData.data.algorithms.count; + printf("Supported algorithms (%u):\n", (unsigned)count); + for (i = 0; i < count; i++) { + alg = out.capabilityData.data.algorithms.algProperties[i].alg; + name = algName(alg); + if (name != NULL) { + printf(" 0x%04x %s\n", (unsigned)alg, name); + } + else { + printf(" 0x%04x\n", (unsigned)alg); + } + } + return rc; +} + +static int do_selftest(WOLFTPM2_DEV* dev) +{ + int rc = wolfTPM2_SelfTest(dev); + if (rc == TPM_RC_SUCCESS) + printf("PASS SelfTest\n"); + else + printf("FAIL SelfTest 0x%x: %s\n", rc, wolfTPM2_GetRCString(rc)); + return rc; +} + +static int do_getrandom(WOLFTPM2_DEV* dev, int len) +{ + int rc; + int i; + byte buf[64]; + + if (len <= 0 || len > (int)sizeof(buf)) { + printf("FAIL GetRandom invalid length %d (max %d)\n", + len, (int)sizeof(buf)); + return BAD_FUNC_ARG; + } + XMEMSET(buf, 0, sizeof(buf)); + + rc = wolfTPM2_GetRandom(dev, buf, (word32)len); + if (rc != TPM_RC_SUCCESS) { + printf("FAIL GetRandom 0x%x: %s\n", rc, wolfTPM2_GetRCString(rc)); + return rc; + } + printf("Random %d bytes: ", len); + for (i = 0; i < len; i++) + printf("%02x", buf[i]); + printf("\n"); + return rc; +} + +/* Read a PCR, trying the SHA-256 bank first then SHA-384 (a PQC part such as + * SealSQ QVault TPM allocates only the SHA-384 bank). */ +static int do_pcrread(WOLFTPM2_DEV* dev, int idx) +{ + int rc; + int i; + int hashAlg = TPM_ALG_SHA256; + int digestSz = 0; + byte digest[TPM_MAX_DIGEST_SIZE]; + + XMEMSET(digest, 0, sizeof(digest)); + digestSz = (int)sizeof(digest); + rc = wolfTPM2_ReadPCR(dev, idx, hashAlg, digest, &digestSz); + /* Fall back to SHA-384 when SHA-256 errors or returns an empty bank. */ + if (rc != TPM_RC_SUCCESS || digestSz == 0) { + hashAlg = TPM_ALG_SHA384; + digestSz = (int)sizeof(digest); + rc = wolfTPM2_ReadPCR(dev, idx, hashAlg, digest, &digestSz); + } + if (rc != TPM_RC_SUCCESS) { + printf("FAIL PCR%d read 0x%x: %s\n", + idx, rc, wolfTPM2_GetRCString(rc)); + return rc; + } + if (digestSz == 0) { + printf("FAIL PCR%d read no digest returned (no allocated bank)\n", idx); + return TPM_RC_FAILURE; + } + printf("PCR%d (%s): ", idx, hashAlg == TPM_ALG_SHA384 ? "SHA384" : "SHA256"); + for (i = 0; i < digestSz; i++) + printf("%02x", digest[i]); + printf("\n"); + return rc; +} + +/* PCR extend is irreversible, so extend exactly one bank: the TPM's first + * bank that actually has PCRs allocated (a part may allocate only SHA-384). + * Never fall back to a second extend, which would touch two banks. */ +static int do_pcrextend(WOLFTPM2_DEV* dev, int idx) +{ + int rc; + int hashAlg; + int digestSz; + word32 b; + TPML_PCR_SELECTION* banks; + GetCapability_In in; + GetCapability_Out out; + byte digest[TPM_MAX_DIGEST_SIZE]; + + XMEMSET(&in, 0, sizeof(in)); + XMEMSET(&out, 0, sizeof(out)); + in.capability = TPM_CAP_PCRS; + in.property = 0; + in.propertyCount = HASH_COUNT; /* return every assigned bank, not just one */ + rc = TPM2_GetCapability(&in, &out); + if (rc != TPM_RC_SUCCESS) { + printf("FAIL PCR%d extend 0x%x: %s\n", + idx, rc, wolfTPM2_GetRCString(rc)); + return rc; + } + banks = &out.capabilityData.data.assignedPCR; + if (out.capabilityData.capability != TPM_CAP_PCRS || banks->count == 0) { + printf("FAIL PCR%d extend no assigned PCR bank\n", idx); + return TPM_RC_FAILURE; + } + + /* Pick the first bank that has the requested PCR index allocated. */ + hashAlg = TPM_ALG_ERROR; + for (b = 0; b < banks->count && hashAlg == TPM_ALG_ERROR; b++) { + if ((idx / 8) < (int)banks->pcrSelections[b].sizeofSelect && + (banks->pcrSelections[b].pcrSelect[idx / 8] & + (1 << (idx % 8))) != 0) { + hashAlg = banks->pcrSelections[b].hash; + } + } + if (hashAlg == TPM_ALG_ERROR) { + printf("FAIL PCR%d extend index not allocated in any bank\n", idx); + return TPM_RC_FAILURE; + } + digestSz = TPM2_GetHashDigestSize((TPMI_ALG_HASH)hashAlg); + if (digestSz <= 0 || digestSz > (int)sizeof(digest)) { + printf("FAIL PCR%d extend unsupported bank 0x%x\n", idx, hashAlg); + return TPM_RC_FAILURE; + } + + XMEMSET(digest, 0x11, sizeof(digest)); + rc = wolfTPM2_ExtendPCR(dev, idx, hashAlg, digest, digestSz); + if (rc == TPM_RC_SUCCESS) { + printf("PASS PCR%d extend (bank 0x%x)\n", idx, hashAlg); + } + else { + printf("FAIL PCR%d extend 0x%x: %s\n", + idx, rc, wolfTPM2_GetRCString(rc)); + } + return rc; +} + +static int do_clear(WOLFTPM2_DEV* dev) +{ + int rc; + + printf("WARNING: TPM2_Clear wipes the owner hierarchy and persistent keys\n"); + rc = wolfTPM2_Clear(dev); + if (rc == TPM_RC_SUCCESS) + printf("PASS Clear (TPM reset to defaults)\n"); + else + printf("FAIL Clear 0x%x: %s\n", rc, wolfTPM2_GetRCString(rc)); + return rc; +} + +static int do_mldsa(WOLFTPM2_DEV* dev, TPMI_MLDSA_PARAMETER_SET ps) +{ + int rc; + WOLFTPM2_KEY key; + TPMT_PUBLIC tpl; + TPM_HANDLE seq = 0; + TPMT_TK_VERIFIED validation; + FlushContext_In flushCtx; + byte* sig = NULL; + int sigSz = MAX_MLDSA_SIG_SIZE; + + XMEMSET(&key, 0, sizeof(key)); + XMEMSET(&tpl, 0, sizeof(tpl)); + XMEMSET(&validation, 0, sizeof(validation)); + XMEMSET(&flushCtx, 0, sizeof(flushCtx)); + + (void)pqc_flush_transient(dev); + + sig = (byte*)XMALLOC(MAX_MLDSA_SIG_SIZE, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (sig == NULL) + return MEMORY_E; + + rc = wolfTPM2_GetKeyTemplate_MLDSA(&tpl, + TPMA_OBJECT_sign | TPMA_OBJECT_fixedTPM | TPMA_OBJECT_fixedParent | + TPMA_OBJECT_sensitiveDataOrigin | TPMA_OBJECT_userWithAuth | + TPMA_OBJECT_noDA, ps, 0); + if (rc != TPM_RC_SUCCESS) goto exit; + + rc = wolfTPM2_CreatePrimaryKey(dev, &key, TPM_RH_OWNER, &tpl, NULL, 0); + if (rc != TPM_RC_SUCCESS) goto exit; + + rc = wolfTPM2_SignSequenceStart(dev, &key, NULL, 0, &seq); + if (rc != TPM_RC_SUCCESS) goto exit; + rc = wolfTPM2_SignSequenceComplete(dev, seq, &key, + gPqcMsg, (int)sizeof(gPqcMsg) - 1, sig, &sigSz); + if (rc != TPM_RC_SUCCESS) goto exit; + seq = 0; /* Complete consumed the sequence object */ + + rc = wolfTPM2_VerifySequenceStart(dev, &key, NULL, 0, &seq); + if (rc != TPM_RC_SUCCESS) goto exit; + rc = wolfTPM2_VerifySequenceUpdate(dev, seq, + gPqcMsg, (int)sizeof(gPqcMsg) - 1); + if (rc != TPM_RC_SUCCESS) goto exit; + rc = wolfTPM2_VerifySequenceComplete(dev, seq, &key, NULL, 0, + sig, sigSz, &validation); + if (rc != TPM_RC_SUCCESS) goto exit; + seq = 0; /* Complete consumed the sequence object */ + + if (validation.tag != TPM_ST_MESSAGE_VERIFIED) { + printf("ML-DSA-%s verify: unexpected ticket tag 0x%x\n", + mldsaName(ps), (unsigned)validation.tag); + rc = TPM_RC_FAILURE; + } + +exit: + if (rc == TPM_RC_SUCCESS) { + printf("PASS ML-DSA-%-3s sign+verify (sig %d bytes)\n", + mldsaName(ps), sigSz); + } + else { + printf("FAIL ML-DSA-%-3s 0x%x: %s\n", + mldsaName(ps), rc, wolfTPM2_GetRCString(rc)); + } + if (seq != 0) { + flushCtx.flushHandle = seq; + (void)TPM2_FlushContext(&flushCtx); + } + wolfTPM2_UnloadHandle(dev, &key.handle); + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return rc; +} + +static int do_hash_mldsa(WOLFTPM2_DEV* dev, TPMI_MLDSA_PARAMETER_SET ps) +{ + int rc; + WOLFTPM2_KEY key; + TPMT_PUBLIC tpl; + TPMT_TK_VERIFIED validation; + byte digest[TPM_SHA256_DIGEST_SIZE]; + byte* sig = NULL; + int sigSz = MAX_MLDSA_SIG_SIZE; + + XMEMSET(&key, 0, sizeof(key)); + XMEMSET(&tpl, 0, sizeof(tpl)); + XMEMSET(&validation, 0, sizeof(validation)); + XMEMSET(digest, 0xAA, sizeof(digest)); + + (void)pqc_flush_transient(dev); + + sig = (byte*)XMALLOC(MAX_MLDSA_SIG_SIZE, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (sig == NULL) + return MEMORY_E; + + rc = wolfTPM2_GetKeyTemplate_HASH_MLDSA(&tpl, + TPMA_OBJECT_sign | TPMA_OBJECT_fixedTPM | TPMA_OBJECT_fixedParent | + TPMA_OBJECT_sensitiveDataOrigin | TPMA_OBJECT_userWithAuth | + TPMA_OBJECT_noDA, ps, TPM_ALG_SHA256); + if (rc != TPM_RC_SUCCESS) goto exit; + + rc = wolfTPM2_CreatePrimaryKey(dev, &key, TPM_RH_OWNER, &tpl, NULL, 0); + if (rc != TPM_RC_SUCCESS) goto exit; + + rc = wolfTPM2_SignDigest(dev, &key, digest, (int)sizeof(digest), + NULL, 0, sig, &sigSz); + if (rc != TPM_RC_SUCCESS) goto exit; + + rc = wolfTPM2_VerifyDigestSignature(dev, &key, digest, (int)sizeof(digest), + sig, sigSz, NULL, 0, &validation); + if (rc != TPM_RC_SUCCESS) goto exit; + + if (validation.tag != TPM_ST_DIGEST_VERIFIED) { + printf("HashML-DSA-%s verify: unexpected ticket tag 0x%x\n", + mldsaName(ps), (unsigned)validation.tag); + rc = TPM_RC_FAILURE; + } + +exit: + if (rc == TPM_RC_SUCCESS) { + printf("PASS HashML-DSA-%-3s signdigest+verify (sig %d bytes)\n", + mldsaName(ps), sigSz); + } + else { + printf("FAIL HashML-DSA-%-3s 0x%x: %s\n", + mldsaName(ps), rc, wolfTPM2_GetRCString(rc)); + } + wolfTPM2_UnloadHandle(dev, &key.handle); + XFREE(sig, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return rc; +} + +static int do_mlkem(WOLFTPM2_DEV* dev, TPMI_MLKEM_PARAMETER_SET ps) +{ + int rc; + WOLFTPM2_KEY key; + TPMT_PUBLIC tpl; + byte ss1[64]; + byte ss2[64]; + int ss1Sz = (int)sizeof(ss1); + int ss2Sz = (int)sizeof(ss2); + byte* ct = NULL; + int ctSz = MAX_MLKEM_CT_SIZE; + + XMEMSET(&key, 0, sizeof(key)); + XMEMSET(&tpl, 0, sizeof(tpl)); + XMEMSET(ss1, 0, sizeof(ss1)); + XMEMSET(ss2, 0, sizeof(ss2)); + + (void)pqc_flush_transient(dev); + + ct = (byte*)XMALLOC(MAX_MLKEM_CT_SIZE, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (ct == NULL) + return MEMORY_E; + + rc = wolfTPM2_GetKeyTemplate_MLKEM(&tpl, + TPMA_OBJECT_decrypt | TPMA_OBJECT_fixedTPM | TPMA_OBJECT_fixedParent | + TPMA_OBJECT_sensitiveDataOrigin | TPMA_OBJECT_userWithAuth | + TPMA_OBJECT_noDA, ps); + if (rc != TPM_RC_SUCCESS) goto exit; + + rc = wolfTPM2_CreatePrimaryKey(dev, &key, TPM_RH_OWNER, &tpl, NULL, 0); + if (rc != TPM_RC_SUCCESS) goto exit; + + rc = wolfTPM2_Encapsulate(dev, &key, ct, &ctSz, ss1, &ss1Sz); + if (rc != TPM_RC_SUCCESS) goto exit; + + rc = wolfTPM2_Decapsulate(dev, &key, ct, ctSz, ss2, &ss2Sz); + if (rc != TPM_RC_SUCCESS) goto exit; + + if (ss1Sz <= 0 || ss1Sz != ss2Sz || XMEMCMP(ss1, ss2, ss1Sz) != 0) { + printf("ML-KEM-%s: decapsulated secret does not match\n", + mlkemName(ps)); + rc = TPM_RC_FAILURE; + } + +exit: + if (rc == TPM_RC_SUCCESS) { + printf("PASS ML-KEM-%-3s encap+decap (ct %d bytes)\n", + mlkemName(ps), ctSz); + } + else { + printf("FAIL ML-KEM-%-3s 0x%x: %s\n", + mlkemName(ps), rc, wolfTPM2_GetRCString(rc)); + } + wc_ForceZero(ss1, sizeof(ss1)); + wc_ForceZero(ss2, sizeof(ss2)); + wolfTPM2_UnloadHandle(dev, &key.handle); + XFREE(ct, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return rc; +} + +/* Run one --all step and keep the first non-zero return code. */ +static void allStep(int rc, int* firstErr) +{ + if (rc != 0 && *firstErr == 0) { + *firstErr = rc; + } +} + +static int do_all(WOLFTPM2_DEV* dev) +{ + int firstErr = 0; + + printf("=== TPM control + PQC full matrix ===\n"); + allStep(do_caps(dev), &firstErr); + allStep(do_algs(dev), &firstErr); + allStep(do_selftest(dev), &firstErr); + allStep(do_getrandom(dev, 16), &firstErr); + allStep(do_pcrread(dev, 0), &firstErr); + allStep(do_mldsa(dev, TPM_MLDSA_44), &firstErr); + allStep(do_mldsa(dev, TPM_MLDSA_65), &firstErr); + allStep(do_mldsa(dev, TPM_MLDSA_87), &firstErr); + allStep(do_hash_mldsa(dev, TPM_MLDSA_44), &firstErr); + allStep(do_hash_mldsa(dev, TPM_MLDSA_65), &firstErr); + allStep(do_hash_mldsa(dev, TPM_MLDSA_87), &firstErr); + allStep(do_mlkem(dev, TPM_MLKEM_512), &firstErr); + allStep(do_mlkem(dev, TPM_MLKEM_768), &firstErr); + allStep(do_mlkem(dev, TPM_MLKEM_1024), &firstErr); + return firstErr; +} + +/* Match "name" (bare flag, sets val="") or "name=VALUE" (sets val=VALUE). + * Returns 0 when arg is a different option, so a missing '=' such as + * "--pcrextend16" does not match and is reported as unrecognized. */ +static int matchOpt(const char* arg, const char* name, const char** val) +{ + word32 nameLen = (word32)XSTRLEN(name); + + if (XSTRNCMP(arg, name, nameLen) != 0) { + return 0; + } + if (arg[nameLen] == '\0') { + *val = ""; + return 1; + } + if (arg[nameLen] == '=') { + if (arg[nameLen + 1] == '\0') { + return 0; /* "name=" with no value is not a valid option */ + } + *val = arg + nameLen + 1; + return 1; + } + return 0; +} + +/* Return 1 if val is all digits and parses to a number in [lo, hi]. An empty + * string returns 0, so callers decide whether a bare flag is acceptable. */ +static int validNum(const char* val, int lo, int hi) +{ + int i; + int n; + + /* Reject empty and over-long inputs (guards XATOI against overflow, which + * could otherwise wrap a huge value back into [lo, hi]). */ + if (val[0] == '\0' || XSTRLEN(val) > 3) { + return 0; + } + for (i = 0; val[i] != '\0'; i++) { + if (val[i] < '0' || val[i] > '9') { + return 0; + } + } + n = XATOI(val); + return (n >= lo && n <= hi); +} + +/* Handle one option. When validateOnly is set, only check that the option is + * recognized and its value is valid — no TPM access, dev may be NULL; otherwise + * execute it. This is the single source of truth for the option set, shared by + * the pre-execution validation pass and the execution pass, so there is no + * duplicated option table. Returns 0 on success, BAD_FUNC_ARG for an unknown + * option or bad value, or a TPM return code from the executed operation. */ +static int handleOpt(WOLFTPM2_DEV* dev, const char* arg, int validateOnly) +{ + int rc; + const char* val; + TPMI_MLDSA_PARAMETER_SET ms; + TPMI_MLKEM_PARAMETER_SET ks; + + if (XSTRCMP(arg, "--caps") == 0) { + return validateOnly ? 0 : do_caps(dev); + } + if (XSTRCMP(arg, "--algs") == 0) { + return validateOnly ? 0 : do_algs(dev); + } + if (XSTRCMP(arg, "--selftest") == 0) { + return validateOnly ? 0 : do_selftest(dev); + } + if (XSTRCMP(arg, "--clear") == 0) { + return validateOnly ? 0 : do_clear(dev); + } + if (XSTRCMP(arg, "--all") == 0) { + return validateOnly ? 0 : do_all(dev); + } + if (XSTRCMP(arg, "--flush") == 0) { + if (validateOnly) { + return 0; + } + rc = pqc_flush_transient(dev); + if (rc != TPM_RC_SUCCESS) { + printf("FAIL flush 0x%x: %s\n", rc, wolfTPM2_GetRCString(rc)); + return rc; + } + printf("Flushed transient objects\n"); + return TPM_RC_SUCCESS; + } + if (matchOpt(arg, "--getrandom", &val)) { + if (val[0] != '\0' && !validNum(val, 1, 64)) { + printf("Error: --getrandom length must be 1..64: %s\n", arg); + return BAD_FUNC_ARG; + } + return validateOnly ? 0 : + do_getrandom(dev, val[0] == '\0' ? 16 : XATOI(val)); + } + if (matchOpt(arg, "--pcrread", &val)) { + if (val[0] != '\0' && !validNum(val, 0, 23)) { + printf("Error: --pcrread index must be 0..23: %s\n", arg); + return BAD_FUNC_ARG; + } + return validateOnly ? 0 : do_pcrread(dev, XATOI(val)); + } + if (matchOpt(arg, "--pcrextend", &val)) { + /* extend is irreversible: require an explicit in-range index */ + if (!validNum(val, 0, 23)) { + printf("Error: --pcrextend needs an explicit PCR 0..23: %s\n", arg); + return BAD_FUNC_ARG; + } + return validateOnly ? 0 : do_pcrextend(dev, XATOI(val)); + } + if (matchOpt(arg, "--hash-mldsa", &val)) { + if (parseMldsaSet(val, &ms) != 0) { + printf("Error: invalid ML-DSA parameter set: %s\n", arg); + return BAD_FUNC_ARG; + } + return validateOnly ? 0 : do_hash_mldsa(dev, ms); + } + if (matchOpt(arg, "--mldsa", &val)) { + if (parseMldsaSet(val, &ms) != 0) { + printf("Error: invalid ML-DSA parameter set: %s\n", arg); + return BAD_FUNC_ARG; + } + return validateOnly ? 0 : do_mldsa(dev, ms); + } + if (matchOpt(arg, "--mlkem", &val)) { + if (parseMlkemSet(val, &ks) != 0) { + printf("Error: invalid ML-KEM parameter set: %s\n", arg); + return BAD_FUNC_ARG; + } + return validateOnly ? 0 : do_mlkem(dev, ks); + } + printf("Error: Unrecognized option: %s\n", arg); + return BAD_FUNC_ARG; +} + +static int pqc_ctrl_run(int argc, char* argv[]) +{ + int rc = 0; + int opRc; + int i; + WOLFTPM2_DEV dev; + + for (i = 1; i < argc; i++) { + if (XSTRCMP(argv[i], "-?") == 0 || XSTRCMP(argv[i], "-h") == 0 || + XSTRCMP(argv[i], "--help") == 0) { + usage(); + return 0; + } + } + + /* Validation pass: reject a bad argument set before touching the TPM or + * running any op, so a typo cannot run an earlier --clear or PCR extend. */ + for (i = 1; i < argc; i++) { + if (handleOpt(NULL, argv[i], 1) != 0) { + usage(); + return BAD_FUNC_ARG; + } + } + + XMEMSET(&dev, 0, sizeof(dev)); + rc = wolfTPM2_Init(&dev, TPM2_IoCb, NULL); + if (rc != TPM_RC_SUCCESS) { + printf("wolfTPM2_Init failed 0x%x: %s\n", + rc, wolfTPM2_GetRCString(rc)); + return rc; + } + + /* No arguments: run the full control + PQC matrix. */ + if (argc <= 1) { + rc = do_all(&dev); + } + + /* Execution pass: every argument was validated above. */ + for (i = 1; i < argc; i++) { + opRc = handleOpt(&dev, argv[i], 0); + if (opRc != 0 && rc == 0) { + rc = opRc; + } + } + + wolfTPM2_Cleanup(&dev); + return rc; +} + +#endif /* !WOLFTPM2_NO_WRAPPER && PQC ops */ + +#ifndef NO_MAIN_DRIVER +int main(int argc, char* argv[]) +{ +#if !defined(WOLFTPM2_NO_WRAPPER) && \ + defined(WOLFTPM_MLDSA_SIGN) && defined(WOLFTPM_MLDSA_VERIFY) && \ + defined(WOLFTPM_MLKEM_ENCAP) && defined(WOLFTPM_MLKEM_DECAP) && \ + defined(WOLFTPM_HASH_MLDSA) + int rc = pqc_ctrl_run(argc, argv); + return (rc == 0) ? 0 : 1; +#else + (void)argc; + (void)argv; + printf("pqc_ctrl requires --enable-v185 (ML-DSA + ML-KEM + Hash-ML-DSA)\n"); + return 0; +#endif +} +#endif /* NO_MAIN_DRIVER */ diff --git a/examples/pqc/pqc_ctrl.sh b/examples/pqc/pqc_ctrl.sh new file mode 100755 index 00000000..05590822 --- /dev/null +++ b/examples/pqc/pqc_ctrl.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# pqc_ctrl.sh - PQC control-center command tests (SealSQ QVault TPM / fwTPM) +# +# Copyright (C) 2006-2026 wolfSSL Inc. +# +# This file is part of wolfTPM. +# +# wolfTPM is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# wolfTPM is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + +# Exercises every pqc_ctrl command against a PQC TPM. Point it at the SealSQ +# QVault TPM part (built with --enable-sealsq --enable-pqc) or the fwTPM (built +# with --enable-fwtpm --enable-swtpm, with fwtpm_server listening). + +PQC_CTRL="${1:-./examples/pqc/pqc_ctrl}" +PASS=0 FAIL=0 TOTAL=0 + +if [ -t 1 ]; then + GREEN='\033[0;32m' RED='\033[0;31m' NC='\033[0m' +else + GREEN='' RED='' NC='' +fi + +# run_test NAME EXPECT CMD... +# EXPECT is text that must appear in the output ("" requires only exit 0). +run_test() { + local name="$1"; local expect="$2"; shift 2 + local out rc + TOTAL=$((TOTAL + 1)) + echo "[$TOTAL] $name" + out=$("$@" 2>&1); rc=$? + if [ $rc -eq 0 ] && { [ -z "$expect" ] || echo "$out" | grep -q "$expect"; }; then + echo -e " ${GREEN}PASS${NC}"; PASS=$((PASS + 1)) + else + echo -e " ${RED}FAIL${NC} (rc=$rc)"; FAIL=$((FAIL + 1)) + echo "$out" | tail -3 | sed 's/^/ /' + fi + echo "" +} + +# run_neg NAME CMD... — a bad argument set must exit non-zero without running +# any operation. Passes only when the tool rejects it. +run_neg() { + local name="$1"; shift + local out rc + TOTAL=$((TOTAL + 1)) + echo "[$TOTAL] $name (must reject)" + out=$("$@" 2>&1); rc=$? + # Must exit non-zero AND print a validation error, so the test proves the + # argument was rejected — not that the tool failed for some other reason + # (e.g. no TPM available). + if [ $rc -ne 0 ] && echo "$out" | grep -q "Error"; then + echo -e " ${GREEN}PASS${NC}"; PASS=$((PASS + 1)) + else + echo -e " ${RED}FAIL${NC} (rc=$rc, no validation error)"; FAIL=$((FAIL + 1)) + fi + echo "" +} + +if [ ! -x "$PQC_CTRL" ]; then + echo "Error: $PQC_CTRL not found." + echo "Build with: ./configure --enable-sealsq --enable-pqc && make" + echo "Usage: $0 [path-to-pqc_ctrl]" + exit 1 +fi + +echo "=== PQC Control Center Tests ===" +echo "Tool: $PQC_CTRL" +echo "" + +# Board control / management commands +run_test "Capabilities" "Vendor" "$PQC_CTRL" --caps +run_test "Algorithm list" "ML-KEM" "$PQC_CTRL" --algs +run_test "Self test" "SelfTest" "$PQC_CTRL" --selftest +run_test "Get random (32 bytes)" "Random 32" "$PQC_CTRL" --getrandom=32 +run_test "PCR read (index 0)" "PCR0" "$PQC_CTRL" --pcrread=0 +run_test "Flush transient" "Flushed" "$PQC_CTRL" --flush + +# Pure ML-DSA — every parameter set +for PS in 44 65 87; do + run_test "ML-DSA-$PS sign/verify" "ML-DSA-$PS" "$PQC_CTRL" --mldsa=$PS +done + +# Hash-ML-DSA (SHA-256 pre-hash) — every parameter set +for PS in 44 65 87; do + run_test "Hash-ML-DSA-$PS sign/verify" "HashML-DSA-$PS" \ + "$PQC_CTRL" --hash-mldsa=$PS +done + +# ML-KEM — every parameter set +for PS in 512 768 1024; do + run_test "ML-KEM-$PS encap/decap" "ML-KEM-$PS" "$PQC_CTRL" --mlkem=$PS +done + +# Full matrix in a single process (expect a real op line so a not-compiled-in +# stub, which exits 0 with a "requires --enable-v185" message, cannot pass). +run_test "Full matrix (--all)" "ML-KEM-1024" "$PQC_CTRL" --all + +# Irreversible state-modifying commands (a PCR extend cannot be undone without a +# TPM reset; TPM2_Clear wipes the owner hierarchy). Both are off by default so +# the suite never changes TPM state unexpectedly; opt in with PQC_CTRL_CLEAR=1. +if [ "${PQC_CTRL_CLEAR:-0}" = "1" ]; then + run_test "PCR extend (index 16)" "PCR16 extend" "$PQC_CTRL" --pcrextend=16 + run_test "TPM clear" "Clear" "$PQC_CTRL" --clear +fi + +# Argument-validation negatives: each must be rejected before any op runs. +run_neg "Invalid ML-DSA set" "$PQC_CTRL" --mldsa=abc +run_neg "Out-of-range getrandom" "$PQC_CTRL" --getrandom=999 +run_neg "Bare pcrextend" "$PQC_CTRL" --pcrextend +run_neg "Unknown option" "$PQC_CTRL" --bogus --clear + +echo "" +echo "=== Results: $TOTAL total, $PASS passed, $FAIL failed ===" +if [ $FAIL -eq 0 ]; then + echo -e "${GREEN}ALL TESTS PASSED${NC}"; exit 0 +else + echo -e "${RED}$FAIL TEST(S) FAILED${NC}"; exit 1 +fi diff --git a/examples/run_examples.sh b/examples/run_examples.sh index 6edd6bc7..4cd69373 100755 --- a/examples/run_examples.sh +++ b/examples/run_examples.sh @@ -376,6 +376,13 @@ if [ $ENABLE_V185 -eq 1 ]; then [ $RESULT -ne 0 ] && echo -e "mlkem_encap mlkem=$PS failed! $RESULT" && exit 1 done + echo -e "PQC control tool (pqc_ctrl board commands)" + # Board-control commands only; the ML-DSA/ML-KEM matrix is already covered + # by the standalone example loops above, so do not re-run it here. + ./examples/pqc/pqc_ctrl --caps --algs --selftest --getrandom=16 >> $TPMPWD/run.out 2>&1 + RESULT=$? + [ $RESULT -ne 0 ] && echo -e "pqc_ctrl board commands failed! $RESULT" && exit 1 + echo -e "PQC primary key (create_primary -mldsa)" for PS in 44 65 87; do ./examples/keygen/create_primary -mldsa=$PS -oh >> $TPMPWD/run.out 2>&1 diff --git a/hal/tpm_io_linux.c b/hal/tpm_io_linux.c index 4ecf6eca..626ab352 100644 --- a/hal/tpm_io_linux.c +++ b/hal/tpm_io_linux.c @@ -92,6 +92,9 @@ #elif defined(WOLFTPM_NATIONS) /* Nations Technology NS350 uses CE0 */ #define TPM2_SPI_DEV_CS "0" + #elif defined(WOLFTPM_SEALSQ) + /* SealSQ QVault TPM uses CE0 */ + #define TPM2_SPI_DEV_CS "0" #else /* OPTIGA SLB9670/SLB9762 and LetsTrust TPM use CE1 */ #define TPM2_SPI_DEV_CS "1" diff --git a/src/tpm2_wrap.c b/src/tpm2_wrap.c index 1dd1a26b..868d77f0 100644 --- a/src/tpm2_wrap.c +++ b/src/tpm2_wrap.c @@ -861,6 +861,10 @@ static int wolfTPM2_ParseCapabilities(WOLFTPM2_CAPS* caps, caps->mfg = TPM_MFG_NATIONTECH; caps->req_wait_state = 1; } + else if (XMEMCMP(&caps->mfgStr, "SEAL", 4) == 0) { + caps->mfg = TPM_MFG_SEALSQ; + caps->req_wait_state = 1; + } break; case TPM_PT_VENDOR_STRING_1: case TPM_PT_VENDOR_STRING_2: diff --git a/wolftpm/tpm2.h b/wolftpm/tpm2.h index dbba0b8d..d572c19a 100644 --- a/wolftpm/tpm2.h +++ b/wolftpm/tpm2.h @@ -4386,6 +4386,7 @@ typedef enum { TPM_VENDOR_MCHP = 0x1114, TPM_VENDOR_NUVOTON = 0x1050, TPM_VENDOR_NATIONTECH = 0x1B4E, + TPM_VENDOR_SEALSQ = 0x2406, } TPM_Vendor_t; diff --git a/wolftpm/tpm2_types.h b/wolftpm/tpm2_types.h index a30c00d7..41702f2f 100644 --- a/wolftpm/tpm2_types.h +++ b/wolftpm/tpm2_types.h @@ -474,6 +474,17 @@ typedef int64_t INT64; /* Max: 43MHz */ #define TPM2_SPI_MAX_HZ TPM2_SPI_MAX_HZ_NUVOTON #endif +#elif defined(WOLFTPM_SEALSQ) + /* SealSQ QVault TPM */ + /* Requires wait state support */ + #ifndef WOLFTPM_CHECK_WAIT_STATE + #define WOLFTPM_CHECK_WAIT_STATE + #endif + #define TPM2_SPI_MAX_HZ_SEALSQ 33000000 + #ifndef TPM2_SPI_MAX_HZ + /* Max: 33MHz */ + #define TPM2_SPI_MAX_HZ TPM2_SPI_MAX_HZ_SEALSQ + #endif #else /* Infineon OPTIGA SLB9670/SLB9672/SLB9673 */ #ifdef WOLFTPM_SLB9670 diff --git a/wolftpm/tpm2_wrap.h b/wolftpm/tpm2_wrap.h index 4f40378b..eab5fc51 100644 --- a/wolftpm/tpm2_wrap.h +++ b/wolftpm/tpm2_wrap.h @@ -149,6 +149,7 @@ typedef enum WOLFTPM2_MFG { TPM_MFG_MCHP, TPM_MFG_NUVOTON, TPM_MFG_NATIONTECH, + TPM_MFG_SEALSQ, } WOLFTPM2_MFG; typedef struct WOLFTPM2_CAPS {