Skip to content

feat(tpm): hold an RSA CA key in the TPM - #282

Merged
Bugs5382 merged 2 commits into
mainfrom
feat/197-tpm-rsa-ca-key
Sep 30, 2026
Merged

Bugs5382 merged 2 commits into
mainfrom
feat/197-tpm-rsa-ca-key

Conversation

@Bugs5382

@Bugs5382 Bugs5382 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

What and why

With state_key.mode: tpm, an RSA pki.root_key_alg made the ceremony fail, because every layer of the TPM path only handled ECC. This PR lets a TPM create and hold an RSA-3072 or RSA-4096 CA key. The key is loaded through the same RootKeyBackend and key loader as the ECDSA key, and it has the same fixedTPM, fixedParent and sensitiveDataOrigin attributes. The crypto path still uses only stdlib and go-tpm, and there are no new Go dependencies.

  • Template, parser, signer. RSA keys get a template with a null scheme, so the key has no padding or hash baked in. Sign takes the hash (SHA-256, SHA-384 or SHA-512) and the padding (PKCS#1 v1.5, or PSS for *rsa.PSSOptions) from crypto.SignerOpts, and returns the raw signature, not ASN.1-wrapped. Every RSA signature is checked against the public key before it's returned. That catches a faulty signature, and it also catches a PSS salt length the TPM picked that differs from the one the caller asked for. ECDSA signing behaves as before.
  • The floor. RSA-2048 is still refused for a TPM-held CA key, in CreateKey and in LoadKey, to match the 3072-bit subject-key rule.
  • When the TPM lacks the size. The TPM 2.0 spec only requires RSA-2048, and many parts stop there. For RSA, CreateKey first calls TPM2_TestParms. If the TPM doesn't implement the size, it returns tpm.ErrKeyAlgorithmUnsupported and creates nothing. The root ceremony and CA-key rotation turn that into FailedPrecondition, and the message names the algorithm. The ceremony can then be run again with another algorithm. An intermediate or issuing node creates its key at first boot, so on that node boot stops with the same error (fail closed, as before). Nothing ever falls back to a smaller size or to a software key.
  • Probing. Probe now also reports which RSA sizes the TPM accepts (Capabilities.RSAKeyBits, SupportsRSAKeyBits), and boot logs init: TPM capabilities: ECC curves [...], RSA key sizes [...]. The P-384 boot check hasn't changed.
  • Export. Export is still refused for any TPM-held key. The mode check is now a named helper, caKeyExportable, so it can be tested.
  • Test hooks. init.NewTPMRootBackend and tpm.OpenTransport exist so the end-to-end test can drive the production backend against swtpm.

Testing against a TPM that has RSA-3072

The in-process simulator the TPM tests use (go-tpm-tools' build of the Microsoft reference TPM) implements RSA-2048 only. Its build profile turns RSA-3072 off, and enabling it with a define crashes the simulator at reset. So the tests are split:

  • In the simulator (always runs):
    • RSA-3072 and RSA-4096 are refused with ErrKeyAlgorithmUnsupported, and no TPM2_Create is sent. This is a real refusal from the simulator, not a fake.
    • The ceremony returns FailedPrecondition naming RSA-3072, commits no identity, and then succeeds on a retry with ECDSA-P384.
    • Rotation returns FailedPrecondition and stages nothing.
    • The RSA signing mechanics are tested on a 2048-bit key made through the same template and load path: PKCS#1 v1.5 with SHA-256, SHA-384 and SHA-512, PSS honoured from opts, and bad input rejected.
    • The key attributes are identical to the ECDSA key's.
  • Against swtpm (internal/e2e/tpm_rsa_e2e_test.go): the real ceremony in tpm mode with RSA-3072. Then a reboot (swtpm restarts on its persisted state), and a reload through the production backend. The reloaded key then signs a leaf, a subordinate CA, a CSR, a CRL and a delegated OCSP responder. Everything verifies and is SHA-2 RSA signed. The test also checks that the stored key is a TPM RSA object with fixedTPM, fixedParent and sensitiveDataOrigin. It skips when swtpm isn't installed, but fails under CI, and ci-go now installs swtpm from apt, as ci-image already does for its toolchain.

swtpm 0.10.2 (locally) and swtpm 0.7.3 with libtpms 0.9.3 (the Ubuntu 24.04 packages CI installs) implement RSA-3072, and 0.10.2 does not implement RSA-4096. So RSA-4096 has only been checked on its refusal path, not created in a TPM.

Tests changed

TestPublicTemplateRejectsRSA became TestPublicTemplateRSAFloor. The behaviour it pinned (every RSA size refused on the TPM path) is what this PR changes. It now pins RSA-2048 refused (the error names the 3072 floor) and RSA-3072 and RSA-4096 accepted. No other existing test was changed.

Docs

Overlap with other open PRs

Closes #197

Verification

  • Lint clean
  • Tests pass
  • Build succeeds
  • Documentation updated (if behavior or API changed)

How this was verified

  • task ci (fmt, golangci-lint, vet, go test ./..., build) passes locally with swtpm 0.10.2 on PATH, so the swtpm end-to-end test ran and passed. Locally, TMPDIR=/tmp was needed because macOS's long default temp path breaks the existing internal/grpc unix-socket test. That's unrelated to this change, and the test passes with a short path.
  • CI's first run caught something real: Ubuntu's libtpms answers the ceremony's RSA signature with TPM_RC_RETRY. On a node, the Linux TPM driver behind /dev/tpmrm0 resends a command that gets TPM_RC_RETRY or TPM_RC_TESTING. The test's raw swtpm socket has no driver, so the test transport now resends the same way (second commit). Production code is unchanged by that fix. CI is now green, and the end-to-end test ran there against swtpm.
  • golic license check: 0 files changed. actionlint is clean on ci-go.yml. The pre-push hook passed.
  • Red first: the new simulator tests and the ceremony and rotation FailedPrecondition tests failed before the implementation. The ceremony test failed with Internal, and the probe and signer tests didn't compile.

@Bugs5382 Bugs5382 added this to the v1.0.0 milestone Sep 30, 2026
@Bugs5382 Bugs5382 added the enhancement New feature (feat). Minor version bump. label Sep 30, 2026
@Bugs5382 Bugs5382 linked an issue Sep 30, 2026 that may be closed by this pull request
4 tasks
@Bugs5382 Bugs5382 self-assigned this Sep 30, 2026
Bugs5382 added a commit to CryptOS-PKI/docs that referenced this pull request Sep 30, 2026
machine-config-pki and machine-config said the TPM path holds ECDSA P-384 keys only and quoted the old refusal. With CryptOS-PKI/cryptos-node#282 an RSA-3072 or RSA-4096 CA key is created in the TPM when the TPM implements that size, and key creation fails with FailedPrecondition, never falling back, when it does not. The callouts now say that. Depends on CryptOS-PKI/cryptos-node#282 and should merge with it.

Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
Bugs5382 added a commit to CryptOS-PKI/docs that referenced this pull request Sep 30, 2026
air-gapped-root said an RSA CA key can't be held in the TPM. With CryptOS-PKI/cryptos-node#282 it can, at a size the TPM implements; the table and a new caution callout cover the RSA-2048-only TPMs and the FailedPrecondition refusal. Depends on CryptOS-PKI/cryptos-node#282 and should merge with it.

Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
Bugs5382 added a commit to CryptOS-PKI/docs that referenced this pull request Sep 30, 2026
keys-never-leave-tpm said the TPM path was ECC only and listed RSA CA keys in the TPM as not built. With CryptOS-PKI/cryptos-node#282 the page covers the RSA template, the TestParms size check at boot and before creation, RSA signing through the TPM, and the refusal on TPMs without the size. Depends on CryptOS-PKI/cryptos-node#282 and should merge with it.

Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
@Bugs5382
Bugs5382 marked this pull request as ready for review September 30, 2026 03:38
An RSA-3072 or RSA-4096 CA key can now be created and held in the TPM in
state_key.mode tpm, loaded through the same RootKeyBackend and key loader as
the ECDSA key, with the same fixedTPM, fixedParent and sensitiveDataOrigin
attributes.

- The key template, public-area parser and signer handle RSA. The key carries
  a null scheme; Sign takes the hash and padding (PKCS#1 v1.5 or PSS) from
  crypto.SignerOpts, returns the raw signature, and verifies it before
  returning it.
- CreateKey asks the TPM with TPM2_TestParms whether it implements the size
  first and returns ErrKeyAlgorithmUnsupported, creating nothing, when it does
  not. The ceremony and CA-key rotation turn that into FailedPrecondition
  naming the algorithm. Nothing falls back to a smaller size or to software.
- RSA-2048 stays refused for a TPM-held CA key, matching the 3072-bit floor.
- Probe reports the RSA sizes the TPM accepts, and boot logs them.
- Export stays refused for any TPM-held key; the mode check is now a named
  helper.

The in-process simulator implements RSA-2048 only, so the RSA-3072 path is
tested end to end against swtpm (ceremony, reboot and reload, leaf,
subordinate, CSR, CRL, OCSP), which CI now installs.

Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
The Linux TPM driver behind /dev/tpmrm0 resends a command the TPM answers with TPM_RC_RETRY or TPM_RC_TESTING. The test talks to swtpm over a raw socket with no driver, and the libtpms on the CI runner answers the RSA signature in the ceremony with TPM_RC_RETRY, so the test transport now resends as the driver does.

Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
@Bugs5382
Bugs5382 force-pushed the feat/197-tpm-rsa-ca-key branch from 49156bd to 50b5fe6 Compare September 30, 2026 10:09
Bugs5382 added a commit to CryptOS-PKI/docs that referenced this pull request Sep 30, 2026
air-gapped-root said an RSA CA key can't be held in the TPM. With CryptOS-PKI/cryptos-node#282 it can, at a size the TPM implements; the table and a new caution callout cover the RSA-2048-only TPMs and the FailedPrecondition refusal. Depends on CryptOS-PKI/cryptos-node#282 and should merge with it.

Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
Bugs5382 added a commit to CryptOS-PKI/docs that referenced this pull request Sep 30, 2026
keys-never-leave-tpm said the TPM path was ECC only and listed RSA CA keys in the TPM as not built. With CryptOS-PKI/cryptos-node#282 the page covers the RSA template, the TestParms size check at boot and before creation, RSA signing through the TPM, and the refusal on TPMs without the size. Depends on CryptOS-PKI/cryptos-node#282 and should merge with it.

Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
Bugs5382 added a commit to CryptOS-PKI/docs that referenced this pull request Sep 30, 2026
machine-config-pki and machine-config said the TPM path holds ECDSA P-384 keys only and quoted the old refusal. With CryptOS-PKI/cryptos-node#282 an RSA-3072 or RSA-4096 CA key is created in the TPM when the TPM implements that size, and key creation fails with FailedPrecondition, never falling back, when it does not. The callouts now say that. Depends on CryptOS-PKI/cryptos-node#282 and should merge with it.

Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
@Bugs5382

Copy link
Copy Markdown
Contributor Author

Closing summary: an RSA CA key (3072 bits and up) is now created and held in the TPM through the single key loader, and the ceremony fails early when the TPM can't do the requested size. The swtpm transport resends on TPM_RC_RETRY. Rebased onto main after #285; CI green on 50b5fe6, including the swtpm RSA-3072 end-to-end test. Squash-merging with the owner's approval; closes #197.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature (feat). Minor version bump.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(tpm): hold an RSA CA key in the TPM

1 participant