tpm: certify a persisted key against the caller's qualifying data - #1092
tpm: certify a persisted key against the caller's qualifying data#1092tedmalone wants to merge 2 commits into
Conversation
CreateAttestation has only ever returned the certification recorded when a key was created — key.CertificationParameters loads the stored blob — so the qualifying data in its response is whatever AttestKey was given, no matter what the request URI carries. A caller that supplies qualifying data per request gets the first request's nonce back every time. ACME device-attest-01 is such a caller: the expected nonce comes from each order's keyAuthorization. Only the order that created the key can be satisfied by the stored statement, so every subsequent order needs a new key, and the credential rotates on every issuance. Add Key.Recertify, which loads the key and the AK that attested it and runs a fresh TPM2_Certify against a nonce supplied now, and have CreateAttestation use it whenever the request URI carries qualifying data. With no qualifying data there is nothing to bind and the stored statement is returned unchanged, so existing callers are unaffected. Requires the Key.Recertify added in smallstep/go-attestation#11.
|
|
|
Reviewed (PRO-506 batch). Clean and correct. Two notes:
|
Validated against a real TPM and the production CAThe simulator tests in this PR are still unrun (no CGO on the machine this was written on), but the change has now been exercised end to end on hardware — a Windows box with a discrete TPM, running the agent against the production Two service restarts, with the agent side changed to use a stable key name (smallstep/agent#1149) so the same persisted key is presented to consecutive ACME orders: Same key material, new certificates. Each of those issuances is an ACME order whose So the |
Pins github.com/smallstep/go-attestation to the re-certification change (Key.Recertify + key.handle()) rebased onto the previously-pinned e1a87a0, so tpm.Key.Recertify / tpmkms.CreateAttestation compile and the branch is no longer blocked. Verified: `go build ./tpm/... ./kms/tpmkms/...` passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Update: bumped
So the re-certify path works end-to-end against the software TPM simulator through the crypto → go-attestation chain. Remaining validation is real-hardware on ted-flex (restart twice; fingerprint holds while the cert still re-issues). |
Why
CreateAttestationreturns the certification recorded when the key was created:key.CertificationParametersloads the stored blob (tpm/key.go), and the request URI'squalifying-datais never consulted (kms/tpmkms/tpmkms.go:1653). A caller that supplies qualifying data per request therefore gets the first request's nonce back every time.ACME
device-attest-01is such a caller — the expected nonce is derived from each order'skeyAuthorization. Only the order that created the key can be satisfied by the stored statement, so every subsequent order requires a new key. Consumers end up rotating a hardware credential on every issuance, which silently invalidates anything registered against its public key. This is the root cause behind smallstep/agent's PRO-506.What
tpm.Key.Recertify(ctx, qualifyingData)— loads the key and the AK that attested it, runs a freshTPM2_Certifyagainst the supplied nonce, and returns the resulting parameters. The key is untouched; only a new signed statement is produced.tpmkms.CreateAttestationcalls it when the request URI carriesqualifying-data.With no qualifying data there is nothing to bind, so the stored statement is returned unchanged and existing callers see no behavior change.
Why this is safe for relying parties
The statement
Recertifyproduces is the sameTPMS_ATTEST/TagAttestCertifyshape as the stored one, over the same keyNameand the same key attributes, signed by the same AK. OnlyExtraDatadiffers. Verifiers that check the certification (includingCertificationParameters.Verify) enforce nothing that binds a statement to key-creation time — confirmed against the CA verifier insmallstep/certificates(acme/challenge.go) and the pinnedsmallstep/go-attestationfork.Testing
tpm/recertify_simulator_test.go(-tags tpmsimulator) asserts, against the simulator:Publicbytes are unchanged — same key, new statement;Confirmed passing (Go 1.26 + CGO):
go build ./tpm/... ./kms/tpmkms/...is clean against the go-attestation pin. Real-hardware validation on ted-flex (restart twice; fingerprint holds while the cert still re-issues) is the remaining check.