diff --git a/tpm2/export_test.go b/tpm2/export_test.go index d545aa24..2a0a9272 100644 --- a/tpm2/export_test.go +++ b/tpm2/export_test.go @@ -90,6 +90,7 @@ func NewSealedObjectKeySealer(tpm *Connection) keySealer { type KeyDataConstructor = keyDataConstructor type KeySealer = keySealer +type LockoutAuthParams = lockoutAuthParams type PcrPolicyVersionOption = pcrPolicyVersionOption type PolicyDataError = policyDataError diff --git a/tpm2/lockoutauth.go b/tpm2/lockoutauth.go new file mode 100644 index 00000000..6fb4e70f --- /dev/null +++ b/tpm2/lockoutauth.go @@ -0,0 +1,223 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2026 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package tpm2 + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/canonical/go-tpm2" + "github.com/canonical/go-tpm2/mu" + "github.com/canonical/go-tpm2/policyutil" +) + +var ( + // ErrEmptyLockoutAuthValue is returned from Connection.ResetDictionaryAttackLock if + // the authorization value for the lockout hierarchy is unset. + ErrEmptyLockoutAuthValue = errors.New("the authorization value for the lockout hierarchy is empty") + + // ErrInvalidLockoutAuthPolicy is returned from Connection.ResetDictionaryAttackLock if + // the authorization policy for the lockout hierarchy is not consistent with the supplied + // data. + ErrInvalidLockoutAuthPolicy = errors.New("the authorization policy for the lockout hierarchy is invalid") +) + +// InvalidLockoutAuthDataError is returned from [Connection.ResetDictionaryAttackLock] if the +// supplied lockout hierarchy authorization data is invalid. +type InvalidLockoutAuthDataError struct { + err error +} + +func (e *InvalidLockoutAuthDataError) Error() string { + return "invalid lockout hierarchy authorization data: " + e.err.Error() +} + +func (e *InvalidLockoutAuthDataError) Unwrap() error { + return e.err +} + +type lockoutAuthParamsJson struct { + AuthValue []byte `json:"auth-value"` + AuthPolicy []byte `json:"auth-policy,omitempty"` + NewAuthValue []byte `json:"new-auth-value,omitempty"` + NewAuthPolicy []byte `json:"new-auth-policy,omitempty"` +} + +type lockoutAuthParams struct { + AuthValue tpm2.Auth + AuthPolicy *policyutil.Policy + NewAuthValue tpm2.Auth + NewAuthPolicy *policyutil.Policy +} + +func (p *lockoutAuthParams) MarshalJSON() ([]byte, error) { + j := &lockoutAuthParamsJson{ + AuthValue: p.AuthValue, + NewAuthValue: p.NewAuthValue, + } + if p.AuthPolicy != nil { + data, err := mu.MarshalToBytes(p.AuthPolicy) + if err != nil { + return nil, fmt.Errorf("cannot encode auth-policy: %w", err) + } + j.AuthPolicy = data + } + if p.NewAuthPolicy != nil { + data, err := mu.MarshalToBytes(p.NewAuthPolicy) + if err != nil { + return nil, fmt.Errorf("cannot encode new-auth-policy: %w", err) + } + j.NewAuthPolicy = data + } + + return json.Marshal(j) +} + +func (p *lockoutAuthParams) UnmarshalJSON(data []byte) error { + var j *lockoutAuthParamsJson + if err := json.Unmarshal(data, &j); err != nil { + return err + } + + *p = lockoutAuthParams{ + AuthValue: j.AuthValue, + NewAuthValue: j.NewAuthValue, + } + if len(j.AuthPolicy) > 0 { + if _, err := mu.UnmarshalFromBytes(j.AuthPolicy, &p.AuthPolicy); err != nil { + return fmt.Errorf("cannot decode auth-policy: %w", err) + } + } + if len(j.NewAuthPolicy) > 0 { + if _, err := mu.UnmarshalFromBytes(j.NewAuthPolicy, &p.NewAuthPolicy); err != nil { + return fmt.Errorf("cannot decode new-auth-policy: %w", err) + } + } + + return nil +} + +func (t *Connection) resetDictionaryAttackLockImpl(params *lockoutAuthParams) error { + if len(params.NewAuthValue) > 0 || params.NewAuthPolicy != nil { + return errors.New("lockout hierarchy auth value change not supported yet") + } + + var authValue []byte + + val, err := t.GetCapabilityTPMProperty(tpm2.PropertyPermanent) + if err != nil { + return fmt.Errorf("cannot obtain value of TPM_PT_PERMANENT: %w", err) + } + lockoutAuthSet := tpm2.PermanentAttributes(val)&tpm2.AttrLockoutAuthSet > 0 + if lockoutAuthSet { + authValue = params.AuthValue + } + + var session tpm2.SessionContext + switch { + case params.AuthPolicy == nil: + session = t.HmacSession() + default: + session, err = t.StartAuthSession(nil, nil, tpm2.SessionTypePolicy, nil, defaultSessionHashAlgorithm) + if err != nil { + return fmt.Errorf("cannot start policy session: %w", err) + } + defer t.FlushContext(session) + + // Execute policy session, constraining the use to the TPM2_DictionaryAttackLockReset command so + // that the correct branch executes. + _, err := params.AuthPolicy.Execute( + policyutil.NewPolicyExecuteSession(t.TPMContext, session), + policyutil.WithSessionUsageCommandConstraint(tpm2.CommandDictionaryAttackLockReset, []policyutil.NamedHandle{t.LockoutHandleContext()}), + ) + if err != nil { + return ErrInvalidLockoutAuthPolicy + } + } + + t.LockoutHandleContext().SetAuthValue(authValue) + defer t.LockoutHandleContext().SetAuthValue(nil) + + switch err := t.DictionaryAttackLockReset(t.LockoutHandleContext(), session); { + case isAuthFailError(err, tpm2.CommandDictionaryAttackLockReset, 1): + return AuthFailError{tpm2.HandleLockout} + case tpm2.IsTPMWarning(err, tpm2.WarningLockout, tpm2.CommandDictionaryAttackLockReset): + return ErrTPMLockout + case tpm2.IsTPMSessionError(err, tpm2.ErrorPolicyFail, tpm2.CommandDictionaryAttackLockReset, 1): + return ErrInvalidLockoutAuthPolicy + case err != nil: + return fmt.Errorf("cannot reset dictionary attack counter: %w", err) + } + + if !lockoutAuthSet { + return ErrEmptyLockoutAuthValue + } + return nil +} + +// ResetDictionaryAttackLock resets the TPM's dictionary attack counter using the +// TPM2_DictionaryAttackLockReset command. The caller supplies authorization data for the TPM's +// lockout hierarchy which will have been supplied by a previous call to +// [Connection.EnsureProvisioned] (XXX: in a future PR). +// +// If the supplied authorization data is invalid, a *[InvalidLockoutAuthDataError] error will +// be returned. +// +// If the TPM indicates that the lockout hierarchy has an empty authorization value, this function +// will still succeed but will return an [ErrEmptyLockoutAuthValue] error. +// +// If authorization of the TPM's lockout hierarchy fails, an [AuthFailError] error will be returned. +// In this case, the lockout hierarchy will become unavailable for the current lockout recovery +// time ([Connection.EnsureProvisioned] sets it to 86400 seconds). +// +// If the TPM's lockout hierarchy is unavailable because of a previous authorization failure, an +// [ErrTPMLockout] error will be returned. +// +// If the authorization policy for the TPM's lockout hierarchy is invalid, an +// [ErrInvalidLockoutAuthPolicy] error will be returned. +func (t *Connection) ResetDictionaryAttackLock(lockoutAuthData []byte) error { + var params *lockoutAuthParams + if err := json.Unmarshal(lockoutAuthData, ¶ms); err != nil { + return &InvalidLockoutAuthDataError{err: err} + } + return t.resetDictionaryAttackLockImpl(params) +} + +// ResetDictionaryAttackLockWithAuthValue resets the TPM's dictionary attack counter using the +// TPM2_DictionaryAttackLockReset command. The caller supplies the authorization value for the +// TPM's lockout hierarchy. This API is for systems that were configured with an older version +// of [Connection.EnsureProvisioned] (XXX: not yet) where an authorization value was chosen and +// supplied by the caller. +// +// If the TPM indicates that the lockout hierarchy has an empty authorization value, this function +// will still succeed but will return an [ErrEmptyLockoutAuthValue] error. +// +// If authorization of the TPM's lockout hierarchy fails, an [AuthFailError] error will be returned. +// In this case, the lockout hierarchy will become unavailable for the current lockout recovery +// time ([Connection.EnsureProvisioned] sets it to 86400 seconds). +// +// If the TPM's lockout hierarchy is unavailable because of a previous authorization failure, an +// [ErrTPMLockout] error will be returned. +func (t *Connection) ResetDictionaryAttackLockWithAuthValue(lockoutAuthValue []byte) error { + return t.resetDictionaryAttackLockImpl(&lockoutAuthParams{ + AuthValue: lockoutAuthValue, + }) +} diff --git a/tpm2/lockoutauth_test.go b/tpm2/lockoutauth_test.go new file mode 100644 index 00000000..af8455a3 --- /dev/null +++ b/tpm2/lockoutauth_test.go @@ -0,0 +1,447 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2026 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package tpm2_test + +import ( + "crypto/elliptic" + "crypto/rand" + "encoding/json" + "errors" + + "golang.org/x/crypto/hkdf" + . "gopkg.in/check.v1" + + "github.com/canonical/go-tpm2" + "github.com/canonical/go-tpm2/objectutil" + "github.com/canonical/go-tpm2/policyutil" + internal_crypto "github.com/snapcore/secboot/internal/crypto" + "github.com/snapcore/secboot/internal/testutil" + "github.com/snapcore/secboot/internal/tpm2test" + . "github.com/snapcore/secboot/tpm2" +) + +type lockoutauthSuiteMixin struct{} + +func (*lockoutauthSuiteMixin) newDefaultLockoutAuthPolicy(c *C, alg tpm2.HashAlgorithmId) (tpm2.Digest, *policyutil.Policy) { + builder := policyutil.NewPolicyBuilder(alg) + builder.RootBranch().AddBranchNode(func(n *policyutil.PolicyBuilderBranchNode) { + n.AddBranch("", func(b *policyutil.PolicyBuilderBranch) { + b.AddBranchNode(func(n *policyutil.PolicyBuilderBranchNode) { + n.AddBranch("", func(b *policyutil.PolicyBuilderBranch) { + b.PolicyCommandCode(tpm2.CommandDictionaryAttackLockReset) + }) + n.AddBranch("", func(b *policyutil.PolicyBuilderBranch) { + b.PolicyCommandCode(tpm2.CommandDictionaryAttackParameters) + }) + n.AddBranch("", func(b *policyutil.PolicyBuilderBranch) { + b.PolicyCommandCode(tpm2.CommandClearControl) + }) + n.AddBranch("", func(b *policyutil.PolicyBuilderBranch) { + b.PolicyCommandCode(tpm2.CommandClear) + }) + }) + b.PolicyAuthValue() + }) + }) + + digest, policy, err := builder.Policy() + c.Assert(err, IsNil) + return digest, policy +} + +func (*lockoutauthSuiteMixin) newRotateAuthValueLockoutAuthPolicy(c *C, alg tpm2.HashAlgorithmId, oldAuthValue []byte) (tpm2.Digest, *policyutil.Policy) { + r := hkdf.Expand(alg.NewHash, oldAuthValue, []byte("CHANGE-AUTH")) + key, err := internal_crypto.GenerateECDSAKey(elliptic.P256(), r) + c.Assert(err, IsNil) + pubKey, err := objectutil.NewECCPublicKey(&key.PublicKey) + c.Assert(err, IsNil) + + builder := policyutil.NewPolicyBuilder(alg) + builder.RootBranch().AddBranchNode(func(n *policyutil.PolicyBuilderBranchNode) { + n.AddBranch("", func(b *policyutil.PolicyBuilderBranch) { + b.AddBranchNode(func(n *policyutil.PolicyBuilderBranchNode) { + n.AddBranch("", func(b *policyutil.PolicyBuilderBranch) { + b.PolicyCommandCode(tpm2.CommandDictionaryAttackLockReset) + }) + n.AddBranch("", func(b *policyutil.PolicyBuilderBranch) { + b.PolicyCommandCode(tpm2.CommandDictionaryAttackParameters) + }) + n.AddBranch("", func(b *policyutil.PolicyBuilderBranch) { + b.PolicyCommandCode(tpm2.CommandClearControl) + }) + n.AddBranch("", func(b *policyutil.PolicyBuilderBranch) { + b.PolicyCommandCode(tpm2.CommandClear) + }) + }) + b.PolicyAuthValue() + }) + n.AddBranch("", func(b *policyutil.PolicyBuilderBranch) { + b.PolicyCommandCode(tpm2.CommandHierarchyChangeAuth) + b.PolicySigned(pubKey, []byte("CHANGE-AUTH")) + }) + }) + + digest, policy, err := builder.Policy() + c.Assert(err, IsNil) + return digest, policy +} + +type lockoutauthSuiteNoTPM struct { + lockoutauthSuiteMixin +} + +func (s *lockoutauthSuiteNoTPM) newDefaultLockoutAuthPolicy(c *C, alg tpm2.HashAlgorithmId) *policyutil.Policy { + _, policy := s.lockoutauthSuiteMixin.newDefaultLockoutAuthPolicy(c, alg) + return policy +} + +func (s *lockoutauthSuiteNoTPM) newRotateAuthValueLockoutAuthPolicy(c *C, alg tpm2.HashAlgorithmId, oldAuthValue []byte) *policyutil.Policy { + _, policy := s.lockoutauthSuiteMixin.newRotateAuthValueLockoutAuthPolicy(c, alg, oldAuthValue) + return policy +} + +type lockoutauthSuite struct { + tpm2test.TPMTest + lockoutauthSuiteMixin +} + +func (s *lockoutauthSuite) SetUpSuite(c *C) { + s.TPMFeatures = tpm2test.TPMFeatureLockoutHierarchy | + tpm2test.TPMFeaturePlatformHierarchy | + tpm2test.TPMFeatureClear | + tpm2test.TPMFeatureNV +} + +func (s *lockoutauthSuite) SetUpTest(c *C) { + s.TPMTest.SetUpTest(c) + + c.Assert(s.TPM().DictionaryAttackParameters(s.TPM().LockoutHandleContext(), 32, 7200, 86400, nil), IsNil) +} + +func (s *lockoutauthSuite) makeLockoutAuthData(c *C, params *LockoutAuthParams) []byte { + data, err := json.Marshal(params) + c.Assert(err, IsNil) + return data +} + +var _ = Suite(&lockoutauthSuiteNoTPM{}) +var _ = Suite(&lockoutauthSuite{}) + +func (s *lockoutauthSuiteNoTPM) TestLockoutAuthParamsMarshalJSON(c *C) { + params := &LockoutAuthParams{ + AuthValue: testutil.DecodeHexString(c, "c7da0ed6f6ba3f3ea741e7863a0a1748138b6eccb0e084132b04a9c976f0d0b1"), + AuthPolicy: s.newDefaultLockoutAuthPolicy(c, tpm2.HashAlgorithmSHA256), + } + + data, err := json.Marshal(params) + c.Check(err, IsNil) + c.Check(data, DeepEquals, []byte(`{"auth-value":"x9oO1va6Pz6nQeeGOgoXSBOLbsyw4IQTKwSpyXbw0LE=","auth-policy":"AAAAAAAAAAEAC5xRENPNjPxvymnylptEkkmB67kMJSALrpC4PA2joYWCAAAAAAAAAAEgAQFxAAAAAQAAAAAAAQAL+21OPQovgBAFA+/1biwvpZu8ItTlnZBiGL/DKXTgoIIAAAACIAEBcQAAAAQAAAAAAAEAC7bFwF5YGQnN6n33pfkcDy7tN/128VUi7uW1X4lvLVY/AAAAAQAAAWwAAAE5AAAAAAABAAscaCd8nWVk3YG8z35Wuj7cqziPxgzpWzpEK9JyWPYN/AAAAAEAAAFsAAABOgAAAAAAAQALlAz7Qhe7Htz3+0GTfKl0qmjmmKt4uBJLBwET4hH9RvwAAAABAAABbAAAAScAAAAAAAEAC8Tfq87ajeg2yVZhlSiSsd73IDr7Rv7+xD/8/JO+VAcwAAAAAQAAAWwAAAEmAAABaw=="}`)) +} + +func (s *lockoutauthSuiteNoTPM) TestLockoutAuthParamsMarshalJSONNoPolicy(c *C) { + params := &LockoutAuthParams{ + AuthValue: testutil.DecodeHexString(c, "c7da0ed6f6ba3f3ea741e7863a0a1748138b6eccb0e084132b04a9c976f0d0b1"), + } + + data, err := json.Marshal(params) + c.Check(err, IsNil) + c.Check(data, DeepEquals, []byte(`{"auth-value":"x9oO1va6Pz6nQeeGOgoXSBOLbsyw4IQTKwSpyXbw0LE="}`)) +} + +func (s *lockoutauthSuiteNoTPM) TestLockoutAuthParamsMarshalJSONForChangeAuth(c *C) { + authValue := testutil.DecodeHexString(c, "c7da0ed6f6ba3f3ea741e7863a0a1748138b6eccb0e084132b04a9c976f0d0b1") + params := &LockoutAuthParams{ + AuthValue: authValue, + AuthPolicy: s.newDefaultLockoutAuthPolicy(c, tpm2.HashAlgorithmSHA256), + NewAuthValue: testutil.DecodeHexString(c, "db82cbebd10ebd831b48ff8ae7275a23029074ba622c0416d97cd34dd38d8186"), + NewAuthPolicy: s.newRotateAuthValueLockoutAuthPolicy(c, tpm2.HashAlgorithmSHA256, authValue), + } + + data, err := json.Marshal(params) + c.Check(err, IsNil) + c.Check(data, DeepEquals, []byte(`{"auth-value":"x9oO1va6Pz6nQeeGOgoXSBOLbsyw4IQTKwSpyXbw0LE=","auth-policy":"AAAAAAAAAAEAC5xRENPNjPxvymnylptEkkmB67kMJSALrpC4PA2joYWCAAAAAAAAAAEgAQFxAAAAAQAAAAAAAQAL+21OPQovgBAFA+/1biwvpZu8ItTlnZBiGL/DKXTgoIIAAAACIAEBcQAAAAQAAAAAAAEAC7bFwF5YGQnN6n33pfkcDy7tN/128VUi7uW1X4lvLVY/AAAAAQAAAWwAAAE5AAAAAAABAAscaCd8nWVk3YG8z35Wuj7cqziPxgzpWzpEK9JyWPYN/AAAAAEAAAFsAAABOgAAAAAAAQALlAz7Qhe7Htz3+0GTfKl0qmjmmKt4uBJLBwET4hH9RvwAAAABAAABbAAAAScAAAAAAAEAC8Tfq87ajeg2yVZhlSiSsd73IDr7Rv7+xD/8/JO+VAcwAAAAAQAAAWwAAAEmAAABaw==","new-auth-value":"24LL69EOvYMbSP+K5ydaIwKQdLpiLAQW2XzTTdONgYY=","new-auth-policy":"AAAAAAAAAAEAC8iuOzJsfCEvz5HdnLSO98fhopBFpLgo9fX7/1TF/6KqAAAAAAAAAAEgAQFxAAAAAgAAAAAAAQAL+21OPQovgBAFA+/1biwvpZu8ItTlnZBiGL/DKXTgoIIAAAACIAEBcQAAAAQAAAAAAAEAC7bFwF5YGQnN6n33pfkcDy7tN/128VUi7uW1X4lvLVY/AAAAAQAAAWwAAAE5AAAAAAABAAscaCd8nWVk3YG8z35Wuj7cqziPxgzpWzpEK9JyWPYN/AAAAAEAAAFsAAABOgAAAAAAAQALlAz7Qhe7Htz3+0GTfKl0qmjmmKt4uBJLBwET4hH9RvwAAAABAAABbAAAAScAAAAAAAEAC8Tfq87ajeg2yVZhlSiSsd73IDr7Rv7+xD/8/JO+VAcwAAAAAQAAAWwAAAEmAAABawAAAAAAAQALDDnMvDFtHshfTn3M6F3KHOta8q5u4GWsqsqB8JnLJCYAAAACAAABbAAAASkAAAFgACMACwAEAAAAAAAQABAAAwAQACC2BaF5zNUOUWsO9Vxdw5PNDslawcvHjS3x54a1VHxZfAAgaOCKN2rpEFpajypuc/XSGSr0LnK/e8W9IyZMM8DufpUAC0NIQU5HRS1BVVRIAAAAAAAA"}`)) +} + +func (s *lockoutauthSuiteNoTPM) TestLockoutAuthParamsUnmarshalJSON(c *C) { + data := []byte(`{"auth-value":"x9oO1va6Pz6nQeeGOgoXSBOLbsyw4IQTKwSpyXbw0LE=","auth-policy":"AAAAAAAAAAEAC5xRENPNjPxvymnylptEkkmB67kMJSALrpC4PA2joYWCAAAAAAAAAAEgAQFxAAAAAQAAAAAAAQAL+21OPQovgBAFA+/1biwvpZu8ItTlnZBiGL/DKXTgoIIAAAACIAEBcQAAAAQAAAAAAAEAC7bFwF5YGQnN6n33pfkcDy7tN/128VUi7uW1X4lvLVY/AAAAAQAAAWwAAAE5AAAAAAABAAscaCd8nWVk3YG8z35Wuj7cqziPxgzpWzpEK9JyWPYN/AAAAAEAAAFsAAABOgAAAAAAAQALlAz7Qhe7Htz3+0GTfKl0qmjmmKt4uBJLBwET4hH9RvwAAAABAAABbAAAAScAAAAAAAEAC8Tfq87ajeg2yVZhlSiSsd73IDr7Rv7+xD/8/JO+VAcwAAAAAQAAAWwAAAEmAAABaw=="}`) + + expected := &LockoutAuthParams{ + AuthValue: testutil.DecodeHexString(c, "c7da0ed6f6ba3f3ea741e7863a0a1748138b6eccb0e084132b04a9c976f0d0b1"), + AuthPolicy: s.newDefaultLockoutAuthPolicy(c, tpm2.HashAlgorithmSHA256), + } + + var params *LockoutAuthParams + c.Assert(json.Unmarshal(data, ¶ms), IsNil) + c.Check(params, DeepEquals, expected) +} + +func (s *lockoutauthSuiteNoTPM) TestLockoutAuthParamsUnmarshalJSONForChangeAuth(c *C) { + data := []byte(`{"auth-value":"x9oO1va6Pz6nQeeGOgoXSBOLbsyw4IQTKwSpyXbw0LE=","auth-policy":"AAAAAAAAAAEAC5xRENPNjPxvymnylptEkkmB67kMJSALrpC4PA2joYWCAAAAAAAAAAEgAQFxAAAAAQAAAAAAAQAL+21OPQovgBAFA+/1biwvpZu8ItTlnZBiGL/DKXTgoIIAAAACIAEBcQAAAAQAAAAAAAEAC7bFwF5YGQnN6n33pfkcDy7tN/128VUi7uW1X4lvLVY/AAAAAQAAAWwAAAE5AAAAAAABAAscaCd8nWVk3YG8z35Wuj7cqziPxgzpWzpEK9JyWPYN/AAAAAEAAAFsAAABOgAAAAAAAQALlAz7Qhe7Htz3+0GTfKl0qmjmmKt4uBJLBwET4hH9RvwAAAABAAABbAAAAScAAAAAAAEAC8Tfq87ajeg2yVZhlSiSsd73IDr7Rv7+xD/8/JO+VAcwAAAAAQAAAWwAAAEmAAABaw==","new-auth-value":"24LL69EOvYMbSP+K5ydaIwKQdLpiLAQW2XzTTdONgYY=","new-auth-policy":"AAAAAAAAAAEAC8iuOzJsfCEvz5HdnLSO98fhopBFpLgo9fX7/1TF/6KqAAAAAAAAAAEgAQFxAAAAAgAAAAAAAQAL+21OPQovgBAFA+/1biwvpZu8ItTlnZBiGL/DKXTgoIIAAAACIAEBcQAAAAQAAAAAAAEAC7bFwF5YGQnN6n33pfkcDy7tN/128VUi7uW1X4lvLVY/AAAAAQAAAWwAAAE5AAAAAAABAAscaCd8nWVk3YG8z35Wuj7cqziPxgzpWzpEK9JyWPYN/AAAAAEAAAFsAAABOgAAAAAAAQALlAz7Qhe7Htz3+0GTfKl0qmjmmKt4uBJLBwET4hH9RvwAAAABAAABbAAAAScAAAAAAAEAC8Tfq87ajeg2yVZhlSiSsd73IDr7Rv7+xD/8/JO+VAcwAAAAAQAAAWwAAAEmAAABawAAAAAAAQALDDnMvDFtHshfTn3M6F3KHOta8q5u4GWsqsqB8JnLJCYAAAACAAABbAAAASkAAAFgACMACwAEAAAAAAAQABAAAwAQACC2BaF5zNUOUWsO9Vxdw5PNDslawcvHjS3x54a1VHxZfAAgaOCKN2rpEFpajypuc/XSGSr0LnK/e8W9IyZMM8DufpUAC0NIQU5HRS1BVVRIAAAAAAAA"}`) + + authValue := testutil.DecodeHexString(c, "c7da0ed6f6ba3f3ea741e7863a0a1748138b6eccb0e084132b04a9c976f0d0b1") + expected := &LockoutAuthParams{ + AuthValue: authValue, + AuthPolicy: s.newDefaultLockoutAuthPolicy(c, tpm2.HashAlgorithmSHA256), + NewAuthValue: testutil.DecodeHexString(c, "db82cbebd10ebd831b48ff8ae7275a23029074ba622c0416d97cd34dd38d8186"), + NewAuthPolicy: s.newRotateAuthValueLockoutAuthPolicy(c, tpm2.HashAlgorithmSHA256, authValue), + } + + var params *LockoutAuthParams + c.Assert(json.Unmarshal(data, ¶ms), IsNil) + c.Check(params, DeepEquals, expected) +} + +func (s *lockoutauthSuiteNoTPM) TestLockoutAuthParamsUnmarshalJSONInvalidAuthPolicy(c *C) { + data := []byte(`{"auth-value":"x9oO1va6Pz6nQeeGOgoXSBOLbsyw4IQTKwSpyXbw0LE=","auth-policy":"AA=="}`) + + var params *LockoutAuthParams + c.Assert(json.Unmarshal(data, ¶ms), ErrorMatches, `cannot decode auth-policy: cannot unmarshal argument 0 whilst processing element of type uint32: unexpected EOF + +=== BEGIN STACK === +\.\.\. policyutil\.Policy location .+\.go:[0-9]+, argument 0 +=== END STACK === +`) +} + +type testResetDictionaryAttackLockParams struct { + authValue tpm2.Auth + policyDigest tpm2.Digest + policyAlg tpm2.HashAlgorithmId + prepare func() + data []byte +} + +func (s *lockoutauthSuite) testResetDictionaryAttackLock(c *C, params *testResetDictionaryAttackLockParams) error { + // Setup hierarchy authorization + // XXX: A subequent PR will make EnsureProvisioned do this instead + s.HierarchyChangeAuth(c, tpm2.HandleLockout, params.authValue) + c.Assert(s.TPM().SetPrimaryPolicy(s.TPM().LockoutHandleContext(), params.policyDigest, params.policyAlg, nil), IsNil) + s.TPM().LockoutHandleContext().SetAuthValue(nil) // Make sure ResetDictionaryAttackLock sets this. + + // Increment the DA counter by 1 + pub, sensitive, err := objectutil.NewSealedObject(rand.Reader, []byte("foo"), []byte("5678")) + c.Assert(err, IsNil) + key, err := s.TPM().LoadExternal(sensitive, pub, tpm2.HandleNull) + c.Assert(err, IsNil) + key.SetAuthValue(nil) + _, err = s.TPM().Unseal(key, nil) + c.Assert(tpm2.IsTPMSessionError(err, tpm2.ErrorAuthFail, tpm2.CommandUnseal, 1), testutil.IsTrue) + + // Check the DA counter + val, err := s.TPM().GetCapabilityTPMProperty(tpm2.PropertyLockoutCounter) + c.Assert(err, IsNil) + c.Assert(val, Equals, uint32(1)) + + if params.prepare != nil { + params.prepare() + } + + resetErr := s.TPM().ResetDictionaryAttackLock(params.data) + if resetErr != nil && !errors.Is(resetErr, ErrEmptyLockoutAuthValue) { + return resetErr + } + + val, err = s.TPM().GetCapabilityTPMProperty(tpm2.PropertyLockoutCounter) + c.Assert(err, IsNil) + c.Assert(val, Equals, uint32(0)) + + c.Check(s.TPM().LockoutHandleContext().AuthValue(), DeepEquals, []byte(nil)) + + return resetErr +} + +func (s *lockoutauthSuite) TestResetDictionaryAttackLock(c *C) { + authValue := testutil.DecodeHexString(c, "c7da0ed6f6ba3f3ea741e7863a0a1748138b6eccb0e084132b04a9c976f0d0b1") + digest, policy := s.newDefaultLockoutAuthPolicy(c, tpm2.HashAlgorithmSHA256) + + err := s.testResetDictionaryAttackLock(c, &testResetDictionaryAttackLockParams{ + authValue: authValue, + policyDigest: digest, + policyAlg: tpm2.HashAlgorithmSHA256, + data: s.makeLockoutAuthData(c, &LockoutAuthParams{ + AuthValue: authValue, + AuthPolicy: policy, + }), + }) + c.Check(err, IsNil) + + cmds := s.CommandLog() + c.Assert(len(cmds) > 2, testutil.IsTrue) + cmd := cmds[len(cmds)-3] + c.Check(cmd.CmdCode, Equals, tpm2.CommandDictionaryAttackLockReset) + c.Assert(cmd.CmdAuthArea, HasLen, 1) + c.Check(cmd.CmdAuthArea[0].SessionHandle.Type(), Equals, tpm2.HandleTypePolicySession) + + c.Check(s.TPM().DoesHandleExist(cmd.CmdAuthArea[0].SessionHandle), testutil.IsFalse) +} + +func (s *lockoutauthSuite) TestResetDictionaryAttackLockAuthValueUnset(c *C) { + authValue := testutil.DecodeHexString(c, "c7da0ed6f6ba3f3ea741e7863a0a1748138b6eccb0e084132b04a9c976f0d0b1") + digest, policy := s.newDefaultLockoutAuthPolicy(c, tpm2.HashAlgorithmSHA256) + + err := s.testResetDictionaryAttackLock(c, &testResetDictionaryAttackLockParams{ + policyDigest: digest, + policyAlg: tpm2.HashAlgorithmSHA256, + data: s.makeLockoutAuthData(c, &LockoutAuthParams{ + AuthValue: authValue, + AuthPolicy: policy, + }), + }) + c.Check(err, ErrorMatches, `the authorization value for the lockout hierarchy is empty`) + c.Check(err, Equals, ErrEmptyLockoutAuthValue) + + cmds := s.CommandLog() + c.Assert(len(cmds) > 2, testutil.IsTrue) + cmd := cmds[len(cmds)-3] + c.Check(cmd.CmdCode, Equals, tpm2.CommandDictionaryAttackLockReset) + c.Assert(cmd.CmdAuthArea, HasLen, 1) + c.Check(cmd.CmdAuthArea[0].SessionHandle.Type(), Equals, tpm2.HandleTypePolicySession) + + c.Check(s.TPM().DoesHandleExist(cmd.CmdAuthArea[0].SessionHandle), testutil.IsFalse) +} + +func (s *lockoutauthSuite) TestResetDictionaryAttackLockWithAuthValue(c *C) { + authValue := testutil.DecodeHexString(c, "c7da0ed6f6ba3f3ea741e7863a0a1748138b6eccb0e084132b04a9c976f0d0b1") + + // Setup hierarchy authorization + // XXX: A subequent PR will make EnsureProvisioned do this instead + s.HierarchyChangeAuth(c, tpm2.HandleLockout, authValue) + s.TPM().LockoutHandleContext().SetAuthValue(nil) // Make sure ResetDictionaryAttackLock sets this. + + // Increment the DA counter by 1 + pub, sensitive, err := objectutil.NewSealedObject(rand.Reader, []byte("foo"), []byte("5678")) + c.Assert(err, IsNil) + key, err := s.TPM().LoadExternal(sensitive, pub, tpm2.HandleNull) + c.Assert(err, IsNil) + key.SetAuthValue(nil) + _, err = s.TPM().Unseal(key, nil) + c.Assert(tpm2.IsTPMSessionError(err, tpm2.ErrorAuthFail, tpm2.CommandUnseal, 1), testutil.IsTrue) + + // Check the DA counter + val, err := s.TPM().GetCapabilityTPMProperty(tpm2.PropertyLockoutCounter) + c.Assert(err, IsNil) + c.Assert(val, Equals, uint32(1)) + + c.Check(s.TPM().ResetDictionaryAttackLockWithAuthValue(authValue), IsNil) + + val, err = s.TPM().GetCapabilityTPMProperty(tpm2.PropertyLockoutCounter) + c.Assert(err, IsNil) + c.Assert(val, Equals, uint32(0)) + + c.Check(s.TPM().LockoutHandleContext().AuthValue(), DeepEquals, []byte(nil)) + + cmds := s.CommandLog() + c.Assert(len(cmds) > 1, testutil.IsTrue) + cmd := cmds[len(cmds)-2] + c.Check(cmd.CmdCode, Equals, tpm2.CommandDictionaryAttackLockReset) + c.Assert(cmd.CmdAuthArea, HasLen, 1) + c.Check(cmd.CmdAuthArea[0].SessionHandle, Equals, s.TPM().HmacSession().Handle()) + + c.Check(s.TPM().DoesHandleExist(s.TPM().HmacSession().Handle()), testutil.IsTrue) +} + +func (s *lockoutauthSuite) TestResetDictionaryAttackLockInvalidData(c *C) { + authValue := testutil.DecodeHexString(c, "c7da0ed6f6ba3f3ea741e7863a0a1748138b6eccb0e084132b04a9c976f0d0b1") + digest, _ := s.newDefaultLockoutAuthPolicy(c, tpm2.HashAlgorithmSHA256) + + err := s.testResetDictionaryAttackLock(c, &testResetDictionaryAttackLockParams{ + authValue: authValue, + policyDigest: digest, + policyAlg: tpm2.HashAlgorithmSHA256, + data: []byte(`foo`), + }) + c.Check(err, ErrorMatches, `invalid lockout hierarchy authorization data: invalid character 'o' in literal false \(expecting 'a'\)`) + c.Check(err, testutil.ConvertibleTo, &InvalidLockoutAuthDataError{}) +} + +func (s *lockoutauthSuite) TestResetDictionaryAttackLockUnsupportedAuthValueRotation(c *C) { + authValue := testutil.DecodeHexString(c, "c7da0ed6f6ba3f3ea741e7863a0a1748138b6eccb0e084132b04a9c976f0d0b1") + digest, policy := s.newDefaultLockoutAuthPolicy(c, tpm2.HashAlgorithmSHA256) + + err := s.testResetDictionaryAttackLock(c, &testResetDictionaryAttackLockParams{ + authValue: authValue, + policyDigest: digest, + policyAlg: tpm2.HashAlgorithmSHA256, + data: s.makeLockoutAuthData(c, &LockoutAuthParams{ + AuthValue: authValue, + AuthPolicy: policy, + NewAuthValue: testutil.DecodeHexString(c, "db82cbebd10ebd831b48ff8ae7275a23029074ba622c0416d97cd34dd38d8186"), + }), + }) + c.Check(err, ErrorMatches, `lockout hierarchy auth value change not supported yet`) +} + +func (s *lockoutauthSuite) TestResetDictionaryAttackLockAuthFail(c *C) { + authValue := testutil.DecodeHexString(c, "c7da0ed6f6ba3f3ea741e7863a0a1748138b6eccb0e084132b04a9c976f0d0b1") + digest, policy := s.newDefaultLockoutAuthPolicy(c, tpm2.HashAlgorithmSHA256) + + defer s.ClearTPMUsingPlatformHierarchy(c) + + err := s.testResetDictionaryAttackLock(c, &testResetDictionaryAttackLockParams{ + authValue: authValue, + policyDigest: digest, + policyAlg: tpm2.HashAlgorithmSHA256, + data: s.makeLockoutAuthData(c, &LockoutAuthParams{ + AuthPolicy: policy, + }), + }) + c.Check(err, ErrorMatches, `cannot access resource at handle TPM_RH_LOCKOUT because an authorization check failed`) + c.Assert(err, testutil.ConvertibleTo, AuthFailError{}) + c.Check(err.(AuthFailError).Handle, Equals, tpm2.HandleLockout) +} + +func (s *lockoutauthSuite) TestResetDictionaryAttackLockLockout(c *C) { + authValue := testutil.DecodeHexString(c, "c7da0ed6f6ba3f3ea741e7863a0a1748138b6eccb0e084132b04a9c976f0d0b1") + digest, policy := s.newDefaultLockoutAuthPolicy(c, tpm2.HashAlgorithmSHA256) + + defer s.ClearTPMUsingPlatformHierarchy(c) + + err := s.testResetDictionaryAttackLock(c, &testResetDictionaryAttackLockParams{ + authValue: authValue, + policyDigest: digest, + policyAlg: tpm2.HashAlgorithmSHA256, + prepare: func() { + c.Check(s.TPM().HierarchyChangeAuth(s.TPM().LockoutHandleContext(), nil, nil), testutil.ErrorIs, + &tpm2.TPMSessionError{TPMError: &tpm2.TPMError{Command: tpm2.CommandHierarchyChangeAuth, Code: tpm2.ErrorAuthFail}, Index: 1}) + }, + data: s.makeLockoutAuthData(c, &LockoutAuthParams{ + AuthValue: authValue, + AuthPolicy: policy, + }), + }) + c.Check(err, ErrorMatches, `the TPM is in DA lockout mode`) + c.Check(err, Equals, ErrTPMLockout) +} + +func (s *lockoutauthSuite) TestResetDictionaryAttackLockInvalidPolicy(c *C) { + authValue := testutil.DecodeHexString(c, "c7da0ed6f6ba3f3ea741e7863a0a1748138b6eccb0e084132b04a9c976f0d0b1") + _, policy := s.newDefaultLockoutAuthPolicy(c, tpm2.HashAlgorithmSHA256) + + err := s.testResetDictionaryAttackLock(c, &testResetDictionaryAttackLockParams{ + authValue: authValue, + policyDigest: testutil.DecodeHexString(c, "5e517fa9d3184d1b37338b34a0a8aa4fb8f4c74cdde8cade3ba4357d31af7b7c"), + policyAlg: tpm2.HashAlgorithmSHA256, + data: s.makeLockoutAuthData(c, &LockoutAuthParams{ + AuthPolicy: policy, + }), + }) + c.Check(err, ErrorMatches, `the authorization policy for the lockout hierarchy is invalid`) + c.Check(err, Equals, ErrInvalidLockoutAuthPolicy) +}