Skip to content

Commit

Permalink
[WIP]: add plainkey platform
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisccoulson committed Feb 2, 2024
1 parent c7d0f3b commit cd5594c
Show file tree
Hide file tree
Showing 4 changed files with 399 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
KeyDataVersion = keyDataVersion
)

type ProtectedKeys = protectedKeys
Expand Down Expand Up @@ -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
}
}
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 (
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}
Expand Down Expand Up @@ -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),
Expand Down
246 changes: 246 additions & 0 deletions plainkey/keydata.go
Original file line number Diff line number Diff line change
@@ -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 <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}
)

// 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
}
Loading

0 comments on commit cd5594c

Please sign in to comment.