From 8f214a428f83d46e9672bd6f6abcba115956b033 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 2 Jul 2026 12:57:03 -0700 Subject: [PATCH 1/5] wolfSSH: enable password/keyboard auth in TPM builds and prefer client TPM key --- src/internal.c | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/internal.c b/src/internal.c index 22c070ca9..050caf8fa 100644 --- a/src/internal.c +++ b/src/internal.c @@ -8760,11 +8760,6 @@ static int DoUserAuthFailure(WOLFSSH* ssh, for (j = 0; j < sizeof(ssh->supportedAuth); j++) { if (authList[i] == ssh->supportedAuth[j]) { switch(authList[i]) { -#ifdef WOLFSSH_TPM - case ID_USERAUTH_PUBLICKEY: - authType |= WOLFSSH_USERAUTH_PUBLICKEY; - break; -#else /* !WOLFSSH_TPM */ case ID_USERAUTH_PASSWORD: authType |= WOLFSSH_USERAUTH_PASSWORD; break; @@ -8776,12 +8771,12 @@ static int DoUserAuthFailure(WOLFSSH* ssh, } break; #endif -#if !defined(WOLFSSH_NO_RSA) || !defined(WOLFSSH_NO_ECDSA) +#if !defined(WOLFSSH_NO_RSA) || !defined(WOLFSSH_NO_ECDSA) || \ + defined(WOLFSSH_TPM) case ID_USERAUTH_PUBLICKEY: authType |= WOLFSSH_USERAUTH_PUBLICKEY; break; #endif -#endif /* WOLFSSH_TPM */ default: break; } @@ -16422,6 +16417,19 @@ int SendUserAuthRequest(WOLFSSH* ssh, byte authType, int addSig) keySig_ptr->sigId = ID_NONE; keySig_ptr->heap = ssh->ctx->heap; +#ifdef WOLFSSH_TPM + /* When the client has a TPM key configured, prefer publickey auth so + * the TPM key is used even if the server also offers password or + * keyboard-interactive. Applied before any method-specific branch. */ + if (ssh->ctx->tpmKey != NULL + && (authType & WOLFSSH_USERAUTH_PUBLICKEY)) { + authType &= ~WOLFSSH_USERAUTH_PASSWORD; + #ifdef WOLFSSH_KEYBOARD_INTERACTIVE + authType &= ~WOLFSSH_USERAUTH_KEYBOARD; + #endif + } +#endif + #ifdef WOLFSSH_KEYBOARD_INTERACTIVE /* Callback happens later for keyboard auth */ if (authType & WOLFSSH_USERAUTH_KEYBOARD) { From 68a6c2d569be3a21775b662629b58d8bc91e3bf1 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 2 Jul 2026 12:57:03 -0700 Subject: [PATCH 2/5] examples: add TPM-backed X.509 host certificate server and client --- examples/include.am | 1 + examples/tpmcertserver/.gitignore | 3 + examples/tpmcertserver/include.am | 21 ++ examples/tpmcertserver/tpmcertclient.c | 238 +++++++++++++++ examples/tpmcertserver/tpmcertserver.c | 393 +++++++++++++++++++++++++ 5 files changed, 656 insertions(+) create mode 100644 examples/tpmcertserver/.gitignore create mode 100644 examples/tpmcertserver/include.am create mode 100644 examples/tpmcertserver/tpmcertclient.c create mode 100644 examples/tpmcertserver/tpmcertserver.c diff --git a/examples/include.am b/examples/include.am index fb3d8a844..b43b1d7de 100644 --- a/examples/include.am +++ b/examples/include.am @@ -4,6 +4,7 @@ include examples/client/include.am include examples/echoserver/include.am +include examples/tpmcertserver/include.am include examples/portfwd/include.am include examples/sftpclient/include.am include examples/scpclient/include.am diff --git a/examples/tpmcertserver/.gitignore b/examples/tpmcertserver/.gitignore new file mode 100644 index 000000000..31c577208 --- /dev/null +++ b/examples/tpmcertserver/.gitignore @@ -0,0 +1,3 @@ +/tpmcertserver +/tpmcertclient +/tpm-server-cert.der diff --git a/examples/tpmcertserver/include.am b/examples/tpmcertserver/include.am new file mode 100644 index 000000000..0fc68d0f8 --- /dev/null +++ b/examples/tpmcertserver/include.am @@ -0,0 +1,21 @@ +# vim:ft=automake +# All paths should be given relative to the root + +if BUILD_TPM +if BUILD_CERTS +if BUILD_EXAMPLE_SERVERS +noinst_PROGRAMS += examples/tpmcertserver/tpmcertserver +examples_tpmcertserver_tpmcertserver_SOURCES = \ + examples/tpmcertserver/tpmcertserver.c +examples_tpmcertserver_tpmcertserver_LDADD = src/libwolfssh.la +examples_tpmcertserver_tpmcertserver_DEPENDENCIES = src/libwolfssh.la +endif +if BUILD_EXAMPLE_CLIENTS +noinst_PROGRAMS += examples/tpmcertserver/tpmcertclient +examples_tpmcertserver_tpmcertclient_SOURCES = \ + examples/tpmcertserver/tpmcertclient.c +examples_tpmcertserver_tpmcertclient_LDADD = src/libwolfssh.la +examples_tpmcertserver_tpmcertclient_DEPENDENCIES = src/libwolfssh.la +endif +endif +endif diff --git a/examples/tpmcertserver/tpmcertclient.c b/examples/tpmcertserver/tpmcertclient.c new file mode 100644 index 000000000..78fcac7b9 --- /dev/null +++ b/examples/tpmcertserver/tpmcertclient.c @@ -0,0 +1,238 @@ +/* tpmcertclient.c + * + * Copyright (C) 2014-2026 wolfSSL Inc. + * + * This file is part of wolfSSH. + * + * wolfSSH 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. + * + * wolfSSH 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 wolfSSH. If not, see . + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#define WOLFSSH_TEST_CLIENT + +#include +#include + +#ifdef WOLFSSH_CERTS + +#include +#include + +#define TPMCC_USER "jill" +#define TPMCC_PASSWORD "upthehill" +#define TPMCC_BUF_SZ 1024 + +static int TpmCcUserAuth(byte authType, WS_UserAuthData* authData, void* ctx) +{ + int ret = WOLFSSH_USERAUTH_FAILURE; + + WOLFSSH_UNUSED(ctx); + + if (authType == WOLFSSH_USERAUTH_PASSWORD) { + authData->sf.password.password = (const byte*)TPMCC_PASSWORD; + authData->sf.password.passwordSz = (word32)XSTRLEN(TPMCC_PASSWORD); + ret = WOLFSSH_USERAUTH_SUCCESS; + } + + return ret; +} + + +/* The server host key is an X.509 certificate; it was already verified against + * the root CA loaded with wolfSSH_CTX_AddRootCert_buffer(). Accept it. */ +static int TpmCcHostKeyCheck(const byte* pubKey, word32 pubKeySz, void* ctx) +{ + WOLFSSH_UNUSED(pubKey); + WOLFSSH_UNUSED(pubKeySz); + WOLFSSH_UNUSED(ctx); + return 0; +} + + +static int TpmCcLoadFile(const char* file, byte* buf, word32* bufSz) +{ + int ret = 0; + FILE* f = fopen(file, "rb"); + size_t n; + + if (f == NULL) { + ret = -1; + } + else { + n = fread(buf, 1, *bufSz, f); + fclose(f); + if (n == 0) + ret = -1; + else + *bufSz = (word32)n; + } + + return ret; +} + + +int main(int argc, char* argv[]) +{ + int ret; + int i; + word16 port = 22222; + const char* host = "127.0.0.1"; + const char* caFile = "tpm-server-cert.der"; + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WS_SOCKET_T sockFd = WOLFSSH_SOCKET_INVALID; + SOCKADDR_IN_T addr; + byte caDer[2048]; + word32 caDerSz = (word32)sizeof(caDer); + byte txt[] = "hello from tpmcertclient\n"; + byte rxBuf[TPMCC_BUF_SZ]; + + for (i = 1; i < argc; i++) { + if (XSTRCMP(argv[i], "-p") == 0 && i + 1 < argc) + port = (word16)atoi(argv[++i]); + else if (XSTRCMP(argv[i], "-h") == 0 && i + 1 < argc) + host = argv[++i]; + else if (XSTRCMP(argv[i], "-A") == 0 && i + 1 < argc) + caFile = argv[++i]; + } + + if (TpmCcLoadFile(caFile, caDer, &caDerSz) != 0) { + fprintf(stderr, "Could not read CA file %s\n", caFile); + return 1; + } + +#ifdef DEBUG_WOLFSSH + wolfSSH_Debugging_ON(); +#endif + + ret = (wolfSSH_Init() == WS_SUCCESS) ? 0 : -1; + + if (ret == 0) { + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + if (ctx == NULL) + ret = -1; + } + + if (ret == 0) { + wolfSSH_SetUserAuth(ctx, TpmCcUserAuth); + wolfSSH_CTX_SetPublicKeyCheck(ctx, TpmCcHostKeyCheck); + + /* Only accept X.509 certificate host keys. Without this the client + * would also accept a plain host key, and the server's certificate + * (and therefore CA verification) would be bypassed. */ + if (wolfSSH_CTX_SetAlgoListKey(ctx, + "x509v3-ecdsa-sha2-nistp256,x509v3-ecdsa-sha2-nistp384," + "x509v3-ecdsa-sha2-nistp521,x509v3-ssh-rsa") != WS_SUCCESS) { + fprintf(stderr, "Could not set host key algorithm list\n"); + ret = -1; + } + } + + if (ret == 0) { + if (wolfSSH_CTX_AddRootCert_buffer(ctx, caDer, caDerSz, + WOLFSSH_FORMAT_ASN1) != WS_SUCCESS) { + fprintf(stderr, "Could not load root CA certificate\n"); + ret = -1; + } + } + + if (ret == 0) { + ssh = wolfSSH_new(ctx); + if (ssh == NULL) + ret = -1; + } + + if (ret == 0) + ret = (wolfSSH_SetUsername(ssh, TPMCC_USER) == WS_SUCCESS) ? 0 : -1; + + if (ret == 0) { + WSTARTTCP(); + build_addr(&addr, host, port); + tcp_socket(&sockFd, ((struct sockaddr_in*)&addr)->sin_family); + if (connect(sockFd, (const struct sockaddr*)&addr, sizeof(addr)) != 0) { + fprintf(stderr, "Could not connect to %s:%u\n", host, port); + ret = -1; + } + } + + if (ret == 0) { + wolfSSH_set_fd(ssh, (int)sockFd); + ret = wolfSSH_connect(ssh); + if (ret == WS_SUCCESS) { + printf("Connected: verified server X.509 host certificate.\n"); + ret = 0; + } + else { + fprintf(stderr, "wolfSSH_connect failed: %d (%s)\n", ret, + wolfSSH_ErrorToName(ret)); + } + } + + if (ret == 0) { + int txSz = (int)XSTRLEN((char*)txt); + int sent = 0; + int rxSz; + int n; + + /* stream_send may transmit fewer bytes than requested; loop until the + * whole message is sent. */ + while (sent < txSz) { + n = wolfSSH_stream_send(ssh, txt + sent, (word32)(txSz - sent)); + if (n <= 0) { + fprintf(stderr, "stream send failed: %d\n", n); + ret = -1; + break; + } + sent += n; + } + + if (ret == 0) { + rxSz = wolfSSH_stream_read(ssh, rxBuf, sizeof(rxBuf) - 1); + if (rxSz <= 0) { + fprintf(stderr, "stream read failed: %d\n", rxSz); + ret = -1; + } + else { + rxBuf[rxSz] = 0; + printf("Echo from server: %s", (char*)rxBuf); + } + } + } + + if (ssh != NULL) { + wolfSSH_stream_exit(ssh, 0); + wolfSSH_free(ssh); + } + if (sockFd != WOLFSSH_SOCKET_INVALID) + WCLOSESOCKET(sockFd); + if (ctx != NULL) + wolfSSH_CTX_free(ctx); + wolfSSH_Cleanup(); + + printf("Done (ret = %d).\n", ret); + return (ret == 0) ? 0 : 1; +} + +#else /* !WOLFSSH_CERTS */ + +int main(void) +{ + printf("This example requires wolfSSH built with --enable-certs.\n"); + return 0; +} + +#endif /* WOLFSSH_CERTS */ diff --git a/examples/tpmcertserver/tpmcertserver.c b/examples/tpmcertserver/tpmcertserver.c new file mode 100644 index 000000000..6d0b753ba --- /dev/null +++ b/examples/tpmcertserver/tpmcertserver.c @@ -0,0 +1,393 @@ +/* tpmcertserver.c + * + * Copyright (C) 2014-2026 wolfSSL Inc. + * + * This file is part of wolfSSH. + * + * wolfSSH 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. + * + * wolfSSH 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 wolfSSH. If not, see . + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#define WOLFSSH_TEST_SERVER + +#include +#include + +#include +#include + +#if defined(WOLFSSH_TPM) && defined(WOLFSSH_CERTS) + #include +#endif + +/* wolfTPM's self-signed certificate generation and crypto callback (used to + * sign the host certificate through the TPM) require wolfTPM built with cert + * generation and the crypto callback. Match wolfTPM's own csr example. */ +#if defined(WOLFSSH_TPM) && defined(WOLFSSH_CERTS) && \ + !defined(WOLFTPM2_NO_WRAPPER) && defined(WOLFTPM2_CERT_GEN) && \ + defined(WOLFTPM_CRYPTOCB) + +#include + +#include +#include + +#define TPMCS_USER "jill" +#define TPMCS_PASSWORD "upthehill" +#define TPMCS_KEY_AUTH "ThisIsMyKeyAuth" +#define TPMCS_CERT_MAX 2048 +#define TPMCS_BUF_SZ 1024 + +static const char* certOutFile = "tpm-server-cert.der"; + +/* Accept a single fixed user/password. The host identity we are proving to the + * client is the X.509 host certificate, not the user credential. */ +static int TpmCsUserAuth(byte authType, WS_UserAuthData* authData, void* ctx) +{ + int ret = WOLFSSH_USERAUTH_INVALID_PASSWORD; + + WOLFSSH_UNUSED(ctx); + + if (authType == WOLFSSH_USERAUTH_PASSWORD) { + const char* pw = (const char*)authData->sf.password.password; + word32 pwSz = authData->sf.password.passwordSz; + + if (pwSz == (word32)XSTRLEN(TPMCS_PASSWORD) + && XMEMCMP(pw, TPMCS_PASSWORD, pwSz) == 0) { + ret = WOLFSSH_USERAUTH_SUCCESS; + } + } + + return ret; +} + + +/* Create a signing key inside the TPM and produce a self-signed X.509 + * certificate over its public key. The private key never leaves the TPM: the + * certificate is signed through the wolfTPM crypto callback. */ +static int TpmCsMakeKeyAndCert(WOLFTPM2_DEV* dev, WOLFTPM2_KEY* key, + int useEcc, byte* certDer, word32* certDerSz) +{ + int rc; + int devId = INVALID_DEVID; + int sigType; + TpmCryptoDevCtx tpmCtx; + WOLFTPM2_KEY srk; + TPMT_PUBLIC pub; + const char* subject; + const char* keyUsage = "serverAuth,clientAuth"; + TPMA_OBJECT attr; + + XMEMSET(&tpmCtx, 0, sizeof(tpmCtx)); + XMEMSET(&srk, 0, sizeof(srk)); + XMEMSET(&pub, 0, sizeof(pub)); + + attr = TPMA_OBJECT_sensitiveDataOrigin | TPMA_OBJECT_userWithAuth + | TPMA_OBJECT_sign | TPMA_OBJECT_noDA; + + rc = wolfTPM2_SetCryptoDevCb(dev, wolfTPM2_CryptoDevCb, &tpmCtx, &devId); + + if (rc == 0) { + rc = wolfTPM2_CreateSRK(dev, &srk, TPM_ALG_RSA, + (const byte*)TPMCS_KEY_AUTH, (int)XSTRLEN(TPMCS_KEY_AUTH)); + } + + if (rc == 0) { + if (useEcc) { + subject = "/C=US/ST=Washington/L=Seattle/O=wolfSSL" + "/OU=ECC/CN=127.0.0.1"; + sigType = CTC_SHA256wECDSA; + rc = wolfTPM2_GetKeyTemplate_ECC(&pub, attr, + TPM_ECC_NIST_P256, TPM_ALG_ECDSA); + } + else { + subject = "/C=US/ST=Washington/L=Seattle/O=wolfSSL" + "/OU=RSA/CN=127.0.0.1"; + sigType = CTC_SHA256wRSA; + rc = wolfTPM2_GetKeyTemplate_RSA(&pub, + attr | TPMA_OBJECT_decrypt); + } + } + + if (rc == 0) { + rc = wolfTPM2_CreateAndLoadKey(dev, key, &srk.handle, &pub, + (const byte*)TPMCS_KEY_AUTH, (int)XSTRLEN(TPMCS_KEY_AUTH)); + } + + /* Point the crypto callback at the freshly created key so the self-signed + * certificate is signed by the TPM. */ + if (rc == 0) { + if (useEcc) + tpmCtx.eccKey = key; + else + tpmCtx.rsaKey = key; + + rc = wolfTPM2_CSR_Generate_ex(dev, key, subject, keyUsage, + ENCODING_TYPE_ASN1, certDer, (int)*certDerSz, sigType, + 1, devId); + } + + if (rc >= 0) { + *certDerSz = (word32)rc; + rc = 0; + } + + /* The crypto callback is only needed to self-sign the certificate. Clear + * it before wolfSSH runs: host-key signing uses wolfTPM2_SignHashScheme() + * directly, and a registered callback would route wolfSSH's certificate + * parsing through the TPM. This reset is required, so treat a failure as + * fatal. */ + if (rc == 0 && devId != INVALID_DEVID) { + rc = wolfTPM2_ClearCryptoDevCb(dev, devId); + } + + /* Restore a clean password session on the device. The certificate signing + * left the active session state unset; without this, wolfSSH's + * wolfTPM2_SignHashScheme() call fails with ctx->session == NULL. */ + if (rc == 0) { + rc = wolfTPM2_UnsetAuth(dev, 0); + } + + /* On any failure, unload the signing key so a retry does not exhaust TPM + * transient object memory. */ + if (rc != 0) { + wolfTPM2_UnloadHandle(dev, &key->handle); + } + + /* The parent storage key is only needed to create the signing key. */ + wolfTPM2_UnloadHandle(dev, &srk.handle); + + return rc; +} + + +static int TpmCsWriteCert(const char* file, const byte* der, word32 derSz) +{ + int ret = 0; + FILE* f = fopen(file, "wb"); + + if (f == NULL) { + ret = -1; + } + else { + if (fwrite(der, 1, derSz, f) != derSz) + ret = -1; + fclose(f); + } + + return ret; +} + + +static int TpmCsEcho(WOLFSSH* ssh) +{ + int ret = WS_SUCCESS; + int rxSz; + int txSz; + int sent; + byte buf[TPMCS_BUF_SZ]; + + do { + rxSz = wolfSSH_stream_read(ssh, buf, sizeof(buf)); + if (rxSz > 0) { + /* stream_send may transmit fewer bytes than requested; loop until + * the whole chunk is echoed. */ + sent = 0; + while (sent < rxSz) { + txSz = wolfSSH_stream_send(ssh, buf + sent, rxSz - sent); + if (txSz <= 0) { + ret = txSz; + break; + } + sent += txSz; + } + if (ret != WS_SUCCESS) + break; + } + } while (rxSz > 0); + + if (rxSz < 0 && rxSz != WS_EOF) + ret = rxSz; + + return ret; +} + + +int main(int argc, char* argv[]) +{ + int ret; + int useEcc = 1; + word16 port = 22222; + int i; + WOLFTPM2_DEV dev; + WOLFTPM2_KEY hostKey; + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WS_SOCKET_T listenFd = WOLFSSH_SOCKET_INVALID; + WS_SOCKET_T clientFd = WOLFSSH_SOCKET_INVALID; + SOCKADDR_IN_T clientAddr; + socklen_t clientAddrSz = sizeof(clientAddr); + byte certDer[TPMCS_CERT_MAX]; + word32 certDerSz = (word32)sizeof(certDer); + + XMEMSET(&dev, 0, sizeof(dev)); + XMEMSET(&hostKey, 0, sizeof(hostKey)); + + for (i = 1; i < argc; i++) { + if (XSTRCMP(argv[i], "-k") == 0 && i + 1 < argc) { + i++; + if (XSTRCMP(argv[i], "rsa") == 0) + useEcc = 0; + else + useEcc = 1; + } + else if (XSTRCMP(argv[i], "-p") == 0 && i + 1 < argc) { + i++; + port = (word16)atoi(argv[i]); + } + } + + printf("wolfSSH TPM X.509 host-key server (%s)\n", useEcc ? "ECDSA" : "RSA"); + + ret = wolfTPM2_Init(&dev, TPM2_IoCb, NULL); + if (ret != 0) { + fprintf(stderr, "wolfTPM2_Init failed: %d\n", ret); + return 1; + } + + ret = TpmCsMakeKeyAndCert(&dev, &hostKey, useEcc, certDer, &certDerSz); + if (ret != 0) { + fprintf(stderr, "TPM key/cert provisioning failed: %d\n", ret); + wolfTPM2_Cleanup(&dev); + return 1; + } + printf("Provisioned TPM host key and self-signed X.509 cert (%u bytes)\n", + certDerSz); + + /* The companion client trusts this file as its root CA. If it cannot be + * written, fail now rather than serve a certificate the client cannot + * load (or let it read a stale certificate from a previous run). */ + if (TpmCsWriteCert(certOutFile, certDer, certDerSz) != 0) { + fprintf(stderr, "Could not write server certificate to %s\n", + certOutFile); + wolfTPM2_UnloadHandle(&dev, &hostKey.handle); + wolfTPM2_Cleanup(&dev); + return 1; + } + printf("Wrote server certificate to %s (use as client -A CA)\n", + certOutFile); + +#ifdef DEBUG_WOLFSSH + wolfSSH_Debugging_ON(); +#endif + + if (wolfSSH_Init() != WS_SUCCESS) { + fprintf(stderr, "wolfSSH_Init failed\n"); + wolfTPM2_UnloadHandle(&dev, &hostKey.handle); + wolfTPM2_Cleanup(&dev); + return 1; + } + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) { + fprintf(stderr, "wolfSSH_CTX_new failed\n"); + ret = WS_MEMORY_E; + } + + if (ret == 0) { + wolfSSH_SetUserAuth(ctx, TpmCsUserAuth); + + /* Bind the TPM host key first so a private-key slot exists, then load + * the matching X.509 certificate. UpdateHostCertificates() ties the + * certificate to the TPM key so the exchange signs through the TPM. */ + ret = wolfSSH_CTX_UseTpmHostKey(ctx, &dev, &hostKey); + if (ret != WS_SUCCESS) + fprintf(stderr, "wolfSSH_CTX_UseTpmHostKey failed: %d (%s)\n", + ret, wolfSSH_ErrorToName(ret)); + } + + if (ret == 0) { + ret = wolfSSH_CTX_UseCert_buffer(ctx, certDer, certDerSz, + WOLFSSH_FORMAT_ASN1); + if (ret != WS_SUCCESS) + fprintf(stderr, "wolfSSH_CTX_UseCert_buffer failed: %d (%s)\n", + ret, wolfSSH_ErrorToName(ret)); + } + + if (ret == 0) { + WSTARTTCP(); + tcp_listen(&listenFd, &port, 1); + printf("Listening on port %u. Waiting for a client...\n", port); + + clientFd = accept(listenFd, (struct sockaddr*)&clientAddr, + (socklen_t*)&clientAddrSz); + if (clientFd == WOLFSSH_SOCKET_INVALID) + ret = WS_SOCKET_ERROR_E; + } + + if (ret == 0) { + ssh = wolfSSH_new(ctx); + if (ssh == NULL) + ret = WS_MEMORY_E; + } + + if (ret == 0) { + wolfSSH_set_fd(ssh, (int)clientFd); + + ret = wolfSSH_accept(ssh); + if (ret == WS_SUCCESS) { + printf("Client connected and verified the TPM-backed host " + "certificate.\n"); + ret = TpmCsEcho(ssh); + } + else { + fprintf(stderr, "wolfSSH_accept failed: %d (%s)\n", ret, + wolfSSH_ErrorToName(ret)); + } + } + + if (ssh != NULL) { + wolfSSH_stream_exit(ssh, 0); + wolfSSH_free(ssh); + } + if (clientFd != WOLFSSH_SOCKET_INVALID) + WCLOSESOCKET(clientFd); + if (listenFd != WOLFSSH_SOCKET_INVALID) + WCLOSESOCKET(listenFd); + if (ctx != NULL) + wolfSSH_CTX_free(ctx); + wolfSSH_Cleanup(); + + wolfTPM2_UnloadHandle(&dev, &hostKey.handle); + wolfTPM2_Cleanup(&dev); + + printf("Done (ret = %d).\n", (ret == WS_SUCCESS) ? 0 : ret); + return (ret == WS_SUCCESS) ? 0 : 1; +} + +#else /* missing TPM/cert prerequisites */ + +int main(void) +{ + printf("This example requires wolfSSH built with --enable-tpm " + "--enable-certs, and wolfTPM/wolfSSL with certificate generation " + "and the crypto callback (wolfSSL --enable-certgen --enable-certreq " + "--enable-certext --enable-cryptocb, wolfTPM cert generation).\n"); + return 0; +} + +#endif From b608ac3068ceeef9e36c53e62b9ce84e0f4f1581 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 2 Jul 2026 12:57:03 -0700 Subject: [PATCH 3/5] Add TPM X.509 host certificate docs and CI coverage --- .github/workflows/tpm-ssh.yml | 68 ++++++++++++++++++++++++++++++++--- README.md | 59 ++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tpm-ssh.yml b/.github/workflows/tpm-ssh.yml index 419fcd71c..1f31ee39d 100644 --- a/.github/workflows/tpm-ssh.yml +++ b/.github/workflows/tpm-ssh.yml @@ -17,6 +17,10 @@ jobs: matrix: keytype: [ rsa, ecc ] sim: [ ibmswtpm2, fwtpm ] + # raw: plain TPM host key (verified with OpenSSH). + # x509: TPM host key presented as an X.509 certificate, verified by the + # tpmcertserver/tpmcertclient example. + hostkey: [ raw, x509 ] steps: - uses: actions/checkout@v6 @@ -47,8 +51,12 @@ jobs: run: | cd wolfssl ./autogen.sh + # certgen/certreq/certext: generate the X.509 host certificate from the + # TPM key. WC_SIG_MIN_HASH_TYPE=SHA: the RSA x509v3-ssh-rsa host cert is + # SHA-1; modern wolfSSL otherwise rejects SHA-1 RSA signatures. ./configure --enable-wolftpm --enable-wolfssh --enable-keygen \ - CFLAGS="-DWC_RSA_NO_PADDING" + --enable-certgen --enable-certreq --enable-certext \ + CFLAGS="-DWC_RSA_NO_PADDING -DWC_SIG_MIN_HASH_TYPE=WC_HASH_TYPE_SHA" make sudo make install sudo ldconfig @@ -97,13 +105,14 @@ jobs: run: | cd wolfssh ./autogen.sh - ./configure --enable-tpm + ./configure --enable-tpm --enable-certs make sudo make install sudo ldconfig # Server host key resident in the TPM: the private key never enters RAM. - name: Test TPM host key (${{ matrix.keytype }}) + if: matrix.hostkey == 'raw' run: | cd wolftpm ./examples/keygen/keygen hostkey.bin -${{ matrix.keytype }} -t -eh @@ -129,9 +138,55 @@ jobs: cat ssh_out.txt grep -q "Authenticated to localhost" ssh_out.txt + # Server presents an X.509 host certificate whose private key lives in the + # TPM. The client verifies the certificate against it as the trusted CA and + # rejects a mismatched CA; the exchange hash is signed inside the TPM. + - name: Test TPM X.509 host certificate (${{ matrix.keytype }}) + if: matrix.hostkey == 'x509' + run: | + cd wolfssh + ./examples/tpmcertserver/tpmcertserver -k ${{ matrix.keytype }} \ + -p 22223 > tpmcert_server.txt 2>&1 & + echo "tpmcertserver (X.509 ${{ matrix.keytype }}) PID: $!" + # RSA key generation inside the TPM can take longer than ECC. + for i in $(seq 1 40); do + if ss -ltn 2>/dev/null | grep -q ':22223'; then break; fi + sleep 1 + done + ./examples/tpmcertserver/tpmcertclient -A tpm-server-cert.der \ + -p 22223 -h 127.0.0.1 > tpmcert_client.txt 2>&1 + echo "----- server -----"; cat tpmcert_server.txt + echo "----- client -----"; cat tpmcert_client.txt + grep -q "verified server X.509 host certificate" tpmcert_client.txt + grep -q "Client connected and verified the TPM-backed host certificate" \ + tpmcert_server.txt + + # Negative test: a client that trusts an unrelated CA must reject the server. + - name: Test TPM X.509 host certificate rejects wrong CA (${{ matrix.keytype }}) + if: matrix.hostkey == 'x509' + run: | + cd wolfssh + ./examples/tpmcertserver/tpmcertserver -k ${{ matrix.keytype }} \ + -p 22224 > tpmcert_server_neg.txt 2>&1 & + echo "tpmcertserver (X.509 neg ${{ matrix.keytype }}) PID: $!" + for i in $(seq 1 40); do + if ss -ltn 2>/dev/null | grep -q ':22224'; then break; fi + sleep 1 + done + # keys/ca-cert-ecc.der is an unrelated CA; the self-signed host cert + # does not chain to it, so the client must refuse to connect. + if ./examples/tpmcertserver/tpmcertclient -A keys/ca-cert-ecc.der \ + -p 22224 -h 127.0.0.1 > tpmcert_client_neg.txt 2>&1; then + echo "ERROR: client accepted a server with an untrusted CA" + cat tpmcert_client_neg.txt + exit 1 + fi + echo "client correctly rejected the untrusted server:" + cat tpmcert_client_neg.txt + # Client public-key authentication with a TPM-resident key (RSA only). - name: Test TPM client public-key auth - if: matrix.keytype == 'rsa' + if: matrix.keytype == 'rsa' && matrix.hostkey == 'raw' run: | cd wolftpm ./examples/keygen/keygen keyblob.bin -rsa -t -pem -eh @@ -147,7 +202,12 @@ jobs: if: always() uses: actions/upload-artifact@v7 with: - name: test-artifacts-${{ matrix.keytype }}-${{ matrix.sim }} + name: test-artifacts-${{ matrix.keytype }}-${{ matrix.sim }}-${{ matrix.hostkey }} + if-no-files-found: ignore path: | wolftpm/hostkey.bin wolfssh/ssh_out.txt + wolfssh/tpmcert_server.txt + wolfssh/tpmcert_client.txt + wolfssh/tpmcert_server_neg.txt + wolfssh/tpmcert_client_neg.txt diff --git a/README.md b/README.md index 554f1298f..358380a32 100644 --- a/README.md +++ b/README.md @@ -622,6 +622,65 @@ Note: RSA host keys are signed with `rsa-sha2-256`. The default echoserver key auth produced by keygen is `ThisIsMyKeyAuth` (override with the `-G` example's `ECHOSERVER_TPM_KEY_AUTH`). +TPM SERVER HOST KEY WITH X.509 CERTIFICATE (ECDSA / RSA) +======================================================= + +The server can present an X.509 certificate as its host key while the matching +private key stays non-exportable inside the TPM. The client verifies the +certificate against a trusted CA, so the server's identity is authenticated and +a man-in-the-middle cannot impersonate it. The exchange hash is signed inside +the TPM; the private key never enters RAM. + +This requires wolfSSH built with certificate support in addition to TPM support, +and wolfSSL/wolfTPM built with certificate generation: + + wolfSSL + $ ./configure --enable-wolfssh --enable-wolftpm --enable-keygen \ + --enable-certgen --enable-certreq --enable-certext \ + CFLAGS="-DWC_RSA_NO_PADDING" + wolfTPM + $ ./configure --enable-fwtpm --enable-swtpm + wolfSSH + $ ./configure --enable-tpm --enable-certs + +The example under `examples/tpmcertserver` creates a signing key inside the TPM, +generates a self-signed X.509 certificate from it with +`wolfTPM2_CSR_Generate_ex()`, then serves with `wolfSSH_CTX_UseTpmHostKey()` and +`wolfSSH_CTX_UseCert_buffer()`. Run a TPM simulator first (`fwtpm_server` or +`ibmswtpm2`), then: + + ECDSA: $ ./examples/tpmcertserver/tpmcertserver -k ecc + RSA: $ ./examples/tpmcertserver/tpmcertserver -k rsa + +The server writes its certificate to `tpm-server-cert.der`. The companion +client verifies the server against that certificate used as the trusted root: + + $ ./examples/tpmcertserver/tpmcertclient -A tpm-server-cert.der + +To integrate this into your own server, load the certificate and bind the TPM +key. Call `wolfSSH_CTX_UseTpmHostKey()` before `wolfSSH_CTX_UseCert_buffer()` so +the certificate is linked to the TPM key slot: + + wolfSSH_CTX_UseTpmHostKey(ctx, &tpmDev, &tpmKey); + wolfSSH_CTX_UseCert_buffer(ctx, certDer, certDerSz, WOLFSSH_FORMAT_ASN1); + +On the client, restrict the accepted host key algorithms to the certificate +algorithms so the connection cannot silently fall back to a plain host key and +skip certificate (CA) verification: + + wolfSSH_CTX_SetAlgoListKey(ctx, + "x509v3-ecdsa-sha2-nistp256,x509v3-ssh-rsa"); + wolfSSH_CTX_AddRootCert_buffer(ctx, caDer, caDerSz, WOLFSSH_FORMAT_ASN1); + +Notes: + +- ECDSA is recommended. It uses SHA-256 and needs no extra build options. +- RSA certificate host keys use the `x509v3-ssh-rsa` algorithm, which is defined + with SHA-1. Modern wolfSSL rejects SHA-1 RSA signatures by default, so RSA + additionally requires wolfSSL built with + `-DWC_SIG_MIN_HASH_TYPE=WC_HASH_TYPE_SHA`. This re-enables a deprecated hash; + prefer ECDSA unless RSA is mandated. + WOLFSSH APPLICATIONS ==================== From d4271c933c9c64d08ff48b14d7ef51c8345ca7ea Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 2 Jul 2026 13:03:24 -0700 Subject: [PATCH 4/5] examples: line-buffer stdout so tests reliably capture server output --- examples/tpmcertserver/tpmcertclient.c | 3 +++ examples/tpmcertserver/tpmcertserver.c | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/examples/tpmcertserver/tpmcertclient.c b/examples/tpmcertserver/tpmcertclient.c index 78fcac7b9..a0ea12d70 100644 --- a/examples/tpmcertserver/tpmcertclient.c +++ b/examples/tpmcertserver/tpmcertclient.c @@ -101,6 +101,9 @@ int main(int argc, char* argv[]) byte txt[] = "hello from tpmcertclient\n"; byte rxBuf[TPMCC_BUF_SZ]; + /* Line-buffer stdout so output is visible immediately when redirected. */ + setvbuf(stdout, NULL, _IOLBF, 0); + for (i = 1; i < argc; i++) { if (XSTRCMP(argv[i], "-p") == 0 && i + 1 < argc) port = (word16)atoi(argv[++i]); diff --git a/examples/tpmcertserver/tpmcertserver.c b/examples/tpmcertserver/tpmcertserver.c index 6d0b753ba..2e5e4d95f 100644 --- a/examples/tpmcertserver/tpmcertserver.c +++ b/examples/tpmcertserver/tpmcertserver.c @@ -244,6 +244,10 @@ int main(int argc, char* argv[]) byte certDer[TPMCS_CERT_MAX]; word32 certDerSz = (word32)sizeof(certDer); + /* Line-buffer stdout so progress is visible immediately when redirected to + * a file (the server runs while a test reads its output). */ + setvbuf(stdout, NULL, _IOLBF, 0); + XMEMSET(&dev, 0, sizeof(dev)); XMEMSET(&hostKey, 0, sizeof(hostKey)); From 8bd520c2a3d9b7a4331f982c3e3a2965772b9d72 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 2 Jul 2026 13:09:08 -0700 Subject: [PATCH 5/5] Address review: clear crypto callback on error, gate SHA-1 to RSA, cryptocb docs, fix comment --- .github/workflows/tpm-ssh.yml | 15 ++++++++++----- README.md | 1 + examples/tpmcertserver/tpmcertclient.c | 8 ++++++-- examples/tpmcertserver/tpmcertserver.c | 15 ++++++++------- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/.github/workflows/tpm-ssh.yml b/.github/workflows/tpm-ssh.yml index 1f31ee39d..0da943478 100644 --- a/.github/workflows/tpm-ssh.yml +++ b/.github/workflows/tpm-ssh.yml @@ -51,12 +51,17 @@ jobs: run: | cd wolfssl ./autogen.sh - # certgen/certreq/certext: generate the X.509 host certificate from the - # TPM key. WC_SIG_MIN_HASH_TYPE=SHA: the RSA x509v3-ssh-rsa host cert is - # SHA-1; modern wolfSSL otherwise rejects SHA-1 RSA signatures. + # certgen/certreq/certext/cryptocb: generate the X.509 host certificate + # from the TPM key via the crypto callback. + EXTRA_CFLAGS="-DWC_RSA_NO_PADDING" + # The RSA x509v3-ssh-rsa host cert is SHA-1; modern wolfSSL otherwise + # rejects SHA-1 RSA signatures. Only needed for the RSA cells. + if [ "${{ matrix.keytype }}" = "rsa" ]; then + EXTRA_CFLAGS="$EXTRA_CFLAGS -DWC_SIG_MIN_HASH_TYPE=WC_HASH_TYPE_SHA" + fi ./configure --enable-wolftpm --enable-wolfssh --enable-keygen \ - --enable-certgen --enable-certreq --enable-certext \ - CFLAGS="-DWC_RSA_NO_PADDING -DWC_SIG_MIN_HASH_TYPE=WC_HASH_TYPE_SHA" + --enable-certgen --enable-certreq --enable-certext --enable-cryptocb \ + CFLAGS="$EXTRA_CFLAGS" make sudo make install sudo ldconfig diff --git a/README.md b/README.md index 358380a32..020a49a79 100644 --- a/README.md +++ b/README.md @@ -637,6 +637,7 @@ and wolfSSL/wolfTPM built with certificate generation: wolfSSL $ ./configure --enable-wolfssh --enable-wolftpm --enable-keygen \ --enable-certgen --enable-certreq --enable-certext \ + --enable-cryptocb \ CFLAGS="-DWC_RSA_NO_PADDING" wolfTPM $ ./configure --enable-fwtpm --enable-swtpm diff --git a/examples/tpmcertserver/tpmcertclient.c b/examples/tpmcertserver/tpmcertclient.c index a0ea12d70..4262017cb 100644 --- a/examples/tpmcertserver/tpmcertclient.c +++ b/examples/tpmcertserver/tpmcertclient.c @@ -52,8 +52,12 @@ static int TpmCcUserAuth(byte authType, WS_UserAuthData* authData, void* ctx) } -/* The server host key is an X.509 certificate; it was already verified against - * the root CA loaded with wolfSSH_CTX_AddRootCert_buffer(). Accept it. */ +/* Host key acceptance callback. wolfSSH verifies the server's X.509 certificate + * chain against the root CA loaded with wolfSSH_CTX_AddRootCert_buffer() later, + * during the key exchange, when it extracts the public key from the certificate. + * Because the client only accepts x509v3 host key algorithms, that CA + * verification is always performed. This callback just accepts the presented + * host key blob. */ static int TpmCcHostKeyCheck(const byte* pubKey, word32 pubKeySz, void* ctx) { WOLFSSH_UNUSED(pubKey); diff --git a/examples/tpmcertserver/tpmcertserver.c b/examples/tpmcertserver/tpmcertserver.c index 2e5e4d95f..70aaa0f3b 100644 --- a/examples/tpmcertserver/tpmcertserver.c +++ b/examples/tpmcertserver/tpmcertserver.c @@ -146,13 +146,14 @@ static int TpmCsMakeKeyAndCert(WOLFTPM2_DEV* dev, WOLFTPM2_KEY* key, rc = 0; } - /* The crypto callback is only needed to self-sign the certificate. Clear - * it before wolfSSH runs: host-key signing uses wolfTPM2_SignHashScheme() - * directly, and a registered callback would route wolfSSH's certificate - * parsing through the TPM. This reset is required, so treat a failure as - * fatal. */ - if (rc == 0 && devId != INVALID_DEVID) { - rc = wolfTPM2_ClearCryptoDevCb(dev, devId); + /* The crypto callback is only needed to self-sign the certificate. Always + * clear it (including on error paths) before wolfSSH runs: host-key signing + * uses wolfTPM2_SignHashScheme() directly, and a registered callback would + * route wolfSSH's certificate parsing through the TPM. */ + if (devId != INVALID_DEVID) { + int clearRc = wolfTPM2_ClearCryptoDevCb(dev, devId); + if (rc == 0) + rc = clearRc; } /* Restore a clean password session on the device. The certificate signing