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 ad0c2c1
Show file tree
Hide file tree
Showing 4 changed files with 342 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
182 changes: 182 additions & 0 deletions plainkey/keydata.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
*/

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
}
155 changes: 155 additions & 0 deletions plainkey/platform.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
*/

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

0 comments on commit ad0c2c1

Please sign in to comment.