Skip to content

[3/3] Keydata v3 TPM platform changes - #271

Merged
chrisccoulson merged 36 commits into
canonical:masterfrom
sespiros:keydata-v3-platform-tpm
Mar 11, 2024
Merged

[3/3] Keydata v3 TPM platform changes#271
chrisccoulson merged 36 commits into
canonical:masterfrom
sespiros:keydata-v3-platform-tpm

Conversation

@sespiros

Copy link
Copy Markdown

Original commit: chrisccoulson@87d9aa3

I will split the commit above in 3 separate cascading PRs and add tests for it:

  1. [1/3] Keydata v3 platform API changes #265
  2. [2/3] Keydata v3 scope changes #270
  3. This one

This PR is rebased on top of 2. It adds the changes to the TPM platform that use the new v3 cross-platform API + fixes existing tests and adds a few more.

@sespiros
sespiros force-pushed the keydata-v3-platform-tpm branch from 5c8317a to ef66445 Compare November 21, 2023 15:06
@sespiros
sespiros force-pushed the keydata-v3-platform-tpm branch 2 times, most recently from 934b170 to e30fd0c Compare November 23, 2023 10:31
@sespiros
sespiros force-pushed the keydata-v3-platform-tpm branch from e30fd0c to 83ad83a Compare February 7, 2024 19:26
@sespiros
sespiros force-pushed the keydata-v3-platform-tpm branch from 83ad83a to bc9ee04 Compare February 28, 2024 00:47
Spyros Seimenis added 22 commits February 28, 2024 15:16
keyDataVersion should be exported so that platform implementations
can refer to it.
under the new design we change how we compute the authorization
policies of the PCR policy counter:
- the new role field is used to limit the scope of signed policy
  by including it in the policy calculation hash.
- we change how we use the revocation NV index which is now going to
  be incremented on certain authorization policy updates (and this
  is reflected by the new policies around CommandNVIncrement)
Decrypt for v3 keyData objects will perform the AES-GCM
decryption operation. This is not implemented for versions
of keyData objects less than v3.
new v3 keys are decrypted using AES-GCM. The auth value
derivation operation has been moved to the platform agnostic
API.

Legacy keys are handled separately in tpm2/keydata_legacy.go
and RecoverKeys() will now marshal them using ASN1.
New v3 keys will use a PCR policy counter (NV index) with the
TPMA_NV_POLICYREAD attribute set. This policy restricts read access
to the index value only if the policy is satisfied.

createPcrPolicyCounter was split into 2 functions:
- ensurePcrPolicyCounter for v3
- createPcrPolicyCounterLegacy for the rest
under the new v3 design the role field is taken into
account when doing auth policy validation.

For legacy keydata versions ValidateData will return
error if the role argument is non-empty.
pass the role field when validating data
For keydata v3, role needs to be included in data validation
since it's part of the PCR policy.

For legacy keydata, empty role is passed.
updatePCRProtectionPolicyImpl was renamed to
updatePCRProtectionPolicyNoValidate. It also now receives an extra
argument that instructs it to increment the counter or not. The new
counter value is passed as the policySequence argument to
UpdatePCRPolicy.
The following new APIs were introduced:

- NewTPMProtectedKey (was ProtectKeyWithTPM)
- NewTPMPassphraseProtectedKey
- NewExternalTPMProtectedKey (was ProtectKeyWithExternalStorageKey)

The following APIs were removed:

- ProtectKeyWithTPM
- ProtectKeysWithTPM
- ProtectKeyWithExternalStorageKey

In v3 several values will be now derived from a single primary key, such
as the disk unlock key and the passphrase-derived key that encrypts the payload.

makeKeyData(), makeKeyDataPolicy() and makeKeyDataWithPolicy() were
merged in makeSealedKeyData(). The new encrypted payload is encrypted
using an AEAD scheme (AES-GCM) and all keys .
this is now a property of the keydata object.
@sespiros
sespiros force-pushed the keydata-v3-platform-tpm branch from bc9ee04 to 4facbee Compare February 28, 2024 13:16
@pedronis
pedronis self-requested a review February 28, 2024 13:18

