-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[1/3] Keydata v3 platform API changes #265
[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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this - I've left some comments inline.
crypt_test.go
Outdated
@@ -2524,7 +2547,7 @@ func (s *cryptSuite) TestInitializeLUKS2ContainerWithCustomKDFIterations(c *C) { | |||
} | |||
|
|||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
keydata_test.go
Outdated
@@ -1322,6 +1256,7 @@ func (s *keyDataSuite) TestReadAndWriteWithUnsaltedKeyDigest(c *C) { | |||
auxKey := testutil.DecodeHexString(c, "8107f1c65c58934f0d59245d1d94d312ea803e69c8599a7bac8c67fe253232f2") | |||
j := []byte( | |||
`{` + | |||
`"version":0,` + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I added a comment for this separately before responding to this thread: #265 (comment)
keydata.go
Outdated
@@ -42,7 +42,7 @@ import ( | |||
) | |||
|
|||
const ( | |||
KeyDataVersion int = 2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed it to a private variable.
keydata_test.go
Outdated
return xerrors.Errorf("Invalid AuthMode: %d", data.AuthMode) | ||
} | ||
|
||
return nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I had refactored and completely removed that file in the subsequent PR, check 4cea1c7
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this change doesn't exist in the most recently pushed branch.
keydata.go
Outdated
} | ||
|
||
func (d *KeyData) Version() int { | ||
return d.data.Version |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I added a comment for this separately before responding to this thread: #265 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the updates - I've left a few more comments
The test failures are occurring because the callback returned from
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did a first quick pass on the top level changes, I have some questions/comments
platform.go
Outdated
// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all the doc comments here should match the signature changes, for this one and the other methods
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed doc comments. @chrisccoulson could you also take a look?
keydata.go
Outdated
"golang.org/x/crypto/hkdf" | ||
"golang.org/x/xerrors" | ||
) | ||
|
||
const ( | ||
keyDataVersion int = 2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is related to #265 (comment) as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 andunmarshalV1KeyPayload
will 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"generation" would work for me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, generation is ok with me as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this path is not exercised
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this probably could use a doc comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
@@ -932,9 +951,121 @@ func NewKeyData(params *KeyParams) (*KeyData, error) { | |||
return kd, nil | |||
} | |||
|
|||
func NewKeyDataWithPassphrase(params *KeyWithPassphraseParams, passphrase string, kdf KDF) (*KeyData, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needs a doc comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need to import json
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
those are files without the json wrapper?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank you, small comment, and a comment/question
@@ -50,7 +50,7 @@ const ( | |||
) | |||
|
|||
var ( | |||
keyDataVersion int = 2 | |||
keyDataGeneration int = 2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a short doc comment describing this even it's private might not hurt
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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: