[1/3] Keydata v3 platform API changes - #265
Conversation
|
I don't have permissions to assign reviewers, @chrisccoulson could you add @pedronis? |
515a998 to
80297ae
Compare
chrisccoulson
left a comment
There was a problem hiding this comment.
Thanks for working on this - I've left some comments inline.
|
|
||
| func (s *cryptSuite) TestInitializeLUKS2ContainerInvalidKeySize(c *C) { | ||
| c.Check(InitializeLUKS2Container("/dev/sda1", "data", s.newPrimaryKey()[0:16], nil), ErrorMatches, "expected a key length of at least 256-bits \\(got 128\\)") | ||
| c.Check(InitializeLUKS2Container("/dev/sda1", "data", ([]byte)(s.keyDataTestBase.newPrimaryKey(c, 32))[0:16], nil), ErrorMatches, "expected a key length of at least 256-bits \\(got 128\\)") |
There was a problem hiding this comment.
As keyDataTestBase.newPrimaryKey takes a size now, you don't need to create a 32-byte key here and then truncate it to 16 bytes.
Also, is the type conversion to []byte necessary here? That should happen implicitly because []byte is not a named type. This is repeated in a few other places too.
For an idea of when explicit conversions are necessary when converting between types, take a look at this section of the language spec: Assignability (in this case, "V and T have identical underlying types but are not type parameters and at least one of V or T is not a named type").
There was a problem hiding this comment.
Removed the truncation, thanks.
Regarding the type conversion, keyDataTestBase's newPrimaryKey returns type PrimaryKey while all the LUKS related APIs expect type DiskUnlockKey so I believe this doesn't fall under the categories above so the type conversion is needed despite them having the same underlying type []byte.
There was a problem hiding this comment.
Ah, I think the problem here is that perhaps there should have originally been a newUnlockKey method for returning a key with the correct type.
| auxKey := testutil.DecodeHexString(c, "8107f1c65c58934f0d59245d1d94d312ea803e69c8599a7bac8c67fe253232f2") | ||
| j := []byte( | ||
| `{` + | ||
| `"version":0,` + |
There was a problem hiding this comment.
As existing keys won't have this field, we should test that reading works for payloads that don't contain it.
There was a problem hiding this comment.
Correct, I got confused here. So I modified the version behavior to use 0 of 1 for the version field if the Version is less than 2 and by marking the Version field as omitempty but I am not sure if that is correct. Can you think of any other legacy scenario that might be weird here?
I also had to modify the tests in keydata_legacy_test.go to use 0 for legacy version. Is keyData with version 1 possible?
There was a problem hiding this comment.
I added a comment separately for this - I think the omitempty is fine, but I would still fake version 1 in KeyData.Version so that it returns 1 if the field is 0. I'm not sure if it matters either way to be honest, but it's a bit weird going from v0 -> v2.
There was a problem hiding this comment.
Well the main reason I opted for this approach was that I couldn't think of a way to express "omit when 1". I left it as is and faked version 1 as you suggested.
| EncodedHandle []byte // The JSON encoded platform handle | ||
| KDFAlg crypto.Hash | ||
|
|
||
| AuthMode AuthMode |
There was a problem hiding this comment.
There's nothing in the tests inside mockPlatformKeyDataHandler that checks we pass the expected values for these new fields.
There was a problem hiding this comment.
I added some validation for the mockPlatformKeyDataHandler fields. I found a missing field in the legacy tests because of it. Let me know how it looks. I hardcoded crypto.SHA256 in mockProtectKeys for the legacy keyData test bases as there aren't any test cases that test for different KDF algorithms.
There was a problem hiding this comment.
Thanks, I added a comment for this separately before responding to this thread: #265 (comment)
| ) | ||
|
|
||
| const ( | ||
| KeyDataVersion int = 2 |
There was a problem hiding this comment.
It's probably cleaner to just define KeyDataVersion as a variable here rather than having a mockable function, and then MockKeyDataVersion can just set this instead.
There was a problem hiding this comment.
Changed it to a private variable.
| return xerrors.Errorf("Invalid AuthMode: %d", data.AuthMode) | ||
| } | ||
|
|
||
| return nil |
There was a problem hiding this comment.
The validation should check that the supplied fields are the expected values rather than bounds checking them. I'm not sure of the best way to do this though - maybe adding the expected values as fields to mockPlatformKeyDataHandle?
There was a problem hiding this comment.
Reverted the last attempt and pushed some changes to the logic if you could take a look. I added Expected* fields to the mockPlatformKeyDataHandle but tbh I find it a bit confusing to parse.
There was a problem hiding this comment.
I think this is ok. One other way to implement this would be to make mockKeyDataHandler use GCM and have the extra properties as part of the AAD instead, but maybe we can change that in a follow-up.
| . "gopkg.in/check.v1" | ||
| ) | ||
|
|
||
| type keyDataLegacyTestBase struct { |
There was a problem hiding this comment.
This type is currently only used in one place (keyDataLegacySuite) - do subsequent PRs add other uses of it, or should it just be part of keyDataLegacySuite?
There was a problem hiding this comment.
So I had refactored and completely removed that file in the subsequent PR, check 4cea1c7
There was a problem hiding this comment.
It looks like this change doesn't exist in the most recently pushed branch.
| } | ||
|
|
||
| func (d *KeyData) Version() int { | ||
| return d.data.Version |
There was a problem hiding this comment.
I think having this field as omitempty is fine, but I would probably still convert 0 -> 1 here for when the field is not present.
There was a problem hiding this comment.
I reverted it to the previous implementation which fakes 1 for version 0.
| EncodedHandle []byte // The JSON encoded platform handle | ||
| KDFAlg crypto.Hash | ||
|
|
||
| AuthMode AuthMode |
There was a problem hiding this comment.
Thanks, I added a comment for this separately before responding to this thread: #265 (comment)
chrisccoulson
left a comment
There was a problem hiding this comment.
Thanks for the updates - I've left a few more comments
|
The test failures are occurring because the callback returned from |
pedronis
left a comment
There was a problem hiding this comment.
did a first quick pass on the top level changes, I have some questions/comments
| // RecoverKeys attempts to recover the cleartext keys from the supplied key | ||
| // data using this platform's secure device. |
There was a problem hiding this comment.
all the doc comments here should match the signature changes, for this one and the other methods
There was a problem hiding this comment.
Changed doc comments. @chrisccoulson could you also take a look?
| ) | ||
|
|
||
| const ( | ||
| keyDataVersion int = 2 |
There was a problem hiding this comment.
would be good to have a doc comment on this, probably also mentioning some properties for the old 0 value
There was a problem hiding this comment.
I added a slightly more complete doc comment in KeyData's Version(). Should we add something here too?
| }) | ||
| } | ||
|
|
||
| func MakeDiskUnlockKey(rand io.Reader, alg crypto.Hash, primaryKey PrimaryKey) (unlockKey DiskUnlockKey, cleartextPayload []byte, err error) { |
There was a problem hiding this comment.
this needs a doc comment
| func (d *KeyData) recoverKeysCommon(data []byte) (DiskUnlockKey, PrimaryKey, error) { | ||
| switch d.Version() { | ||
| case 0: | ||
| unlockKey, primaryKey, err := unmarshalV1KeyPayload(data) |
There was a problem hiding this comment.
why V1? as Version here is 0? also we have already key own versions, maybe we should use a different naming/termilogy than just version, internal for this level?
There was a problem hiding this comment.
So I already changed it and replied to the comment above but summarizing it again to make sure I don't miss something. The following versions are possible:
- version 0. Key data already used are missing a version field.
Version()called in that keydata will return 1 andunmarshalV1KeyPayloadwill be called. - version 1 shouldn't exist (?), but even then the behavior would be the same as version 0 keys.
- version 2 is the currently introduced format.
When I first saw V1, I assumed there was another V0. Isn't that the case? If not, maybe we could simply call old keys legacy keys?
There was a problem hiding this comment.
Maybe "generation"? So that we can refer to cross-platform keydata versions as generations and then each platform can refer to its internal version simply as "version".
i.e TPM platform's key versions 0-2 use cross-platform keys of generation 1 (or 0, this is still a bit confusing) while TPM platform's version 3 keys use generation 2 cross-platform keys.
There was a problem hiding this comment.
"generation" would work for me
There was a problem hiding this comment.
Yes, generation is ok with me as well.
pedronis
left a comment
There was a problem hiding this comment.
did another pass, a few comments, from a review POV my preference would be for the rename Auxiliary=>Primary to be a separate preparatory PR, it's hard to review the other changes mixed with that mechanical one
| // This is to support the legacy TPM key data created | ||
| // via tpm2.NewKeyDataFromSealedKeyObjectFile. | ||
| return k.Unique |
There was a problem hiding this comment.
this path is not exercised
There was a problem hiding this comment.
I am not sure about this, maybe @chrisccoulson has a better understanding. In my understanding this path is exercised for example by legacy TPM keys (as the comment suggests). For instance platformLegacySuite.TestRecoverKeys is a test in tpm2/platform_legacy_test.go that fails if this code is removed.
There was a problem hiding this comment.
but if Version is 1 we don't arrive here, as I said I'm confused by the role of KDFAlg vs Version here and the KeyParams, aynway if this is useful there should be test in keydata_test.go that shows how this is meant to happen
There was a problem hiding this comment.
This is exercised by the tests in the tpm2 package - specifically, the ones in tpm2/platform_legacy_test.go that call NewKeyDataFromSealedKeyObjectFile. This creates an ephemeral v2 key data but with 0 for the KDFAlg parameter. This path exists just for that case.
| return kd, nil | ||
| } | ||
|
|
||
| type protectedKeys struct { |
There was a problem hiding this comment.
this probably could use a doc comment
| return kd, nil | ||
| } | ||
|
|
||
| func NewKeyDataWithPassphrase(params *KeyWithPassphraseParams, passphrase string, kdf KDF) (*KeyData, error) { |
Under the new design the disk unlock key will be derived from the primary key and a unique ID. The new key payload will be serialized and unserialized to ASN1.
Make the new API more generic by removing the EncryptedPayload from the PlatformKeyData struct and supply it as an extra argument to RecoverKeys*() functions. Also introduce new fields in the PlatformKeyData struct which will be included as authenticated data for the authenticated encryption.
under the new design a keyData object should always have an encrypted payload therefore: - the EncryptedPayload field of keyData cannot be omitted. - passphraseData is removed in favor of a new passphraseParams field. - AuthMode() will return the type of authentication mode used by this particular keyData object based on the existence of the passphraseParams field.
Under the new design, keyData objects will be differentiated based on whether they are passphrase protected or not. This property of new keyData objects will now be immutable. For that reason passphraseData struct is converted to the new passphraseParams struct which holds the parameters of the passphrase and doesn't contain an EncryptedPayload field anymore. For passphrase protected payloads the key derivation has changed and now it should work as follows: - a strong key will be derived from the passphrase - 3 values will be derived from that strong key given as the IKM input to an hkdf - an encryption key enc with info PASSPHRASE-ENC - an IV with info PASSPHRASE-IV - an auth key with info PASSPHRASE-AUTH The encryption key and IV will be used for the encryption/decryption of keyData's payload which happens on the platform agnostic API. The auth key will then need to be used as a way of key validation using the secure device (i.e the authentication value of the sealed object for the TPM case). To support unmarshalling of keys generated with the old format, the new unmarshalV1KeyPayload() was created under keydata_legacy.go.
under the new design, a keyData object will either have or not have a passphrase therefore there is no use for setting a passphrase in keyData objects that don't have one or clearing.
tests that need to test legacy behavior were copied over to keydata_legacy_test.go and then modified to support the new format in keydata_test.go
and also pass passphrase derivation's kdf hash algorithm as an extra argument.
ce4c8df to
25099ff
Compare
| s.checkKeyDataJSONFromReaderAuthModeNone(c, f, protected, 0) | ||
| var j map[string]interface{} | ||
|
|
||
| d := json.NewDecoder(f) |
pedronis
left a comment
There was a problem hiding this comment.
thanks, I'm confused by the role of KDFAlg nilHash in a couple of places, maybe it's really a question for Chris
| // is reserved to support legacy TPM2 key data files, and tells the | ||
| // KeyData to use the unique key as the unlock key rather than using it | ||
| // to derive the unlock key. | ||
| KDFAlg crypto.Hash |
There was a problem hiding this comment.
I'm confused by the comment about the legacy case, as this used for creating a new KeyData and I don't see any code that special case this KDFAlg when it's null, nor I understand what goes into EncryptedPayload in that case
There was a problem hiding this comment.
So I gave it a second look and my guess would be that this is probably forgotten comment/code (maybe from an older implementation?), since we use the version field to differentiate the keys (which now is also marked as omitempty). I removed the unreachable block and the existing tests pass (as expected).
There was a problem hiding this comment.
There is some legacy code that reads the original TPM2 keydata files and creates an ephemeral KeyData with 0 for KDFAlg - see tpm2.NewKeyDataFromSealedKeyObjectFile. Removing the special case here should cause the tests in tpm2/platform_legacy_test.go to fail.
There was a problem hiding this comment.
those are files without the json wrapper?
There was a problem hiding this comment.
Thanks. I would mention tpm2.NewKeyDataFromSealedKeyObjectFile in the comment for clarity.
| // This is to support the legacy TPM key data created | ||
| // via tpm2.NewKeyDataFromSealedKeyObjectFile. | ||
| return k.Unique |
There was a problem hiding this comment.
but if Version is 1 we don't arrive here, as I said I'm confused by the role of KDFAlg vs Version here and the KeyParams, aynway if this is useful there should be test in keydata_test.go that shows how this is meant to happen
pedronis
left a comment
There was a problem hiding this comment.
this looks reasonable to me, question about testing though. The question about what to do with "version" in the JSON is still open though
|
|
||
| key, err := kdf.Derive(passphrase, salt[:], params, uint32(keyLen)) | ||
| auth = make([]byte, params.AuthKeySize) | ||
| r = hkdf.Expand(func() hash.Hash { return kdfAlg.New() }, derived, []byte("PASSPHRASE-AUTH")) |
There was a problem hiding this comment.
if I change the constant string no tests seem to fail, I wonder if and how this bit of the code is tested
There was a problem hiding this comment.
That's expected, as the same code is used to derive keys when both setting and using a passphrase. It would only fail if a key was configured with one string and then unsealed with a different one.
There was a problem hiding this comment.
I added a test case for that for good measure. The test hardcodes a keydata generated with the current valid info fields.
There was a problem hiding this comment.
The added test looks ok - one note for future reference is that we have testutil.DecodeHexString for embedding bytes into tests which will fail the test if an invalid string is supplied without having to check an error.
use "generation" instead of "version" in the cross-platform APIs keydata struct so that it is easier to parse and differentiate it from the TPM platform's implementation which is also using "version" for its keydata interface implementation.
|
Thanks - I think the latest changes look good to me |
pedronis
left a comment
There was a problem hiding this comment.
thank you, small comment, and a comment/question
|
|
||
| var ( | ||
| keyDataVersion int = 2 | ||
| keyDataGeneration int = 2 |
There was a problem hiding this comment.
a short doc comment describing this even it's private might not hurt
There was a problem hiding this comment.
I've actually got a follow-up PR that makes this public (so that the version can be bound into the AAD)
There was a problem hiding this comment.
oh sorry about that, I thought I had that change in here already, I also make it public in the final PR because I needed it for a test for AAD 33445cf
| // Generation is a number used to differentiate between different key formats. | ||
| // i.e Gen1 keys are binary serialized and include a primary and an unlock key while | ||
| // Gen2 keys are ASN1 serialized and include a primary key and a unique key which is | ||
| // used to derive the unlock key. |
There was a problem hiding this comment.
The generations might also support different set of platform data fields?
Keydata testsuite minor refactors and improvements and hashAlg. This addresses: - keyDataLegacyTestBase not used from #265 (comment) - use DecodeHexString from #265 (comment) Also makes hashAlg public as it will be used in a follow-up PR which will slightly rework the mockPlatformKeyDataHandler to use GCM and associated data to check expected values.
Original commit: chrisccoulson@87d9aa3
I will split the commit above in 3 separate cascading PRs and add tests for it: