-
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
Add plainkey platform #282
Merged
chrisccoulson
merged 6 commits into
canonical:master
from
chrisccoulson:add-plainkey-platform
Mar 15, 2024
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
0d14824
Add plainkey platform
chrisccoulson dcdb997
plainkey: add a package description
chrisccoulson a8a8e77
plainkey: remove unused function
chrisccoulson f83d636
plainkey: add comment for key ID
chrisccoulson bcf1deb
plainkey: split up the salt and nonce value
chrisccoulson 3a1580a
plainkey: add additional note to NewProtectedKey
chrisccoulson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// -*- Mode: Go; indent-tabs-mode: t -*- | ||
|
||
/* | ||
* Copyright (C) 2023 Canonical Ltd | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License version 3 as | ||
* published by the Free Software Foundation. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
* | ||
*/ | ||
|
||
package plainkey | ||
|
||
import "github.com/snapcore/secboot" | ||
|
||
const ( | ||
PlatformName = platformName | ||
) | ||
|
||
type ( | ||
AdditionalData = additionalData | ||
HashAlg = hashAlg | ||
KeyData = keyData | ||
PlatformKeyDataHandler = platformKeyDataHandler | ||
PlatformKeyId = platformKeyId | ||
) | ||
|
||
var ( | ||
DeriveAESKey = deriveAESKey | ||
) | ||
|
||
func MockSecbootNewKeyData(fn func(*secboot.KeyParams) (*secboot.KeyData, error)) (restore func()) { | ||
orig := secbootNewKeyData | ||
secbootNewKeyData = fn | ||
return func() { | ||
secbootNewKeyData = orig | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,281 @@ | ||
// -*- Mode: Go; indent-tabs-mode: t -*- | ||
|
||
/* | ||
* Copyright (C) 2023 Canonical Ltd | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License version 3 as | ||
* published by the Free Software Foundation. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
* | ||
*/ | ||
|
||
package plainkey | ||
|
||
import ( | ||
"crypto" | ||
"crypto/aes" | ||
"crypto/cipher" | ||
"crypto/hmac" | ||
"encoding/asn1" | ||
"encoding/json" | ||
"fmt" | ||
"hash" | ||
"io" | ||
|
||
"golang.org/x/crypto/cryptobyte" | ||
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1" | ||
"golang.org/x/crypto/hkdf" | ||
|
||
"github.com/snapcore/secboot" | ||
) | ||
|
||
const ( | ||
symKeySaltSize = 32 | ||
nonceSize = 12 | ||
) | ||
|
||
var ( | ||
nilHash hashAlg = 0 | ||
sha1Oid = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26} | ||
sha224Oid = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 4} | ||
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} | ||
|
||
secbootNewKeyData = secboot.NewKeyData | ||
) | ||
|
||
// hashAlg corresponds to a digest algorithm. | ||
// XXX: This is the third place this appears now - we almost certainly want to put this | ||
// in one place. Maybe for another PR. | ||
type hashAlg crypto.Hash | ||
|
||
func (a hashAlg) Available() bool { | ||
return crypto.Hash(a).Available() | ||
} | ||
|
||
func (a hashAlg) New() hash.Hash { | ||
return crypto.Hash(a).New() | ||
} | ||
|
||
func (a hashAlg) Size() int { | ||
return crypto.Hash(a).Size() | ||
} | ||
|
||
func (a hashAlg) MarshalASN1(b *cryptobyte.Builder) { | ||
b.AddASN1(cryptobyte_asn1.SEQUENCE, func(b *cryptobyte.Builder) { // AlgorithmIdentifier ::= SEQUENCE { | ||
var oid asn1.ObjectIdentifier | ||
|
||
switch crypto.Hash(a) { | ||
case crypto.SHA1: | ||
oid = sha1Oid | ||
case crypto.SHA224: | ||
oid = sha224Oid | ||
case crypto.SHA256: | ||
oid = sha256Oid | ||
case crypto.SHA384: | ||
oid = sha384Oid | ||
case crypto.SHA512: | ||
oid = sha512Oid | ||
default: | ||
b.SetError(fmt.Errorf("unknown hash algorithm: %v", crypto.Hash(a))) | ||
return | ||
} | ||
b.AddASN1ObjectIdentifier(oid) // algorithm OBJECT IDENTIFIER | ||
b.AddASN1NULL() // parameters ANY DEFINED BY algorithm OPTIONAL | ||
}) | ||
} | ||
|
||
func (a hashAlg) MarshalJSON() ([]byte, error) { | ||
var s string | ||
|
||
switch crypto.Hash(a) { | ||
case crypto.SHA1: | ||
s = "sha1" | ||
case crypto.SHA224: | ||
s = "sha224" | ||
case crypto.SHA256: | ||
s = "sha256" | ||
case crypto.SHA384: | ||
s = "sha384" | ||
case crypto.SHA512: | ||
s = "sha512" | ||
case crypto.Hash(nilHash): | ||
s = "null" | ||
default: | ||
return nil, fmt.Errorf("unknown hash algorithm: %v", crypto.Hash(a)) | ||
} | ||
|
||
return json.Marshal(s) | ||
} | ||
|
||
func (a *hashAlg) UnmarshalJSON(b []byte) error { | ||
var s string | ||
if err := json.Unmarshal(b, &s); err != nil { | ||
return err | ||
} | ||
|
||
switch s { | ||
case "sha1": | ||
*a = hashAlg(crypto.SHA1) | ||
case "sha224": | ||
*a = hashAlg(crypto.SHA224) | ||
case "sha256": | ||
*a = hashAlg(crypto.SHA256) | ||
case "sha384": | ||
*a = hashAlg(crypto.SHA384) | ||
case "sha512": | ||
*a = hashAlg(crypto.SHA512) | ||
default: | ||
// be permissive here and allow everything to be | ||
// unmarshalled. | ||
*a = nilHash | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func deriveAESKey(ikm, salt []byte) []byte { | ||
r := hkdf.New(crypto.SHA256.New, ikm, salt, []byte("ENCRYPT")) | ||
|
||
key := make([]byte, 32) | ||
if _, err := io.ReadFull(r, key); err != nil { | ||
panic(fmt.Sprintf("cannot derive key: %v", err)) | ||
} | ||
|
||
return key | ||
} | ||
|
||
type additionalData struct { | ||
Version int | ||
Generation int | ||
KDFAlg hashAlg | ||
AuthMode secboot.AuthMode | ||
} | ||
|
||
func (d additionalData) MarshalASN1(b *cryptobyte.Builder) { | ||
b.AddASN1(cryptobyte_asn1.SEQUENCE, func(b *cryptobyte.Builder) { | ||
b.AddASN1Int64(int64(d.Version)) | ||
b.AddASN1Int64(int64(d.Generation)) | ||
d.KDFAlg.MarshalASN1(b) | ||
b.AddASN1Enum(int64(d.AuthMode)) | ||
}) | ||
} | ||
|
||
// platformKeyId is a HMAC created by the platform key used to protect | ||
// a plainkey key blob. It is used to iedntify the loaded platform key | ||
// to use for key recovery. | ||
type platformKeyId struct { | ||
Alg hashAlg `json:"alg"` // the digest algorithm | ||
Salt []byte `json:"salt"` // the salt, used as data to the HMAC | ||
Digest []byte `json:"digest"` // the resulting HMAC. | ||
} | ||
|
||
type keyData struct { | ||
Version int `json:"version"` | ||
|
||
Salt []byte `json:"salt"` // Used to derive the symmetric key from the platform key | ||
Nonce []byte `json:"nonce"` // the GCM nonce | ||
|
||
// PlatformKeyID is used to identify the loaded platform key to | ||
// use for key recovery. | ||
PlatformKeyID platformKeyId `json:"platform-key-id"` | ||
} | ||
|
||
// NewProtectedKey creates a new key that is protected by this platform with the supplied | ||
// platform key. The platform key is typically stored inside of an encrypted container that | ||
// is unlocked via another mechanism, such as a TPM, and then loaded via [SetPlatformKeys] | ||
// after unlocking that container. | ||
// | ||
// If primaryKey isn't supplied, then one will be generated. | ||
// | ||
// This function requires some cryptographically strong randomness, obtained from the rand | ||
// argument. Whilst this will normally be from [rand.Reader], it can be provided from other | ||
// secure sources or mocked during tests. Note that the underlying implementation of this | ||
// platform uses GCM, so rand must be cryptographically secure in order to prevent nonce | ||
// reuse problems. Calling this function more than once in production with the same platform | ||
// key and the same sequence of random bytes is a bug. | ||
func NewProtectedKey(rand io.Reader, platformKey []byte, primaryKey secboot.PrimaryKey) (protectedKey *secboot.KeyData, primaryKeyOut secboot.PrimaryKey, unlockKey secboot.DiskUnlockKey, err error) { | ||
if len(primaryKey) == 0 { | ||
primaryKey = make(secboot.PrimaryKey, 32) | ||
if _, err := io.ReadFull(rand, primaryKey); err != nil { | ||
return nil, nil, nil, fmt.Errorf("cannot obtain primary key: %w", err) | ||
} | ||
|
||
} | ||
|
||
kdfAlg := crypto.SHA256 | ||
unlockKey, payload, err := secboot.MakeDiskUnlockKey(rand, kdfAlg, primaryKey) | ||
if err != nil { | ||
return nil, nil, nil, fmt.Errorf("cannot create new unlock key: %w", err) | ||
} | ||
|
||
idAlg := crypto.SHA256 | ||
|
||
// Obtain a 32-byte salt for deriving the symmetric key, a 12-byte GCM nonce and | ||
// a 32-byte salt for the platform key ID. | ||
randBytes := make([]byte, symKeySaltSize+nonceSize+idAlg.Size()) | ||
if _, err := io.ReadFull(rand, randBytes); err != nil { | ||
return nil, nil, nil, fmt.Errorf("cannot obtain required random bytes: %w", err) | ||
} | ||
|
||
salt := randBytes[:symKeySaltSize] | ||
nonce := randBytes[symKeySaltSize : symKeySaltSize+nonceSize] | ||
idSalt := randBytes[symKeySaltSize+nonceSize:] | ||
|
||
aad := additionalData{ | ||
Version: 1, | ||
Generation: secboot.KeyDataGeneration, | ||
KDFAlg: hashAlg(kdfAlg), | ||
AuthMode: secboot.AuthModeNone, | ||
} | ||
builder := cryptobyte.NewBuilder(nil) | ||
aad.MarshalASN1(builder) | ||
aadBytes, err := builder.Bytes() | ||
if err != nil { | ||
return nil, nil, nil, fmt.Errorf("cannot serialize AAD: %w", err) | ||
} | ||
|
||
id := platformKeyId{ | ||
Alg: hashAlg(idAlg), | ||
Salt: idSalt, | ||
} | ||
h := hmac.New(id.Alg.New, platformKey) | ||
h.Write(id.Salt) | ||
id.Digest = h.Sum(nil) | ||
|
||
b, err := aes.NewCipher(deriveAESKey(platformKey, salt)) | ||
if err != nil { | ||
return nil, nil, nil, fmt.Errorf("cannot create cipher: %w", err) | ||
} | ||
aead, err := cipher.NewGCM(b) | ||
if err != nil { | ||
return nil, nil, nil, fmt.Errorf("cannot create AEAD: %w", err) | ||
} | ||
ciphertext := aead.Seal(nil, nonce, payload, aadBytes) | ||
|
||
kd, err := secbootNewKeyData(&secboot.KeyParams{ | ||
Handle: &keyData{ | ||
Version: 1, | ||
Salt: salt, | ||
Nonce: nonce, | ||
PlatformKeyID: id, | ||
}, | ||
EncryptedPayload: ciphertext, | ||
PlatformName: platformName, | ||
KDFAlg: kdfAlg, | ||
}) | ||
if err != nil { | ||
return nil, nil, nil, fmt.Errorf("cannot create key data: %w", err) | ||
} | ||
|
||
return kd, primaryKey, unlockKey, nil | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 would be good to have a doc comment on this 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 comment here.
I added a comment for the Nonce field as well and then realized it didn't make sense because the field was holding 2 unrelated values. I've pushed another change that splits this into 2 distinct fields.