Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 69 additions & 4 deletions .github/workflows/tpm-ssh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -47,8 +51,17 @@ jobs:
run: |
cd wolfssl
./autogen.sh
# 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 \
CFLAGS="-DWC_RSA_NO_PADDING"
--enable-certgen --enable-certreq --enable-certext --enable-cryptocb \
CFLAGS="$EXTRA_CFLAGS"
make
sudo make install
sudo ldconfig
Expand Down Expand Up @@ -97,13 +110,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
Expand All @@ -129,9 +143,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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [Low] Negative CI test can false-pass on server startup failure

The negative test asserts rejection purely via the client's non-zero exit code (if tpmcertclient ...; then ERROR). If the server never comes up (TPM provisioning failure, port bind failure, etc.), the client also exits non-zero -- from a plain connection failure, not certificate rejection -- and the negative test passes anyway, hiding the real problem. This is largely mitigated because the positive test step (lines 149-167) runs first in the same matrix cell and would fail on a broken server, but the negative step is not self-guarding.

Fix: After the client fails, additionally assert the failure was a certificate/verification error (e.g. grep -q "wolfSSH_connect failed" tpmcert_client_neg.txt and confirm the server logged that it started listening) so a startup failure cannot masquerade as a correct rejection.

- 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
Expand All @@ -147,7 +207,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
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,66 @@ 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 \
--enable-cryptocb \
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
====================

Expand Down
1 change: 1 addition & 0 deletions examples/include.am
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions examples/tpmcertserver/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/tpmcertserver
/tpmcertclient
/tpm-server-cert.der
21 changes: 21 additions & 0 deletions examples/tpmcertserver/include.am
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading