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..8e9d3621
--- /dev/null
+++ b/plainkey/keydata.go
@@ -0,0 +1,182 @@
+// -*- 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"
+ "fmt"
+ "io"
+
+ "golang.org/x/crypto/cryptobyte"
+ cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
+
+ "github.com/snapcore/secboot"
+)
+
+var (
+ 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) 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
+ })
+}
+
+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 stored inside of an encrypted container that is unlocked
+// via another mechanism, such as a TPM, and then loaded via [InitPlatform] 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(idAlg.New, 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..488f8aa6
--- /dev/null
+++ b/plainkey/platform.go
@@ -0,0 +1,155 @@
+// -*- 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"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/hmac"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "runtime"
+ "sync/atomic"
+
+ "golang.org/x/crypto/cryptobyte"
+
+ "github.com/snapcore/secboot"
+)
+
+const (
+ platformName = "plainkey"
+)
+
+var (
+ platformStatus uint32
+ platformKeys [][]byte
+)
+
+const (
+ statusUninitialized uint32 = 0
+ statusInitializing uint32 = 1
+ statusInitialized uint32 = 2
+)
+
+// InitPlatform sets the key used to protect and recover objects with this platform.
+func InitPlatform(keys ...[]byte) {
+ if !atomic.CompareAndSwapUint32(&platformStatus, statusUninitialized, statusInitializing) {
+ panic("cannot call InitPlatform more than once")
+ }
+ platformKeys = keys
+ secboot.RegisterPlatformKeyDataHandler(platformName, &platformKeyDataHandler{})
+ atomic.StoreUint32(&platformStatus, statusInitialized)
+}
+
+func getPlatformKey(id *platformKeyId) ([]byte, error) {
+ alg := crypto.Hash(id.Alg)
+ if !alg.Available() {
+ return nil, errors.New("digest algorithm unavailable")
+ }
+
+ for {
+ switch atomic.LoadUint32(&platformStatus) {
+ case statusUninitialized:
+ // Nothing has called InitPlatform yet
+ panic("must call InitPlatform")
+ case statusInitializing:
+ // InitPlatform is currently executing, so it's ok to busy loop here
+ runtime.Gosched()
+ case statusInitialized:
+ // InitPlatform was called
+ for _, key := range platformKeys {
+ h := hmac.New(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")
+ default:
+ panic("unexpected status value")
+ }
+ }
+}
+
+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")
+}