Add TPM backed ML-DSA authentication for TLS 1.3 with PQC example and tests

pull/559/head
aidan garske 2026-07-22 11:37:40 -07:00 committed by aidan garske
parent 7d02b58e4c
commit 0063c16e7e
15 changed files with 1490 additions and 4 deletions

View File

@ -172,3 +172,87 @@ jobs:
config.log
run.out
retention-days: 5
# Full TPM-backed PQC TLS 1.3 handshake: ML-KEM key exchange + ML-DSA
# CertificateVerify signed on the TPM (device key never leaves the chip).
pqc-tls-examples:
name: PQC TLS 1.3 examples (ML-DSA auth + ML-KEM)
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
runs-on: ubuntu-latest
container:
image: ghcr.io/wolfssl/wolftpm-ci:v1.0
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 30
steps:
- name: Checkout wolfTPM
uses: actions/checkout@v4
# TODO(revert-when-merged): build wolfSSL from the aidangarske/wolfssl
# mldsa-signctx-cryptocb-devkey branch because the wc_MlDsaKey_SignCtx
# crypto-callback fix (device-key ML-DSA signing) is not yet upstream.
# Once that wolfSSL PR merges, switch this back to a wolfSSL/wolfssl tag
# (>= the release carrying the fix) and delete this note.
- name: Build wolfSSL (PQC TLS + crypto-cb ML-DSA device-key fix)
run: |
cd ~
git clone --depth 1 \
--branch mldsa-signctx-cryptocb-devkey \
https://github.com/aidangarske/wolfssl.git
cd wolfssl
./autogen.sh
./configure --enable-wolftpm --enable-pkcallbacks --enable-keygen \
--enable-certgen --enable-dilithium --enable-mlkem \
--enable-experimental --enable-tls-mlkem-standalone --enable-harden \
CPPFLAGS="-DWOLFSSL_TLSX_PQC_MLKEM_STORE_OBJ -DWC_RSA_NO_PADDING" \
--prefix="$HOME/wolfssl-install"
make -j"$(nproc)"
make install
- name: wolfSSL version info
run: grep LIBWOLFSSL_VERSION_STRING "$HOME/wolfssl-install/include/wolfssl/version.h"
- name: Build wolfTPM with v1.85 + fwTPM + debug
run: |
./autogen.sh
CPPFLAGS="-I$HOME/wolfssl-install/include" \
LDFLAGS="-L$HOME/wolfssl-install/lib -Wl,-rpath,$HOME/wolfssl-install/lib" \
./configure --enable-v185 --enable-fwtpm --enable-debug=verbose
make -j"$(nproc)"
# A stub means wolfSSL was missing a required feature (cert-gen, ML-DSA
# keygen/sign, private-key-id, or TLS 1.3) — fail loudly rather than skip.
- name: Verify PQC TLS examples built (not stubs)
run: |
for b in examples/pqc/gen_pqc_certs \
examples/tls/tls_server_pq examples/tls/tls_client_pq; do
if ./$b -h 2>&1 | grep -q "Requires"; then
echo "FAIL: $b is a stub — wolfSSL missing a required feature"
exit 1
fi
done
- name: PQC TLS handshake E2E (ML-KEM x ML-DSA matrix)
run: |
export LD_LIBRARY_PATH="$HOME/wolfssl-install/lib"
rc=0
for combo in "ML_KEM_768 65" "ML_KEM_512 44" \
"ML_KEM_1024 87" "SECP256R1MLKEM768 65"; do
set -- $combo
echo "== group $1 / ML-DSA-$2 =="
if ! ./tests/tls_pq_e2e.sh "$1" "$2"; then rc=1; break; fi
done
exit $rc
- name: Upload failure logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: pqc-tls-examples-logs
path: |
/tmp/tls_pq_e2e_*.log
config.log
tests/*.log
retention-days: 5

5
.gitignore vendored
View File

@ -64,11 +64,14 @@ pkcs7tpmsigned.p7s
pkcs7tpmsignedex.p7s
examples/tls/tls_server
examples/tls/tls_client_notpm
examples/tls/tls_client_pq
examples/tls/tls_server_pq
tests/unit.test
tests/unit.log
tests/fwtpm_unit.log
tests/fwtpm_unit.test
tests/fwtpm_check.sh.log
tests/tls_pq_e2e.sh.log
examples/keygen/create_primary
examples/keygen/keyload
examples/keygen/keygen
@ -78,6 +81,7 @@ examples/keygen/ecdh
examples/pqc/mldsa_sign
examples/pqc/mlkem_encap
examples/pqc/pqc_mssim_e2e
examples/pqc/gen_pqc_certs
examples/nvram/extend
examples/nvram/store
examples/nvram/read
@ -118,6 +122,7 @@ certs/server-*.der
certs/server-*.pem
certs/client-*.der
certs/client-*.pem
certs/pq-*.der
certs/serial.old
certs/0*.pem
certs/1*.pem

View File

@ -14,11 +14,15 @@ the full fwTPM PQC reference.
```
./configure --enable-wolftpm --enable-mldsa --enable-mlkem \
--enable-harden --enable-keygen
--enable-harden --enable-keygen --enable-certgen
make
sudo make install
```
`--enable-certgen` is needed by the TLS `gen_pqc_certs` tool below;
`--enable-wolftpm` provides the crypto callback and private-key-id support the
TLS server uses.
**wolfTPM**:
```
@ -172,3 +176,56 @@ restricted key with no symmetric algorithm via `TPM_RC_SYMMETRIC`).
./examples/keygen/create_primary -mldsa # default MLDSA-65
./examples/keygen/create_primary -mldsa=87 -oh
```
## Post-Quantum TLS 1.3 (ML-KEM + TPM ML-DSA)
A full TLS 1.3 handshake where the server's ML-DSA identity key lives in the
TPM. The server signs the CertificateVerify on-chip via the wolfTPM crypto
callback; the client performs an ML-KEM key exchange and validates the server
against a software CA.
Requires wolfSSL with a fix that routes `wc_MlDsaKey_SignCtx` to the crypto
callback for device keys (private key in the TPM). No shipping TPM implements
TCG v1.85 PQC yet, so this runs against the in-tree fwTPM.
Demo scope: the identity key is an unauthenticated deterministic TPM primary
(empty auth), reproducible by both `gen_pqc_certs` and the server from the owner
hierarchy. A production deployment should protect the identity key with a
non-empty auth value or policy so it cannot be recreated from the public cert.
The client validates the server chain against the demo CA but does not bind the
certificate to the host name; a production client should also issue the leaf with
a matching subjectAltName and call `wolfSSL_check_domain_name` before connecting.
Three programs:
- `examples/pqc/gen_pqc_certs` — makes a software ML-DSA CA and a device leaf
cert whose subject key is the TPM ML-DSA key.
- `examples/tls/tls_server_pq` — recreates that TPM key and serves TLS 1.3.
- `examples/tls/tls_client_pq` — connects, ML-KEM key exchange, verifies the CA.
```
./src/fwtpm/fwtpm_server --clear &
# 1. certificate chain bound to the TPM key (-mldsa must match the server)
./examples/pqc/gen_pqc_certs -mldsa=65
# 2. server (same -mldsa as gen_pqc_certs)
./examples/tls/tls_server_pq -p=11111 -mldsa=65 &
# 3. client (choose the ML-KEM group)
./examples/tls/tls_client_pq -h=localhost -p=11111 -group=ML_KEM_768
```
Options:
- `gen_pqc_certs -mldsa=44/65/87` — ML-DSA parameter set.
- `tls_server_pq -p=<port> -mldsa=44/65/87`.
- `tls_client_pq -h=<host> -p=<port> -group=<name>` where `<name>` is
`ML_KEM_512/768/1024` or a hybrid `SECP256R1MLKEM768` / `X25519MLKEM768`
(hybrids need the matching classical curve enabled in wolfSSL).
The one-shot end-to-end test drives all three and asserts the ML-KEM group,
TPM-signed ML-DSA authentication, CA verification, and app data:
```
./tests/tls_pq_e2e.sh # ML_KEM_768 + ML-DSA-65
./tests/tls_pq_e2e.sh SECP256R1MLKEM768 87
```

View File

@ -0,0 +1,299 @@
/* gen_pqc_certs.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
*/
/* Generate a software ML-DSA CA and a device leaf certificate whose subject key
* is a TPM-resident ML-DSA key, for the TLS PQC examples. */
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <wolftpm/tpm2_wrap.h>
#if !defined(WOLFTPM2_NO_WRAPPER) && !defined(WOLFTPM2_NO_WOLFCRYPT) && \
!defined(NO_FILESYSTEM) && \
defined(WOLFTPM_MLDSA) && defined(WOLFSSL_CERT_GEN) && \
!defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \
!defined(WOLFSSL_DILITHIUM_NO_MAKE_KEY) && \
!defined(WOLFSSL_MLDSA_NO_SIGN) && !defined(WOLFSSL_DILITHIUM_NO_SIGN) && \
!defined(WOLFSSL_MLDSA_NO_ASN1) && !defined(WOLFSSL_DILITHIUM_NO_ASN1)
#include <hal/tpm_io.h>
#include <examples/tpm_test.h>
#include <wolfssl/wolfcrypt/wc_mldsa.h>
#include <wolfssl/wolfcrypt/asn_public.h>
#include <wolfssl/wolfcrypt/random.h>
#include <stdio.h>
#include <string.h>
#define CA_CERT_FILE "./certs/pq-ca-cert.der"
#define SERVER_CERT_FILE "./certs/pq-server-cert.der"
/* generous for ML-DSA-87 cert (pub ~2592 + sig ~4627 + overhead) */
#define PQC_CERT_BUF_SZ 10000
static void usage(void)
{
printf("Expected usage:\n");
printf("./examples/pqc/gen_pqc_certs [-mldsa=44/65/87]\n");
printf("* -mldsa=44/65/87: ML-DSA parameter set (default 65)\n");
printf("Outputs %s and %s\n", CA_CERT_FILE, SERVER_CERT_FILE);
}
static int parseParamSet(const char* v, TPMI_MLDSA_PARAMETER_SET* ps)
{
if (XSTRCMP(v, "44") == 0) { *ps = TPM_MLDSA_44; return 0; }
if (XSTRCMP(v, "65") == 0) { *ps = TPM_MLDSA_65; return 0; }
if (XSTRCMP(v, "87") == 0) { *ps = TPM_MLDSA_87; return 0; }
return BAD_FUNC_ARG;
}
static int mldsaTypes(TPMI_MLDSA_PARAMETER_SET ps, int* keyType, int* sigType,
int* wcLevel)
{
switch (ps) {
case TPM_MLDSA_44:
*keyType = ML_DSA_44_TYPE; *sigType = CTC_ML_DSA_44;
*wcLevel = WC_ML_DSA_44; return 0;
case TPM_MLDSA_65:
*keyType = ML_DSA_65_TYPE; *sigType = CTC_ML_DSA_65;
*wcLevel = WC_ML_DSA_65; return 0;
case TPM_MLDSA_87:
*keyType = ML_DSA_87_TYPE; *sigType = CTC_ML_DSA_87;
*wcLevel = WC_ML_DSA_87; return 0;
default:
return BAD_FUNC_ARG;
}
}
static int writeDer(const char* file, const byte* der, int derSz)
{
XFILE f = XFOPEN(file, "wb");
if (f == XBADFILE) {
printf("Failed to open %s for write\n", file);
return -1;
}
if (XFWRITE(der, 1, (size_t)derSz, f) != (size_t)derSz) {
XFCLOSE(f);
return -1;
}
XFCLOSE(f);
return 0;
}
static int TPM2_PQC_GenCerts(void* userCtx, int argc, char *argv[])
{
int rc = 0, sz = 0;
WOLFTPM2_DEV dev;
WOLFTPM2_KEY mldsaKey;
TPMT_PUBLIC pub;
TPMI_MLDSA_PARAMETER_SET paramSet = TPM_MLDSA_65;
int keyType = ML_DSA_65_TYPE, sigType = CTC_ML_DSA_65;
int wcLevel = WC_ML_DSA_65;
WC_RNG rng;
wc_MlDsaKey caKey;
wc_MlDsaKey tpmPubKey;
Cert caCert;
Cert leafCert;
byte* caDer = NULL;
byte* leafDer = NULL;
int caSz = 0, leafSz = 0;
int haveRng = 0, haveCaKey = 0, havePubKey = 0;
XMEMSET(&dev, 0, sizeof(dev));
XMEMSET(&mldsaKey, 0, sizeof(mldsaKey));
XMEMSET(&pub, 0, sizeof(pub));
XMEMSET(&caKey, 0, sizeof(caKey));
XMEMSET(&tpmPubKey, 0, sizeof(tpmPubKey));
XMEMSET(&caCert, 0, sizeof(caCert));
XMEMSET(&leafCert, 0, sizeof(leafCert));
if (argc >= 2 && (XSTRCMP(argv[1], "-?") == 0 ||
XSTRCMP(argv[1], "-h") == 0 || XSTRCMP(argv[1], "--help") == 0)) {
usage();
return 0;
}
while (argc > 1) {
if (XSTRNCMP(argv[argc-1], "-mldsa=", 7) == 0) {
if (parseParamSet(argv[argc-1] + 7, &paramSet) != 0) {
usage();
return BAD_FUNC_ARG;
}
}
argc--;
}
(void)mldsaTypes(paramSet, &keyType, &sigType, &wcLevel);
caDer = (byte*)XMALLOC(PQC_CERT_BUF_SZ, NULL, DYNAMIC_TYPE_TMP_BUFFER);
leafDer = (byte*)XMALLOC(PQC_CERT_BUF_SZ, NULL, DYNAMIC_TYPE_TMP_BUFFER);
if (caDer == NULL || leafDer == NULL) {
rc = MEMORY_E;
}
if (rc == 0) {
rc = wolfTPM2_Init(&dev, TPM2_IoCb, userCtx);
if (rc != TPM_RC_SUCCESS) {
printf("wolfTPM2_Init failed 0x%x: %s\n", rc,
wolfTPM2_GetRCString(rc));
}
}
if (rc == 0) {
/* TPM ML-DSA device key (deterministic; the server recreates it) */
rc = wolfTPM2_GetKeyTemplate_MLDSA(&pub,
TPMA_OBJECT_sign | TPMA_OBJECT_fixedTPM | TPMA_OBJECT_fixedParent |
TPMA_OBJECT_sensitiveDataOrigin | TPMA_OBJECT_userWithAuth |
TPMA_OBJECT_noDA, paramSet, 0);
if (rc == TPM_RC_SUCCESS) {
rc = wolfTPM2_CreatePrimaryKey(&dev, &mldsaKey, TPM_RH_OWNER, &pub,
NULL, 0);
}
if (rc != TPM_RC_SUCCESS) {
printf("Create TPM ML-DSA key failed 0x%x: %s\n",
rc, wolfTPM2_GetRCString(rc));
}
else {
printf("TPM ML-DSA device key: handle 0x%08x, pub %u bytes\n",
(unsigned)mldsaKey.handle.hndl,
(unsigned)mldsaKey.pub.publicArea.unique.mldsa.size);
}
}
if (rc == 0) {
rc = wc_InitRng(&rng);
if (rc == 0) haveRng = 1;
}
if (rc == 0) {
/* software CA key */
rc = wc_MlDsaKey_Init(&caKey, NULL, INVALID_DEVID);
if (rc == 0) haveCaKey = 1;
if (rc == 0) rc = wc_MlDsaKey_SetParams(&caKey, wcLevel);
if (rc == 0) rc = wc_MlDsaKey_MakeKey(&caKey, &rng);
if (rc != 0) printf("CA key gen failed %d\n", rc);
}
if (rc == 0) {
/* self-signed CA cert */
rc = wc_InitCert(&caCert);
}
if (rc == 0) {
caCert.daysValid = 365;
caCert.selfSigned = 1;
caCert.isCA = 1;
caCert.sigType = sigType;
XSTRNCPY(caCert.subject.country, "US", CTC_NAME_SIZE);
XSTRNCPY(caCert.subject.org, "wolfSSL", CTC_NAME_SIZE);
XSTRNCPY(caCert.subject.commonName, "wolfTPM PQC Demo CA",
CTC_NAME_SIZE);
XMEMCPY(&caCert.issuer, &caCert.subject, sizeof(CertName));
sz = wc_MakeCert_ex(&caCert, caDer, PQC_CERT_BUF_SZ, keyType, &caKey,
&rng);
if (sz < 0) rc = sz;
}
if (rc == 0) {
sz = wc_SignCert_ex(caCert.bodySz, caCert.sigType, caDer,
PQC_CERT_BUF_SZ, keyType, &caKey, &rng);
if (sz < 0) rc = sz;
else caSz = sz;
}
if (rc == 0) {
/* TPM public key for the leaf subject */
rc = wc_MlDsaKey_Init(&tpmPubKey, NULL, INVALID_DEVID);
if (rc == 0) havePubKey = 1;
if (rc == 0) rc = wc_MlDsaKey_SetParams(&tpmPubKey, wcLevel);
if (rc == 0) rc = wc_MlDsaKey_ImportPubRaw(&tpmPubKey,
mldsaKey.pub.publicArea.unique.mldsa.buffer,
mldsaKey.pub.publicArea.unique.mldsa.size);
if (rc != 0) printf("import TPM pub failed %d\n", rc);
}
if (rc == 0) {
/* device leaf cert: subject = TPM key, issuer = CA, CA-signed */
rc = wc_InitCert(&leafCert);
}
if (rc == 0) {
leafCert.daysValid = 365;
leafCert.isCA = 0;
leafCert.sigType = sigType;
XSTRNCPY(leafCert.subject.country, "US", CTC_NAME_SIZE);
XSTRNCPY(leafCert.subject.org, "wolfSSL", CTC_NAME_SIZE);
XSTRNCPY(leafCert.subject.commonName, "wolfTPM ML-DSA Device",
CTC_NAME_SIZE);
rc = wc_SetIssuerBuffer(&leafCert, caDer, caSz);
}
if (rc == 0) {
sz = wc_MakeCert_ex(&leafCert, leafDer, PQC_CERT_BUF_SZ, keyType,
&tpmPubKey, &rng);
if (sz < 0) rc = sz;
}
if (rc == 0) {
sz = wc_SignCert_ex(leafCert.bodySz, leafCert.sigType, leafDer,
PQC_CERT_BUF_SZ, keyType, &caKey, &rng);
if (sz < 0) rc = sz;
else leafSz = sz;
}
if (rc == 0) {
rc = writeDer(CA_CERT_FILE, caDer, caSz);
if (rc == 0) rc = writeDer(SERVER_CERT_FILE, leafDer, leafSz);
if (rc == 0) {
printf("Wrote %s (%d bytes) and %s (%d bytes)\n",
CA_CERT_FILE, caSz, SERVER_CERT_FILE, leafSz);
}
}
if (havePubKey) wc_MlDsaKey_Free(&tpmPubKey);
if (haveCaKey) wc_MlDsaKey_Free(&caKey);
if (haveRng) wc_FreeRng(&rng);
if (mldsaKey.handle.hndl != 0)
wolfTPM2_UnloadHandle(&dev, &mldsaKey.handle);
wolfTPM2_Cleanup(&dev);
XFREE(caDer, NULL, DYNAMIC_TYPE_TMP_BUFFER);
XFREE(leafDer, NULL, DYNAMIC_TYPE_TMP_BUFFER);
return rc;
}
#endif /* !WOLFTPM2_NO_WRAPPER && !WOLFTPM2_NO_WOLFCRYPT && WOLFTPM_MLDSA */
#ifndef NO_MAIN_DRIVER
int main(int argc, char *argv[])
{
int rc = -1;
#if !defined(WOLFTPM2_NO_WRAPPER) && !defined(WOLFTPM2_NO_WOLFCRYPT) && \
!defined(NO_FILESYSTEM) && \
defined(WOLFTPM_MLDSA) && defined(WOLFSSL_CERT_GEN) && \
!defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \
!defined(WOLFSSL_DILITHIUM_NO_MAKE_KEY) && \
!defined(WOLFSSL_MLDSA_NO_SIGN) && !defined(WOLFSSL_DILITHIUM_NO_SIGN) && \
!defined(WOLFSSL_MLDSA_NO_ASN1) && !defined(WOLFSSL_DILITHIUM_NO_ASN1)
rc = TPM2_PQC_GenCerts(NULL, argc, argv);
#else
(void)argc;
(void)argv;
printf("Requires --enable-pqc (v1.85 ML-DSA) and wolfSSL cert gen\n");
#endif
return rc;
}
#endif