@pedronis pedronis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did a first pass, seal.go is nicely streamlined

Comment thread keydata.go Outdated
sha256Oid = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}
sha384Oid = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 2}
sha512Oid = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3}
KeyDataGeneration = 2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs a doc comment now, also as style is probably better to use a separate var stanza for exported vs not exported vars

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a comment to this

Comment thread keydata.go

// MarshalKeys serializes the supplied disk unlock key and auxiliary key in
// to a format that is ready to be encrypted by a platform's secure device.
func MarshalKeys(key DiskUnlockKey, auxKey PrimaryKey) []byte {

@pedronis pedronis Feb 28, 2024

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

snapd is calling this to interact with fde hooks that support only V1 keys? I don't remember, do we still have a way to make v1 keys if needed (non-TPM ones to be precise)?

Comment thread tpm2/update_legacy.go
Comment on lines +163 to +166
if isKeyDataError(err) {
return InvalidKeyDataError{fmt.Sprintf("%v (%d)", err.Error(), i+1)}
}
return xerrors.Errorf("cannot validate related key data: %w", err)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this error path doesn't seem tested

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code was copied from updateKeyPCRProtectionPoliciesCommon where it was also not tested. It's probably worth adding a case for this in a follow-up.

Comment thread tpm2/update.go Outdated
// PCR policy computed from the supplied PCRProtectionProfile. They must be persisted using
// secboot.KeyData.WriteAtomic.
func UpdateKeyDataPCRProtectionPolicy(tpm *Connection, authKey secboot.PrimaryKey, pcrProfile *PCRProtectionProfile, keys ...*secboot.KeyData) error {
func UpdateKeyDataPCRProtectionPolicy(tpm *Connection, authKey secboot.PrimaryKey, pcrProfile *PCRProtectionProfile, incrementPolicyVersion bool, keys ...*secboot.KeyData) error {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function is not tested afaict

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a test for this one now.

Comment thread tpm2/update.go Outdated
func (k *sealedKeyDataBase) updatePCRProtectionPolicyImpl(tpm *tpm2.TPMContext, key secboot.PrimaryKey,
counterPub *tpm2.NVPublic, profile *PCRProtectionProfile, session tpm2.SessionContext) error {
func (k *sealedKeyDataBase) updatePCRProtectionPolicyNoValidate(tpm *tpm2.TPMContext, key secboot.PrimaryKey,
counterPub *tpm2.NVPublic, profile *PCRProtectionProfile, incrementPolicyVersion bool,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using a bool for incrementPolicyVersion is not very readable, we usually work around that keeping the bool type but always passing a const in the invocations:

const incrementPolicyVersion = true|false
updatePCRProtectionPolicyNoValidate(..., incrementPolicyVersion, ...)

or we need to switch the param to an enum or pass an option struct, the latter is not a obvious change here though

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've cleaned this up a bit with an enumerated type. I also ended up restoring the old behaviour for the SealedKeyObject type where every update increments the counter as opposed to changing the behaviour in this PR, so this function has 3 options now.

Comment thread tpm2/update.go Outdated

func (k *sealedKeyDataBase) revokeOldPCRProtectionPoliciesImpl(tpm *tpm2.TPMContext, key secboot.PrimaryKey, session tpm2.SessionContext) error {
pcrPolicyCounterPub, err := k.validateData(tpm, session)
func (k *sealedKeyDataBase) updatePCRProtectionPolicy(tpm *tpm2.TPMContext, authKey secboot.PrimaryKey, role string, pcrProfile *PCRProtectionProfile, incrementPolicyVersion bool, session tpm2.SessionContext) error {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same issue with the bool

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a public enumerated type for the new SealedKeyData API to replace this.

Comment thread tpm2/seal.go Outdated
}
stream := cipher.NewCFBEncrypter(b, symKey[32:])
stream.XORKeyStream(payload, payload)
func (fn keyDataConstructor) NewKeyData(skd *SealedKeyData, role string, encryptedPayload []byte, kdfAlg crypto.Hash) (*secboot.KeyData, error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't find the indirection of a method on a func type more readable or idiomatic of simply using the func type alone but maybe I'm missing something

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the indirections here.

Comment thread tpm2/seal.go Outdated
}

kd, err := secbootNewKeyData(&secboot.KeyParams{
var makeKeyDataNoAuth keyDataConstructor = func(skd *SealedKeyData, role string, encryptedPayload []byte, kdfAlg crypto.Hash) (*secboot.KeyData, error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for the var def vs just func definition

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto - this is just a normal function definition now.

Comment thread tpm2/policy.go
if err == nil {
return
}
tpm.NVUndefineSpace(tpm.OwnerHandleContext(), index, hmacSession)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this bit of undo logic does not seem tested

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code was copied from createPcrPolicyCounter where this path was also untested. It's a bit tricky to test this case without mocking the calls to go-tpm2 and there's a bunch of other paths that depend on TPM errors that aren't tested.

I think it's worth addressing these in follow-ups, but by:

  • adding an API to github.com/canonical/go-tpm2/mssim to put the simulator into failure mode.
  • adding an API to github.com/canonical/go-tpm2/testutil to call the new API after a specific number of commands are executed successfully.
  • using the new APIs here to trigger failures at specific points.

Comment thread tpm2/seal_test.go
}
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explanation PR comments or pointers to relevant commit messages if they exist of why the various tests here have been dropped would be good? some are obviously tied to model auth which is gone, but for some is less obvious why they are gone

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like these are dropped because there's not an equivalent API that creates multiple keys in a single call, as we aren't sharing PCR policies anymore.

@chrisccoulson chrisccoulson left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've responded to some of your comments now (I had an in-progress review that I had to finish to release them)

Comment thread keydata.go Outdated
sha256Oid = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}
sha384Oid = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 2}
sha512Oid = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3}
KeyDataGeneration = 2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a comment to this

Comment thread tpm2/update_legacy.go
Comment on lines +163 to +166
if isKeyDataError(err) {
return InvalidKeyDataError{fmt.Sprintf("%v (%d)", err.Error(), i+1)}
}
return xerrors.Errorf("cannot validate related key data: %w", err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code was copied from updateKeyPCRProtectionPoliciesCommon where it was also not tested. It's probably worth adding a case for this in a follow-up.

Comment thread tpm2/update.go Outdated
// PCR policy computed from the supplied PCRProtectionProfile. They must be persisted using
// secboot.KeyData.WriteAtomic.
func UpdateKeyDataPCRProtectionPolicy(tpm *Connection, authKey secboot.PrimaryKey, pcrProfile *PCRProtectionProfile, keys ...*secboot.KeyData) error {
func UpdateKeyDataPCRProtectionPolicy(tpm *Connection, authKey secboot.PrimaryKey, pcrProfile *PCRProtectionProfile, incrementPolicyVersion bool, keys ...*secboot.KeyData) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a test for this one now.

Comment thread tpm2/update.go Outdated
func (k *sealedKeyDataBase) updatePCRProtectionPolicyImpl(tpm *tpm2.TPMContext, key secboot.PrimaryKey,
counterPub *tpm2.NVPublic, profile *PCRProtectionProfile, session tpm2.SessionContext) error {
func (k *sealedKeyDataBase) updatePCRProtectionPolicyNoValidate(tpm *tpm2.TPMContext, key secboot.PrimaryKey,
counterPub *tpm2.NVPublic, profile *PCRProtectionProfile, incrementPolicyVersion bool,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've cleaned this up a bit with an enumerated type. I also ended up restoring the old behaviour for the SealedKeyObject type where every update increments the counter as opposed to changing the behaviour in this PR, so this function has 3 options now.

Comment thread tpm2/update.go Outdated

func (k *sealedKeyDataBase) revokeOldPCRProtectionPoliciesImpl(tpm *tpm2.TPMContext, key secboot.PrimaryKey, session tpm2.SessionContext) error {
pcrPolicyCounterPub, err := k.validateData(tpm, session)
func (k *sealedKeyDataBase) updatePCRProtectionPolicy(tpm *tpm2.TPMContext, authKey secboot.PrimaryKey, role string, pcrProfile *PCRProtectionProfile, incrementPolicyVersion bool, session tpm2.SessionContext) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a public enumerated type for the new SealedKeyData API to replace this.

Comment thread tpm2/seal.go Outdated
}
stream := cipher.NewCFBEncrypter(b, symKey[32:])
stream.XORKeyStream(payload, payload)
func (fn keyDataConstructor) NewKeyData(skd *SealedKeyData, role string, encryptedPayload []byte, kdfAlg crypto.Hash) (*secboot.KeyData, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the indirections here.

Comment thread tpm2/seal.go Outdated
}

kd, err := secbootNewKeyData(&secboot.KeyParams{
var makeKeyDataNoAuth keyDataConstructor = func(skd *SealedKeyData, role string, encryptedPayload []byte, kdfAlg crypto.Hash) (*secboot.KeyData, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto - this is just a normal function definition now.

Comment thread tpm2/policy.go
if err == nil {
return
}
tpm.NVUndefineSpace(tpm.OwnerHandleContext(), index, hmacSession)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code was copied from createPcrPolicyCounter where this path was also untested. It's a bit tricky to test this case without mocking the calls to go-tpm2 and there's a bunch of other paths that depend on TPM errors that aren't tested.

I think it's worth addressing these in follow-ups, but by:

  • adding an API to github.com/canonical/go-tpm2/mssim to put the simulator into failure mode.
  • adding an API to github.com/canonical/go-tpm2/testutil to call the new API after a specific number of commands are executed successfully.
  • using the new APIs here to trigger failures at specific points.

Comment thread tpm2/seal_test.go
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like these are dropped because there's not an equivalent API that creates multiple keys in a single call, as we aren't sharing PCR policies anymore.

@pedronis
pedronis self-requested a review March 5, 2024 13:54
Comment thread tpm2/update.go
Comment on lines +47 to +49
// incrementPolicyVersion indicates that the new policy version should be the
// previous policy version plus 1.
incrementPcrPolicyVersion

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got slightly confused by this and how it differs from the newPcrPolicyVersion option. Should we expand its comment a bit to indicate that this is used only by legacy/v0,v1,v2 TPM keydata versions? (I believe the rest of the difference in logic can be inferred by following the corresponding flows and reading other doc comments)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that some further clarification about the 3 modes would be good

Comment thread tpm2/policy_v0.go
}

pcrData.addRevocationCheck(trial, params.policyCounterName, params.policySequence+1)
pcrData.addRevocationCheck(trial, params.policyCounterName, params.policySequence)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this change?

@chrisccoulson chrisccoulson Mar 11, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR moves incrementing the value earlier so the change here avoids a double increment that I'd missed earlier.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, I followed the code a bit more and makes sense

Comment thread tpm2/policy_v1.go

if params.policyCounterName != nil {
pcrData.addRevocationCheck(trial, params.policyCounterName, params.policySequence+1)
pcrData.addRevocationCheck(trial, params.policyCounterName, params.policySequence)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question

Comment thread tpm2/update.go
Comment on lines +47 to +49
// incrementPolicyVersion indicates that the new policy version should be the
// previous policy version plus 1.
incrementPcrPolicyVersion

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that some further clarification about the 3 modes would be good

@pedronis pedronis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some questions

@pedronis
pedronis self-requested a review March 11, 2024 16:00

@pedronis pedronis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, reminder about the discussion/suggestion here though: #271 (review)

@chrisccoulson

Copy link
Copy Markdown
Contributor

+1, reminder about the discussion/suggestion here though: #271 (review)

Thanks, I've added a comment here.

I think I've had an idea for how to clean this up and remove these values (moving the logic to the tpm2/policy_v* implementations and supplying the counter value as an argument instead), but I'll leave that for a follow-up.

@chrisccoulson
chrisccoulson merged commit 691fc82 into canonical:master Mar 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants