Add SealSQ QVault TPM post-quantum support and pqc_ctrl example

pull/570/head
Aidan Garske 2026-08-14 19:44:00 +01:00
parent 46d2d337a4
commit b57d851156
15 changed files with 1108 additions and 13 deletions

View File

@ -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

View File

@ -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

3
.gitignore vendored
View File

@ -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

View File

@ -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:
```

View File

@ -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"

View File

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

View File

@ -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

View File

@ -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 <config.h>
#endif
#include <wolftpm/tpm2_wrap.h>
#include <hal/tpm_io.h>
#include <examples/tpm_test.h>
#include <stdio.h>
#include <string.h>
#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 */

View File

@ -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

View File

@ -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

View File

@ -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"

View File

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

View File

@ -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;

View File

@ -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

View File

@ -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 {