From 4ae9047ff6d7fcf338d6f365f9fc6a74733ebadd Mon Sep 17 00:00:00 2001 From: Diego Fronza Date: Fri, 24 Apr 2026 18:13:29 -0300 Subject: [PATCH 1/5] Add store-location=machine URI option for Windows machine key store Introduces WithMachineKey() TPM option wired to the store-location=machine URI parameter in tpmkms. On Windows, this causes NCRYPT_MACHINE_KEY_FLAG to be passed to NCrypt key operations, directing key creation and access to the local machine key store rather than the current user key store. Has no effect on non-Windows platforms. Also bumps go-attestation from pre-release v0.4.4-... to released v0.4.5, which introduces the MachineKey field used by this option. Change-Type: feature Release-Note: yes Audience: operator, admin Impact: low Breaking: false Co-Authored-By: Claude --- go.mod | 2 +- go.sum | 2 ++ kms/tpmkms/tpmkms.go | 3 +++ tpm/tpm.go | 11 +++++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2c00e9fd..587ef760 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,6 @@ require ( github.com/peterbourgon/diskv/v3 v3.0.1 github.com/pkg/errors v0.9.1 github.com/schollz/jsonstore v1.1.0 - github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca github.com/stretchr/testify v1.11.1 go.uber.org/mock v0.6.0 golang.org/x/crypto v0.50.0 @@ -83,6 +82,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/shopspring/decimal v1.4.0 // indirect + github.com/smallstep/go-attestation v0.4.5 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/thales-e-security/pool v0.0.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect diff --git a/go.sum b/go.sum index 3db95143..d1efc833 100644 --- a/go.sum +++ b/go.sum @@ -760,6 +760,8 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4= github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= +github.com/smallstep/go-attestation v0.4.5 h1:fWXNXWKPjImGj3yLELonRb7YK+KEx/whfN8POmWZPWU= +github.com/smallstep/go-attestation v0.4.5/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= diff --git a/kms/tpmkms/tpmkms.go b/kms/tpmkms/tpmkms.go index f766e4e0..08f982cf 100644 --- a/kms/tpmkms/tpmkms.go +++ b/kms/tpmkms/tpmkms.go @@ -195,6 +195,9 @@ func ParseTPMOptions(u *uri.URI) []tpm.NewTPMOption { if storageDirectory := u.Get("storage-directory"); storageDirectory != "" { opts = append(opts, tpm.WithStore(storage.NewDirstore(storageDirectory))) } + if u.Get("store-location") == "machine" { + opts = append(opts, tpm.WithMachineKey()) + } return opts } diff --git a/tpm/tpm.go b/tpm/tpm.go index bd0dde01..7b14b176 100644 --- a/tpm/tpm.go +++ b/tpm/tpm.go @@ -96,6 +96,17 @@ func WithCommandChannel(commandChannel CommandChannel) NewTPMOption { } } +// WithMachineKey configures the TPM to create and open keys in the machine +// (local machine) key store rather than the current user key store. On Windows +// this causes NCRYPT_MACHINE_KEY_FLAG to be passed to NCrypt key operations. +// This option has no effect on non-Windows platforms. +func WithMachineKey() NewTPMOption { + return func(o *options) error { + o.attestConfig.MachineKey = true + return nil + } +} + // WithCapabilities explicitly sets the capabilities rather // than acquiring them from the TPM directly. The primary use // for this option is to ease testing different TPM capabilities. From ffcc3a20bd59c8308cd5254416689534b21fccf6 Mon Sep 17 00:00:00 2001 From: Diego Fronza Date: Fri, 24 Apr 2026 19:37:45 -0300 Subject: [PATCH 2/5] Fix MachineKey propagation and file-store cleanup on Windows Three fixes for Windows PCP key management during reset: 1. tpm/tpm.go: initializeCommandChannel() was dropping MachineKey when creating new attest.OpenConfig structs in both branches. Even though WithMachineKey() correctly set o.attestConfig.MachineKey=true, the TPM was always opened with openPCP(false), placing keys in the current-user key store rather than the machine key store. Session-0 (agent service) and other sessions (reset command) see different user key stores, so NCryptOpenKey could not find keys. Fix: copy MachineKey in both branches. 2. tpm/key.go: DeleteKey() returned early on PCP failure without calling t.store.DeleteKey(), leaving stale file-store entries. Subsequent ListKeys() calls returned the same key, causing reset to fail repeatedly on the same key. Fix: save the PCP error, always clean up the file store, return PCP error only after successful file-store cleanup. 3. tpm/ak.go: same file-store cleanup issue as key.go for DeleteAK(). Also bumps github.com/smallstep/go-attestation to v0.4.6, which fixes LoadKeyByName to return the actual NCrypt HRESULT instead of GetLastError(). Co-Authored-By: Claude Sonnet 4.6 --- go.mod | 2 +- go.sum | 6 ++---- tpm/ak.go | 6 ++++-- tpm/key.go | 8 ++++++-- tpm/tpm.go | 2 ++ 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 587ef760..4f1d4e79 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/peterbourgon/diskv/v3 v3.0.1 github.com/pkg/errors v0.9.1 github.com/schollz/jsonstore v1.1.0 + github.com/smallstep/go-attestation v0.4.6 github.com/stretchr/testify v1.11.1 go.uber.org/mock v0.6.0 golang.org/x/crypto v0.50.0 @@ -82,7 +83,6 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/smallstep/go-attestation v0.4.5 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/thales-e-security/pool v0.0.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect diff --git a/go.sum b/go.sum index d1efc833..c3c97668 100644 --- a/go.sum +++ b/go.sum @@ -758,10 +758,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4= -github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= -github.com/smallstep/go-attestation v0.4.5 h1:fWXNXWKPjImGj3yLELonRb7YK+KEx/whfN8POmWZPWU= -github.com/smallstep/go-attestation v0.4.5/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= +github.com/smallstep/go-attestation v0.4.6 h1:RyoZsIS68HGXOodvCNEKoTtFYGBHF5YwpGOlsmx7F3M= +github.com/smallstep/go-attestation v0.4.6/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= diff --git a/tpm/ak.go b/tpm/ak.go index 82903b51..e450eb6e 100644 --- a/tpm/ak.go +++ b/tpm/ak.go @@ -286,8 +286,10 @@ func (t *TPM) DeleteAK(ctx context.Context, name string) (err error) { return fmt.Errorf("failed deleting AK %q because %d key(s) exist that were attested by it", name, len(keys)) } + var attestErr error if err := t.attestTPM.DeleteKey(ak.Data); err != nil { // TODO: we could add a DeleteAK to go-attestation; under the hood it's loaded the same as a key though. - return fmt.Errorf("failed deleting AK %q: %w", name, err) + // Preserve the PCP error but still clean up file-store metadata below. + attestErr = fmt.Errorf("failed deleting AK %q: %w", name, err) } if err := t.store.DeleteAK(name); err != nil { @@ -298,7 +300,7 @@ func (t *TPM) DeleteAK(ctx context.Context, name string) (err error) { return fmt.Errorf("failed persisting storage: %w", err) } - return + return attestErr } // AttestationParameters returns information about the AK, typically used to diff --git a/tpm/key.go b/tpm/key.go index b8c267b3..aea3c239 100644 --- a/tpm/key.go +++ b/tpm/key.go @@ -386,8 +386,12 @@ func (t *TPM) DeleteKey(ctx context.Context, name string) (err error) { return fmt.Errorf("failed getting key %q: %w", name, err) } + var attestErr error if err := t.attestTPM.DeleteKey(key.Data); err != nil { - return fmt.Errorf("failed deleting key %q: %w", name, err) + // Preserve the PCP error but still clean up file-store metadata below. + // On Windows the PCP key may be inaccessible when the reset command runs + // in a different session than the agent service that created the key. + attestErr = fmt.Errorf("failed deleting key %q: %w", name, err) } if err := t.store.DeleteKey(name); err != nil { @@ -398,7 +402,7 @@ func (t *TPM) DeleteKey(ctx context.Context, name string) (err error) { return fmt.Errorf("failed persisting storage: %w", err) } - return + return attestErr } // Signer returns a crypto.Signer backed by the Key. diff --git a/tpm/tpm.go b/tpm/tpm.go index 7b14b176..a3c1de7b 100644 --- a/tpm/tpm.go +++ b/tpm/tpm.go @@ -252,6 +252,7 @@ func (t *TPM) initializeCommandChannel() error { t.attestConfig = &attest.OpenConfig{ TPMVersion: t.options.attestConfig.TPMVersion, CommandChannel: t.commandChannel, + MachineKey: t.options.attestConfig.MachineKey, } return nil } @@ -286,6 +287,7 @@ func (t *TPM) initializeCommandChannel() error { t.attestConfig = &attest.OpenConfig{ TPMVersion: t.options.attestConfig.TPMVersion, CommandChannel: t.commandChannel, + MachineKey: t.options.attestConfig.MachineKey, } return nil From ba8ac52e4d4a3971a6b054238a12ca97d1442244 Mon Sep 17 00:00:00 2001 From: Diego Fronza Date: Fri, 24 Apr 2026 20:35:16 -0300 Subject: [PATCH 3/5] Propagate MachineKey through CreateKey on Windows NCrypt PCP provider Added MachineKey bool to CreateConfig and threaded it from TPM.CreateKey down to the internal/key Windows PCP layer. NCryptCreatePersistedKey now passes NCRYPT_MACHINE_KEY_FLAG when MachineKey is true, so unattested endpoint keys land in the machine key store when store-location=machine is set, matching the behaviour of attested AKs. Co-Authored-By: Claude Sonnet 4.6 --- tpm/internal/key/key.go | 4 ++++ tpm/internal/key/key_windows.go | 2 +- tpm/internal/key/pcp_windows.go | 18 +++++++++++++++--- tpm/key.go | 5 +++-- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/tpm/internal/key/key.go b/tpm/internal/key/key.go index cd9141d1..a3a11ead 100644 --- a/tpm/internal/key/key.go +++ b/tpm/internal/key/key.go @@ -106,6 +106,10 @@ type CreateConfig struct { // Size is used to specify the bit size of the key or elliptic curve. For // example, '256' is used to specify curve P-256. Size int + // MachineKey instructs the Windows NCrypt PCP provider to create the key in + // the machine (local machine) key store rather than the current user store. + // Only relevant on Windows; ignored on other platforms. + MachineKey bool } func (c *CreateConfig) Validate() error { diff --git a/tpm/internal/key/key_windows.go b/tpm/internal/key/key_windows.go index 65636a6d..7c2bda7d 100644 --- a/tpm/internal/key/key_windows.go +++ b/tpm/internal/key/key_windows.go @@ -8,7 +8,7 @@ import ( ) func create(_ io.ReadWriteCloser, keyName string, config CreateConfig) ([]byte, error) { - pcp, err := openPCP() + pcp, err := openPCP(config.MachineKey) if err != nil { return nil, fmt.Errorf("failed to open PCP: %w", err) } diff --git a/tpm/internal/key/pcp_windows.go b/tpm/internal/key/pcp_windows.go index 3626d9a6..455d2711 100644 --- a/tpm/internal/key/pcp_windows.go +++ b/tpm/internal/key/pcp_windows.go @@ -35,6 +35,9 @@ const ( // The below is documented in this Microsoft whitepaper: // https://github.com/Microsoft/TSS.MSR/blob/master/PCPTool.v11/Using%20the%20Windows%208%20Platform%20Crypto%20Provider%20and%20Associated%20TPM%20Functionality.pdf ncryptOverwriteKeyFlag = 0x80 + // ncryptMachineKeyFlag instructs NCrypt to create or open a key in the + // machine (local machine) key store rather than the current user key store. + ncryptMachineKeyFlag uint32 = 0x00000020 // Key usage value for generic keys nCryptPropertyPCPKeyUsagePolicyGeneric = 0x3 // Key usage value for AKs. @@ -282,7 +285,8 @@ func getNCryptBufferProperty(hnd uintptr, field string) ([]byte, error) { // winPCP represents a reference to the Platform Crypto Provider. type winPCP struct { - hProv uintptr + hProv uintptr + machineKey bool } // Close releases all resources managed by the Handle. @@ -301,8 +305,13 @@ func (h *winPCP) newKey(name string, alg string, length uint32, policy uint32) ( return 0, nil, nil, err } + var flags uint32 + if h.machineKey { + flags = ncryptMachineKeyFlag + } + // Create a persistent RSA key of the specified name. - r, _, msg := nCryptCreatePersistedKey.Call(h.hProv, uintptr(unsafe.Pointer(&kh)), uintptr(unsafe.Pointer(&utf16RSA[0])), uintptr(unsafe.Pointer(&utf16Name[0])), 0, 0) + r, _, msg := nCryptCreatePersistedKey.Call(h.hProv, uintptr(unsafe.Pointer(&kh)), uintptr(unsafe.Pointer(&utf16RSA[0])), uintptr(unsafe.Pointer(&utf16Name[0])), 0, uintptr(flags)) if r != 0 { if tpmErr := maybeWinErr(r); tpmErr != nil { msg = tpmErr @@ -491,10 +500,13 @@ func decodeKeyBlob(keyBlob []byte) ([]byte, []byte, error) { } // openPCP initializes a reference to the Microsoft PCP provider. +// Pass machineKey=true to create and open keys in the machine (local machine) +// key store rather than the current user key store. // The Caller is expected to call Close() when they are done. -func openPCP() (*winPCP, error) { +func openPCP(machineKey bool) (*winPCP, error) { var err error var h winPCP + h.machineKey = machineKey pname, err := windows.UTF16FromString(pcpProviderName) if err != nil { return nil, err diff --git a/tpm/key.go b/tpm/key.go index aea3c239..45b7f54c 100644 --- a/tpm/key.go +++ b/tpm/key.go @@ -186,8 +186,9 @@ func (t *TPM) CreateKey(ctx context.Context, name string, config CreateKeyConfig } createConfig := internalkey.CreateConfig{ - Algorithm: config.Algorithm, - Size: config.Size, + Algorithm: config.Algorithm, + Size: config.Size, + MachineKey: t.options.attestConfig.MachineKey, } if err := t.validate(&createConfig); err != nil { return nil, fmt.Errorf("invalid key creation parameters: %w", err) From bd953784f57b09500288914c89c7eec78aa78a38 Mon Sep 17 00:00:00 2001 From: Diego Fronza Date: Fri, 24 Apr 2026 20:57:05 -0300 Subject: [PATCH 4/5] Update go-attestation to v0.4.8 Co-Authored-By: Claude Sonnet 4.6 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4f1d4e79..83f24827 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/peterbourgon/diskv/v3 v3.0.1 github.com/pkg/errors v0.9.1 github.com/schollz/jsonstore v1.1.0 - github.com/smallstep/go-attestation v0.4.6 + github.com/smallstep/go-attestation v0.4.8 github.com/stretchr/testify v1.11.1 go.uber.org/mock v0.6.0 golang.org/x/crypto v0.50.0 diff --git a/go.sum b/go.sum index c3c97668..f0d61e3c 100644 --- a/go.sum +++ b/go.sum @@ -758,8 +758,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smallstep/go-attestation v0.4.6 h1:RyoZsIS68HGXOodvCNEKoTtFYGBHF5YwpGOlsmx7F3M= -github.com/smallstep/go-attestation v0.4.6/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= +github.com/smallstep/go-attestation v0.4.8 h1:yX7oiDFYlXywl+9Hss1RvhcSyPB7q9mFmGZSNhEfi1Q= +github.com/smallstep/go-attestation v0.4.8/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= From 64eab7fd8d5a488884893caed8a74123019a7701 Mon Sep 17 00:00:00 2001 From: Diego Fronza Date: Sat, 25 Apr 2026 10:43:05 -0300 Subject: [PATCH 5/5] Update go-attestation to v0.4.9 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 83f24827..de128572 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/peterbourgon/diskv/v3 v3.0.1 github.com/pkg/errors v0.9.1 github.com/schollz/jsonstore v1.1.0 - github.com/smallstep/go-attestation v0.4.8 + github.com/smallstep/go-attestation v0.4.9 github.com/stretchr/testify v1.11.1 go.uber.org/mock v0.6.0 golang.org/x/crypto v0.50.0 diff --git a/go.sum b/go.sum index f0d61e3c..d76c8ffb 100644 --- a/go.sum +++ b/go.sum @@ -760,6 +760,8 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smallstep/go-attestation v0.4.8 h1:yX7oiDFYlXywl+9Hss1RvhcSyPB7q9mFmGZSNhEfi1Q= github.com/smallstep/go-attestation v0.4.8/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= +github.com/smallstep/go-attestation v0.4.9 h1:/fVmzB8A8tk7B2PCGPlszWzyl/GDcEQbynqcg2PMaZ0= +github.com/smallstep/go-attestation v0.4.9/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=