View File

@ -29,6 +29,11 @@ examples_pqc_mlkem_decap_neg_SOURCES = examples/pqc/mlkem_decap_neg.c
examples_pqc_mlkem_decap_neg_LDADD = src/libwolftpm.la $(LIB_STATIC_ADD)
examples_pqc_mlkem_decap_neg_DEPENDENCIES = src/libwolftpm.la
noinst_PROGRAMS += examples/pqc/gen_pqc_certs
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
EXTRA_DIST += examples/pqc/README.md
endif

View File

@ -26,12 +26,30 @@ examples_tls_tls_server_SOURCES = examples/tls/tls_server.c \
examples_tls_tls_server_LDADD = src/libwolftpm.la $(LIB_STATIC_ADD)
examples_tls_tls_server_DEPENDENCIES = src/libwolftpm.la
if BUILD_PQC
noinst_PROGRAMS += examples/tls/tls_client_pq
noinst_HEADERS += examples/tls/tls_client_pq.h
examples_tls_tls_client_pq_SOURCES = examples/tls/tls_client_pq.c
examples_tls_tls_client_pq_LDADD = src/libwolftpm.la $(LIB_STATIC_ADD)
examples_tls_tls_client_pq_DEPENDENCIES = src/libwolftpm.la
noinst_PROGRAMS += examples/tls/tls_server_pq
noinst_HEADERS += examples/tls/tls_server_pq.h
examples_tls_tls_server_pq_SOURCES = examples/tls/tls_server_pq.c
examples_tls_tls_server_pq_LDADD = src/libwolftpm.la $(LIB_STATIC_ADD)
examples_tls_tls_server_pq_DEPENDENCIES = src/libwolftpm.la
endif
endif
example_tlsdir = $(exampledir)/tls
dist_example_tls_DATA = \
examples/tls/tls_client.c \
examples/tls/tls_server.c
examples/tls/tls_server.c \
examples/tls/tls_client_pq.c \
examples/tls/tls_server_pq.c
DISTCLEANFILES+= examples/tls/.libs/tls_client
DISTCLEANFILES+= examples/tls/.libs/tls_server
DISTCLEANFILES+= examples/tls/.libs/tls_client_pq
DISTCLEANFILES+= examples/tls/.libs/tls_server_pq

