Skip to content
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

Merged
merged 23 commits into from
Feb 19, 2024

Conversation

sespiros
Copy link
Collaborator

@sespiros sespiros commented Oct 23, 2023

Original commit: chrisccoulson@87d9aa3

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

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

@sespiros
Copy link
Collaborator Author

I don't have permissions to assign reviewers, @chrisccoulson could you add @pedronis?

@sespiros sespiros changed the title Keydata v3 platform API changes [1/3] Keydata v3 platform API changes Oct 27, 2023
@sespiros sespiros force-pushed the keydata-v3-platform branch 2 times, most recently from 515a998 to 80297ae Compare November 23, 2023 10:28
Copy link
Collaborator

@chrisccoulson chrisccoulson left a 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 Show resolved Hide resolved
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\\)")
Copy link
Collaborator

@chrisccoulson chrisccoulson Jan 5, 2024

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").

Copy link
Collaborator Author

@sespiros sespiros Jan 10, 2024

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.

Copy link
Collaborator

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_legacy_test.go Outdated Show resolved Hide resolved
keydata_test.go Outdated Show resolved Hide resolved
keydata_test.go Outdated Show resolved Hide resolved
keydata_test.go Outdated Show resolved Hide resolved
keydata_test.go Outdated Show resolved Hide resolved
keydata_test.go Outdated
@@ -1322,6 +1256,7 @@ func (s *keyDataSuite) TestReadAndWriteWithUnsaltedKeyDigest(c *C) {
auxKey := testutil.DecodeHexString(c, "8107f1c65c58934f0d59245d1d94d312ea803e69c8599a7bac8c67fe253232f2")
j := []byte(
`{` +
`"version":0,` +
Copy link
Collaborator

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.

Copy link
Collaborator Author

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?

Copy link
Collaborator

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.

Copy link
Collaborator Author

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
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

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_legacy_test.go Outdated Show resolved Hide resolved
keydata.go Outdated
@@ -42,7 +42,7 @@ import (
)

const (
KeyDataVersion int = 2
Copy link
Collaborator

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.

Copy link
Collaborator Author

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 Show resolved Hide resolved
keydata_test.go Outdated
return xerrors.Errorf("Invalid AuthMode: %d", data.AuthMode)
}

return nil
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator

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 {
Copy link
Collaborator

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?

Copy link
Collaborator Author

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

Copy link
Collaborator

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
Copy link
Collaborator

@chrisccoulson chrisccoulson Jan 12, 2024

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.

Copy link
Collaborator Author

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
Copy link
Collaborator

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)

Copy link
Collaborator

@chrisccoulson chrisccoulson left a 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

@chrisccoulson
Copy link
Collaborator

The test failures are occurring because the callback returned from MockKeyDataVersion isn't being called at the end of the test. It should be used something like this:

restore := MockKeyDataVersion(0)
defer restore()

Copy link
Collaborator

@pedronis pedronis left a 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 Show resolved Hide resolved
platform.go Outdated
Comment on lines 79 to 80
// RecoverKeys attempts to recover the cleartext keys from the supplied key
// data using this platform's secure device.
Copy link
Collaborator

@pedronis pedronis Jan 15, 2024

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

Copy link
Collaborator Author

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
Copy link
Collaborator

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

Copy link
Collaborator Author

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) {
Copy link
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

Copy link
Collaborator Author

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)
Copy link
Collaborator

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?

Copy link
Collaborator

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.

Copy link
Collaborator Author

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 and unmarshalV1KeyPayload 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?

Copy link
Collaborator Author

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.

Copy link
Collaborator

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

Copy link
Collaborator

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.

@pedronis pedronis self-requested a review January 16, 2024 10:44
Copy link
Collaborator

@pedronis pedronis left a 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

keydata.go Show resolved Hide resolved
Comment on lines +1022 to +1030
// This is to support the legacy TPM key data created
// via tpm2.NewKeyDataFromSealedKeyObjectFile.
return k.Unique
Copy link
Collaborator

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

Copy link
Collaborator Author

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.

Copy link
Collaborator

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

Copy link
Collaborator

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 {
Copy link
Collaborator

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

Copy link
Collaborator Author

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

needs a doc comment

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added.

keydata.go Show resolved Hide resolved
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
@sespiros sespiros force-pushed the keydata-v3-platform branch from ce4c8df to 25099ff Compare January 29, 2024 09:09
s.checkKeyDataJSONFromReaderAuthModeNone(c, f, protected, 0)
var j map[string]interface{}

d := json.NewDecoder(f)
Copy link
Collaborator

Choose a reason for hiding this comment

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

need to import json

Copy link
Collaborator

@pedronis pedronis left a 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
Copy link
Collaborator

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

Copy link
Collaborator Author

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).

Copy link
Collaborator

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.

Copy link
Collaborator

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?

Copy link
Collaborator

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.

Comment on lines +1022 to +1030
// This is to support the legacy TPM key data created
// via tpm2.NewKeyDataFromSealedKeyObjectFile.
return k.Unique
Copy link
Collaborator

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

@pedronis pedronis self-requested a review February 1, 2024 17:32
Copy link
Collaborator

@pedronis pedronis left a 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"))
Copy link
Collaborator

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

Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

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.
@chrisccoulson
Copy link
Collaborator

Thanks - I think the latest changes look good to me

@pedronis pedronis self-requested a review February 15, 2024 07:59
Copy link
Collaborator

@pedronis pedronis left a 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
Copy link
Collaborator

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

Copy link
Collaborator

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)

Copy link
Collaborator Author

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.
Copy link
Collaborator

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?

@chrisccoulson chrisccoulson merged commit 179584a into canonical:master Feb 19, 2024
0 of 2 checks passed
chrisccoulson added a commit that referenced this pull request Apr 4, 2024
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.
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