[3/3] Keydata v3 TPM platform changes - #271
Conversation
5c8317a to
ef66445
Compare
934b170 to
e30fd0c
Compare
e30fd0c to
83ad83a
Compare
83ad83a to
bc9ee04
Compare
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.
bc9ee04 to
4facbee
Compare
pedronis
left a comment
There was a problem hiding this comment.
did a first pass, seal.go is nicely streamlined
| 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 |
There was a problem hiding this comment.
this needs a doc comment now, also as style is probably better to use a separate var stanza for exported vs not exported vars
There was a problem hiding this comment.
I've added a comment to this
|
|
||
| // 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 { |
There was a problem hiding this comment.
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)?
| if isKeyDataError(err) { | ||
| return InvalidKeyDataError{fmt.Sprintf("%v (%d)", err.Error(), i+1)} | ||
| } | ||
| return xerrors.Errorf("cannot validate related key data: %w", err) |
There was a problem hiding this comment.
this error path doesn't seem tested
There was a problem hiding this comment.
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.
| // 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 { |
There was a problem hiding this comment.
this function is not tested afaict
There was a problem hiding this comment.
I've added a test for this one now.
| 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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
|
|
||
| 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 { |
There was a problem hiding this comment.
same issue with the bool
There was a problem hiding this comment.
I've added a public enumerated type for the new SealedKeyData API to replace this.
| } | ||
| 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) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
I've removed the indirections here.
| } | ||
|
|
||
| kd, err := secbootNewKeyData(&secboot.KeyParams{ | ||
| var makeKeyDataNoAuth keyDataConstructor = func(skd *SealedKeyData, role string, encryptedPayload []byte, kdfAlg crypto.Hash) (*secboot.KeyData, error) { |
There was a problem hiding this comment.
same for the var def vs just func definition
There was a problem hiding this comment.
Ditto - this is just a normal function definition now.
| if err == nil { | ||
| return | ||
| } | ||
| tpm.NVUndefineSpace(tpm.OwnerHandleContext(), index, hmacSession) |
There was a problem hiding this comment.
this bit of undo logic does not seem tested
There was a problem hiding this comment.
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.
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
This replaces the bool argument with an enum. It also restores the previous behavior for keys that use the SealedKeyObject type (where all policy updates increment the policy version).
chrisccoulson
left a comment
There was a problem hiding this comment.
I've responded to some of your comments now (I had an in-progress review that I had to finish to release them)
| 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 |
There was a problem hiding this comment.
I've added a comment to this
| if isKeyDataError(err) { | ||
| return InvalidKeyDataError{fmt.Sprintf("%v (%d)", err.Error(), i+1)} | ||
| } | ||
| return xerrors.Errorf("cannot validate related key data: %w", err) |
There was a problem hiding this comment.
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.
| // 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 { |
There was a problem hiding this comment.
I've added a test for this one now.
| 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, |
There was a problem hiding this comment.
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.
|
|
||
| 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 { |
There was a problem hiding this comment.
I've added a public enumerated type for the new SealedKeyData API to replace this.
| } | ||
| 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) { |
There was a problem hiding this comment.
I've removed the indirections here.
| } | ||
|
|
||
| kd, err := secbootNewKeyData(&secboot.KeyParams{ | ||
| var makeKeyDataNoAuth keyDataConstructor = func(skd *SealedKeyData, role string, encryptedPayload []byte, kdfAlg crypto.Hash) (*secboot.KeyData, error) { |
There was a problem hiding this comment.
Ditto - this is just a normal function definition now.
| if err == nil { | ||
| return | ||
| } | ||
| tpm.NVUndefineSpace(tpm.OwnerHandleContext(), index, hmacSession) |
There was a problem hiding this comment.
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.
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
| // incrementPolicyVersion indicates that the new policy version should be the | ||
| // previous policy version plus 1. | ||
| incrementPcrPolicyVersion |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
I agree that some further clarification about the 3 modes would be good
| } | ||
|
|
||
| pcrData.addRevocationCheck(trial, params.policyCounterName, params.policySequence+1) | ||
| pcrData.addRevocationCheck(trial, params.policyCounterName, params.policySequence) |
There was a problem hiding this comment.
This PR moves incrementing the value earlier so the change here avoids a double increment that I'd missed earlier.
There was a problem hiding this comment.
thanks, I followed the code a bit more and makes sense
|
|
||
| if params.policyCounterName != nil { | ||
| pcrData.addRevocationCheck(trial, params.policyCounterName, params.policySequence+1) | ||
| pcrData.addRevocationCheck(trial, params.policyCounterName, params.policySequence) |
| // incrementPolicyVersion indicates that the new policy version should be the | ||
| // previous policy version plus 1. | ||
| incrementPcrPolicyVersion |
There was a problem hiding this comment.
I agree that some further clarification about the 3 modes would be good
pedronis
left a comment
There was a problem hiding this comment.
+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. |
Original commit: chrisccoulson@87d9aa3
I will split the commit above in 3 separate cascading PRs and add tests for it:
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.