View File

@ -0,0 +1,242 @@
/* tls_client_pq.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
*/
/* TLS 1.3 client for the PQC example: ML-KEM key exchange, validates the
* server's TPM ML-DSA certificate against the CA. Pair with tls_server_pq. */
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <wolftpm/tpm2_wrap.h>
#if !defined(WOLFTPM2_NO_WRAPPER) && defined(WOLFTPM_CRYPTOCB) && \
!defined(WOLFTPM2_NO_WOLFCRYPT) && !defined(NO_WOLFSSL_CLIENT) && \
!defined(WOLFCRYPT_ONLY) && defined(WOLFTPM_MLDSA_SIGN) && \
defined(WOLFSSL_TLS13) && !defined(NO_FILESYSTEM)
#ifndef _WIN32
/* host-resolve declarations tls_common.h needs on POSIX */
#include <netdb.h>
#include <arpa/inet.h>
#endif
#include <examples/tls/tls_client_pq.h>
#include <examples/tls/tls_common.h>
#include <stdio.h>
#include <string.h>
#define PQ_CA_CERT "./certs/pq-ca-cert.der"
#define PQ_CERT_BUF_SZ 10000
static const char kHelloMsg[] = "hello wolfssl!\n";
static void usage(void)
{
printf("Expected usage:\n");
printf("./examples/tls/tls_client_pq [-h=host] [-p=port] [-group=name]\n");
printf("* -group: ML_KEM_512/768/1024, or hybrid SECP256R1MLKEM768 /\n");
printf(" X25519MLKEM768 (default ML_KEM_768)\n");
}
static int parseGroup(const char* v, int* group)
{
if (XSTRCMP(v, "ML_KEM_512") == 0) { *group = WOLFSSL_ML_KEM_512; return 0; }
if (XSTRCMP(v, "ML_KEM_768") == 0) { *group = WOLFSSL_ML_KEM_768; return 0; }
if (XSTRCMP(v, "ML_KEM_1024") == 0) {
*group = WOLFSSL_ML_KEM_1024; return 0;
}
if (XSTRCMP(v, "SECP256R1MLKEM768") == 0) {
*group = WOLFSSL_SECP256R1MLKEM768; return 0;
}
if (XSTRCMP(v, "X25519MLKEM768") == 0) {
*group = WOLFSSL_X25519MLKEM768; return 0;
}
return BAD_FUNC_ARG;
}
static int readDer(const char* file, byte* der, int* derSz)
{
XFILE f = XFOPEN(file, "rb");
long sz;
if (f == XBADFILE) {
printf("Cannot open %s (run gen_pqc_certs first)\n", file);
return -1;
}
XFSEEK(f, 0, XSEEK_END); sz = XFTELL(f); XREWIND(f);
if (sz <= 0 || sz > *derSz) { XFCLOSE(f); return -1; }
if (XFREAD(der, 1, (size_t)sz, f) != (size_t)sz) { XFCLOSE(f); return -1; }
XFCLOSE(f);
*derSz = (int)sz;
return 0;
}
int TPM2_TLS_ClientPQArgs(void* userCtx, int argc, char *argv[])
{
int rc = 0;
WOLFSSL_CTX* ctx = NULL;
WOLFSSL* ssl = NULL;
SockIoCbCtx sockIoCtx;
const char* host = TLS_HOST;
word32 port = TLS_PORT;
int group = WOLFSSL_ML_KEM_768;
byte* caDer = NULL;
int caSz = PQ_CERT_BUF_SZ;
char reply[MAX_MSG_SZ];
int replySz;
(void)userCtx;
XMEMSET(&sockIoCtx, 0, sizeof(sockIoCtx));
sockIoCtx.fd = -1;
sockIoCtx.listenFd = -1;
while (argc > 1) {
if (XSTRNCMP(argv[argc-1], "-h=", 3) == 0) {
host = argv[argc-1] + 3;
}
else if (XSTRNCMP(argv[argc-1], "-p=", 3) == 0) {
port = (word32)XATOI(argv[argc-1] + 3);
}
else if (XSTRNCMP(argv[argc-1], "-group=", 7) == 0) {
if (parseGroup(argv[argc-1] + 7, &group) != 0) {
usage();
return BAD_FUNC_ARG;
}
}
else if (XSTRCMP(argv[argc-1], "-?") == 0 ||
XSTRCMP(argv[argc-1], "-h") == 0) {
usage();
return 0;
}
argc--;
}
caDer = (byte*)XMALLOC(PQ_CERT_BUF_SZ, NULL, DYNAMIC_TYPE_TMP_BUFFER);
if (caDer == NULL) {
return MEMORY_E;
}
ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method());
if (ctx == NULL) {
rc = MEMORY_E;
}
if (rc == 0) {
wolfSSL_CTX_SetIORecv(ctx, SockIORecv);
wolfSSL_CTX_SetIOSend(ctx, SockIOSend);
/* require the server chain to validate against the software CA */
wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_PEER, NULL);
rc = readDer(PQ_CA_CERT, caDer, &caSz);
}
if (rc == 0) {
if (wolfSSL_CTX_load_verify_buffer(ctx, caDer, caSz,
WOLFSSL_FILETYPE_ASN1) != WOLFSSL_SUCCESS) {
printf("load_verify_buffer failed\n");
rc = -1;
}
}
if (rc == 0) {
ssl = wolfSSL_new(ctx);
if (ssl == NULL) {
rc = MEMORY_E;
}
}
if (rc == 0) {
wolfSSL_SetIOReadCtx(ssl, &sockIoCtx);
wolfSSL_SetIOWriteCtx(ssl, &sockIoCtx);
if (wolfSSL_UseKeyShare(ssl, (word16)group) != WOLFSSL_SUCCESS) {
printf("UseKeyShare failed\n");
rc = -1;
}
}
if (rc == 0) {
/* advertise only the chosen group; no silent downgrade */
if (wolfSSL_set_groups(ssl, &group, 1) != WOLFSSL_SUCCESS) {
printf("set_groups failed\n");
rc = -1;
}
}
if (rc == 0) {
rc = SetupSocketAndConnect(&sockIoCtx, host, port);
}
if (rc == 0) {
do {
rc = wolfSSL_connect(ssl);
} while (rc != WOLFSSL_SUCCESS &&
(wolfSSL_want_read(ssl) || wolfSSL_want_write(ssl)));
if (rc != WOLFSSL_SUCCESS) {
printf("connect failed: %s\n",
wolfSSL_ERR_reason_error_string(wolfSSL_get_error(ssl, rc)));
}
else {
rc = 0;
}
}
if (rc == 0) {
printf("Handshake: %s, group %s\n",
wolfSSL_get_cipher(ssl), wolfSSL_get_curve_name(ssl));
printf("Server ML-DSA identity verified against the CA\n");
if (wolfSSL_write(ssl, kHelloMsg, (int)XSTRLEN(kHelloMsg)) <= 0) {
printf("write failed: %s\n",
wolfSSL_ERR_reason_error_string(wolfSSL_get_error(ssl, 0)));
rc = -1;
}
}
if (rc == 0) {
replySz = wolfSSL_read(ssl, reply, sizeof(reply) - 1);
if (replySz <= 0) {
printf("read failed: %s\n",
wolfSSL_ERR_reason_error_string(
wolfSSL_get_error(ssl, replySz)));
rc = -1;
}
else {
reply[replySz] = 0;
printf("Server: %s", reply);
}
}
if (ssl != NULL) { wolfSSL_shutdown(ssl); wolfSSL_free(ssl); }
if (ctx != NULL) wolfSSL_CTX_free(ctx);
CloseAndCleanupSocket(&sockIoCtx);
XFREE(caDer, NULL, DYNAMIC_TYPE_TMP_BUFFER);
return rc;
}
#endif
#ifndef NO_MAIN_DRIVER
int main(int argc, char *argv[])
{
int rc = -1;
#if !defined(WOLFTPM2_NO_WRAPPER) && defined(WOLFTPM_CRYPTOCB) && \
!defined(WOLFTPM2_NO_WOLFCRYPT) && !defined(NO_WOLFSSL_CLIENT) && \
!defined(WOLFCRYPT_ONLY) && defined(WOLFTPM_MLDSA_SIGN) && \
defined(WOLFSSL_TLS13) && !defined(NO_FILESYSTEM)
rc = TPM2_TLS_ClientPQArgs(NULL, argc, argv);
#else
(void)argc;
(void)argv;
printf("Requires wolfTPM --enable-pqc and wolfSSL crypto callbacks\n");
#endif
return rc;
}
#endif

