From cd5594ca3e0809b7fd239d8ff56f5cf0d6ed1c54 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Thu, 1 Feb 2024 23:58:50 +0000 Subject: [PATCH] [WIP]: add plainkey platform --- export_test.go | 7 +- keydata.go | 4 +- plainkey/keydata.go | 246 +++++++++++++++++++++++++++++++++++++++++++ plainkey/platform.go | 148 ++++++++++++++++++++++++++ 4 files changed, 399 insertions(+), 6 deletions(-) create mode 100644 plainkey/keydata.go create mode 100644 plainkey/platform.go diff --git a/export_test.go b/export_test.go index 572cedbc..0cc9ee13 100644 --- a/export_test.go +++ b/export_test.go @@ -29,7 +29,6 @@ import ( var ( UnmarshalV1KeyPayload = unmarshalV1KeyPayload UnmarshalProtectedKeys = unmarshalProtectedKeys - KeyDataVersion = keyDataVersion ) type ProtectedKeys = protectedKeys @@ -129,9 +128,9 @@ func MockStderr(w io.Writer) (restore func()) { } func MockKeyDataVersion(n int) (restore func()) { - orig := keyDataVersion - keyDataVersion = n + orig := KeyDataVersion + KeyDataVersion = n return func() { - keyDataVersion = orig + KeyDataVersion = orig } } diff --git a/keydata.go b/keydata.go index 3a33edb1..7959d52f 100644 --- a/keydata.go +++ b/keydata.go @@ -50,7 +50,7 @@ const ( ) var ( - keyDataVersion int = 2 + KeyDataVersion int = 2 snapModelHMACKDFLabel = []byte("SNAP-MODEL-HMAC") sha1Oid = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26} sha224Oid = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 4} @@ -926,7 +926,7 @@ func NewKeyData(params *KeyParams) (*KeyData, error) { kd := &KeyData{ data: keyData{ - Version: keyDataVersion, + Version: KeyDataVersion, PlatformName: params.PlatformName, PlatformHandle: json.RawMessage(encodedHandle), KDFAlg: hashAlg(params.KDFAlg), diff --git a/plainkey/keydata.go b/plainkey/keydata.go new file mode 100644 index 00000000..2cc90afb --- /dev/null +++ b/plainkey/keydata.go @@ -0,0 +1,246 @@ +// -*- 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 . + * + */ + +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" + + "github.com/snapcore/secboot" +) + +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} +) + +// 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 +} + +type additionalData struct { + version int + baseVersion 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.baseVersion)) + d.kdfAlg.marshalASN1(b) + b.AddASN1Enum(int64(d.authMode)) + }) +} + +type platformKeyId struct { + Alg hashAlg `json:"alg"` + Salt []byte `json:"salt"` + Digest []byte `json:"digest"` +} + +type keyData struct { + Version int `json:"version"` + Nonce []byte `json:"nonce"` + + 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] +// or [AddPlatformKeys] after unlocking that container. +// +// If primaryKey isn't supplied, then one will be generated. The kdfAlg specifies the algorithm +// used to derive the unlock key from the primary key and internally generated unique key. +func NewProtectedKey(rand io.Reader, platformKey []byte, primaryKey secboot.PrimaryKey, kdfAlg crypto.Hash) (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) + } + + } + + unlockKey, payload, err := secboot.MakeDiskUnlockKey(rand, crypto.SHA256, primaryKey) + if err != nil { + return nil, nil, nil, fmt.Errorf("cannot create new unlock key: %w", err) + } + + nonce := make([]byte, 12) + if _, err := io.ReadFull(rand, nonce); err != nil { + return nil, nil, nil, fmt.Errorf("cannot obtain nonce: %w", err) + } + + aad := additionalData{ + version: 1, + baseVersion: secboot.KeyDataVersion, + 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) + } + + idAlg := crypto.SHA256 + salt := make([]byte, idAlg.Size()) + if _, err := io.ReadFull(rand, salt); err != nil { + return nil, nil, nil, fmt.Errorf("cannot obtain salt for platform key ID: %w", err) + } + id := platformKeyId{ + Alg: hashAlg(idAlg), + Salt: salt, + } + h := hmac.New(id.Alg.New, id.Salt) + h.Write(platformKey) + id.Digest = h.Sum(nil) + + b, err := aes.NewCipher(platformKey) + 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 := secboot.NewKeyData(&secboot.KeyParams{ + Handle: &keyData{ + Version: 1, + Nonce: nonce, + PlatformKeyID: id, + }, + EncryptedPayload: ciphertext, + PrimaryKey: primaryKey, // XXX: will be removed in a pending PR + SnapModelAuthHash: crypto.SHA256, // XXX: will be removed in a pending PR + PlatformName: platformName, + KDFAlg: kdfAlg, + }) + if err != nil { + return nil, nil, nil, fmt.Errorf("cannot create key data: %w", err) + } + + return kd, primaryKey, unlockKey, nil +} diff --git a/plainkey/platform.go b/plainkey/platform.go new file mode 100644 index 00000000..2e7de279 --- /dev/null +++ b/plainkey/platform.go @@ -0,0 +1,148 @@ +// -*- 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 . + * + */ + +package plainkey + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "encoding/json" + "errors" + "fmt" + "sync" + + "golang.org/x/crypto/cryptobyte" + + "github.com/snapcore/secboot" +) + +const ( + platformName = "plainkey" +) + +var ( + platformKeysMu sync.RWMutex + platformKeys [][]byte +) + +// SetPlatformKeys sets the keys that will be used by this platform to recover other +// keys. These are typically stored in and loaded from an encrypted container that is +// unlocked via some other mechanism. +func SetPlatformKeys(keys ...[]byte) { + platformKeysMu.Lock() + platformKeys = keys + platformKeysMu.Unlock() +} + +// AddPlatformKeys adds keys that will be used by this platform to recover other +// keys. These are typically stored in and loaded from an encrypted container that is +// unlocked via some other mechanism. +func AddPlatformKeys(keys ...[]byte) { + platformKeysMu.Lock() + platformKeys = append(platformKeys, keys...) + platformKeysMu.Unlock() +} + +func getPlatformKey(id *platformKeyId) ([]byte, error) { + if !id.Alg.Available() { + return nil, errors.New("digest algorithm unavailable") + } + + platformKeysMu.RLock() + keys := platformKeys + platformKeysMu.RUnlock() + + for _, key := range keys { + h := hmac.New(id.Alg.New, id.Salt) + h.Write(key) + if bytes.Equal(h.Sum(nil), id.Digest) { + return key, nil + } + } + return nil, errors.New("no key available") +} + +type platformKeyDataHandler struct{} + +func (*platformKeyDataHandler) RecoverKeys(data *secboot.PlatformKeyData, encryptedPayload []byte) ([]byte, error) { + var kd keyData + if err := json.Unmarshal(data.EncodedHandle, &kd); err != nil { + return nil, &secboot.PlatformHandlerError{ + Type: secboot.PlatformHandlerErrorInvalidData, + Err: err, + } + } + + aad := additionalData{ + version: kd.Version, + baseVersion: data.Version, + kdfAlg: hashAlg(data.KDFAlg), + authMode: data.AuthMode, + } + builder := cryptobyte.NewBuilder(nil) + aad.marshalASN1(builder) + aadBytes, err := builder.Bytes() + if err != nil { + return nil, &secboot.PlatformHandlerError{ + Type: secboot.PlatformHandlerErrorInvalidData, + Err: fmt.Errorf("cannot serialize AAD: %w", err), + } + } + + key, err := getPlatformKey(&kd.PlatformKeyID) + if err != nil { + return nil, &secboot.PlatformHandlerError{ + Type: secboot.PlatformHandlerErrorInvalidData, + Err: fmt.Errorf("cannot select platform key: %w", err), + } + } + + b, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("cannot create cipher: %w", err) + } + aead, err := cipher.NewGCM(b) + if err != nil { + return nil, fmt.Errorf("cannot create AEAD: %w", err) + } + + payload, err := aead.Open(nil, kd.Nonce, encryptedPayload, aadBytes) + if err != nil { + return nil, &secboot.PlatformHandlerError{ + Type: secboot.PlatformHandlerErrorInvalidData, + Err: fmt.Errorf("cannot open payload: %w", err), + } + } + + return payload, nil +} + +func (*platformKeyDataHandler) RecoverKeysWithAuthKey(data *secboot.PlatformKeyData, encryptedPayload, key []byte) ([]byte, error) { + return nil, errors.New("unsupported action") +} + +func (*platformKeyDataHandler) ChangeAuthKey(data *secboot.PlatformKeyData, old, new []byte) ([]byte, error) { + return nil, errors.New("unsupported action") +} + +func init() { + secboot.RegisterPlatformKeyDataHandler(platformName, &platformKeyDataHandler{}) +}