From 416fbe20ec41b7ed89dacc193e6bf722273687ec Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Mon, 8 Jul 2024 14:17:16 +0100 Subject: [PATCH] tpm2: update some doc comments This also adds an explanation of what a session is used for, for some functions that still accept a HMAC session. --- tpm2/export_test.go | 8 -------- tpm2/platform.go | 5 +++++ tpm2/policy.go | 10 ++++++++-- tpm2/policy_v1.go | 2 +- tpm2/policy_v3.go | 2 +- tpm2/provisioning.go | 29 ++++++++++++++++++++++++++--- tpm2/seal.go | 6 ++++++ tpm2/tpm.go | 20 ++++++++++++-------- tpm2/unseal.go | 38 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 97 insertions(+), 23 deletions(-) diff --git a/tpm2/export_test.go b/tpm2/export_test.go index 67fcc7f1..90c5742a 100644 --- a/tpm2/export_test.go +++ b/tpm2/export_test.go @@ -215,14 +215,6 @@ func MockSecbootNewKeyData(fn func(*secboot.KeyParams) (*secboot.KeyData, error) } } -func MockSkdbUpdatePCRProtectionPolicyImpl(fn func(*sealedKeyDataBase, *tpm2.TPMContext, secboot.AuxiliaryKey, *tpm2.NVPublic, *PCRProtectionProfile) error) (restore func()) { - orig := skdbUpdatePCRProtectionPolicyImpl - skdbUpdatePCRProtectionPolicyImpl = fn - return func() { - skdbUpdatePCRProtectionPolicyImpl = orig - } -} - func MockSecbootNewKeyDataWithPassphrase(fn func(*secboot.KeyWithPassphraseParams, string) (*secboot.KeyData, error)) (restore func()) { orig := secbootNewKeyDataWithPassphrase secbootNewKeyDataWithPassphrase = fn diff --git a/tpm2/platform.go b/tpm2/platform.go index 0d0099c2..3f324222 100644 --- a/tpm2/platform.go +++ b/tpm2/platform.go @@ -199,6 +199,11 @@ func (h *platformKeyDataHandler) ChangeAuthKey(data *secboot.PlatformKeyData, ol defer tpm.FlushContext(session) keyObject.SetAuthValue(old) + + // Use a HMAC session for authentication. This avoids sending the old value in the clear. + // It also encrypts the new value, although this only provides protection against passive + // interposers as we don't verify the key that is used to salt the session is actually a + // TPM protected key. priv, err := tpm.ObjectChangeAuth(keyObject, srk, new, tpm.HmacSession().IncludeAttrs(tpm2.AttrCommandEncrypt)) if err != nil { if tpm2.IsTPMSessionError(err, tpm2.ErrorAuthFail, tpm2.CommandObjectChangeAuth, 1) { diff --git a/tpm2/policy.go b/tpm2/policy.go index 0936764e..756a5be3 100644 --- a/tpm2/policy.go +++ b/tpm2/policy.go @@ -134,6 +134,9 @@ type keyDataPolicy interface { // // The NV index will be created with attributes that allow anyone to read the index, and an authorization // policy that permits TPM2_NV_Increment with a signed authorization policy. +// +// If hmacSession is supplied, it is used for authenticating with the storage hierarchy, in order to avoid +// transmitting the cleartext auth value, and must have the AttrContinueSession attribute set func createPcrPolicyCounterLegacy(tpm *tpm2.TPMContext, handle tpm2.Handle, updateKey *tpm2.Public, hmacSession tpm2.SessionContext) (public *tpm2.NVPublic, value uint64, err error) { nameAlg := tpm2.HashAlgorithmSHA256 @@ -198,6 +201,9 @@ func createPcrPolicyCounterLegacy(tpm *tpm2.TPMContext, handle tpm2.Handle, upda // // The NV index will be created with attributes that allow anyone to read the index, and an authorization // policy that permits TPM2_NV_Increment with a signed authorization policy. +// +// If hmacSession is supplied, it is used for authenticating with the storage hierarchy, in order to avoid +// transmitting the cleartext auth value, and must have the AttrContinueSession attribute set var ensurePcrPolicyCounter = func(tpm *tpm2.TPMContext, handle tpm2.Handle, updateKey *tpm2.Public, hmacSession tpm2.SessionContext) (public *tpm2.NVPublic, err error) { nameAlg := tpm2.HashAlgorithmSHA256 @@ -213,7 +219,7 @@ var ensurePcrPolicyCounter = func(tpm *tpm2.TPMContext, handle tpm2.Handle, upda AuthPolicy: trial.GetDigest(), Size: 8} - index, err := tpm.CreateResourceContextFromTPM(handle, hmacSession.IncludeAttrs(tpm2.AttrAudit)) + index, err := tpm.CreateResourceContextFromTPM(handle) switch { case tpm2.IsResourceUnavailableError(err, handle): // ok, need to create @@ -247,7 +253,7 @@ var ensurePcrPolicyCounter = func(tpm *tpm2.TPMContext, handle tpm2.Handle, upda } // Initialize the index - if err := tpm.NVIncrement(index, index, policySession, hmacSession.IncludeAttrs(tpm2.AttrAudit)); err != nil { + if err := tpm.NVIncrement(index, index, policySession); err != nil { return nil, err } case err != nil: diff --git a/tpm2/policy_v1.go b/tpm2/policy_v1.go index 6e33ae3c..30a94e81 100644 --- a/tpm2/policy_v1.go +++ b/tpm2/policy_v1.go @@ -172,7 +172,7 @@ func (p *keyDataPolicy_v1) SetPCRPolicyFrom(src keyDataPolicy) { p.PCRData = src.(*keyDataPolicy_v1).PCRData } -func (p *keyDataPolicy_v1) ExecutePCRPolicy(tpm *tpm2.TPMContext, policySession, hmacSession tpm2.SessionContext) error { +func (p *keyDataPolicy_v1) ExecutePCRPolicy(tpm *tpm2.TPMContext, policySession, _ tpm2.SessionContext) error { if err := p.PCRData.executePcrAssertions(tpm, policySession); err != nil { return xerrors.Errorf("cannot execute PCR assertions: %w", err) } diff --git a/tpm2/policy_v3.go b/tpm2/policy_v3.go index 20565fa0..69fa7252 100644 --- a/tpm2/policy_v3.go +++ b/tpm2/policy_v3.go @@ -205,7 +205,7 @@ func (p *keyDataPolicy_v3) SetPCRPolicyFrom(src keyDataPolicy) { p.PCRData = src.(*keyDataPolicy_v3).PCRData } -func (p *keyDataPolicy_v3) ExecutePCRPolicy(tpm *tpm2.TPMContext, policySession, hmacSession tpm2.SessionContext) error { +func (p *keyDataPolicy_v3) ExecutePCRPolicy(tpm *tpm2.TPMContext, policySession, _ tpm2.SessionContext) error { if err := p.PCRData.executePcrAssertions(tpm, policySession); err != nil { return xerrors.Errorf("cannot execute PCR assertions: %w", err) } diff --git a/tpm2/provisioning.go b/tpm2/provisioning.go index f820ab89..f3e41908 100644 --- a/tpm2/provisioning.go +++ b/tpm2/provisioning.go @@ -68,6 +68,10 @@ const ( ProvisionModeClear ) +// provisionPrimaryKey provisions a primary key in the specified hierarchy at the specified persistent +// handle. If session is supplied, it is expected to be a HMAC session with the AttrContinueSession +// attribute set, and is used for authenticating with the relevant hierarchies to avoid sending the +// authorization value in the clear. func provisionPrimaryKey(tpm *tpm2.TPMContext, hierarchy tpm2.ResourceContext, template *tpm2.Public, handle tpm2.Handle, session tpm2.SessionContext) (tpm2.ResourceContext, error) { obj, err := tpm.CreateResourceContextFromTPM(handle) switch { @@ -97,6 +101,11 @@ func provisionPrimaryKey(tpm *tpm2.TPMContext, hierarchy tpm2.ResourceContext, t return obj, nil } +// selectSrkTemplate chooses a template to use for the storage primary key. Either the default +// template will be returned or a custom one stored in a decidcated NV index. The supplied +// HMAC session is used for authenticating with the storage hierarchy and is used to avoid sending +// the authorization value in the clear. +// XXX: The NV index should be created with the TPMA_NV_AUTHREAD attribute to avoid this entirely. func selectSrkTemplate(tpm *tpm2.TPMContext, session tpm2.SessionContext) *tpm2.Public { nv, err := tpm.CreateResourceContextFromTPM(srkTemplateHandle) if err != nil { @@ -125,10 +134,17 @@ func selectSrkTemplate(tpm *tpm2.TPMContext, session tpm2.SessionContext) *tpm2. return tmpl } +// provisionStoragePrimaryKey provisions a storage primary key at the well known persistent +// handle. If session is supplied, it is expected to be a HMAC session with the AttrContinueSession +// attribute set, and is used for authenticating with the relevant hierarchies to avoid sending +// authorization values in the clear. func provisionStoragePrimaryKey(tpm *tpm2.TPMContext, session tpm2.SessionContext) (tpm2.ResourceContext, error) { return provisionPrimaryKey(tpm, tpm.OwnerHandleContext(), selectSrkTemplate(tpm, session), tcg.SRKHandle, session) } +// storeSrkTemplate stores the supplied template at a well known handle. If session is supplied, +// it must be a HMAC session and is used for authenticating with the storage hierarchy to avoid sending +// authorization values in the clear. func storeSrkTemplate(tpm *tpm2.TPMContext, template *tpm2.Public, session tpm2.SessionContext) error { tmplB, err := mu.MarshalToBytes(template) if err != nil { @@ -156,6 +172,9 @@ func storeSrkTemplate(tpm *tpm2.TPMContext, template *tpm2.Public, session tpm2. return nil } +// removeStoredSrkTemplate removes the SRK template stored at the well known handle, if there +// is one. If a session is supplied, it must be a HMAC session and is used for authenticating +// with the storage hierarchy to avoid sending the authorization value in the clear. func removeStoredSrkTemplate(tpm *tpm2.TPMContext, session tpm2.SessionContext) error { nv, err := tpm.CreateResourceContextFromTPM(srkTemplateHandle) switch { @@ -189,6 +208,7 @@ func (t *Connection) ensureProvisionedInternal(mode ProvisionMode, newLockoutAut return ErrTPMClearRequiresPPI } + // Use HMAC session to authenticate with lockout hierarchy. if err := t.Clear(t.LockoutHandleContext(), session); err != nil { switch { case isAuthFailError(err, tpm2.CommandClear, 1): @@ -275,7 +295,8 @@ func (t *Connection) ensureProvisionedInternal(mode ProvisionMode, newLockoutAut // Perform actions that require the lockout hierarchy authorization. - // Set the DA parameters. + // Set the DA parameters. Pass the HMAC session here so we don't supply the cleartext auth + // value for the lockout hierarchy. if err := t.DictionaryAttackParameters(t.LockoutHandleContext(), maxTries, recoveryTime, lockoutRecovery, session); err != nil { switch { case isAuthFailError(err, tpm2.CommandDictionaryAttackParameters, 1): @@ -286,13 +307,15 @@ func (t *Connection) ensureProvisionedInternal(mode ProvisionMode, newLockoutAut return xerrors.Errorf("cannot configure dictionary attack parameters: %w", err) } - // Disable owner clear + // Disable owner clear. Pass the HMAC session here so we don't supply the cleartext auth + // value for the lockout hierarchy. if err := t.ClearControl(t.LockoutHandleContext(), true, session); err != nil { // Lockout auth failure or lockout mode would have been caught by DictionaryAttackParameters return xerrors.Errorf("cannot disable owner clear: %w", err) } - // Set the lockout hierarchy authorization. + // Set the lockout hierarchy authorization. Use command parameter encryption here for the new value. + // Note that this only offers protections against passive interposers. if err := t.HierarchyChangeAuth(t.LockoutHandleContext(), newLockoutAuth, session.IncludeAttrs(tpm2.AttrCommandEncrypt)); err != nil { return xerrors.Errorf("cannot set the lockout hierarchy authorization value: %w", err) } diff --git a/tpm2/seal.go b/tpm2/seal.go index cb5a4f06..9419df7c 100644 --- a/tpm2/seal.go +++ b/tpm2/seal.go @@ -103,6 +103,12 @@ type makeSealedKeyDataParams struct { AuthMode secboot.AuthMode } +// makeSealedKeyData makes a sealed key data using the supplied parameters, keySealer implementation, +// and keyDataConstructor implementation. +// +// 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) { // Create a primary key, if required. primaryKey := params.PrimaryKey diff --git a/tpm2/tpm.go b/tpm2/tpm.go index c5e65e7d..a2238935 100644 --- a/tpm2/tpm.go +++ b/tpm2/tpm.go @@ -57,14 +57,18 @@ func (t *Connection) LockoutAuthSet() bool { return tpm2.PermanentAttributes(value)&tpm2.AttrLockoutAuthSet > 0 } -// HmacSession returns a HMAC session instance which was created in order to conduct a proof-of-ownership check of the private part -// of the endorsement key on the TPM. It is retained in order to reduce the number of sessions that need to be created during unseal -// operations, and is created with a symmetric algorithm so that it is suitable for parameter encryption. -// If the connection was created with SecureConnectToDefaultTPM, the session is salted with a value protected by the public part -// of the key associated with the verified endorsement key certificate. The session key can only be retrieved by and used on the TPM -// for which the endorsement certificate was issued. If the connection was created with ConnectToDefaultTPM, the session may be -// salted with a value protected by the public part of the endorsement key if one exists or one is able to be created, but as the key -// is not associated with a verified credential, there is no guarantee that only the TPM is able to retrieve the session key. +// HmacSession returns a HMAC session with the AttrContinueSession attribute +// set. If an endorsement key exists, it is also salted with this and configured +// with parameter encryption. Note that this relies on reading the public area +// from the TPM and there is no validation of the endorsement key against the +// supplied manufacturer certificate, so this is vulnerable to active interposer +// type attacks where an adversary could provide a public area for a non-TPM protected +// key to us, whilst making it look like a TPM protected key, in order to perform MITM +// attacks. If used for parameter encryption, this only provides protection against +// passive interposer attacks. Other types of attacks are outside of the scope of this +// package due to limitations in the way that TPM2_Unseal works, and the fact that the +// platform firmware doesn't integrity protect commands that are critical to measured +// boot such as PCR extends. func (t *Connection) HmacSession() tpm2.SessionContext { if t.hmacSession == nil { return nil diff --git a/tpm2/unseal.go b/tpm2/unseal.go index 66065476..36be975b 100644 --- a/tpm2/unseal.go +++ b/tpm2/unseal.go @@ -47,6 +47,11 @@ const ( // // If a transient SRK is created, it is flushed from the TPM before this function // returns. +// +// If session is supplied, it must be a HMAC session with the AttrContinueSession attribute +// set, used for authenticating the use of the storage hirearchy if a transient strorage +// primary key needs to be created, in order to avoid transmitting the cleartext authorzation +// value. func (k *sealedKeyDataBase) loadForUnseal(tpm *tpm2.TPMContext, session tpm2.SessionContext) (keyObject tpm2.ResourceContext, policySession tpm2.SessionContext, err error) { for try := tryPersistentSRK; try <= tryMax; try++ { var srk tpm2.ResourceContext @@ -98,6 +103,11 @@ func (k *sealedKeyDataBase) loadForUnseal(tpm *tpm2.TPMContext, session tpm2.Ses }() // Begin policy session with parameter encryption support and salted with the SRK. + // Note that this only provides protection against passive interposers, as active + // interposers can modify session attributes supplied in the TPM2_Unseal command and + // 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}, @@ -114,6 +124,34 @@ func (k *sealedKeyDataBase) loadForUnseal(tpm *tpm2.TPMContext, session tpm2.Ses return nil, nil, err } +// unsealDataFromTPM unseals the data from this sealed object. +// +// Unsealing uses a policy session that is salted with the SRK that is used to protect the key +// and uses the same session for response encryption. Note that this only provides protection +// against passive interposer attacks. There is no effort to protection against active +// interposer attacks (where an advesary can modify communications) for a few reasons: +// - An adversary can remove the encrypt attribute from the session and redirect the command +// to a session for which they can compute the HMAC. The host CPU will detect this because the +// response HMAC will be invalid, but it's too late at this point. Using TPM2_EncryptDecrypt2 +// with both command and response parameter encryption could mitigate this attack. +// - An adversary can modify and spoof PCR extends earlier in the boot chain, unless all +// critical commands like these are integrity protected with a session that is salted with a +// verified TPM key. This is not the case with any firmware today. +// - Protecting against an active interposer would require the use of a verified key. There is +// one - the endorsement key, but what happens if we don't have access to and can't download +// intermediate certs for verification? Does the boot fail? How is this communicated securely +// to the TPM? +// - The use of go's crypto/x509 for certificate verification doesn't permit disabling validity +// period valdidation. Hardware certificates will generally expire during the lifetime of a +// device and ignoring validity periods is generally quite normal here (eg, such as in UEFI +// secure boot). +// - An adversary could go one step further and just unsolder the discrete TPM and place +// it into a malcious platform that spoofs a specific host. +// +// If a session is supplied, it should be a HMAC session with the AttrContinueSession +// attribute set, used for authenticating use of the storage hierarchy if a transient +// 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)