View File

@ -0,0 +1,35 @@
/* tls_client_pq.h
*
* 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
*/
#ifndef _TPM_TLS_CLIENT_PQ_H_
#define _TPM_TLS_CLIENT_PQ_H_
#ifdef __cplusplus
extern "C" {
#endif
int TPM2_TLS_ClientPQArgs(void* userCtx, int argc, char *argv[]);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* _TPM_TLS_CLIENT_PQ_H_ */

View File

@ -0,0 +1,274 @@
/* tls_server_pq.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
*/
/* TLS 1.3 server whose ML-DSA identity key lives in the TPM. The TPM signs the
* CertificateVerify via the wolfTPM crypto callback. Pair with tls_client_pq. */
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <wolftpm/tpm2_wrap.h>
#if !defined(WOLFTPM2_NO_WRAPPER) && defined(WOLFTPM_CRYPTOCB) && \
!defined(WOLFTPM2_NO_WOLFCRYPT) && !defined(NO_WOLFSSL_SERVER) && \
!defined(WOLFCRYPT_ONLY) && defined(WOLFTPM_MLDSA_SIGN) && \
defined(WOLF_PRIVATE_KEY_ID) && defined(WOLFSSL_TLS13) && \
!defined(NO_FILESYSTEM)
#ifndef _WIN32
/* host-resolve declarations tls_common.h needs on POSIX */
#include <netdb.h>
#include <arpa/inet.h>
#endif
#include <examples/tls/tls_server_pq.h>
#include <examples/tls/tls_common.h>
#include <hal/tpm_io.h>
#include <examples/tpm_test.h>
#include <stdio.h>
#include <string.h>
#define PQ_SERVER_CERT "./certs/pq-server-cert.der"
#define PQ_CERT_BUF_SZ 10000
static const char kReplyMsg[] = "I hear you fa shizzle!\n";
static void usage(void)
{
printf("Expected usage:\n");
printf("./examples/tls/tls_server_pq [-p=port] [-mldsa=44/65/87]\n");
printf("* -mldsa=44/65/87: ML-DSA set (must match gen_pqc_certs, def 65)\n");
}
static int parseParamSet(const char* v, TPMI_MLDSA_PARAMETER_SET* ps)
{
if (XSTRCMP(v, "44") == 0) { *ps = TPM_MLDSA_44; return 0; }
if (XSTRCMP(v, "65") == 0) { *ps = TPM_MLDSA_65; return 0; }
if (XSTRCMP(v, "87") == 0) { *ps = TPM_MLDSA_87; return 0; }
return BAD_FUNC_ARG;
}
static int readDer(const char* file, byte* der, int* derSz)
{
XFILE f = XFOPEN(file, "rb");
long sz;
if (f == XBADFILE) {
printf("Cannot open %s (run gen_pqc_certs first)\n", file);
return -1;
}
XFSEEK(f, 0, XSEEK_END); sz = XFTELL(f); XREWIND(f);
if (sz <= 0 || sz > *derSz) { XFCLOSE(f); return -1; }
if (XFREAD(der, 1, (size_t)sz, f) != (size_t)sz) { XFCLOSE(f); return -1; }
XFCLOSE(f);
*derSz = (int)sz;
return 0;
}
int TPM2_TLS_ServerPQArgs(void* userCtx, int argc, char *argv[])
{
int rc;
WOLFTPM2_DEV dev;
WOLFTPM2_KEY mldsaKey;
TPMT_PUBLIC pub;
TpmCryptoDevCtx tpmCtx;
int tpmDevId = INVALID_DEVID;
WOLFSSL_CTX* ctx = NULL;
WOLFSSL* ssl = NULL;
SockIoCbCtx sockIoCtx;
word32 port = TLS_PORT;
TPMI_MLDSA_PARAMETER_SET paramSet = TPM_MLDSA_65;
const byte keyId[] = { 't','p','m','m','l','d','s','a' };
byte* certDer = NULL;
int certSz = PQ_CERT_BUF_SZ;
char msg[MAX_MSG_SZ];
int msgSz;
XMEMSET(&dev, 0, sizeof(dev));
XMEMSET(&mldsaKey, 0, sizeof(mldsaKey));
XMEMSET(&pub, 0, sizeof(pub));
XMEMSET(&tpmCtx, 0, sizeof(tpmCtx));
XMEMSET(&sockIoCtx, 0, sizeof(sockIoCtx));
sockIoCtx.fd = -1;
sockIoCtx.listenFd = -1;
while (argc > 1) {
if (XSTRCMP(argv[argc-1], "-?") == 0 ||
XSTRCMP(argv[argc-1], "-h") == 0) {
usage();
return 0;
}
else if (XSTRNCMP(argv[argc-1], "-p=", 3) == 0) {
port = (word32)XATOI(argv[argc-1] + 3);
}
else if (XSTRNCMP(argv[argc-1], "-mldsa=", 7) == 0) {
if (parseParamSet(argv[argc-1] + 7, &paramSet) != 0) {
usage();
return BAD_FUNC_ARG;
}
}
argc--;
}
certDer = (byte*)XMALLOC(PQ_CERT_BUF_SZ, NULL, DYNAMIC_TYPE_TMP_BUFFER);
if (certDer == NULL) {
return MEMORY_E;
}
rc = wolfTPM2_Init(&dev, TPM2_IoCb, userCtx);
if (rc != TPM_RC_SUCCESS) {
printf("wolfTPM2_Init failed 0x%x: %s\n", rc, wolfTPM2_GetRCString(rc));
}
if (rc == 0) {
/* recreate the same TPM ML-DSA identity key gen_pqc_certs certified */
rc = wolfTPM2_GetKeyTemplate_MLDSA(&pub,
TPMA_OBJECT_sign | TPMA_OBJECT_fixedTPM | TPMA_OBJECT_fixedParent |
TPMA_OBJECT_sensitiveDataOrigin | TPMA_OBJECT_userWithAuth |
TPMA_OBJECT_noDA, paramSet, 0);
if (rc == TPM_RC_SUCCESS) {
rc = wolfTPM2_CreatePrimaryKey(&dev, &mldsaKey, TPM_RH_OWNER, &pub,
NULL, 0);
}
if (rc != TPM_RC_SUCCESS) {
printf("Create TPM ML-DSA key failed 0x%x: %s\n",
rc, wolfTPM2_GetRCString(rc));
}
}
if (rc == 0) {
tpmCtx.dev = &dev;
tpmCtx.mldsaKey = &mldsaKey;
rc = wolfTPM2_SetCryptoDevCb(&dev, wolfTPM2_CryptoDevCb, &tpmCtx,
&tpmDevId);
if (rc != TPM_RC_SUCCESS) {
printf("SetCryptoDevCb failed 0x%x\n", rc);
}
}
if (rc == 0) {
ctx = wolfSSL_CTX_new(wolfTLSv1_3_server_method());
if (ctx == NULL) {
rc = MEMORY_E;
}
}
if (rc == 0) {
wolfSSL_CTX_SetDevId(ctx, tpmDevId);
wolfSSL_CTX_SetIORecv(ctx, SockIORecv);
wolfSSL_CTX_SetIOSend(ctx, SockIOSend);
rc = readDer(PQ_SERVER_CERT, certDer, &certSz);
}
if (rc == 0) {
if (wolfSSL_CTX_use_certificate_buffer(ctx, certDer, certSz,
WOLFSSL_FILETYPE_ASN1) != WOLFSSL_SUCCESS) {
printf("use_certificate_buffer failed\n");
rc = -1;
}
}
if (rc == 0) {
/* private key stays in the TPM: reference it by id + devId */
if (wolfSSL_CTX_use_PrivateKey_Id(ctx, keyId, sizeof(keyId), tpmDevId)
!= WOLFSSL_SUCCESS) {
printf("use_PrivateKey_Id failed\n");
rc = -1;
}
}
if (rc == 0) {
ssl = wolfSSL_new(ctx);
if (ssl == NULL) {
rc = MEMORY_E;
}
}
if (rc == 0) {
wolfSSL_SetIOReadCtx(ssl, &sockIoCtx);
wolfSSL_SetIOWriteCtx(ssl, &sockIoCtx);
rc = SetupSocketAndListen(&sockIoCtx, port);
}
if (rc == 0) {
printf("PQC TLS server listening on port %u\n", (unsigned)port);
rc = SocketWaitClient(&sockIoCtx);
}
if (rc == 0) {
do {
rc = wolfSSL_accept(ssl);
} while (rc != WOLFSSL_SUCCESS &&
(wolfSSL_want_read(ssl) || wolfSSL_want_write(ssl)));
if (rc != WOLFSSL_SUCCESS) {
printf("accept failed: %s\n",
wolfSSL_ERR_reason_error_string(wolfSSL_get_error(ssl, rc)));
}
else {
rc = 0;
}
}
if (rc == 0) {
printf("Handshake: %s, group %s (ML-DSA identity signed on TPM)\n",
wolfSSL_get_cipher(ssl), wolfSSL_get_curve_name(ssl));
msgSz = wolfSSL_read(ssl, msg, sizeof(msg) - 1);
if (msgSz <= 0) {
printf("read failed: %s\n",
wolfSSL_ERR_reason_error_string(wolfSSL_get_error(ssl, msgSz)));
rc = -1;
}
else {
msg[msgSz] = 0;
printf("Client: %s", msg);
}
}
if (rc == 0) {
if (wolfSSL_write(ssl, kReplyMsg, (int)XSTRLEN(kReplyMsg)) <= 0) {
printf("write failed: %s\n",
wolfSSL_ERR_reason_error_string(wolfSSL_get_error(ssl, 0)));
rc = -1;
}
}
if (ssl != NULL) { wolfSSL_shutdown(ssl); wolfSSL_free(ssl); }
if (ctx != NULL) wolfSSL_CTX_free(ctx);
CloseAndCleanupSocket(&sockIoCtx);
if (mldsaKey.handle.hndl != 0)
wolfTPM2_UnloadHandle(&dev, &mldsaKey.handle);
wolfTPM2_Cleanup(&dev);
XFREE(certDer, NULL, DYNAMIC_TYPE_TMP_BUFFER);
return rc;
}
#endif
#ifndef NO_MAIN_DRIVER
int main(int argc, char *argv[])
{
int rc = -1;
#if !defined(WOLFTPM2_NO_WRAPPER) && defined(WOLFTPM_CRYPTOCB) && \
!defined(WOLFTPM2_NO_WOLFCRYPT) && !defined(NO_WOLFSSL_SERVER) && \
!defined(WOLFCRYPT_ONLY) && defined(WOLFTPM_MLDSA_SIGN) && \
defined(WOLF_PRIVATE_KEY_ID) && defined(WOLFSSL_TLS13) && \
!defined(NO_FILESYSTEM)
rc = TPM2_TLS_ServerPQArgs(NULL, argc, argv);
#else
(void)argc;
(void)argv;
printf("Requires wolfTPM --enable-pqc and wolfSSL crypto callbacks\n");
#endif
return rc;
}
#endif

View File

@ -0,0 +1,35 @@
/* tls_server_pq.h
*
* 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
*/
#ifndef _TPM_TLS_SERVER_PQ_H_
#define _TPM_TLS_SERVER_PQ_H_
#ifdef __cplusplus
extern "C" {
#endif
int TPM2_TLS_ServerPQArgs(void* userCtx, int argc, char *argv[]);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* _TPM_TLS_SERVER_PQ_H_ */

View File

@ -82,7 +82,7 @@ int wolfTPM2_CryptoDevCb(int devId, wc_CryptoInfo* info, void* ctx)
rc = wolfTPM2_GetRandom(tlsCtx->dev, info->seed.seed, info->seed.sz);
#endif /* !WC_NO_RNG */
}
#if !defined(NO_RSA) || defined(HAVE_ECC)
#if !defined(NO_RSA) || defined(HAVE_ECC) || defined(WOLFTPM_MLDSA_SIGN)
else if (info->algo_type == WC_ALGO_TYPE_PK) {
#ifndef NO_RSA
/* RSA */
@ -425,8 +425,51 @@ int wolfTPM2_CryptoDevCb(int devId, wc_CryptoInfo* info, void* ctx)
#endif /* !WOLFTPM2_USE_SW_ECDHE */
}
#endif /* HAVE_ECC */
#ifdef WOLFTPM_MLDSA_SIGN
if (info->pk.type == WC_PK_TYPE_PQC_SIG_SIGN) {
TPM_HANDLE seqHandle = 0;
WOLFTPM2_HANDLE seqHandleObj;
int sigSz;
/* pure ML-DSA one-shot only; pre-hash, oversized, no-key, bad-arg
* fall back */
if (info->pk.pqc_sign.type != WC_PQC_SIG_TYPE_MLDSA ||
info->pk.pqc_sign.preHashType != WC_HASH_TYPE_NONE ||
info->pk.pqc_sign.out == NULL ||
info->pk.pqc_sign.outlen == NULL ||
(info->pk.pqc_sign.in == NULL &&
info->pk.pqc_sign.inlen > 0) ||
info->pk.pqc_sign.inlen > MAX_DIGEST_BUFFER ||
tlsCtx->mldsaKey == NULL) {
return exit_rc;
}
sigSz = (int)*info->pk.pqc_sign.outlen;
rc = wolfTPM2_SignSequenceStart(tlsCtx->dev, tlsCtx->mldsaKey,
info->pk.pqc_sign.context, (int)info->pk.pqc_sign.contextLen,
&seqHandle);
if (rc == 0) {
rc = wolfTPM2_SignSequenceComplete(tlsCtx->dev, seqHandle,
tlsCtx->mldsaKey, info->pk.pqc_sign.in,
(int)info->pk.pqc_sign.inlen, info->pk.pqc_sign.out, &sigSz);
if (rc == 0) {
*info->pk.pqc_sign.outlen = (word32)sigSz;
}
}
if (rc != 0 && seqHandle != 0) {
/* free seq on failure */
XMEMSET(&seqHandleObj, 0, sizeof(seqHandleObj));
seqHandleObj.hndl = seqHandle;
wolfTPM2_UnloadHandle(tlsCtx->dev, &seqHandleObj);
}
if (rc == BUFFER_E) {
/* preserve caller size-error; don't mask it as WC_HW_E */
return BUFFER_E;
}
}
#endif /* WOLFTPM_MLDSA_SIGN */
}
#endif /* !NO_RSA || HAVE_ECC */
#endif /* !NO_RSA || HAVE_ECC || WOLFTPM_MLDSA_SIGN */
#ifndef NO_AES
else if (info->algo_type == WC_ALGO_TYPE_CIPHER) {
if (info->cipher.type != WC_CIPHER_AES_CBC) {

View File

@ -41,3 +41,10 @@ if BUILD_FWTPM
dist_noinst_SCRIPTS += tests/fwtpm_check.sh \
tests/fwtpm_da_retry.sh
endif
# tls_pq_e2e.sh is intentionally NOT a `make check` test: it needs the wolfSSL
# crypto-cb ML-DSA device-key fix (not yet upstream) and a cert-gen build, and
# it manages its own fwTPM server + NV file (which would collide with the
# parallel fwtpm_check.sh). It is distributed and run standalone by the
# pqc-examples CI workflow instead.
EXTRA_DIST += tests/tls_pq_e2e.sh

221
tests/tls_pq_e2e.sh 100755
View File

@ -0,0 +1,221 @@
#!/bin/bash
# tls_pq_e2e.sh
#
# 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
#
# End-to-end harness for the post-quantum TLS example: spawn fwtpm_server,
# generate a TPM-keyed ML-DSA cert chain, run tls_server_pq and tls_client_pq
# over a real TCP socket, and assert the full PQC handshake (ML-KEM key
# exchange + TPM-signed ML-DSA authentication, CA-verified) plus app data.
set -u
# returns: 0 = in use, 1 = free, 2 = no probe tool
check_port_in_use() {
if command -v nc >/dev/null 2>&1; then
nc -z localhost "$1" 2>/dev/null; return $?
elif command -v ss >/dev/null 2>&1; then
ss -tln 2>/dev/null | grep -qE "[:.]$1 "; return $?
elif command -v netstat >/dev/null 2>&1; then
netstat -an 2>/dev/null | grep -qE "[:.]$1 .*LISTEN"; return $?
fi
return 2
}
pick_available_port() {
local port attempts=0
while [ $attempts -lt 20 ]; do
if command -v shuf >/dev/null 2>&1; then port=$(shuf -i 10000-60000 -n 1)
else port=$(( (RANDOM % 50000) + 10000 )); fi
check_port_in_use "$port"; [ $? -ne 0 ] && { echo "$port"; return 0; }
attempts=$((attempts + 1))
done
return 1
}
# pick a port whose consecutive platform port (port+1) is also free
pick_available_pair() {
local port attempts=0
while [ $attempts -lt 20 ]; do
port=$(pick_available_port) || return 1
check_port_in_use "$((port + 1))" || { echo "$port"; return 0; }
attempts=$((attempts + 1))
done
return 1
}
# wait for $1 to reach LISTEN (ss/netstat, not nc -z which eats the accept slot)
wait_for_listen() {
local port="$1" pid="$2" elapsed=0
if ! command -v ss >/dev/null 2>&1 && ! command -v netstat >/dev/null 2>&1; then
sleep 2; return 0
fi
while [ $elapsed -lt 400 ]; do
kill -0 "$pid" 2>/dev/null || return 1
if command -v ss >/dev/null 2>&1; then
ss -tln 2>/dev/null | grep -qE "[:.]${port} " && return 0
elif netstat -an 2>/dev/null | grep -qE "[:.]${port} .*LISTEN"; then
return 0
fi
sleep 0.02
elapsed=$((elapsed + 1))
done
return 1
}
# binaries and generated artifacts live in the build tree (VPATH-safe)
BUILD_DIR="$(pwd)"
cd "$BUILD_DIR" || exit 1
SERVER="$BUILD_DIR/src/fwtpm/fwtpm_server"
GEN="$BUILD_DIR/examples/pqc/gen_pqc_certs"
TLS_SRV="$BUILD_DIR/examples/tls/tls_server_pq"
TLS_CLI="$BUILD_DIR/examples/tls/tls_client_pq"
# fwTPM uses the compile-time FWTPM_NV_FILE (cwd-relative), not an env var
NV_FILE="$BUILD_DIR/fwtpm_nv.bin"
LOGP="/tmp/tls_pq_e2e_$$"
# examples honor TPM2_SWTPM_PORT only when built with getenv support
HAS_GETENV=1
WOLFTPM_OPTS="$BUILD_DIR/wolftpm/options.h"
if [ -f "$WOLFTPM_OPTS" ] && grep -q "^#define NO_GETENV" "$WOLFTPM_OPTS"; then
HAS_GETENV=0
fi
PORT="${TPM2_SWTPM_PORT:-2321}"
if check_port_in_use "$PORT" || check_port_in_use "$((PORT + 1))"; then
if [ "$HAS_GETENV" -eq 1 ]; then
PORT="$(pick_available_pair)"
else
echo "SKIP: TPM ports $PORT/$((PORT + 1)) busy (NO_GETENV build)" >&2
exit 77
fi
fi
PLATFORM_PORT=$((PORT + 1))
TLS_PORT="$(pick_available_port)"
port_tries=0
while { [ "$TLS_PORT" = "$PORT" ] || [ "$TLS_PORT" = "$PLATFORM_PORT" ]; } \
&& [ $port_tries -lt 10 ]; do
TLS_PORT="$(pick_available_port)"
port_tries=$((port_tries + 1))
done
GROUP="${1:-ML_KEM_768}"
MLDSA="${2:-65}"
if [ -z "$PORT" ] || [ -z "$TLS_PORT" ]; then
echo "SKIP: could not allocate a free port" >&2; exit 77
fi
for bin in "$SERVER" "$GEN" "$TLS_SRV" "$TLS_CLI"; do
if [ ! -x "$bin" ]; then
echo "SKIP: $(basename "$bin") not built — configure --enable-fwtpm --enable-swtpm --enable-pqc" >&2
exit 77
fi
done
# the examples compile executable stubs when wolfSSL cert-gen / private-key-id /
# TLS 1.3 support is missing; skip rather than fail in that build matrix
for bin in "$GEN" "$TLS_SRV" "$TLS_CLI"; do
if "$bin" -h 2>&1 | grep -q "Requires"; then
echo "SKIP: $(basename "$bin") built without required wolfSSL features" >&2
exit 77
fi
done
export TPM2_SWTPM_PORT=$PORT
rm -f "$NV_FILE"
echo "== Starting fwtpm_server on port $PORT =="
"$SERVER" \
--port $PORT --platform-port $PLATFORM_PORT --clear \
>"${LOGP}_fwtpm.log" 2>&1 &
SERVER_PID=$!
(
for _ in 1 2 3 4 5 6 7 8 9 10; do
if exec 3<>/dev/tcp/127.0.0.1/$PORT; then exec 3>&-; exit 0; fi
sleep 0.5
done
exit 1
) 2>/dev/null
if [ $? -ne 0 ] || ! kill -0 "$SERVER_PID" 2>/dev/null; then
echo "FAIL: fwtpm_server not accepting connections on port $PORT" >&2
kill "$SERVER_PID" 2>/dev/null; rm -f "$NV_FILE"; exit 1
fi
TLS_SRV_PID=""
cleanup() {
[ -n "$TLS_SRV_PID" ] && kill "$TLS_SRV_PID" 2>/dev/null
TLS_SRV_PID=""
[ -n "$SERVER_PID" ] && { kill "$SERVER_PID" 2>/dev/null; \
wait "$SERVER_PID" 2>/dev/null; }
SERVER_PID=""
rm -f "$NV_FILE" "$BUILD_DIR/certs/pq-ca-cert.der" \
"$BUILD_DIR/certs/pq-server-cert.der"
}
trap cleanup EXIT
mkdir -p "$BUILD_DIR/certs"
echo "== Generating TPM ML-DSA-$MLDSA cert chain =="
if ! "$GEN" -mldsa=$MLDSA >"${LOGP}_gen.log" 2>&1; then
echo "FAIL: gen_pqc_certs" >&2; cat "${LOGP}_gen.log" >&2; cleanup; exit 1
fi
echo "== Starting tls_server_pq on port $TLS_PORT =="
"$TLS_SRV" -p=$TLS_PORT -mldsa=$MLDSA >"${LOGP}_server.log" 2>&1 &
TLS_SRV_PID=$!
if ! wait_for_listen "$TLS_PORT" "$TLS_SRV_PID"; then
echo "FAIL: tls_server_pq not listening on port $TLS_PORT" >&2
cat "${LOGP}_server.log" >&2; cleanup; exit 1
fi
echo "== Running tls_client_pq (group $GROUP) =="
"$TLS_CLI" -h=localhost -p=$TLS_PORT -group=$GROUP >"${LOGP}_client.log" 2>&1
CLI_RC=$?
# if the client connected, the server serves it and exits; wait to flush its
# log. Otherwise (e.g. unsupported group) kill it so we do not hang forever.
if [ $CLI_RC -eq 0 ]; then
wait "$TLS_SRV_PID" 2>/dev/null
else
kill "$TLS_SRV_PID" 2>/dev/null
fi
TLS_SRV_PID=""
cleanup
CLI=$(cat "${LOGP}_client.log")
SRV=$(cat "${LOGP}_server.log")
FAIL=0
echo "$CLI" | grep -qi "group $GROUP" || { echo "FAIL: client did not negotiate $GROUP" >&2; FAIL=1; }
echo "$CLI" | grep -q "verified against the CA" || { echo "FAIL: client did not verify server cert" >&2; FAIL=1; }
echo "$CLI" | grep -q "I hear you fa shizzle" || { echo "FAIL: no app data from server" >&2; FAIL=1; }
echo "$SRV" | grep -q "signed on TPM" || { echo "FAIL: server did not sign on the TPM" >&2; FAIL=1; }
if [ $CLI_RC -eq 0 ] && [ $FAIL -eq 0 ]; then
echo "OK: PQC TLS handshake (ML-KEM $GROUP + TPM ML-DSA-$MLDSA) passed"
exit 0
fi
echo "FAIL: PQC TLS E2E (client rc=$CLI_RC)" >&2
if ! echo "$SRV" | grep -q "signed on TPM"; then
echo "HINT: server never signed on the TPM — the linked wolfSSL may lack the" >&2
echo " wc_MlDsaKey_SignCtx crypto-callback route (see examples/pqc/README.md)" >&2
fi
echo "--- client ---" >&2; echo "$CLI" >&2
echo "--- server ---" >&2; echo "$SRV" >&2
exit 1

View File

@ -32,6 +32,9 @@
#include <wolftpm/tpm2_swtpm.h>
#include <wolftpm/tpm2_tis.h>
#include <wolftpm/tpm2_spdm.h>
#ifdef WOLFTPM_MLDSA_SIGN
#include <wolfssl/wolfcrypt/wc_mldsa.h>
#endif
#include <hal/tpm_io.h>
#include <examples/tpm_test.h>
@ -4667,6 +4670,160 @@ static void test_wolfTPM2_CryptoDevCb_EccVerifyOversizedRS(void)
#endif
}
static void test_wolfTPM2_CryptoDevCb_MlDsaSign(void)
{
#if !defined(WOLFTPM2_NO_WRAPPER) && defined(WOLFTPM_CRYPTOCB) && \
!defined(WOLFTPM2_NO_WOLFCRYPT) && defined(WOLFTPM_MLDSA_SIGN)
int rc;
WOLFTPM2_DEV dev;
WOLFTPM2_KEY dummyKey;
WOLFTPM2_KEY tpmKey;
TPMT_PUBLIC pub;
TpmCryptoDevCtx tpmCtx;
wc_CryptoInfo info;
byte msg[32];
byte sig[64];
byte signContext[4];
word32 sigLen = (word32)sizeof(sig);
byte* bigSig = NULL;
word32 bigSigLen;
#ifdef WOLFTPM_MLDSA_VERIFY
byte otherContext[4];
TPM_HANDLE vSeq;
TPMT_TK_VERIFIED vtk;
#endif
XMEMSET(&tpmCtx, 0, sizeof(tpmCtx));
XMEMSET(&dummyKey, 0, sizeof(dummyKey));
XMEMSET(&tpmKey, 0, sizeof(tpmKey));
XMEMSET(&pub, 0, sizeof(pub));
XMEMSET(msg, 0x5A, sizeof(msg));
rc = wolfTPM2_Init(&dev, TPM2_IoCb, NULL);
AssertIntEQ(rc, 0);
tpmCtx.dev = &dev;
XMEMSET(&info, 0, sizeof(info));
info.algo_type = WC_ALGO_TYPE_PK;
info.pk.type = WC_PK_TYPE_PQC_SIG_SIGN;
info.pk.pqc_sign.type = WC_PQC_SIG_TYPE_MLDSA;
info.pk.pqc_sign.in = msg;
info.pk.pqc_sign.inlen = (word32)sizeof(msg);
info.pk.pqc_sign.out = sig;
info.pk.pqc_sign.outlen = &sigLen;
/* no key: fall back */
tpmCtx.mldsaKey = NULL;
rc = wolfTPM2_CryptoDevCb(INVALID_DEVID, &info, &tpmCtx);
AssertIntEQ(rc, CRYPTOCB_UNAVAILABLE);
/* wrong PQC type: fall back */
info.pk.pqc_sign.type = WC_PQC_SIG_TYPE_MLDSA + 1;
tpmCtx.mldsaKey = &dummyKey;
rc = wolfTPM2_CryptoDevCb(INVALID_DEVID, &info, &tpmCtx);
AssertIntEQ(rc, CRYPTOCB_UNAVAILABLE);
/* pre-hash: fall back, not pure ML-DSA */
info.pk.pqc_sign.type = WC_PQC_SIG_TYPE_MLDSA;
info.pk.pqc_sign.preHashType = WC_HASH_TYPE_SHA256;
rc = wolfTPM2_CryptoDevCb(INVALID_DEVID, &info, &tpmCtx);
AssertIntEQ(rc, CRYPTOCB_UNAVAILABLE);
info.pk.pqc_sign.preHashType = WC_HASH_TYPE_NONE;
/* too big for one-shot: fall back */
info.pk.pqc_sign.inlen = MAX_DIGEST_BUFFER + 1;
rc = wolfTPM2_CryptoDevCb(INVALID_DEVID, &info, &tpmCtx);
AssertIntEQ(rc, CRYPTOCB_UNAVAILABLE);
info.pk.pqc_sign.inlen = (word32)sizeof(msg);
/* NULL outlen: reject before dereference */
info.pk.pqc_sign.outlen = NULL;
rc = wolfTPM2_CryptoDevCb(INVALID_DEVID, &info, &tpmCtx);
AssertIntEQ(rc, CRYPTOCB_UNAVAILABLE);
info.pk.pqc_sign.outlen = &sigLen;
/* happy path (needs v1.85 ML-DSA) */
rc = wolfTPM2_GetKeyTemplate_MLDSA(&pub,
TPMA_OBJECT_sign | TPMA_OBJECT_fixedTPM | TPMA_OBJECT_fixedParent |
TPMA_OBJECT_sensitiveDataOrigin | TPMA_OBJECT_userWithAuth |
TPMA_OBJECT_noDA, TPM_MLDSA_65, 0);
if (rc == 0) {
rc = wolfTPM2_CreatePrimaryKey(&dev, &tpmKey, TPM_RH_OWNER, &pub,
NULL, 0);
}
if (rc == TPM_RC_VALUE || rc == TPM_RC_SCHEME ||
rc == TPM_RC_COMMAND_CODE || rc == (int)(RC_VER1 + 0x043)) {
/* TPM lacks ML-DSA: skip */
printf("Test TPM Wrapper: %-40s Skipped (not supported)\n",
"CryptoDevCb ML-DSA sign:");
}
else {
AssertIntEQ(rc, 0);
bigSig = (byte*)XMALLOC(WC_MLDSA_65_SIG_SIZE, NULL,
DYNAMIC_TYPE_TMP_BUFFER);
AssertNotNull(bigSig);
tpmCtx.mldsaKey = &tpmKey;
info.pk.pqc_sign.type = WC_PQC_SIG_TYPE_MLDSA;
info.pk.pqc_sign.out = bigSig;
/* full buffer: signs, sets len */
bigSigLen = (word32)WC_MLDSA_65_SIG_SIZE;
info.pk.pqc_sign.outlen = &bigSigLen;
rc = wolfTPM2_CryptoDevCb(INVALID_DEVID, &info, &tpmCtx);
AssertIntEQ(rc, 0);
AssertIntEQ((int)bigSigLen, WC_MLDSA_65_SIG_SIZE);
/* context passthrough: sig must verify under the same context */
XMEMSET(signContext, 0x42, sizeof(signContext));
bigSigLen = (word32)WC_MLDSA_65_SIG_SIZE;
info.pk.pqc_sign.outlen = &bigSigLen;
info.pk.pqc_sign.context = signContext;
info.pk.pqc_sign.contextLen = (byte)sizeof(signContext);
rc = wolfTPM2_CryptoDevCb(INVALID_DEVID, &info, &tpmCtx);
AssertIntEQ(rc, 0);
AssertIntEQ((int)bigSigLen, WC_MLDSA_65_SIG_SIZE);
info.pk.pqc_sign.context = NULL;
info.pk.pqc_sign.contextLen = 0;
#ifdef WOLFTPM_MLDSA_VERIFY
rc = wolfTPM2_VerifySequenceStart(&dev, &tpmKey, signContext,
(int)sizeof(signContext), &vSeq);
AssertIntEQ(rc, 0);
rc = wolfTPM2_VerifySequenceUpdate(&dev, vSeq, msg, (int)sizeof(msg));
AssertIntEQ(rc, 0);
XMEMSET(&vtk, 0, sizeof(vtk));
rc = wolfTPM2_VerifySequenceComplete(&dev, vSeq, &tpmKey,
NULL, 0, bigSig, (int)bigSigLen, &vtk);
AssertIntEQ(rc, 0);
/* a different context must not verify the same signature */
XMEMSET(otherContext, 0x24, sizeof(otherContext));
rc = wolfTPM2_VerifySequenceStart(&dev, &tpmKey, otherContext,
(int)sizeof(otherContext), &vSeq);
AssertIntEQ(rc, 0);
rc = wolfTPM2_VerifySequenceUpdate(&dev, vSeq, msg, (int)sizeof(msg));
AssertIntEQ(rc, 0);
XMEMSET(&vtk, 0, sizeof(vtk));
rc = wolfTPM2_VerifySequenceComplete(&dev, vSeq, &tpmKey,
NULL, 0, bigSig, (int)bigSigLen, &vtk);
AssertIntNE(rc, 0);
#endif /* WOLFTPM_MLDSA_VERIFY */
/* small buffer: preserves the size-error contract, not WC_HW_E */
bigSigLen = 16;
info.pk.pqc_sign.outlen = &bigSigLen;
rc = wolfTPM2_CryptoDevCb(INVALID_DEVID, &info, &tpmCtx);
AssertIntEQ(rc, BUFFER_E);
XFREE(bigSig, NULL, DYNAMIC_TYPE_TMP_BUFFER);
wolfTPM2_UnloadHandle(&dev, &tpmKey.handle);
printf("Test TPM Wrapper: %-40s Passed\n", "CryptoDevCb ML-DSA sign:");
}
wolfTPM2_Cleanup(&dev);
#endif
}
static void test_TPM2_ASN_DecodeX509Cert_Errors(void)
{
#if !defined(WOLFTPM2_NO_WRAPPER) && !defined(WOLFTPM2_NO_ASN)
@ -6909,6 +7066,7 @@ int unit_tests(int argc, char *argv[])
test_wolfTPM2_ReadPublicKey();
test_wolfTPM2_CSR();
test_wolfTPM2_CryptoDevCb_EccVerifyOversizedRS();
test_wolfTPM2_CryptoDevCb_MlDsaSign();
test_TPM2_ASN_DecodeX509Cert_Errors();
test_TPM2_ASN_DecodeX509Cert_Valid();
test_TPM2_ASN_DecodeTag_Errors();

View File

@ -4339,6 +4339,9 @@ typedef struct TpmCryptoDevCtx {
unsigned short useSymmetricOnTPM:1; /* if set indicates desire to use symmetric algorithms on TPM */
#endif
unsigned short useFIPSMode:1; /* if set requires FIPS mode on TPM and no fallback to software algos */
#ifdef WOLFTPM_MLDSA_SIGN
WOLFTPM2_KEY* mldsaKey; /* ML-DSA identity key; private key stays in TPM */
#endif
} TpmCryptoDevCtx;
#endif /* WOLFTPM_CRYPTOCB || HAVE_PK_CALLBACKS */