From b5882cd209fcc6a6a7d140e6ea0694117c04ddd6 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Tue, 10 Sep 2024 11:23:41 +0100 Subject: [PATCH 1/2] tpm2: Create TPM2 keys with noDA attribute if no user auth is required By default, all TPM2 keys are protected by the TPM's dictionary attack protection logic. Given that the user auth mode is immutable, we should take advantage of this to create keys that require no user auth with the noDA attribute, so that they aren't protected by the TPM's dictionary attack protection logic. This means that these keys will still be recoverable, even if the TPM is in lockout mode. --- tpm2/export_test.go | 13 +++- tpm2/key_sealer.go | 12 +++- tpm2/key_sealer_test.go | 52 +++++++++++++--- tpm2/keydata.go | 26 +++++++- tpm2/platform_test.go | 130 ++++++++++++++++++++++++++++++++++------ tpm2/policy.go | 4 ++ tpm2/policy_test.go | 42 ++++++++++++- tpm2/policy_v0.go | 4 ++ tpm2/policy_v0_test.go | 5 ++ tpm2/policy_v1.go | 4 ++ tpm2/policy_v1_test.go | 5 ++ tpm2/policy_v3.go | 4 ++ tpm2/policy_v3_test.go | 12 ++++ tpm2/seal.go | 4 +- tpm2/seal_test.go | 14 +++-- tpm2/unseal.go | 13 +--- 16 files changed, 294 insertions(+), 50 deletions(-) diff --git a/tpm2/export_test.go b/tpm2/export_test.go index 90c5742a..40d86b05 100644 --- a/tpm2/export_test.go +++ b/tpm2/export_test.go @@ -68,7 +68,7 @@ type KeyData_v2 = keyData_v2 type KeyData_v3 = keyData_v3 type AdditionalData_v3 = additionalData_v3 type KeyDataError = keyDataError -type SealedKeyDataParams = makeSealedKeyDataParams +type MakeSealedKeyDataParams = makeSealedKeyDataParams type KeyDataPolicy = keyDataPolicy type KeyDataPolicy_v0 = keyDataPolicy_v0 type KeyDataPolicy_v1 = keyDataPolicy_v1 @@ -83,6 +83,9 @@ func NewSealedObjectKeySealer(tpm *Connection) keySealer { return &sealedObjectKeySealer{tpm} } +type KeyDataConstructor = keyDataConstructor +type KeySealer = keySealer + type PcrPolicyVersionOption = pcrPolicyVersionOption type PolicyDataError = policyDataError type PolicyOrData_v0 = policyOrData_v0 @@ -191,6 +194,14 @@ func MockEnsurePcrPolicyCounter(fn func(*tpm2.TPMContext, tpm2.Handle, *tpm2.Pub } } +func MockMakeSealedKeyData(fn func(*tpm2.TPMContext, *MakeSealedKeyDataParams, KeySealer, KeyDataConstructor, tpm2.SessionContext) (*secboot.KeyData, secboot.PrimaryKey, secboot.DiskUnlockKey, error)) (restore func()) { + orig := makeSealedKeyData + makeSealedKeyData = fn + return func() { + makeSealedKeyData = orig + } +} + func MockNewKeyDataPolicy(fn func(tpm2.HashAlgorithmId, *tpm2.Public, string, *tpm2.NVPublic, bool) (KeyDataPolicy, tpm2.Digest, error)) (restore func()) { orig := newKeyDataPolicy newKeyDataPolicy = fn diff --git a/tpm2/key_sealer.go b/tpm2/key_sealer.go index ef764422..6bee58af 100644 --- a/tpm2/key_sealer.go +++ b/tpm2/key_sealer.go @@ -33,7 +33,7 @@ type keySealer interface { // and with the specified name algorithm and authorization policy. It returns // the private and public parts of the object, and an optional secret value if // the returned object has to be imported. - CreateSealedObject(data []byte, nameAlg tpm2.HashAlgorithmId, policy tpm2.Digest) (tpm2.Private, *tpm2.Public, tpm2.EncryptedSecret, error) + CreateSealedObject(data []byte, nameAlg tpm2.HashAlgorithmId, policy tpm2.Digest, noDA bool) (tpm2.Private, *tpm2.Public, tpm2.EncryptedSecret, error) } // sealedObjectKeySealer is an implementation of keySealer that seals data to @@ -42,7 +42,7 @@ type sealedObjectKeySealer struct { tpm *Connection } -func (s *sealedObjectKeySealer) CreateSealedObject(data []byte, nameAlg tpm2.HashAlgorithmId, policy tpm2.Digest) (tpm2.Private, *tpm2.Public, tpm2.EncryptedSecret, error) { +func (s *sealedObjectKeySealer) CreateSealedObject(data []byte, nameAlg tpm2.HashAlgorithmId, policy tpm2.Digest, noDA bool) (tpm2.Private, *tpm2.Public, tpm2.EncryptedSecret, error) { // Obtain a context for the SRK now. If we're called immediately after ProvisionTPM without // closing the Connection, we use the context cached by ProvisionTPM, which corresponds to // the object provisioned. If not, we just unconditionally provision a new SRK as this function @@ -79,6 +79,9 @@ func (s *sealedObjectKeySealer) CreateSealedObject(data []byte, nameAlg tpm2.Has // Define the template template := templates.NewSealedObject(nameAlg) template.Attrs &^= tpm2.AttrUserWithAuth + if noDA { + template.Attrs |= tpm2.AttrNoDA + } template.AuthPolicy = policy // Now create the sealed key object. The command is integrity protected so if the object @@ -101,9 +104,12 @@ type importableObjectKeySealer struct { tpmKey *tpm2.Public } -func (s *importableObjectKeySealer) CreateSealedObject(data []byte, nameAlg tpm2.HashAlgorithmId, policy tpm2.Digest) (tpm2.Private, *tpm2.Public, tpm2.EncryptedSecret, error) { +func (s *importableObjectKeySealer) CreateSealedObject(data []byte, nameAlg tpm2.HashAlgorithmId, policy tpm2.Digest, noDA bool) (tpm2.Private, *tpm2.Public, tpm2.EncryptedSecret, error) { pub, sensitive := util.NewExternalSealedObject(nameAlg, nil, data) pub.Attrs &^= tpm2.AttrUserWithAuth + if noDA { + pub.Attrs |= tpm2.AttrNoDA + } pub.AuthPolicy = policy // Now create the importable sealed key object (duplication object). diff --git a/tpm2/key_sealer_test.go b/tpm2/key_sealer_test.go index 843b09d2..e916490b 100644 --- a/tpm2/key_sealer_test.go +++ b/tpm2/key_sealer_test.go @@ -59,19 +59,24 @@ type testCreateSealedObjectData struct { data tpm2.SensitiveData nameAlg tpm2.HashAlgorithmId policyDigest tpm2.Digest + noDA bool session tpm2.SessionContext } func (s *sealedObjectKeySealerSuite) testCreateSealedObject(c *C, data *testCreateSealedObjectData) { sealer := NewSealedObjectKeySealer(s.TPM()) - priv, pub, importSymSeed, err := sealer.CreateSealedObject(data.data, data.nameAlg, data.policyDigest) + priv, pub, importSymSeed, err := sealer.CreateSealedObject(data.data, data.nameAlg, data.policyDigest, data.noDA) c.Assert(err, IsNil) c.Check(importSymSeed, IsNil) c.Check(pub.Type, Equals, tpm2.ObjectTypeKeyedHash) c.Check(pub.NameAlg, Equals, data.nameAlg) - c.Check(pub.Attrs, Equals, tpm2.AttrFixedParent|tpm2.AttrFixedTPM) + expectedAttrs := tpm2.AttrFixedParent | tpm2.AttrFixedTPM + if data.noDA { + expectedAttrs |= tpm2.AttrNoDA + } + c.Check(pub.Attrs, Equals, expectedAttrs) c.Check(pub.AuthPolicy, DeepEquals, data.policyDigest) c.Check(pub.Params, DeepEquals, &tpm2.PublicParamsU{ @@ -96,6 +101,7 @@ func (s *sealedObjectKeySealerSuite) TestCreateSealedObject(c *C) { data: []byte("foo"), nameAlg: tpm2.HashAlgorithmSHA256, policyDigest: make([]byte, 32), + noDA: true, session: s.StartAuthSession(c, nil, nil, tpm2.SessionTypePolicy, nil, tpm2.HashAlgorithmSHA256)}) } @@ -108,6 +114,7 @@ func (s *sealedObjectKeySealerSuite) TestCreateSealedObjectWithNewConnection(c * data: []byte("foo"), nameAlg: tpm2.HashAlgorithmSHA256, policyDigest: make([]byte, 32), + noDA: true, session: s.StartAuthSession(c, nil, nil, tpm2.SessionTypePolicy, nil, tpm2.HashAlgorithmSHA256)}) } @@ -122,6 +129,7 @@ func (s *sealedObjectKeySealerSuite) TestCreateSealedObjectMissingSRK(c *C) { data: []byte("foo"), nameAlg: tpm2.HashAlgorithmSHA256, policyDigest: make([]byte, 32), + noDA: true, session: s.StartAuthSession(c, nil, nil, tpm2.SessionTypePolicy, nil, tpm2.HashAlgorithmSHA256)}) } @@ -130,6 +138,7 @@ func (s *sealedObjectKeySealerSuite) TestCreateSealedObjectDifferentData(c *C) { data: []byte("bar"), nameAlg: tpm2.HashAlgorithmSHA256, policyDigest: make([]byte, 32), + noDA: true, session: s.StartAuthSession(c, nil, nil, tpm2.SessionTypePolicy, nil, tpm2.HashAlgorithmSHA256)}) } @@ -138,6 +147,7 @@ func (s *sealedObjectKeySealerSuite) TestCreateSealedObjectDifferentNameAlg(c *C data: []byte("foo"), nameAlg: tpm2.HashAlgorithmSHA1, policyDigest: make([]byte, 20), + noDA: true, session: s.StartAuthSession(c, nil, nil, tpm2.SessionTypePolicy, nil, tpm2.HashAlgorithmSHA1)}) } @@ -152,9 +162,19 @@ func (s *sealedObjectKeySealerSuite) TestCreateSealedObjectDifferentPolicy(c *C) data: []byte("foo"), nameAlg: tpm2.HashAlgorithmSHA256, policyDigest: trial.GetDigest(), + noDA: true, session: session}) } +func (s *sealedObjectKeySealerSuite) TestCreateSealedObjectWithDA(c *C) { + s.testCreateSealedObject(c, &testCreateSealedObjectData{ + data: []byte("foo"), + nameAlg: tpm2.HashAlgorithmSHA256, + policyDigest: make([]byte, 32), + noDA: false, + session: s.StartAuthSession(c, nil, nil, tpm2.SessionTypePolicy, nil, tpm2.HashAlgorithmSHA256)}) +} + type importableObjectKeySealerSuite struct{} var _ = Suite(&importableObjectKeySealerSuite{}) @@ -167,12 +187,16 @@ func (s *importableObjectKeySealerSuite) testCreateSealedObject(c *C, data *test sealer := NewImportableObjectKeySealer(srk) - priv, pub, importSymSeed, err := sealer.CreateSealedObject(data.data, data.nameAlg, data.policyDigest) + priv, pub, importSymSeed, err := sealer.CreateSealedObject(data.data, data.nameAlg, data.policyDigest, data.noDA) c.Assert(err, IsNil) c.Check(pub.Type, Equals, tpm2.ObjectTypeKeyedHash) c.Check(pub.NameAlg, Equals, data.nameAlg) - c.Check(pub.Attrs, Equals, tpm2.ObjectAttributes(0)) + expectedAttrs := tpm2.ObjectAttributes(0) + if data.noDA { + expectedAttrs |= tpm2.AttrNoDA + } + c.Check(pub.Attrs, Equals, expectedAttrs) c.Check(pub.AuthPolicy, DeepEquals, data.policyDigest) c.Check(pub.Params, DeepEquals, &tpm2.PublicParamsU{ @@ -191,21 +215,24 @@ func (s *importableObjectKeySealerSuite) TestCreateSealedObject(c *C) { s.testCreateSealedObject(c, &testCreateSealedObjectData{ data: []byte("foo"), nameAlg: tpm2.HashAlgorithmSHA256, - policyDigest: make([]byte, 32)}) + policyDigest: make([]byte, 32), + noDA: true}) } func (s *importableObjectKeySealerSuite) TestCreateSealedObjectDifferentData(c *C) { s.testCreateSealedObject(c, &testCreateSealedObjectData{ data: []byte("bar"), nameAlg: tpm2.HashAlgorithmSHA256, - policyDigest: make([]byte, 32)}) + policyDigest: make([]byte, 32), + noDA: true}) } func (s *importableObjectKeySealerSuite) TestCreateSealedObjectiDifferentNameAlg(c *C) { s.testCreateSealedObject(c, &testCreateSealedObjectData{ data: []byte("foo"), nameAlg: tpm2.HashAlgorithmSHA1, - policyDigest: make([]byte, 20)}) + policyDigest: make([]byte, 20), + noDA: true}) } func (s *importableObjectKeySealerSuite) TestCreateSealedObjectWithDifferentPolicy(c *C) { @@ -215,5 +242,14 @@ func (s *importableObjectKeySealerSuite) TestCreateSealedObjectWithDifferentPoli s.testCreateSealedObject(c, &testCreateSealedObjectData{ data: []byte("foo"), nameAlg: tpm2.HashAlgorithmSHA256, - policyDigest: trial.GetDigest()}) + policyDigest: trial.GetDigest(), + noDA: true}) +} + +func (s *importableObjectKeySealerSuite) TestCreateSealedObjectWithDA(c *C) { + s.testCreateSealedObject(c, &testCreateSealedObjectData{ + data: []byte("foo"), + nameAlg: tpm2.HashAlgorithmSHA256, + policyDigest: make([]byte, 32), + noDA: false}) } diff --git a/tpm2/keydata.go b/tpm2/keydata.go index 32d8c5ed..d056db0c 100644 --- a/tpm2/keydata.go +++ b/tpm2/keydata.go @@ -28,6 +28,7 @@ import ( "github.com/canonical/go-tpm2" "github.com/canonical/go-tpm2/mu" + "github.com/canonical/go-tpm2/objectutil" "golang.org/x/xerrors" @@ -162,14 +163,33 @@ func (k *sealedKeyDataBase) load(tpm *tpm2.TPMContext, parent tpm2.ResourceConte // validateData performs correctness checks on this object. func (k *sealedKeyDataBase) validateData(tpm *tpm2.TPMContext, role string) (*tpm2.NVPublic, error) { - sealedKeyTemplate := makeImportableSealedKeyTemplate() + optsInternal := []objectutil.PublicTemplateOption{ + objectutil.WithUserAuthMode(objectutil.RequirePolicy), + objectutil.WithProtectionGroupMode(objectutil.NonDuplicable), + objectutil.WithDuplicationMode(objectutil.FixedParent), + } + optsExternal := []objectutil.PublicTemplateOption{ + objectutil.WithUserAuthMode(objectutil.RequirePolicy), + objectutil.WithProtectionGroupMode(objectutil.NonDuplicable), + objectutil.WithDuplicationMode(objectutil.DuplicationRoot), + } + if k.data.Policy().RequireUserAuth() { + optsInternal = append(optsInternal, objectutil.WithDictionaryAttackProtection()) + optsExternal = append(optsExternal, objectutil.WithDictionaryAttackProtection()) + } else { + optsInternal = append(optsInternal, objectutil.WithoutDictionaryAttackProtection()) + optsExternal = append(optsExternal, objectutil.WithoutDictionaryAttackProtection()) + } + internalSealedKeyTemplate := objectutil.NewSealedObjectTemplate(optsInternal...) + externalSealedKeyTemplate := objectutil.NewSealedObjectTemplate(optsExternal...) // Perform some initial checks on the sealed data object's public area to // make sure it's a sealed data object. - if k.data.Public().Type != sealedKeyTemplate.Type { + if k.data.Public().Type != internalSealedKeyTemplate.Type { return nil, keyDataError{errors.New("sealed key object has the wrong type")} } - if k.data.Public().Attrs&^(tpm2.AttrFixedTPM|tpm2.AttrFixedParent) != sealedKeyTemplate.Attrs { + attrs := k.data.Public().Attrs + if attrs != internalSealedKeyTemplate.Attrs && attrs != externalSealedKeyTemplate.Attrs { return nil, keyDataError{errors.New("sealed key object has the wrong attributes")} } diff --git a/tpm2/platform_test.go b/tpm2/platform_test.go index 0384e686..7ecfd865 100644 --- a/tpm2/platform_test.go +++ b/tpm2/platform_test.go @@ -255,6 +255,17 @@ func (s *platformSuite) TestRecoverKeysNoPCRPolicyCounter(c *C) { PCRPolicyCounterHandle: tpm2.HandleNull}) } +func (s *platformSuite) TestRecoverKeysTPMLockout(c *C) { + // Put the TPM in DA lockout mode. Keys without user auth should still be recoverable. + c.Check(s.TPM().DictionaryAttackParameters(s.TPM().LockoutHandleContext(), 0, 7200, 86400, nil), IsNil) + + s.testRecoverKeys(c, &ProtectKeyParams{ + PCRProfile: tpm2test.NewPCRProfileFromCurrentValues(tpm2.HashAlgorithmSHA256, []int{7}), + PCRPolicyCounterHandle: s.NextAvailableHandle(c, 0x0181fff0), + Role: "", + }) +} + func (s *platformSuite) testRecoverKeysNoValidSRK(c *C, prepareSrk func()) { params := &ProtectKeyParams{ PCRProfile: tpm2test.NewPCRProfileFromCurrentValues(tpm2.HashAlgorithmSHA256, []int{7}), @@ -409,17 +420,6 @@ func (s *platformSuite) testRecoverKeysUnsealErrorHandling(c *C, prepare func(*s return err } -func (s *platformSuite) TestRecoverKeysUnsealErrorHandlingLockout(c *C) { - err := s.testRecoverKeysUnsealErrorHandling(c, func(_ *secboot.KeyData, _ secboot.PrimaryKey) { - // Put the TPM in DA lockout mode - c.Check(s.TPM().DictionaryAttackParameters(s.TPM().LockoutHandleContext(), 0, 7200, 86400, nil), IsNil) - }) - c.Assert(err, testutil.ConvertibleTo, &secboot.PlatformHandlerError{}) - c.Check(err.(*secboot.PlatformHandlerError).Type, Equals, secboot.PlatformHandlerErrorUnavailable) - c.Check(err, testutil.ErrorIs, ErrTPMLockout) - c.Check(err, ErrorMatches, "the TPM is in DA lockout mode") -} - func (s *platformSuite) TestRecoverKeysUnsealErrorHandlingInvalidPCRProfile(c *C) { err := s.testRecoverKeysUnsealErrorHandling(c, func(_ *secboot.KeyData, _ secboot.PrimaryKey) { _, err := s.TPM().PCREvent(s.TPM().PCRHandleContext(23), []byte("foo"), nil) @@ -474,11 +474,25 @@ func (s *platformSuite) TestRecoverKeysUnsealErrorHandlingProvisioningError(c *C c.Check(err, ErrorMatches, "the TPM is not correctly provisioned") } -func (s *platformSuite) TestRecoverKeysWithAuthKey(c *C) { +type daKeySealer struct { + orig KeySealer +} +func (s *daKeySealer) CreateSealedObject(data []byte, nameAlg tpm2.HashAlgorithmId, policy tpm2.Digest, noDA bool) (tpm2.Private, *tpm2.Public, tpm2.EncryptedSecret, error) { + return s.orig.CreateSealedObject(data, nameAlg, policy, false) +} + +func (s *platformSuite) TestRecoverKeysWithAuthKey(c *C) { // Need to mock newKeyDataPolicy to force require an auth value when using NewTPMProtectedKey so that we don't // have to use the passphrase APIs. - restore := MockNewKeyDataPolicy(func(alg tpm2.HashAlgorithmId, key *tpm2.Public, role string, pcrPolicyCounterPub *tpm2.NVPublic, requireAuthValue bool) (KeyDataPolicy, tpm2.Digest, error) { + makeSealedKeyDataOrig := MakeSealedKeyData + restore := MockMakeSealedKeyData(func(tpm *tpm2.TPMContext, params *MakeSealedKeyDataParams, sealer KeySealer, constructor KeyDataConstructor, session tpm2.SessionContext) (*secboot.KeyData, secboot.PrimaryKey, secboot.DiskUnlockKey, error) { + sealer = &daKeySealer{sealer} + return makeSealedKeyDataOrig(tpm, params, sealer, constructor, session) + }) + defer restore() + + restore = MockNewKeyDataPolicy(func(alg tpm2.HashAlgorithmId, key *tpm2.Public, role string, pcrPolicyCounterPub *tpm2.NVPublic, requireAuthValue bool) (KeyDataPolicy, tpm2.Digest, error) { index := tpm2.HandleNull var indexName tpm2.Name if pcrPolicyCounterPub != nil { @@ -552,10 +566,16 @@ func (s *platformSuite) TestRecoverKeysWithAuthKey(c *C) { } func (s *platformSuite) TestRecoverKeysWithIncorrectAuthKey(c *C) { - // Need to mock newKeyDataPolicy to force require an auth value when using NewTPMProtectedKey so that we don't // have to use the passphrase APIs. - restore := MockNewKeyDataPolicy(func(alg tpm2.HashAlgorithmId, key *tpm2.Public, role string, pcrPolicyCounterPub *tpm2.NVPublic, requireAuthValue bool) (KeyDataPolicy, tpm2.Digest, error) { + makeSealedKeyDataOrig := MakeSealedKeyData + restore := MockMakeSealedKeyData(func(tpm *tpm2.TPMContext, params *MakeSealedKeyDataParams, sealer KeySealer, constructor KeyDataConstructor, session tpm2.SessionContext) (*secboot.KeyData, secboot.PrimaryKey, secboot.DiskUnlockKey, error) { + sealer = &daKeySealer{sealer} + return makeSealedKeyDataOrig(tpm, params, sealer, constructor, session) + }) + defer restore() + + restore = MockNewKeyDataPolicy(func(alg tpm2.HashAlgorithmId, key *tpm2.Public, role string, pcrPolicyCounterPub *tpm2.NVPublic, requireAuthValue bool) (KeyDataPolicy, tpm2.Digest, error) { index := tpm2.HandleNull var indexName tpm2.Name if pcrPolicyCounterPub != nil { @@ -624,10 +644,16 @@ func (s *platformSuite) TestRecoverKeysWithIncorrectAuthKey(c *C) { } func (s *platformSuite) TestChangeAuthKeyWithIncorrectAuthKey(c *C) { - // Need to mock newKeyDataPolicy to force require an auth value when using NewTPMProtectedKey so that we don't // have to use the passphrase APIs. - restore := MockNewKeyDataPolicy(func(alg tpm2.HashAlgorithmId, key *tpm2.Public, role string, pcrPolicyCounterPub *tpm2.NVPublic, requireAuthValue bool) (KeyDataPolicy, tpm2.Digest, error) { + makeSealedKeyDataOrig := MakeSealedKeyData + restore := MockMakeSealedKeyData(func(tpm *tpm2.TPMContext, params *MakeSealedKeyDataParams, sealer KeySealer, constructor KeyDataConstructor, session tpm2.SessionContext) (*secboot.KeyData, secboot.PrimaryKey, secboot.DiskUnlockKey, error) { + sealer = &daKeySealer{sealer} + return makeSealedKeyDataOrig(tpm, params, sealer, constructor, session) + }) + defer restore() + + restore = MockNewKeyDataPolicy(func(alg tpm2.HashAlgorithmId, key *tpm2.Public, role string, pcrPolicyCounterPub *tpm2.NVPublic, requireAuthValue bool) (KeyDataPolicy, tpm2.Digest, error) { index := tpm2.HandleNull var indexName tpm2.Name if pcrPolicyCounterPub != nil { @@ -693,3 +719,73 @@ func (s *platformSuite) TestChangeAuthKeyWithIncorrectAuthKey(c *C) { c.Check(err, ErrorMatches, "TPM returned an error for session 1 whilst executing command TPM_CC_ObjectChangeAuth: "+ "TPM_RC_AUTH_FAIL \\(the authorization HMAC check failed and DA counter incremented\\)") } + +func (s *platformSuite) TestRecoverKeysWithAuthKeyTPMLockout(c *C) { + // Put the TPM in DA lockout mode + c.Check(s.TPM().DictionaryAttackParameters(s.TPM().LockoutHandleContext(), 0, 7200, 86400, nil), IsNil) + + // Need to mock newKeyDataPolicy to force require an auth value when using NewTPMProtectedKey so that we don't + // have to use the passphrase APIs. + makeSealedKeyDataOrig := MakeSealedKeyData + restore := MockMakeSealedKeyData(func(tpm *tpm2.TPMContext, params *MakeSealedKeyDataParams, sealer KeySealer, constructor KeyDataConstructor, session tpm2.SessionContext) (*secboot.KeyData, secboot.PrimaryKey, secboot.DiskUnlockKey, error) { + sealer = &daKeySealer{sealer} + return makeSealedKeyDataOrig(tpm, params, sealer, constructor, session) + }) + defer restore() + + restore = MockNewKeyDataPolicy(func(alg tpm2.HashAlgorithmId, key *tpm2.Public, role string, pcrPolicyCounterPub *tpm2.NVPublic, requireAuthValue bool) (KeyDataPolicy, tpm2.Digest, error) { + index := tpm2.HandleNull + var indexName tpm2.Name + if pcrPolicyCounterPub != nil { + index = pcrPolicyCounterPub.Index + indexName = pcrPolicyCounterPub.Name() + } + + pcrPolicyRef := ComputeV3PcrPolicyRef(key.NameAlg, []byte(role), indexName) + + trial := util.ComputeAuthPolicy(alg) + trial.PolicyAuthorize(pcrPolicyRef, key.Name()) + trial.PolicyAuthValue() + + mockPolicyData := &KeyDataPolicy_v3{ + StaticData: &StaticPolicyData_v3{ + AuthPublicKey: key, + PCRPolicyRef: pcrPolicyRef, + PCRPolicyCounterHandle: index, + RequireAuthValue: true}, + PCRData: &PcrPolicyData_v3{ + AuthorizedPolicySignature: &tpm2.Signature{SigAlg: tpm2.SigSchemeAlgNull}, + }} + + mockPolicyDigest := trial.GetDigest() + + return mockPolicyData, mockPolicyDigest, nil + }) + defer restore() + + params := &ProtectKeyParams{ + PCRProfile: tpm2test.NewPCRProfileFromCurrentValues(tpm2.HashAlgorithmSHA256, []int{7}), + PCRPolicyCounterHandle: s.NextAvailableHandle(c, 0x0181fff0), + Role: "", + } + + k, _, _, err := NewTPMProtectedKey(s.TPM(), params) + c.Assert(err, IsNil) + + var platformHandle json.RawMessage + c.Check(k.UnmarshalPlatformHandle(&platformHandle), IsNil) + + platformKeyData := &secboot.PlatformKeyData{ + Generation: k.Generation(), + EncodedHandle: platformHandle, + KDFAlg: crypto.Hash(crypto.SHA256), + AuthMode: k.AuthMode(), + } + + var handler PlatformKeyDataHandler + _, err = handler.RecoverKeysWithAuthKey(platformKeyData, s.lastEncryptedPayload, []byte{}) + c.Assert(err, testutil.ConvertibleTo, &secboot.PlatformHandlerError{}) + c.Check(err.(*secboot.PlatformHandlerError).Type, Equals, secboot.PlatformHandlerErrorUnavailable) + c.Check(err, testutil.ErrorIs, ErrTPMLockout) + c.Check(err, ErrorMatches, `the TPM is in DA lockout mode`) +} diff --git a/tpm2/policy.go b/tpm2/policy.go index 756a5be3..67a5156b 100644 --- a/tpm2/policy.go +++ b/tpm2/policy.go @@ -124,6 +124,10 @@ type keyDataPolicy interface { // the public area of the counter associated with this policy. PCRPolicyCounterContext(tpm *tpm2.TPMContext, pub *tpm2.NVPublic) (pcrPolicyCounterContext, error) + // RequireUserAuth returns true if the object has an authorization value that is needed + // from the user. + RequireUserAuth() bool + // ValidateAuthKey verifies that the supplied key is associated with this // keyDataPolicy. ValidateAuthKey(key secboot.PrimaryKey) error diff --git a/tpm2/policy_test.go b/tpm2/policy_test.go index ea59afc7..faabfc4f 100644 --- a/tpm2/policy_test.go +++ b/tpm2/policy_test.go @@ -208,8 +208,10 @@ func (s *policySuiteNoTPM) TestNewPolicyOrTreeTooManyDigests(c *C) { type testNewKeyDataPolicyData struct { alg tpm2.HashAlgorithmId key string + role string pcrPolicyCounterPub *tpm2.NVPublic pcrPolicySequence uint64 + requireAuthValue bool expected tpm2.Digest } @@ -228,13 +230,15 @@ func (s *policySuiteNoTPM) testNewKeyDataPolicy(c *C, data *testNewKeyDataPolicy pcrPolicyCounterHandle = data.pcrPolicyCounterPub.Index } - policy, digest, err := NewKeyDataPolicy(data.alg, authKey, "", data.pcrPolicyCounterPub, false) + policy, digest, err := NewKeyDataPolicy(data.alg, authKey, data.role, data.pcrPolicyCounterPub, data.requireAuthValue) c.Assert(err, IsNil) c.Assert(policy, testutil.ConvertibleTo, &KeyDataPolicy_v3{}) c.Check(policy.(*KeyDataPolicy_v3).StaticData.AuthPublicKey, DeepEquals, authKey) c.Check(policy.PCRPolicyCounterHandle(), Equals, pcrPolicyCounterHandle) c.Check(policy.PCRPolicySequence(), Equals, data.pcrPolicySequence) + c.Check(policy.RequireUserAuth(), Equals, data.requireAuthValue) + c.Logf("%x", digest) c.Check(digest, DeepEquals, data.expected) } @@ -300,6 +304,42 @@ xtjPyepMPNg3K7iPmPopFLA5Ap8RjR1Eu9B8LllUHTqYHJY6YQ3o+CP5TQ== expected: testutil.DecodeHexString(c, "aaf8226b1df9aefc9d03533b58abaf514b0f4ab6c10af0e26ef5d9db0d8aff24")}) } +func (s *policySuiteNoTPM) TestNewKeyDataPolicyDifferentRole(c *C) { + s.testNewKeyDataPolicy(c, &testNewKeyDataPolicyData{ + alg: tpm2.HashAlgorithmSHA256, + key: ` +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE49+rltJgmI3V7QqrkLBpB4V3xunW +xtjPyepMPNg3K7iPmPopFLA5Ap8RjR1Eu9B8LllUHTqYHJY6YQ3o+CP5TQ== +-----END PUBLIC KEY-----`, + role: "foo", + pcrPolicyCounterPub: &tpm2.NVPublic{ + Index: 0x0181fff0, + NameAlg: tpm2.HashAlgorithmSHA256, + Attrs: tpm2.NVTypeCounter.WithAttrs(tpm2.AttrNVPolicyWrite | tpm2.AttrNVAuthRead | tpm2.AttrNVNoDA | tpm2.AttrNVWritten), + Size: 8}, + pcrPolicySequence: 0, + expected: testutil.DecodeHexString(c, "18520b70f76191008b461386d54d6219ffb068bbd4f3b82e495a6c8eca81c8cd")}) +} + +func (s *policySuiteNoTPM) TestNewKeyDataPolicyWithUserAuth(c *C) { + s.testNewKeyDataPolicy(c, &testNewKeyDataPolicyData{ + alg: tpm2.HashAlgorithmSHA256, + key: ` +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE49+rltJgmI3V7QqrkLBpB4V3xunW +xtjPyepMPNg3K7iPmPopFLA5Ap8RjR1Eu9B8LllUHTqYHJY6YQ3o+CP5TQ== +-----END PUBLIC KEY-----`, + pcrPolicyCounterPub: &tpm2.NVPublic{ + Index: 0x0181fff0, + NameAlg: tpm2.HashAlgorithmSHA256, + Attrs: tpm2.NVTypeCounter.WithAttrs(tpm2.AttrNVPolicyWrite | tpm2.AttrNVAuthRead | tpm2.AttrNVNoDA | tpm2.AttrNVWritten), + Size: 8}, + pcrPolicySequence: 0, + requireAuthValue: true, + expected: testutil.DecodeHexString(c, "1cefd40aad2757cd72a238a8d208e8b1a31d94adf7fa88dd5333b43e006b09a6")}) +} + type testNewKeyDataPolicyLegacyData struct { alg tpm2.HashAlgorithmId key string diff --git a/tpm2/policy_v0.go b/tpm2/policy_v0.go index f49a3fcd..b1d28219 100644 --- a/tpm2/policy_v0.go +++ b/tpm2/policy_v0.go @@ -605,6 +605,10 @@ func (p *keyDataPolicy_v0) PCRPolicyCounterContext(tpm *tpm2.TPMContext, pub *tp authPolicies: p.StaticData.PCRPolicyCounterAuthPolicies}, nil } +func (p *keyDataPolicy_v0) RequireUserAuth() bool { + return true +} + func (p *keyDataPolicy_v0) ValidateAuthKey(key secboot.PrimaryKey) error { rsaKey, err := x509.ParsePKCS1PrivateKey(key) if err != nil { diff --git a/tpm2/policy_v0_test.go b/tpm2/policy_v0_test.go index 2014552e..96247131 100644 --- a/tpm2/policy_v0_test.go +++ b/tpm2/policy_v0_test.go @@ -1632,3 +1632,8 @@ func (s *policyV0SuiteNoTPM) TestValidateAuthKeyWrongKey(c *C) { c.Check(IsPolicyDataError(err), testutil.IsTrue) c.Check(err, ErrorMatches, "dynamic authorization policy signing private key doesn't match public key") } + +func (s *policyV0SuiteNoTPM) TestRequireUserAuth(c *C) { + data := &KeyDataPolicy_v0{} + c.Check(data.RequireUserAuth(), testutil.IsTrue) +} diff --git a/tpm2/policy_v1.go b/tpm2/policy_v1.go index 30a94e81..4c91bd85 100644 --- a/tpm2/policy_v1.go +++ b/tpm2/policy_v1.go @@ -318,6 +318,10 @@ func (p *keyDataPolicy_v1) PCRPolicyCounterContext(tpm *tpm2.TPMContext, pub *tp updateKey: p.StaticData.AuthPublicKey}, nil } +func (p *keyDataPolicy_v1) RequireUserAuth() bool { + return true +} + func (p *keyDataPolicy_v1) ValidateAuthKey(key secboot.PrimaryKey) error { pub, ok := p.StaticData.AuthPublicKey.Public().(*ecdsa.PublicKey) if !ok { diff --git a/tpm2/policy_v1_test.go b/tpm2/policy_v1_test.go index 1a722413..31d63f57 100644 --- a/tpm2/policy_v1_test.go +++ b/tpm2/policy_v1_test.go @@ -1431,3 +1431,8 @@ func (s *policyV1SuiteNoTPM) TestValidateAuthKeyWrongKey(c *C) { c.Check(IsPolicyDataError(err), testutil.IsTrue) c.Check(err, ErrorMatches, "dynamic authorization policy signing private key doesn't match public key") } + +func (s *policyV1SuiteNoTPM) TestRequireUserAuth(c *C) { + data := &KeyDataPolicy_v1{} + c.Check(data.RequireUserAuth(), testutil.IsTrue) +} diff --git a/tpm2/policy_v3.go b/tpm2/policy_v3.go index 69fa7252..35b5fac3 100644 --- a/tpm2/policy_v3.go +++ b/tpm2/policy_v3.go @@ -350,6 +350,10 @@ func (p *keyDataPolicy_v3) PCRPolicyCounterContext(tpm *tpm2.TPMContext, pub *tp updateKey: p.StaticData.AuthPublicKey}, nil } +func (p *keyDataPolicy_v3) RequireUserAuth() bool { + return p.StaticData.RequireAuthValue +} + func (p *keyDataPolicy_v3) ValidateAuthKey(key secboot.PrimaryKey) error { priv, err := deriveV3PolicyAuthKey(p.StaticData.AuthPublicKey.NameAlg.GetHash(), key) if err != nil { diff --git a/tpm2/policy_v3_test.go b/tpm2/policy_v3_test.go index 0fa0b8ca..de86d8a4 100644 --- a/tpm2/policy_v3_test.go +++ b/tpm2/policy_v3_test.go @@ -1510,3 +1510,15 @@ func (s *policyV3SuiteNoTPM) TestValidateAuthKeyWrongKey(c *C) { c.Check(IsPolicyDataError(err), testutil.IsTrue) c.Check(err, ErrorMatches, "dynamic authorization policy signing private key doesn't match public key") } + +func (s *policyV3SuiteNoTPM) TestRequireUserAuthTrue(c *C) { + data := &KeyDataPolicy_v3{ + StaticData: &StaticPolicyData_v3{RequireAuthValue: true}} + c.Check(data.RequireUserAuth(), testutil.IsTrue) +} + +func (s *policyV3SuiteNoTPM) TestRequireUserAuthFalse(c *C) { + data := &KeyDataPolicy_v3{ + StaticData: &StaticPolicyData_v3{RequireAuthValue: false}} + c.Check(data.RequireUserAuth(), testutil.IsFalse) +} diff --git a/tpm2/seal.go b/tpm2/seal.go index b107166d..c492901d 100644 --- a/tpm2/seal.go +++ b/tpm2/seal.go @@ -109,7 +109,7 @@ type makeSealedKeyDataParams struct { // If supplied, the session must be a HMAC session with the AttrContinueSession attribute set and is // used for authenticating the storage hierarchy in order to avoid trasmitting the cleartext authorization // value. -func makeSealedKeyData(tpm *tpm2.TPMContext, params *makeSealedKeyDataParams, sealer keySealer, constructor keyDataConstructor, session tpm2.SessionContext) (*secboot.KeyData, secboot.PrimaryKey, secboot.DiskUnlockKey, error) { +var makeSealedKeyData = func(tpm *tpm2.TPMContext, params *makeSealedKeyDataParams, sealer keySealer, constructor keyDataConstructor, session tpm2.SessionContext) (*secboot.KeyData, secboot.PrimaryKey, secboot.DiskUnlockKey, error) { // Create a primary key, if required. primaryKey := params.PrimaryKey if primaryKey == nil { @@ -160,7 +160,7 @@ func makeSealedKeyData(tpm *tpm2.TPMContext, params *makeSealedKeyDataParams, se } // Seal the symmetric key and nonce. - priv, pub, importSymSeed, err := sealer.CreateSealedObject(symKey[:], nameAlg, authPolicyDigest) + priv, pub, importSymSeed, err := sealer.CreateSealedObject(symKey[:], nameAlg, authPolicyDigest, !requireAuthValue) if err != nil { return nil, nil, nil, err } diff --git a/tpm2/seal_test.go b/tpm2/seal_test.go index a1fee850..f0af0f06 100644 --- a/tpm2/seal_test.go +++ b/tpm2/seal_test.go @@ -476,7 +476,7 @@ type mockKeySealer struct { called bool } -func (s *mockKeySealer) CreateSealedObject(data []byte, nameAlg tpm2.HashAlgorithmId, policy tpm2.Digest) (tpm2.Private, *tpm2.Public, tpm2.EncryptedSecret, error) { +func (s *mockKeySealer) CreateSealedObject(data []byte, nameAlg tpm2.HashAlgorithmId, policy tpm2.Digest, noDA bool) (tpm2.Private, *tpm2.Public, tpm2.EncryptedSecret, error) { if s.called { return nil, nil, nil, errors.New("called more than once") } @@ -484,7 +484,11 @@ func (s *mockKeySealer) CreateSealedObject(data []byte, nameAlg tpm2.HashAlgorit pub := templates.NewSealedObject(nameAlg) pub.AuthPolicy = policy - return tpm2.Private(data), pub, nil, nil + var noDAByte byte = 0 + if noDA { + noDAByte = 1 + } + return append(tpm2.Private(data), noDAByte), pub, nil, nil } type mockSessionContext struct { @@ -605,7 +609,7 @@ func (s *sealSuiteNoTPM) testMakeSealedKeyData(c *C, data *testMakeSealedKeyData primaryKey := make(secboot.PrimaryKey, 32) rand.Read(primaryKey) - params := &SealedKeyDataParams{ + params := &MakeSealedKeyDataParams{ PcrProfile: data.PCRProfile, Role: data.Role, PcrPolicyCounterHandle: data.PCRPolicyCounterHandle, @@ -638,7 +642,7 @@ func (s *sealSuiteNoTPM) testMakeSealedKeyData(c *C, data *testMakeSealedKeyData payload := make([]byte, len(s.lastKeyParams.EncryptedPayload)) - c.Assert(skd.Data().Private(), HasLen, 44) + c.Assert(skd.Data().Private(), HasLen, 45) b, err := aes.NewCipher(skd.Data().Private()[:32]) c.Assert(err, IsNil) @@ -651,7 +655,7 @@ func (s *sealSuiteNoTPM) testMakeSealedKeyData(c *C, data *testMakeSealedKeyData aead, err := cipher.NewGCM(b) c.Assert(err, IsNil) - payload, err = aead.Open(nil, skd.Data().Private()[32:], s.lastKeyParams.EncryptedPayload, aad) + payload, err = aead.Open(nil, skd.Data().Private()[32:44], s.lastKeyParams.EncryptedPayload, aad) c.Assert(err, IsNil) keys, err := unmarshalProtectedKeys(payload) diff --git a/tpm2/unseal.go b/tpm2/unseal.go index 36be975b..e143de9d 100644 --- a/tpm2/unseal.go +++ b/tpm2/unseal.go @@ -108,6 +108,7 @@ func (k *sealedKeyDataBase) loadForUnseal(tpm *tpm2.TPMContext, session tpm2.Ses // then redirect the command to an adversary supplied session for which they can calculate // the HMAC for. Whilst the host CPU can detect this tampering (because the response // HMAC will be invalid), this happens after the TPM has already provided the unsealed data. + symmetric := &tpm2.SymDef{ Algorithm: tpm2.SymAlgorithmAES, KeyBits: &tpm2.SymKeyBitsU{Sym: 128}, @@ -153,16 +154,6 @@ func (k *sealedKeyDataBase) loadForUnseal(tpm *tpm2.TPMContext, session tpm2.Ses // storage primary key needs to be created, in order to avoid transmitting the cleartext // authorization value. func (k *sealedKeyDataBase) unsealDataFromTPM(tpm *tpm2.TPMContext, authValue []byte, hmacSession tpm2.SessionContext) (data []byte, err error) { - // Check if the TPM is in lockout mode - props, err := tpm.GetCapabilityTPMProperties(tpm2.PropertyPermanent, 1) - if err != nil { - return nil, xerrors.Errorf("cannot fetch properties from TPM: %w", err) - } - - if tpm2.PermanentAttributes(props[0].Value)&tpm2.AttrInLockout > 0 { - return nil, ErrTPMLockout - } - keyObject, policySession, err := k.loadForUnseal(tpm, hmacSession) if err != nil { return nil, err @@ -189,6 +180,8 @@ func (k *sealedKeyDataBase) unsealDataFromTPM(tpm *tpm2.TPMContext, authValue [] // Unseal data, err = tpm.Unseal(keyObject, policySession) switch { + case tpm2.IsTPMWarning(err, tpm2.WarningLockout, tpm2.CommandUnseal): + return nil, ErrTPMLockout case tpm2.IsTPMSessionError(err, tpm2.ErrorPolicyFail, tpm2.CommandUnseal, 1): return nil, InvalidKeyDataError{"the authorization policy check failed during unsealing"} case err != nil: From f83d97b58fd341c6e1d01561f8d3583436e2fe80 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Fri, 6 Dec 2024 16:25:08 +0000 Subject: [PATCH 2/2] tpm2: Add a note about the use of the noDA argument --- tpm2/seal.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tpm2/seal.go b/tpm2/seal.go index c492901d..6cfa87ce 100644 --- a/tpm2/seal.go +++ b/tpm2/seal.go @@ -159,7 +159,9 @@ var makeSealedKeyData = func(tpm *tpm2.TPMContext, params *makeSealedKeyDataPara return nil, nil, nil, xerrors.Errorf("cannot create symmetric key: %w", err) } - // Seal the symmetric key and nonce. + // Seal the symmetric key and nonce. The final boolean argument is set to true in order + // to disable dictionary attack protection (ie, adding the noDA attribute). We want this + // when no user auth value is required. priv, pub, importSymSeed, err := sealer.CreateSealedObject(symKey[:], nameAlg, authPolicyDigest, !requireAuthValue) if err != nil { return nil, nil, nil, err