Skip to content

Commit

Permalink
Add plainkey platform
Browse files Browse the repository at this point in the history
This platform provides a way to unlock an encrypted container using a
key recovered from unlocking another container, and is suitable when
access to one encrypted container implies access to another one.

The intention of this is to replace the existing mechanism for unlocking
the save partition in run mode with something that uses a proper keyslot.

Keys are protected with a "platform key" - this is the key that is
stored in the first encrypted container. For the intended use case,
this key is stored inside the data partition, and then loaded by
snap-bootstrap, which should call plainkey.SetPlatformKeys before
unlocking the save partition.

Although normally there will only be a single key supplied to
SetPlatformKeys, it supports setting multiple keys and then matching
a key data object to the correct one. This is to support the possibility
of changing this key during reprovisioning (ie, as part of factory
reset), where there would need to exist 2 keys (the old and new one)
temporarily - this would prevent against loss of data if the process
was interrupted at any point.
  • Loading branch information
chrisccoulson committed Feb 19, 2024
1 parent 179584a commit 57c878b
Show file tree
Hide file tree
Showing 8 changed files with 987 additions and 6 deletions.
7 changes: 3 additions & 4 deletions export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import (
var (
UnmarshalV1KeyPayload = unmarshalV1KeyPayload
UnmarshalProtectedKeys = unmarshalProtectedKeys
KeyDataGeneration = keyDataGeneration
)

type ProtectedKeys = protectedKeys
Expand Down Expand Up @@ -129,10 +128,10 @@ func MockStderr(w io.Writer) (restore func()) {
}

func MockKeyDataGeneration(n int) (restore func()) {
orig := keyDataGeneration
keyDataGeneration = n
orig := KeyDataGeneration
KeyDataGeneration = n
return func() {
keyDataGeneration = orig
KeyDataGeneration = orig
}
}

Expand Down
4 changes: 2 additions & 2 deletions keydata.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const (
)

var (
keyDataGeneration int = 2
KeyDataGeneration 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}
Expand Down Expand Up @@ -940,7 +940,7 @@ func NewKeyData(params *KeyParams) (*KeyData, error) {

kd := &KeyData{
data: keyData{
Generation: keyDataGeneration,
Generation: KeyDataGeneration,
PlatformName: params.PlatformName,
PlatformHandle: json.RawMessage(encodedHandle),
KDFAlg: hashAlg(params.KDFAlg),
Expand Down
42 changes: 42 additions & 0 deletions plainkey/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// -*- 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
)

func MockSecbootNewKeyData(fn func(*secboot.KeyParams) (*secboot.KeyData, error)) (restore func()) {
orig := secbootNewKeyData
secbootNewKeyData = fn
return func() {
secbootNewKeyData = orig
}
}
248 changes: 248 additions & 0 deletions plainkey/keydata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
// -*- 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"

"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}

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
}

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

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, kdfAlg, 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,
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)
}

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 := secbootNewKeyData(&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
}
Loading

0 comments on commit 57c878b

Please sign in to comment.