fix(windows): associate machine-scoped TPM keys with stored certificates#1053
Conversation
On Windows the agent stores attested endpoint certificates through the platform KMS, which sets skip-find-certificate-key and uses machine-scoped Platform Crypto Provider (TPM) keys. The certificate-to-key association relied on CryptFindCertificateKeyProvInfo, which (a) is skipped when skip-find-certificate-key is set and (b) cannot discover keys in the local machine keyset. As a result the certificate was stored without a linked private key, leaving it unusable. Associate the certificate with its key explicitly instead of by discovery: - tpmkms hands the CAPI layer the exact key when storing to the Windows certificate store — its CNG container name (tpm.ApplicationKeyName, the "app-"-prefixed key name used to persist the key), the TPM provider, and whether it lives in the local machine keyset. - capi sets CERT_KEY_PROV_INFO_PROP_ID directly via setCertificateKeyProvInfo when a container name is supplied. This needs no container enumeration, so it never prompts for a smart card and runs even with skip-find-certificate-key set. It also marks the prov info with CRYPT_MACHINE_KEYSET for machine-scoped keys. - The CRYPT_KEY_PROV_INFO struct is corrected to use pointer/DWORD fields so it can be used to set the property (it was int-typed and read-only before). - The discovery fallback now searches both the user and machine keysets for machine-location certificates. tpm.ApplicationKeyName is exported as the single source of truth for the persisted application-key name, shared with tpmkms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
maraino
left a comment
There was a problem hiding this comment.
I would try to avoid the new parameter, we know using tpmkms parameter what would be the value of the key-machine-keyset, let's use the same logic, adding the already existing key-scope if needed.
I also have some comments about Microsoft naming.
- Drop the new capi "key-machine-keyset" parameter. tpmkms now forwards the
existing "key-scope", and capi derives the keyset with the same resolution
tpmkms uses (key-scope, falling back to store-location). Avoids a redundant
parameter per review.
- Restrict the discovery fallback to a single keyset by scope instead of
setting both flags (which is the default and searches both anyway).
- Rename the uriAttributes "containerName" field to "keyContainerName" to
match CRYPT_KEY_PROV_INFO's pwszContainerName ("key container").
- Drop the runtime.KeepAlive calls in setCertificateKeyProvInfo; info keeps
the strings reachable through the call, matching the other property helpers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the review. Pushed b0e2839 addressing the feedback:
One thing worth a look: the discovery fallback now restricts to the scope's keyset both ways (machine→machine, user→user), whereas before it searched both. It's more precise and matches the "a machine cert shouldn't probe the user container" point, but it's a subtle behavior change for the general (non-tpmkms) The fix is verified working end-to-end on a Windows TPM host (attested endpoint cert now has its private key linked in |
maraino
left a comment
There was a problem hiding this comment.
Looks good, but I think we might leave keysetFlags as 0 by default if u.isMachineKeySet() is false. See comment.
| keysetFlags := CRYPT_FIND_USER_KEYSET_FLAG | ||
| if u.isMachineKeyset() { | ||
| keysetFlags = CRYPT_FIND_MACHINE_KEYSET_FLAG | ||
| } |
There was a problem hiding this comment.
It can be ok, but I'm not sure if it will be better to use both by default, I think leaving the keysetFlags to 0 will do it.
There was a problem hiding this comment.
CryptFindCertificateKeyProvInfo searches both the user and machine containers when no keyset flag is passed (its documented default, and what the library did before this change). The discovery fallback now defaults to that both-containers search and restricts to a single keyset only when the key scope is known: explicit key-scope=user (new isUserKeyset helper) → user, otherwise the existing isMachineKeyset check → machine. This preserves the legacy behavior for callers that don't specify a key scope while still finding machine-scoped keys. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed 25d69a9 to make the key-discovery fallback backward-compatible. Context: Change: the discovery fallback now defaults to var keysetFlags uint32 // 0 = search both containers (CryptFindCertificateKeyProvInfo default)
switch {
case u.isUserKeyset(): // explicit key-scope=user
keysetFlags = CRYPT_FIND_USER_KEYSET_FLAG
case u.isMachineKeyset(): // key-scope=machine, or store-location=machine fallback
keysetFlags = CRYPT_FIND_MACHINE_KEYSET_FLAG
}Behavior:
So the common legacy case (no One deliberate asymmetry: the new For background, this is consistent with the canonical Windows behavior: a certificate's store location and its private key's keyset are independent axes (the link is the cert's |
Summary
On Windows, certificates stored for machine-scoped TPM (Microsoft Platform Crypto Provider) keys were left without an associated private key, making them unusable (e.g. for Wi-Fi/EAP-TLS, VPN). This was surfaced by the step-agent's
windows-key-scopework, which moves the agent's AK — and therefore its attested endpoint keys — to the local machine keyset.Root cause
The certificate→key association ran exclusively through
CryptFindCertificateKeyProvInfo(discovery), which fails here for two compounding reasons:kms/platform'stransformToTPMKMSsetsskip-find-certificate-key=trueon Windows by default, so discovery never runs for these certificates.CryptFindCertificateKeyProvInfodoes not search the local machine keyset, so it cannot discover a machine-scoped PCP/TPM key even when asked.Keeping endpoint keys user-scoped isn't an option:
tpm.AttestKeyrequires an attested key to share its AK's scope (an attest-session constraint), and the AK is machine-scoped.Fix — associate explicitly instead of by discovery
tpm.ApplicationKeyName, theapp--prefixed key name used to persist the key), the TPM provider, and whether the key lives in the local machine keyset.CERT_KEY_PROV_INFO_PROP_IDdirectly viasetCertificateKeyProvInfowhen a container name is supplied. This needs no container enumeration, so it never prompts for a smart card and runs even whenskip-find-certificate-keyis set; it marks the prov info withCRYPT_MACHINE_KEYSETfor machine-scoped keys.CRYPT_KEY_PROV_INFOstruct is corrected to pointer/DWORD fields so it can be used to set the property (it wasint-typed and effectively read-only).tpm.ApplicationKeyNameis exported as the single source of truth for the persisted application-key name, shared with tpmkms.Testing
GOOS=windows go build ./kms/... ./tpm/...and hostgo buildboth clean;go vetshows only a pre-existingunsafe.Pointerline;tpmname tests pass.LocalMachine\My(previously orphaned).🤖 Generated with Claude Code