diff --git a/go.mod b/go.mod index 6dd1ef3d..64bfc01e 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,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.4-0.20241119153605-2306d5b464ca + github.com/smallstep/go-attestation v0.4.4-0.20260603212853-e1a87a0b07d9 github.com/stretchr/testify v1.11.1 go.uber.org/mock v0.6.0 golang.org/x/crypto v0.52.0 diff --git a/go.sum b/go.sum index 9a5f33d1..5c0683f6 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.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.4-0.20260603212853-e1a87a0b07d9 h1:n+X1wnMKJMcCRd98YKAo/56tMRSPUg+qjAvNNS1EZeM= +github.com/smallstep/go-attestation v0.4.4-0.20260603212853-e1a87a0b07d9/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 ab66f516..77c92478 100644 --- a/kms/tpmkms/tpmkms.go +++ b/kms/tpmkms/tpmkms.go @@ -531,9 +531,17 @@ func (k *TPMKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespons size = v.Curve } + machineKey := properties.isMachineKey() + var privateKey any if properties.ak { - ak, err := k.tpm.CreateAK(ctx, properties.name) // NOTE: size is never passed for AKs; it's hardcoded to 2048 in lower levels. + // NOTE: size is never passed for AKs; it's hardcoded to 2048 in lower + // levels. The AK must honor the requested key-scope: an attested key + // inherits its AK's scope (AttestKey enforces this symmetrically), so + // a machine-scoped attested key requires a machine-scoped AK. Creating + // the AK with the plain (user-default) CreateAK here would make every + // machine-scoped attestation fail with a scope mismatch. + ak, err := k.tpm.CreateAKWithConfig(ctx, properties.name, tpm.CreateAKConfig{MachineKey: machineKey}) if err != nil { if errors.Is(err, tpm.ErrExists) { return nil, apiv1.AlreadyExistsError{Message: err.Error()} @@ -549,7 +557,12 @@ func (k *TPMKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespons privateKey = tpmKey } + // Preserve key-scope in the returned URI so re-opens use the matching + // scope (mirrors the non-AK path below). createdAKURI := fmt.Sprintf("tpmkms:name=%s;ak=true", ak.Name()) + if machineKey { + createdAKURI = fmt.Sprintf("%s;key-scope=machine", createdAKURI) + } return &apiv1.CreateKeyResponse{ Name: createdAKURI, PublicKey: ak.Public(), @@ -563,6 +576,7 @@ func (k *TPMKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespons Algorithm: v.Type, Size: size, QualifyingData: properties.qualifyingData, + MachineKey: machineKey, } key, err = k.tpm.AttestKey(ctx, properties.attestBy, properties.name, config) if err != nil { @@ -573,8 +587,9 @@ func (k *TPMKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespons } } else { config := tpm.CreateKeyConfig{ - Algorithm: v.Type, - Size: size, + Algorithm: v.Type, + Size: size, + MachineKey: machineKey, } key, err = k.tpm.CreateKey(ctx, properties.name, config) if err != nil { @@ -598,10 +613,17 @@ func (k *TPMKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespons return nil, fmt.Errorf("failed getting signer for key: %w", err) } + // Preserve key-scope in the returned URI so subsequent CreateSigner / + // DeleteKey calls land in the right scope. The bug we hit before was + // exactly this: the returned URI had only "name=", so re-opens + // defaulted to user scope and failed to find machine-stored keys. createdKeyURI := fmt.Sprintf("tpmkms:name=%s", key.Name()) if properties.attestBy != "" { createdKeyURI = fmt.Sprintf("%s;attest-by=%s", createdKeyURI, key.AttestedBy()) } + if machineKey { + createdKeyURI = fmt.Sprintf("%s;key-scope=machine", createdKeyURI) + } return &apiv1.CreateKeyResponse{ Name: createdKeyURI, diff --git a/kms/tpmkms/tpmkms_simulator_test.go b/kms/tpmkms/tpmkms_simulator_test.go index 0a06f395..b4526afd 100644 --- a/kms/tpmkms/tpmkms_simulator_test.go +++ b/kms/tpmkms/tpmkms_simulator_test.go @@ -2467,3 +2467,38 @@ func TestTPMKMS_SearchKeys(t *testing.T) { }) } } + +// TestTPMKMS_CreateKey_machineScopedAKAttestation reproduces the agent's +// enrollment path: create a machine-scoped AK via tpmkms (ak=true; +// key-scope=machine), then attest a machine-scoped device key by it. Before +// the fix, the AK was created with the user-default scope (CreateAK ignored +// key-scope), so AttestKey's symmetric check rejected the machine-scoped +// attested key with "MachineKey=true does not match AK ... (MachineKey=false)". +func TestTPMKMS_CreateKey_machineScopedAKAttestation(t *testing.T) { + k := &TPMKMS{tpm: newSimulatedTPM(t)} + + akResp, err := k.CreateKey(&apiv1.CreateKeyRequest{ + Name: "tpmkms:name=ak1;ak=true;key-scope=machine", + }) + require.NoError(t, err) + // The returned AK URI must preserve the machine scope for re-opens. + assert.Equal(t, "tpmkms:name=ak1;ak=true;key-scope=machine", akResp.Name) + + // This is the exact URI the agent builds in attester.Attest; before the + // fix it failed with a scope mismatch. + _, err = k.CreateKey(&apiv1.CreateKeyRequest{ + Name: "tpmkms:name=key1;attest-by=ak1;key-scope=machine;store-location=machine", + SignatureAlgorithm: apiv1.SHA256WithRSA, + Bits: 1024, + }) + require.NoError(t, err) + + // A user-scoped attested key by the machine AK must still be rejected + // (the symmetric check works in both directions). + _, err = k.CreateKey(&apiv1.CreateKeyRequest{ + Name: "tpmkms:name=key2;attest-by=ak1", + SignatureAlgorithm: apiv1.SHA256WithRSA, + Bits: 1024, + }) + require.Error(t, err) +} diff --git a/kms/tpmkms/uri.go b/kms/tpmkms/uri.go index 567f1e1e..26d191a6 100644 --- a/kms/tpmkms/uri.go +++ b/kms/tpmkms/uri.go @@ -27,6 +27,21 @@ type objectProperties struct { sha1 string serial string issuer string + // keyScope, if set, is one of "machine" or "user". It controls + // whether the underlying private key lives in the local machine key + // store or in the current user's key store, and is independent of + // where the certificate is stored. When unset, [parseNameURI] derives + // it from storeLocation for backwards compatibility (machine cert + // store implies machine key scope). + keyScope string +} + +// isMachineKey returns true if the resolved key scope is "machine". +// When keyScope is unset on the object, the value is derived from +// storeLocation: "machine" → true, anything else → false. +func (o objectProperties) isMachineKey() bool { + return o.keyScope == "machine" || + (o.keyScope == "" && o.storeLocation == "machine") } func parseNameURI(nameURI string) (o objectProperties, err error) { @@ -75,10 +90,20 @@ func parseNameURI(nameURI string) (o objectProperties, err error) { o.serial = u.Get("serial") o.issuer = u.Get("issuer") + // key-scope is independent of store-location: cert location and + // key ownership are orthogonal Windows concepts. See [machineKey] + // for the back-compat default when this is unset. + o.keyScope = u.Get("key-scope") + // validation if o.ak && o.attestBy != "" { return o, errors.New(`"ak" and "attest-by" are mutually exclusive`) } + switch o.keyScope { + case "", "machine", "user": + default: + return o, fmt.Errorf(`"key-scope" must be "machine" or "user", got %q`, o.keyScope) + } return } diff --git a/tpm/ak.go b/tpm/ak.go index 82903b51..7a82d003 100644 --- a/tpm/ak.go +++ b/tpm/ak.go @@ -26,11 +26,20 @@ type AK struct { data []byte chain []*x509.Certificate createdAt time.Time + machineKey bool blobs *Blobs attestParams *attest.AttestationParameters tpm *TPM } +// CreateAKConfig is used to pass configuration when creating AKs. +type CreateAKConfig struct { + // MachineKey, when true, requests that the AK be created in the local + // machine key store rather than in the current user's key store. See + // [CreateKeyConfig.MachineKey] for details. + MachineKey bool +} + // Name returns the AK name. The name uniquely // identifies an AK if a TPM with persistent // storage is used. @@ -133,8 +142,16 @@ func (ak *AK) MarshalJSON() ([]byte, error) { // CreateAK creates and stores a new AK identified by `name`. // If no name is provided, a random 10 character name is generated. // If an AK with the same name exists, `ErrExists` is returned. +// +// To request a machine-scoped AK on Windows, use [TPM.CreateAKWithConfig]. func (t *TPM) CreateAK(ctx context.Context, name string) (ak *AK, err error) { - if err = t.open(ctx); err != nil { + return t.CreateAKWithConfig(ctx, name, CreateAKConfig{}) +} + +// CreateAKWithConfig creates and stores a new AK with the given config. +// See [TPM.CreateAK] for the simpler signature. +func (t *TPM) CreateAKWithConfig(ctx context.Context, name string, config CreateAKConfig) (ak *AK, err error) { + if err = t.open(ctx, openOptions{machineKey: config.MachineKey}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, t, &err) @@ -167,10 +184,11 @@ func (t *TPM) CreateAK(ctx context.Context, name string) (ak *AK, err error) { } ak = &AK{ - name: name, - data: data, - createdAt: now, - tpm: t, + name: name, + data: data, + createdAt: now, + machineKey: config.MachineKey, + tpm: t, } if err := t.store.AddAK(ak.toStorage()); err != nil { @@ -187,7 +205,7 @@ func (t *TPM) CreateAK(ctx context.Context, name string) (ak *AK, err error) { // GetAK returns the AK identified by `name`. It returns `ErrNotfound` // if it doesn't exist. func (t *TPM) GetAK(ctx context.Context, name string) (ak *AK, err error) { - if err = t.open(ctx); err != nil { + if err = t.open(ctx, openOptions{}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, t, &err) @@ -211,7 +229,7 @@ var ( // exists with `permanentIdentifier` as one of the Subject Alternative // Names. It returns `ErrNotFound` if it doesn't exist. func (t *TPM) GetAKByPermanentIdentifier(ctx context.Context, permanentIdentifier string) (ak *AK, err error) { - if err = t.open(ctx); err != nil { + if err = t.open(ctx, openOptions{}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, t, &err) @@ -236,7 +254,7 @@ func (t *TPM) GetAKByPermanentIdentifier(ctx context.Context, permanentIdentifie // ListAKs returns a slice of AKs. The result is (currently) // not ordered. func (t *TPM) ListAKs(ctx context.Context) (aks []*AK, err error) { - if err := t.open(ctx); err != nil { + if err := t.open(ctx, openOptions{}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, t, &err) @@ -259,20 +277,31 @@ func (t *TPM) ListAKs(ctx context.Context) (aks []*AK, err error) { // DeleteAK removes the AK identified by `name`. It returns `ErrNotfound` // if it doesn't exist. Keys that were attested by the AK have to be removed // before removing the AK, otherwise an error will be returned. +// +// If the in-TPM deletion fails but the file-store entry is successfully +// removed, the returned error is wrapped in a [PartialDeleteError]. See +// [TPM.DeleteKey] for the rationale. func (t *TPM) DeleteAK(ctx context.Context, name string) (err error) { - if err := t.open(ctx); err != nil { - return fmt.Errorf("failed opening TPM: %w", err) - } - defer closeTPM(ctx, t, &err) - - ak, err := t.store.GetAK(name) - if err != nil { - if errors.Is(err, storage.ErrNotFound) { + // We need the AK's persisted machine-key scope before t.open so we + // can pass it through context. t.open also calls t.store.Load + // internally; calling it here explicitly preserves the invariant that + // every t.store read happens after a t.store.Load. + if err := t.store.Load(); err != nil { + return fmt.Errorf("failed loading from TPM storage: %w", err) + } + ak, getErr := t.store.GetAK(name) + if getErr != nil { + if errors.Is(getErr, storage.ErrNotFound) { return fmt.Errorf("failed getting AK %q: %w", name, ErrNotFound) } - return fmt.Errorf("failed getting AK %q: %w", name, err) + return fmt.Errorf("failed getting AK %q: %w", name, getErr) } + if err := t.open(ctx, openOptions{machineKey: ak.MachineKey}); err != nil { + return fmt.Errorf("failed opening TPM: %w", err) + } + defer closeTPM(ctx, t, &err) + // prevent deleting the AK if the TPM (storage) contains keys that // were attested by it. While keys would still work if the AK were // deleted, some functionalities would no longer work. The AK can @@ -286,19 +315,26 @@ 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)) } - 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) - } + attestErr := t.attestTPM.DeleteKey(ak.Data) // TODO: we could add a DeleteAK to go-attestation; under the hood it's loaded the same as a key though. if err := t.store.DeleteAK(name); err != nil { + if attestErr != nil { + return fmt.Errorf("failed deleting AK %q from storage after TPM delete failed (%w): %w", name, attestErr, err) + } return fmt.Errorf("failed deleting AK %q from storage: %w", name, err) } if err := t.store.Persist(); err != nil { + if attestErr != nil { + return fmt.Errorf("failed persisting storage after TPM delete failed (%w): %w", attestErr, err) + } return fmt.Errorf("failed persisting storage: %w", err) } - return + if attestErr != nil { + return &PartialDeleteError{Name: name, Underlying: attestErr} + } + return nil } // AttestationParameters returns information about the AK, typically used to @@ -308,7 +344,7 @@ func (ak *AK) AttestationParameters(ctx context.Context) (params attest.Attestat return *ak.attestParams, nil } - if err = ak.tpm.open(ctx); err != nil { + if err = ak.tpm.open(ctx, openOptions{machineKey: ak.machineKey}); err != nil { return params, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, ak.tpm, &err) @@ -333,7 +369,7 @@ type EncryptedCredential attest.EncryptedCredential // generated on the same TPM as the EK. This operation is synonymous with // TPM2_ActivateCredential. func (ak *AK) ActivateCredential(ctx context.Context, in EncryptedCredential) (secret []byte, err error) { - if err := ak.tpm.open(ctx); err != nil { + if err = ak.tpm.open(ctx, openOptions{machineKey: ak.machineKey}); err != nil { return secret, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, ak.tpm, &err) @@ -359,7 +395,7 @@ func (ak *AK) Blobs(ctx context.Context) (blobs *Blobs, err error) { return ak.blobs, nil } - if err = ak.tpm.open(ctx); err != nil { + if err = ak.tpm.open(ctx, openOptions{machineKey: ak.machineKey}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, ak.tpm, &err) @@ -383,7 +419,7 @@ func (ak *AK) Blobs(ctx context.Context) (blobs *Blobs, err error) { // If the AK public key doesn't match the public key in the first certificate // in the chain (the leaf), an error is returned. func (ak *AK) SetCertificateChain(ctx context.Context, chain []*x509.Certificate) (err error) { - if err := ak.tpm.open(ctx); err != nil { + if err = ak.tpm.open(ctx, openOptions{machineKey: ak.machineKey}); err != nil { return fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, ak.tpm, &err) @@ -463,10 +499,11 @@ func (ak *AK) HasValidPermanentIdentifier(permanentIdentifier string) bool { // persisting AKs. func (ak *AK) toStorage() *storage.AK { return &storage.AK{ - Name: ak.name, - Data: ak.data, - Chain: ak.chain, - CreatedAt: ak.createdAt.UTC(), + Name: ak.name, + Data: ak.data, + Chain: ak.chain, + CreatedAt: ak.createdAt.UTC(), + MachineKey: ak.machineKey, } } @@ -474,10 +511,11 @@ func (ak *AK) toStorage() *storage.AK { // persisting AKs. func akFromStorage(sak *storage.AK, t *TPM) *AK { return &AK{ - name: sak.Name, - data: sak.Data, - chain: sak.Chain, - createdAt: sak.CreatedAt.Local(), - tpm: t, + name: sak.Name, + data: sak.Data, + chain: sak.Chain, + createdAt: sak.CreatedAt.Local(), + machineKey: sak.MachineKey, + tpm: t, } } diff --git a/tpm/caps.go b/tpm/caps.go index 539a9dc8..db3c7b6b 100644 --- a/tpm/caps.go +++ b/tpm/caps.go @@ -42,7 +42,7 @@ func (t *TPM) GetCapabilities(ctx context.Context) (caps *Capabilities, err erro return t.caps, nil } - if err = t.open(goTPMCall(ctx)); err != nil { + if err = t.open(ctx, openOptions{useGoTPM: true}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, t, &err) diff --git a/tpm/context.go b/tpm/context.go index d8625921..c422b58f 100644 --- a/tpm/context.go +++ b/tpm/context.go @@ -26,14 +26,3 @@ func isInternalCall(ctx context.Context) bool { v, ok := ctx.Value(internalCallContextKey{}).(bool) return ok && v } - -type goTPMCallContextKey struct{} - -func goTPMCall(ctx context.Context) context.Context { - return context.WithValue(ctx, goTPMCallContextKey{}, true) -} - -func isGoTPMCall(ctx context.Context) bool { - v, ok := ctx.Value(goTPMCallContextKey{}).(bool) - return ok && v -} diff --git a/tpm/ek.go b/tpm/ek.go index fce75a44..f153d925 100644 --- a/tpm/ek.go +++ b/tpm/ek.go @@ -153,7 +153,7 @@ func (t *TPM) GetEKs(ctx context.Context) (eks []*EK, err error) { return t.eks, nil } - if err = t.open(ctx); err != nil { + if err = t.open(ctx, openOptions{}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, t, &err) diff --git a/tpm/errors.go b/tpm/errors.go index 08c6b66f..fbb5bac0 100644 --- a/tpm/errors.go +++ b/tpm/errors.go @@ -2,6 +2,7 @@ package tpm import ( "errors" + "fmt" "go.step.sm/crypto/tpm/storage" ) @@ -15,3 +16,26 @@ var ErrExists = errors.New("already exists") // ErrNoStorageConfigured is returned when a TPM operation is // performed that requires a storage to have been configured var ErrNoStorageConfigured = storage.ErrNoStorageConfigured + +// PartialDeleteError is returned by DeleteKey or DeleteAK when the +// in-TPM (NCrypt PCP / attest) deletion failed but the file-store +// entry was successfully removed. Callers can use [errors.As] to +// detect this case and decide whether to treat it as a transient +// failure (the named entry is gone from local bookkeeping; the +// underlying TPM key may or may not still exist). +// +// Cleaning up the file-store entry even on PCP failure prevents +// repeated retries from re-encountering the same failing entry, +// which is the failure mode that motivated this type. +type PartialDeleteError struct { + // Name is the key/AK name that was being deleted. + Name string + // Underlying is the error returned by the in-TPM deletion. + Underlying error +} + +func (e *PartialDeleteError) Error() string { + return fmt.Sprintf("file-store entry %q removed but TPM deletion failed: %v", e.Name, e.Underlying) +} + +func (e *PartialDeleteError) Unwrap() error { return e.Underlying } diff --git a/tpm/info.go b/tpm/info.go index ae9495b7..45af5292 100644 --- a/tpm/info.go +++ b/tpm/info.go @@ -130,7 +130,7 @@ func (t *TPM) Info(ctx context.Context) (info *Info, err error) { return t.info, nil } - if err = t.open(ctx); err != nil { + if err = t.open(ctx, openOptions{}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, t, &err) diff --git a/tpm/internal/key/key.go b/tpm/internal/key/key.go index cd9141d1..90569244 100644 --- a/tpm/internal/key/key.go +++ b/tpm/internal/key/key.go @@ -106,6 +106,11 @@ 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, when true, requests that the key be created in the local + // machine key store rather than the current user key store. On Windows + // this causes NCRYPT_MACHINE_KEY_FLAG to be passed to the underlying + // NCrypt PCP provider. Ignored on other platforms. + MachineKey bool } func (c *CreateConfig) Validate() error { @@ -184,8 +189,9 @@ const ( ) type KeyConfig struct { - Algorithm Algorithm - Size int + Algorithm Algorithm + Size int + MachineKey bool } var ( diff --git a/tpm/internal/key/key_windows.go b/tpm/internal/key/key_windows.go index 65636a6d..13e86752 100644 --- a/tpm/internal/key/key_windows.go +++ b/tpm/internal/key/key_windows.go @@ -14,7 +14,11 @@ func create(_ io.ReadWriteCloser, keyName string, config CreateConfig) ([]byte, } defer pcp.Close() - _, pub, _, err := pcp.NewKey(keyName, &KeyConfig{Algorithm: Algorithm(config.Algorithm), Size: config.Size}) + _, pub, _, err := pcp.NewKey(keyName, &KeyConfig{ + Algorithm: Algorithm(config.Algorithm), + Size: config.Size, + MachineKey: config.MachineKey, + }) if err != nil { return nil, fmt.Errorf("pcp failed to mint application key: %w", err) } diff --git a/tpm/internal/key/pcp_windows.go b/tpm/internal/key/pcp_windows.go index 3626d9a6..af363069 100644 --- a/tpm/internal/key/pcp_windows.go +++ b/tpm/internal/key/pcp_windows.go @@ -35,6 +35,10 @@ 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 the key in the + // local machine key store rather than the current user key store. Defined + // in ncrypt.h as NCRYPT_MACHINE_KEY_FLAG. + ncryptMachineKeyFlag uint32 = 0x00000020 // Key usage value for generic keys nCryptPropertyPCPKeyUsagePolicyGeneric = 0x3 // Key usage value for AKs. @@ -290,7 +294,7 @@ func (h *winPCP) Close() error { return closeNCryptObject(h.hProv) } -func (h *winPCP) newKey(name string, alg string, length uint32, policy uint32) (uintptr, []byte, []byte, error) { +func (h *winPCP) newKey(name string, alg string, length uint32, policy uint32, machineKey bool) (uintptr, []byte, []byte, error) { var kh uintptr utf16Name, err := windows.UTF16FromString(name) if err != nil { @@ -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 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 @@ -382,15 +391,15 @@ func (h *winPCP) newKey(name string, alg string, length uint32, policy uint32) ( // NewKey creates a persistent application key of the specified name. func (h *winPCP) NewKey(name string, config *KeyConfig) (uintptr, []byte, []byte, error) { if config.Algorithm == RSA { - return h.newKey(name, "RSA", uint32(config.Size), 0) + return h.newKey(name, "RSA", uint32(config.Size), 0, config.MachineKey) } else if config.Algorithm == ECDSA { switch config.Size { case 256: - return h.newKey(name, "ECDSA_P256", 0, 0) + return h.newKey(name, "ECDSA_P256", 0, 0, config.MachineKey) case 384: - return h.newKey(name, "ECDSA_P384", 0, 0) + return h.newKey(name, "ECDSA_P384", 0, 0, config.MachineKey) case 521: - return h.newKey(name, "ECDSA_P521", 0, 0) + return h.newKey(name, "ECDSA_P521", 0, 0, config.MachineKey) default: return 0, nil, nil, fmt.Errorf("unsupported ECDSA key size: %v", config.Size) } diff --git a/tpm/key.go b/tpm/key.go index b8c267b3..a112e8bc 100644 --- a/tpm/key.go +++ b/tpm/key.go @@ -25,6 +25,7 @@ type Key struct { attestedBy string chain []*x509.Certificate createdAt time.Time + machineKey bool blobs *Blobs tpm *TPM } @@ -71,7 +72,7 @@ func (k *Key) Public() crypto.PublicKey { err error ctx = context.Background() ) - if err = k.tpm.open(ctx); err != nil { + if err = k.tpm.open(ctx, openOptions{machineKey: k.machineKey}); err != nil { return nil } defer closeTPM(context.Background(), k.tpm, &err) @@ -141,6 +142,14 @@ type CreateKeyConfig 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, when true, requests that the underlying private key be + // created in the local machine key store rather than in the current + // user's key store. On Windows this causes NCRYPT_MACHINE_KEY_FLAG to + // be passed to the NCrypt PCP provider on creation, and to be applied + // every time the key is subsequently opened. The value is persisted + // alongside the key in storage so that load operations use the matching + // scope. Has no effect on non-Windows platforms. + MachineKey bool // TODO(hs): move key name to this struct? } @@ -159,6 +168,10 @@ type AttestKeyConfig struct { // When used with ACME `device-attest-01`, this contains a hash of // the key authorization. QualifyingData []byte + // MachineKey, when true, requests that the underlying private key be + // created in the local machine key store rather than in the current + // user's key store. See [CreateKeyConfig.MachineKey] for details. + MachineKey bool // TODO(hs): add akName and key name to this struct? } @@ -167,7 +180,7 @@ type AttestKeyConfig struct { // a random 10 character name is generated. If a Key with the same name exists, // `ErrExists` is returned. The Key won't be attested by an AK. func (t *TPM) CreateKey(ctx context.Context, name string, config CreateKeyConfig) (key *Key, err error) { - if err = t.open(goTPMCall(ctx)); err != nil { + if err = t.open(ctx, openOptions{machineKey: config.MachineKey, useGoTPM: true}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, t, &err) @@ -186,8 +199,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: config.MachineKey, } if err := t.validate(&createConfig); err != nil { return nil, fmt.Errorf("invalid key creation parameters: %w", err) @@ -198,10 +212,11 @@ func (t *TPM) CreateKey(ctx context.Context, name string, config CreateKeyConfig } key = &Key{ - name: name, - data: data, - createdAt: now, - tpm: t, + name: name, + data: data, + createdAt: now, + machineKey: config.MachineKey, + tpm: t, } if err := t.store.AddKey(key.toStorage()); err != nil { @@ -236,7 +251,13 @@ func (w attestValidationWrapper) Validate() error { // name is generated. If a Key with the same name exists, `ErrExists` is // returned. func (t *TPM) AttestKey(ctx context.Context, akName, name string, config AttestKeyConfig) (key *Key, err error) { - if err = t.open(ctx); err != nil { + // Open with the caller's requested scope. The AK's persisted scope is + // validated below — if it doesn't match, we return an error and the + // deferred close runs. Doing it this way keeps every t.store read + // after the t.store.Load that t.open performs, which matters for + // storage implementations whose in-memory state doesn't reflect + // on-disk state until Load is called. + if err = t.open(ctx, openOptions{machineKey: config.MachineKey}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, t, &err) @@ -261,6 +282,15 @@ func (t *TPM) AttestKey(ctx context.Context, akName, name string, config AttestK } return nil, fmt.Errorf("failed getting AK %q: %w", akName, err) } + // An attested key inherits its AK's scope: the attest session can + // only operate in one scope at a time, so the AK and the new key it + // certifies must share it. Reject explicit [AttestKeyConfig.MachineKey] + // mismatches in either direction so callers don't get a + // silently-promoted (or silently-demoted) key. + if config.MachineKey != ak.MachineKey { + return nil, fmt.Errorf("AttestKeyConfig.MachineKey=%v does not match AK %q scope (MachineKey=%v); attested keys inherit their AK's scope", config.MachineKey, akName, ak.MachineKey) + } + machineKey := ak.MachineKey loadedAK, err := t.attestTPM.LoadAK(ak.Data) if err != nil { @@ -293,6 +323,7 @@ func (t *TPM) AttestKey(ctx context.Context, akName, name string, config AttestK data: data, attestedBy: akName, createdAt: now, + machineKey: machineKey, tpm: t, } @@ -310,7 +341,7 @@ func (t *TPM) AttestKey(ctx context.Context, akName, name string, config AttestK // GetKey returns the Key identified by `name`. It returns `ErrNotfound` // if it doesn't exist. func (t *TPM) GetKey(ctx context.Context, name string) (key *Key, err error) { - if err = t.open(ctx); err != nil { + if err = t.open(ctx, openOptions{}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, t, &err) @@ -329,7 +360,7 @@ func (t *TPM) GetKey(ctx context.Context, name string) (key *Key, err error) { // ListKeys returns a slice of Keys. The result is (currently) // not ordered. func (t *TPM) ListKeys(ctx context.Context) (keys []*Key, err error) { - if err = t.open(ctx); err != nil { + if err = t.open(ctx, openOptions{}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, t, &err) @@ -350,7 +381,7 @@ func (t *TPM) ListKeys(ctx context.Context) (keys []*Key, err error) { // GetKeysAttestedBy returns a slice of Keys attested by the AK // identified by `akName`. The result is (currently) not ordered. func (t *TPM) GetKeysAttestedBy(ctx context.Context, akName string) (keys []*Key, err error) { - if err = t.open(ctx); err != nil { + if err = t.open(ctx, openOptions{}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, t, &err) @@ -372,33 +403,55 @@ func (t *TPM) GetKeysAttestedBy(ctx context.Context, akName string) (keys []*Key // DeleteKey removes the Key identified by `name`. It returns `ErrNotfound` // if it doesn't exist. +// +// If the in-TPM deletion fails but the file-store entry is successfully +// removed, the returned error is wrapped in a [PartialDeleteError] so +// callers can distinguish "leaked from the TPM but not from the file +// store" from a complete failure. Cleaning up the file-store entry even +// on PCP failure prevents repeated retries from re-encountering the same +// failing entry. func (t *TPM) DeleteKey(ctx context.Context, name string) (err error) { - if err := t.open(ctx); err != nil { - return fmt.Errorf("failed opening TPM: %w", err) - } - defer closeTPM(ctx, t, &err) - - key, err := t.store.GetKey(name) - if err != nil { - if errors.Is(err, storage.ErrNotFound) { + // We need the key's persisted machine-key scope before t.open so we + // can pass it through context. t.open also calls t.store.Load + // internally; calling it here explicitly preserves the invariant that + // every t.store read happens after a t.store.Load. Both Loads are + // idempotent for the storage implementations in this package. + if err := t.store.Load(); err != nil { + return fmt.Errorf("failed loading from TPM storage: %w", err) + } + key, getErr := t.store.GetKey(name) + if getErr != nil { + if errors.Is(getErr, storage.ErrNotFound) { return fmt.Errorf("failed getting key %q: %w", name, ErrNotFound) } - return fmt.Errorf("failed getting key %q: %w", name, err) + return fmt.Errorf("failed getting key %q: %w", name, getErr) } - if err := t.attestTPM.DeleteKey(key.Data); err != nil { - return fmt.Errorf("failed deleting key %q: %w", name, err) + if err := t.open(ctx, openOptions{machineKey: key.MachineKey}); err != nil { + return fmt.Errorf("failed opening TPM: %w", err) } + defer closeTPM(ctx, t, &err) + + attestErr := t.attestTPM.DeleteKey(key.Data) if err := t.store.DeleteKey(name); err != nil { + if attestErr != nil { + return fmt.Errorf("failed deleting key %q from storage after TPM delete failed (%w): %w", name, attestErr, err) + } return fmt.Errorf("failed deleting key %q from storage: %w", name, err) } if err := t.store.Persist(); err != nil { + if attestErr != nil { + return fmt.Errorf("failed persisting storage after TPM delete failed (%w): %w", attestErr, err) + } return fmt.Errorf("failed persisting storage: %w", err) } - return + if attestErr != nil { + return &PartialDeleteError{Name: name, Underlying: attestErr} + } + return nil } // Signer returns a crypto.Signer backed by the Key. @@ -409,7 +462,7 @@ func (k *Key) Signer(ctx context.Context) (crypto.Signer, error) { // CertificationParameters returns information about the key that can be used to // verify key certification. func (k *Key) CertificationParameters(ctx context.Context) (params attest.CertificationParameters, err error) { - if err = k.tpm.open(ctx); err != nil { + if err = k.tpm.open(ctx, openOptions{machineKey: k.machineKey}); err != nil { return params, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, k.tpm, &err) @@ -435,7 +488,7 @@ func (k *Key) Blobs(ctx context.Context) (blobs *Blobs, err error) { return k.blobs, nil } - if err = k.tpm.open(ctx); err != nil { + if err = k.tpm.open(ctx, openOptions{machineKey: k.machineKey}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, k.tpm, &err) @@ -459,7 +512,7 @@ func (k *Key) Blobs(ctx context.Context) (blobs *Blobs, err error) { // If the public key doesn't match the public key in the first certificate // in the chain (the leaf), an error is returned. func (k *Key) SetCertificateChain(ctx context.Context, chain []*x509.Certificate) (err error) { - if err = k.tpm.open(ctx); err != nil { + if err = k.tpm.open(ctx, openOptions{machineKey: k.machineKey}); err != nil { return fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, k.tpm, &err) @@ -504,6 +557,7 @@ func (k *Key) toStorage() *storage.Key { AttestedBy: k.attestedBy, Chain: k.chain, CreatedAt: k.createdAt.UTC(), + MachineKey: k.machineKey, } } @@ -516,6 +570,7 @@ func keyFromStorage(sk *storage.Key, t *TPM) *Key { attestedBy: sk.AttestedBy, chain: sk.Chain, createdAt: sk.CreatedAt.Local(), + machineKey: sk.MachineKey, tpm: t, } } diff --git a/tpm/random.go b/tpm/random.go index 384d2ebf..2cd6f564 100644 --- a/tpm/random.go +++ b/tpm/random.go @@ -23,7 +23,7 @@ func (s ShortRandomReadError) Error() string { // GenerateRandom returns `size` number of random bytes generated by the TPM. func (t *TPM) GenerateRandom(ctx context.Context, size uint16) (random []byte, err error) { - if err = t.open(goTPMCall(ctx)); err != nil { + if err = t.open(ctx, openOptions{useGoTPM: true}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, t, &err) @@ -65,7 +65,7 @@ func (g *generator) Read(p []byte) (n int, err error) { } ctx := context.Background() - if err = g.t.open(goTPMCall(ctx)); err != nil { + if err = g.t.open(ctx, openOptions{useGoTPM: true}); err != nil { return 0, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, g.t, &err) diff --git a/tpm/signer.go b/tpm/signer.go index 5b992e2a..fde3b895 100644 --- a/tpm/signer.go +++ b/tpm/signer.go @@ -28,7 +28,7 @@ func (s *signer) Public() crypto.PublicKey { // will reload the TPM key to be used. func (s *signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { ctx := context.Background() - if err = s.tpm.open(ctx); err != nil { + if err = s.tpm.open(ctx, openOptions{machineKey: s.key.machineKey}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, s.tpm, &err) @@ -55,19 +55,26 @@ func (s *signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (si // GetSigner returns a crypto.Signer for a TPM Key identified by `name`. func (t *TPM) GetSigner(ctx context.Context, name string) (csigner crypto.Signer, err error) { - if err = t.open(ctx); err != nil { - return nil, fmt.Errorf("failed opening TPM: %w", err) + // We need the key's persisted machine-key scope before t.open so we + // can pass it through context. t.open also calls t.store.Load + // internally; calling it here explicitly preserves the invariant + // that every t.store read happens after a t.store.Load. + if err := t.store.Load(); err != nil { + return nil, fmt.Errorf("failed loading from TPM storage: %w", err) } - defer closeTPM(ctx, t, &err) - - key, err := t.store.GetKey(name) - if err != nil { - if errors.Is(err, storage.ErrNotFound) { + key, getErr := t.store.GetKey(name) + if getErr != nil { + if errors.Is(getErr, storage.ErrNotFound) { return nil, fmt.Errorf("failed getting signer for key %q: %w", name, ErrNotFound) } - return nil, fmt.Errorf("failed getting signer for key %q: %w", name, err) + return nil, fmt.Errorf("failed getting signer for key %q: %w", name, getErr) } + if err = t.open(ctx, openOptions{machineKey: key.MachineKey}); err != nil { + return nil, fmt.Errorf("failed opening TPM: %w", err) + } + defer closeTPM(ctx, t, &err) + loadedKey, err := t.attestTPM.LoadKey(key.Data) if err != nil { return nil, err @@ -85,7 +92,7 @@ func (t *TPM) GetSigner(ctx context.Context, name string) (csigner crypto.Signer csigner = &signer{ tpm: t, - key: Key{name: name, data: key.Data, attestedBy: key.AttestedBy, createdAt: key.CreatedAt, tpm: t}, + key: Key{name: name, data: key.Data, attestedBy: key.AttestedBy, createdAt: key.CreatedAt, machineKey: key.MachineKey, tpm: t}, public: loadedKey.Public(), } @@ -101,7 +108,7 @@ type tss2Signer struct { func (s *tss2Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { ctx := context.Background() - if err = s.tpm.open(goTPMCall(ctx)); err != nil { + if err = s.tpm.open(ctx, openOptions{useGoTPM: true}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, s.tpm, &err) @@ -112,7 +119,7 @@ func (s *tss2Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) // CreateTSS2Signer returns a crypto.Signer using the given [TPM] and [tss2.TPMKey]. func CreateTSS2Signer(ctx context.Context, t *TPM, key *tss2.TPMKey) (csigner crypto.Signer, err error) { - if err := t.open(goTPMCall(ctx)); err != nil { + if err := t.open(ctx, openOptions{useGoTPM: true}); err != nil { return nil, fmt.Errorf("failed opening TPM: %w", err) } defer closeTPM(ctx, t, &err) diff --git a/tpm/storage/types.go b/tpm/storage/types.go index b3fd5c5c..587bdef4 100644 --- a/tpm/storage/types.go +++ b/tpm/storage/types.go @@ -13,6 +13,11 @@ type AK struct { Data []byte Chain []*x509.Certificate CreatedAt time.Time + // MachineKey records whether the AK was created in the local machine + // key store (Windows: NCRYPT_MACHINE_KEY_FLAG). Persisted so that + // reload uses the matching key scope. Defaults to false for AKs + // created before this field was introduced. + MachineKey bool } // MarshalJSON marshals the AK into JSON. @@ -23,10 +28,11 @@ func (ak *AK) MarshalJSON() ([]byte, error) { } sak := serializedAK{ - Name: ak.Name, - Type: typeAK, - Data: ak.Data, - CreatedAt: ak.CreatedAt, + Name: ak.Name, + Type: typeAK, + Data: ak.Data, + CreatedAt: ak.CreatedAt, + MachineKey: ak.MachineKey, } if len(chain) > 0 { @@ -50,6 +56,7 @@ func (ak *AK) UnmarshalJSON(data []byte) error { ak.Name = sak.Name ak.Data = sak.Data ak.CreatedAt = sak.CreatedAt + ak.MachineKey = sak.MachineKey if len(sak.Chain) > 0 { chain := make([]*x509.Certificate, len(sak.Chain)) @@ -73,6 +80,11 @@ type Key struct { AttestedBy string Chain []*x509.Certificate CreatedAt time.Time + // MachineKey records whether the Key was created in the local machine + // key store (Windows: NCRYPT_MACHINE_KEY_FLAG). Persisted so that + // reload uses the matching key scope. Defaults to false for Keys + // created before this field was introduced. + MachineKey bool } // MarshalJSON marshals the Key into JSON. @@ -88,6 +100,7 @@ func (key *Key) MarshalJSON() ([]byte, error) { Data: key.Data, AttestedBy: key.AttestedBy, CreatedAt: key.CreatedAt, + MachineKey: key.MachineKey, } if len(chain) > 0 { @@ -112,6 +125,7 @@ func (key *Key) UnmarshalJSON(data []byte) error { key.Data = sk.Data key.AttestedBy = sk.AttestedBy key.CreatedAt = sk.CreatedAt + key.MachineKey = sk.MachineKey if len(sk.Chain) > 0 { chain := make([]*x509.Certificate, len(sk.Chain)) @@ -143,11 +157,12 @@ const ( // serializedAK is the struct used when marshaling // a storage AK to JSON. type serializedAK struct { - Name string `json:"name"` - Type tpmObjectType `json:"type"` - Data []byte `json:"data"` - Chain [][]byte `json:"chain"` - CreatedAt time.Time `json:"createdAt"` + Name string `json:"name"` + Type tpmObjectType `json:"type"` + Data []byte `json:"data"` + Chain [][]byte `json:"chain"` + CreatedAt time.Time `json:"createdAt"` + MachineKey bool `json:"machineKey,omitempty"` } // serializedKey is the struct used when marshaling @@ -159,6 +174,7 @@ type serializedKey struct { AttestedBy string `json:"attestedBy"` Chain [][]byte `json:"chain"` CreatedAt time.Time `json:"createdAt"` + MachineKey bool `json:"machineKey,omitempty"` } // keyForAK returns the key to use when storing an AK. diff --git a/tpm/tpm.go b/tpm/tpm.go index bd0dde01..0ad51e79 100644 --- a/tpm/tpm.go +++ b/tpm/tpm.go @@ -160,10 +160,18 @@ func New(opts ...NewTPMOption) (*TPM, error) { }, nil } +type openOptions struct { + machineKey bool + // useGoTPM opens a raw go-tpm command channel (t.rwc) instead of the + // go-attestation backend, for low-level operations such as generating + // random bytes, signing, and reading capabilities. + useGoTPM bool +} + // Open readies the TPM for usage and marks it as being // in use. This makes using the instance safe for // concurrent use. -func (t *TPM) open(ctx context.Context) (err error) { +func (t *TPM) open(ctx context.Context, opts openOptions) (err error) { // prevent opening the TPM multiple times if Open is called // within the package multiple times. if isInternalCall(ctx) { @@ -190,6 +198,12 @@ func (t *TPM) open(ctx context.Context) (err error) { return fmt.Errorf("failed initializing command channel: %w", err) } + // Apply per-call machine-key scope to the attest open config. We hold + // t.lock at this point and t.attestTPM is re-created on every open, so + // mutating attestConfig here is safe and does not leak across callers. + // Always set explicitly so we never carry a previous caller's choice. + t.attestConfig.MachineKey = opts.machineKey + // if a simulator was set, use it as the backing TPM device. // The simulator is currently only used for testing. if t.simulator != nil { @@ -207,7 +221,7 @@ func (t *TPM) open(ctx context.Context) (err error) { // there's a possibility of a nil pointer exception. At the moment, // the only "go-tpm" call is for GetRandom(), but this could change // in the future. - if isGoTPMCall(ctx) { + if opts.useGoTPM { rwc, err := open.TPM(t.deviceName) if err != nil { return fmt.Errorf("failed opening TPM: %w", err) diff --git a/tpm/tpm_test.go b/tpm/tpm_test.go index fc14ccbe..c14de34f 100644 --- a/tpm/tpm_test.go +++ b/tpm/tpm_test.go @@ -41,7 +41,7 @@ func newOpenedTPM(t *testing.T) *TPM { t.Helper() tpm, err := New(WithSimulator(&closeSimulator{})) require.NoError(t, err) - err = tpm.open(context.Background()) + err = tpm.open(context.Background(), openOptions{}) require.NoError(t, err) return tpm } @@ -52,7 +52,7 @@ func newCloseErrorTPM(t *testing.T) *TPM { closeErr: errors.New("closeErr"), })) require.NoError(t, err) - err = tpm.open(context.Background()) + err = tpm.open(context.Background(), openOptions{}) require.NoError(t, err) tpm.simulator = nil // required to skip returning when similator is configured return tpm