-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c7d0f3b
commit ad0c2c1
Showing
4 changed files
with
342 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |