Add TPM-backed X.509 host certificate support and fix TPM-build client auth#1081
Add TPM-backed X.509 host certificate support and fix TPM-build client auth#1081aidangarske wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds TPM-backed X.509 host-certificate support for wolfSSH servers (non-exportable TPM private key, certificate-based server identity verification) and fixes a TPM-build regression where clients could not perform password/keyboard-interactive authentication.
Changes:
- Fixes client auth-method selection so
--enable-tpmbuilds still handle password/keyboard-interactive, with TPM publickey auth preferred when a TPM key is configured. - Adds
examples/tpmcertserver/demonstrating a TPM-resident host key presented via an X.509 certificate, plus a minimal verifying client. - Extends CI (
tpm-ssh.yml) and documentation (README.md) to cover the new X.509 host-certificate flow.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/internal.c |
Fixes TPM-related auth-method handling and adds TPM publickey preference logic. |
README.md |
Documents how to run a TPM-backed X.509 host-certificate server/client flow. |
examples/tpmcertserver/tpmcertserver.c |
New example server: creates TPM key, generates self-signed cert, serves with TPM host key + cert. |
examples/tpmcertserver/tpmcertclient.c |
New example client: loads CA, restricts hostkey algos to x509, connects and performs echo. |
examples/tpmcertserver/include.am |
Builds the new example programs under the appropriate feature gates. |
examples/tpmcertserver/.gitignore |
Ignores built example binaries and generated cert output. |
examples/include.am |
Includes the new example directory in the examples build. |
.github/workflows/tpm-ssh.yml |
Adds matrix coverage and runs positive/negative X.509 TPM-host-certificate tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
dgarske
left a comment
There was a problem hiding this comment.
Skoll Code Review
Scan type: reviewOverall recommendation: COMMENT
Findings: 5 total — 5 posted, 0 skipped
5 finding(s) posted as inline comments (see file-level comments below)
Posted findings
- [Medium] TPM publickey preference has no fallback to password/keyboard on failure —
src/internal.c:16420-16431 - [Medium] TpmCcLoadFile silently truncates CA files larger than the buffer —
examples/tpmcertserver/tpmcertclient.c:70-89 - [Low] sigType/subject may trigger -Wmaybe-uninitialized —
examples/tpmcertserver/tpmcertserver.c:87-91 - [Low] Example server compares password with non-constant-time XMEMCMP —
examples/tpmcertserver/tpmcertserver.c:69-72 - [Low] Negative CI test can false-pass on server startup failure —
.github/workflows/tpm-ssh.yml:169-190
Review generated by Skoll
| keySig_ptr->sigId = ID_NONE; | ||
| keySig_ptr->heap = ssh->ctx->heap; | ||
|
|
||
| #ifdef WOLFSSH_TPM |
There was a problem hiding this comment.
🟠 [Medium] TPM publickey preference has no fallback to password/keyboard on failure
When ssh->ctx->tpmKey != NULL and the server offers publickey, the new block strips the WOLFSSH_USERAUTH_PASSWORD (and WOLFSSH_USERAUTH_KEYBOARD) bits from authType. Because DoUserAuthFailure() re-invokes SendUserAuthRequest() on every server FAILURE, a client with a TPM key configured can never fall back to password/keyboard auth if the TPM publickey attempt is rejected by the server -- it will keep re-sending publickey until the server disconnects. The comment says 'prefer publickey', but the implementation makes it exclusive with no fallback. This matches the pre-PR TPM behavior (which was publickey-only), so it is not a regression, but confirm exclusivity (rather than true 'prefer with fallback') is the intended design. If a client legitimately has both a TPM key and a valid password and the key is not authorized server-side, it will fail to authenticate.
Fix: Confirm the intended behavior. If password/keyboard fallback should occur after publickey fails, gate the stripping so it only applies to the first attempt (e.g. track a publickey-attempted flag) rather than on every DoUserAuthFailure retry.
| } | ||
|
|
||
|
|
||
| static int TpmCcLoadFile(const char* file, byte* buf, word32* bufSz) |
There was a problem hiding this comment.
🟠 [Medium] TpmCcLoadFile silently truncates CA files larger than the buffer
fread(buf, 1, *bufSz, f) reads at most *bufSz (2048) bytes. If the CA file is larger than the buffer, n == *bufSz and the function reports success (*bufSz = n) while silently returning a truncated certificate. The truncated DER then fails to parse in wolfSSH_CTX_AddRootCert_buffer() with a confusing error, or (worse for a test harness) masks the real cause. There is no check that EOF was actually reached.
Fix: Detect the file-larger-than-buffer case and return an error instead of loading a truncated certificate.
| { | ||
| int rc; | ||
| int devId = INVALID_DEVID; | ||
| int sigType; |
There was a problem hiding this comment.
🔵 [Low] sigType/subject may trigger -Wmaybe-uninitialized
int sigType; and const char* subject; are only assigned inside the first if (rc == 0) block and only read inside the later if (rc == 0) block that calls wolfTPM2_CSR_Generate_ex(). The uses are logically guarded (they are reachable only when the assigning block ran), but GCC/Clang flow analysis often cannot correlate the separate rc == 0 blocks across the intervening wolfTPM2_CreateAndLoadKey() call and may emit -Wmaybe-uninitialized. If the example is ever built with -Werror this breaks the build.
Fix: Initialize sigType = 0; and subject = NULL; at declaration to silence potential maybe-uninitialized warnings.
| const char* pw = (const char*)authData->sf.password.password; | ||
| word32 pwSz = authData->sf.password.passwordSz; | ||
|
|
||
| if (pwSz == (word32)XSTRLEN(TPMCS_PASSWORD) |
There was a problem hiding this comment.
🔵 [Low] Example server compares password with non-constant-time XMEMCMP
The password check uses XMEMCMP, which is not constant-time and violates the wolfSSL convention of comparing secrets with ConstantCompare(). This is example/demo code with a hardcoded password, so impact is negligible, but the convention exists and other examples may be copied from this one. Note this is a convention nit, not a security-audit finding.
Fix: Use ConstantCompare() for the password comparison to match repo convention, or leave as-is given it is demo code with a fixed credential.
| 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. |
There was a problem hiding this comment.
🔵 [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.
Description
support for ZD 22094