feat: add per-key key-scope option for Windows machine vs user key store#1021
Draft
feat: add per-key key-scope option for Windows machine vs user key store#1021
Conversation
Introduces a key-scope=machine|user URI parameter on tpmkms that
controls whether the underlying private key lives in the local machine
key store (NCRYPT_MACHINE_KEY_FLAG) or in the current user's key store.
The parameter is independent of store-location, which controls cert
location, so callers can request machine-store certs paired with
user-owned keys (or vice versa) for security-hardening scenarios such
as a service whose cert needs machine visibility but whose key should
only be openable by the service identity.
Architecture:
- MachineKey is a per-key concern, not a per-TPM-instance one. Threaded
through CreateKeyConfig, AttestKeyConfig, and CreateAKConfig, then
via context into TPM.open() which sets attestConfig.MachineKey under
the lock just before attest.OpenTPM. Persisted in storage so reload
after restart uses the matching scope without the URI carrying it.
- tpmkms.CreateKey now preserves key-scope=machine in the returned URI
so downstream CreateSigner/DeleteKey calls don't lose scope.
- AttestKey reads the AK's persisted scope and uses it for the attest
session; the new key inherits the AK's scope. Conflicting explicit
AttestKeyConfig.MachineKey is rejected with a clear error.
- Defaults for back-compat: when key-scope is unset, derive from
store-location (machine cert -> machine key) so existing URIs that
happened to set store-location=machine continue to work.
Also fixes related delete-path issues:
- DeleteKey/DeleteAK now always clean up the file-store entry even if
the in-TPM (NCrypt) deletion fails, preventing repeated retries from
re-encountering the same failing entry.
- Returns a typed *PartialDeleteError when the in-TPM deletion failed
but the file-store cleanup succeeded, so callers can distinguish
that case from a complete failure.
Bumps github.com/smallstep/go-attestation to v0.4.9, which adds the
OpenConfig.MachineKey field this change drives.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
Previously the check only fired when the attested key was requested as machine-scope while the AK was user-scope; the reverse case (AK machine-scope, attested key requested user-scope) silently inherited the AK's scope and produced a machine-scope key, which is surprising behaviour for a caller who explicitly asked for user. Reject both mismatch directions so the inheritance rule is loud, not silent. Documented in the error message that attested keys inherit the AK's scope and that callers must set AttestKeyConfig.MachineKey to match the AK's scope explicitly. Caught by the capi-diag attest-roundtrip matrix: the ak-machine+key-user_xfail row was producing a successful machine-scope attested key instead of erroring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
key-scope=machine|userURI parameter tokms/tpmkms, controlling whether the underlying private key lives in the local machine key store (NCRYPT_MACHINE_KEY_FLAG) or the current user's key store. Independent ofstore-location, so callers can pair a machine-store certificate with a user-owned key (e.g., aLocalSystemservice that wants its cert visible machine-wide but its key only openable bySYSTEM) or vice versa.MachineKeyper-key throughtpm.CreateKeyConfig/tpm.AttestKeyConfig/tpm.CreateAKConfigand into the underlying NCrypt PCP create call. Per-key, not per-TPM-instance — a singletpm.TPMhandle can mint or open keys in either scope.t.open()reads the requested scope from context and applies it toattestConfig.MachineKeyunder the lock just beforeattest.OpenTPM. Safe becauseattestTPMis recreated on everyopen().MachineKeyinstorage.AKandstorage.Keyso reload after process restart uses the matching scope without the URI needing to carry it. JSONomitemptyso existing on-disk stores remain readable and default tofalse(= existing user-scope behavior).tpmkms.CreateKeypreserveskey-scope=machinein the returned URI so downstreamCreateSigner/DeleteKeycalls don't lose scope (closes a returned-URI bug we hit during testing).AttestKeyreads the AK's persisted scope and uses it for the attest session — attested keys inherit their AK's scope; conflictingAttestKeyConfig.MachineKeyis rejected with a clear error.DeleteKey/DeleteAKalways clean up the file-store entry even if the in-TPM (NCrypt) deletion fails, preventing repeated retries from re-encountering the same failing entry. When the file-store cleanup succeeded but the in-TPM step did not, returns a typed*PartialDeleteErrorso callers can distinguish a partial delete from total failure.github.com/smallstep/go-attestationtov0.4.9for theOpenConfig.MachineKeyfield this change drives.Defaults: when
key-scopeis unset, derive fromstore-locationfor back-compat (store-location=machine→key-scope=machine).Background
Code review and a Windows test matrix (interactive admin +
LocalSystemviapsexec -s -i 0, both x86_64) showed that the agent's TPM-backed device-identity keys land underLocalSystem's user-profile PCPKSP scope rather than the machine scope, even whenstore-location=machineis set. Thekms/capimachine-key fix from #802 doesn't apply because the agent useskms/tpmkms(withenable-cng=true) — that path goes throughtpm/internal/key/pcp_windows.go, wherenCryptCreatePersistedKeywas hardcoded todwFlags=0. Fixing it requires (a) plumbing aMachineKeyflag through thetpmpackage and (b) deciding how to express user intent at the URI layer.This PR is an alternative shape to #1006. Differences:
WithMachineKey()being a per-TPM option (would affect all keys going through that handle); the discussion converged on per-key being the right model. This PR putsMachineKeyon the per-key configs (CreateKeyConfig,AttestKeyConfig,CreateAKConfig) and uses a context value to thread it intot.open()for the operations that need it.MachineKeylives onoptions, notattestConfig. Also from Herman's review.key-scopeis decoupled fromstore-location. Cert location and key ownership are orthogonal Windows concepts. There are real scenarios where a service wants its cert inLocalMachine\Myfor visibility but its key restricted to one identity (e.g.,LocalSystem-only) —key-scope=userwithstore-location=machineexpresses that. Default-derive fromstore-locationfor back-compat.*PartialDeleteErrorforDeleteKey/DeleteAK. Also from Herman's review.Test plan
go build ./...clean onlinux/amd64andwindows/amd64go vet ./...clean (one pre-existing master warning inkms/capi/ncrypt_windows.go:569, unrelated to this change)go test ./...green across all packagespsexec -s -i 0LocalSystem):key-scope=machinelands keys atC:\ProgramData\Microsoft\Crypto\PCPKSP\<sid-hash>\<file>.PCPKEY(interactive admin + LocalSystem).key-scope=userlands keys at<userprofile>\AppData\Local\Microsoft\Crypto\PCPKSP\<sid-hash>\<file>.PCPKEY(LocalSystem profile when running as SYSTEM).key-scope=machine+store-location=userputs cert in user store and key in machine PCPKSP;key-scope=user+store-location=machineputs cert in machine store and key in user PCPKSP.key-scopeunset +store-location=machinecorrectly resolves to machine scope.CreateSignervia the URI returned byCreateKeyround-trips successfully in every TPM scenario; sign+verify ok.Out of scope
key-scope=machineon the device-identity TPM URI; lands separately in the agent repo.kms/capi's returned-URI bug (different root cause, different file). Tracked separately.🤖 Generated with Claude Code