Skip to content

Commit

Permalink
[WIP] add PIN support to KeyData
Browse files Browse the repository at this point in the history
This adds PIN support to KeyData which is distinct from the existing
passphrase support.

Passphrases are used both for authentication with the hardware element
(such as the TPM) and for additional encryption on the host CPU with a
passphrase derived key, and are intended to use a memory hard key
derivation. The intention here is that this configuration provides some
additional protection in the event of a TPM compromise (eg, say a TPM
manufacturer is coerced by a government agency to provide firmware that
bypasses authentications), where sensitive data is able to be extracted
without the usual authentication, because extracting the secret from the
TPM will not be sufficient to obtain all of the key material necessary
to unlock a device.

PINs (in the literal sense) have a fairly low entropy - an 8 digit PIN
only has an entropy of 26.5bits, so this additional encryption will
provide little protection in the event of a TPM compromise - if
sensitive data is obtained from the TPM, the 26.5bits of entropy won't
provide a significant barrier to deriving the remaining key material
necessary to unlock a device. We take advantage of this by implementing
distinct PIN support that is only used for authentication. With this in
mind, the memory hard key derivation does not provide a lot of benefit,
so PINs only support PBKDF2, and it can be configured to run faster
than the key derivation for passphrases. In that sense, PIN support is
essentially just a faster and slightly weaker passphrase.

As the PIN is a PIN in the literal sense, it is encoded as a length
prefixed binary number before going through the key derivation.

This only implements the support to KeyData for now - unlocking support
will be added in another PR.
  • Loading branch information
chrisccoulson committed Mar 9, 2024
1 parent 80b2a5f commit 0ba2bf4
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 1 deletion.
147 changes: 146 additions & 1 deletion keydata.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ type AuthMode uint8
const (
AuthModeNone AuthMode = iota
AuthModePassphrase
AuthModePIN
)

// KeyParams provides parameters required to create a new KeyData object.
Expand Down Expand Up @@ -162,6 +163,15 @@ type KeyWithPassphraseParams struct {
AuthKeySize int
}

type KeyWithPINParams struct {
KeyParams
KDFOptions *PBKDF2Options // The PIN KDF options

// AuthKeySize is the size of key to derive from the PIN for
// use by the platform implementation.
AuthKeySize int
}

// KeyID is the unique ID for a KeyData object. It is used to facilitate the
// sharing of state between the early boot environment and OS runtime.
type KeyID []byte
Expand Down Expand Up @@ -297,6 +307,11 @@ type passphraseParams struct {
AuthKeySize int `json:"auth_key_size"` // Size of auth key to derive from passphrase derived key
}

type pinParams struct {
KDF kdfData `json:"kdf"`
AuthKeySize int `json:"auth_key_size"`
}

type keyData struct {
// Generation is a number used to differentiate between different key formats.
// i.e Gen1 keys are binary serialized and include a primary and an unlock key while
Expand Down Expand Up @@ -331,6 +346,7 @@ type keyData struct {
EncryptedPayload []byte `json:"encrypted_payload"`

PassphraseParams *passphraseParams `json:"passphrase_params,omitempty"`
PINParams *pinParams `json:"pin_params,omitempty"`

// AuthorizedSnapModels contains information about the Snap models
// that have been authorized to access the data protected by this key.
Expand Down Expand Up @@ -389,7 +405,7 @@ func (d *KeyData) derivePassphraseKeys(passphrase string) (key, iv, auth []byte,
return nil, nil, nil, fmt.Errorf("unavailable leaf KDF digest algorithm %v", kdfAlg)
}

// Include derivation parameters in the Argon2 salt in order to protect them
// Include derivation parameters in the KDF salt in order to protect them
builder := cryptobyte.NewBuilder(nil)
builder.AddASN1(cryptobyte_asn1.SEQUENCE, func(b *cryptobyte.Builder) { // SEQUENCE {
b.AddASN1OctetString(params.KDF.Salt) // salt OCTET STRING
Expand Down Expand Up @@ -462,6 +478,32 @@ func (d *KeyData) derivePassphraseKeys(passphrase string) (key, iv, auth []byte,
return key, iv, auth, nil
}

func (d *KeyData) derivePINAuthKey(pin PIN) ([]byte, error) {
if d.data.PINParams == nil {
return nil, errors.New("no PIN params")
}

params := d.data.PINParams
if params.AuthKeySize < 0 {
return nil, fmt.Errorf("invalid auth key size (%d bytes)", params.AuthKeySize)
}
if params.KDF.Time < 0 {
return nil, fmt.Errorf("invalid KDF time (%d)", params.KDF.Time)
}
if params.KDF.Type != pbkdf2Type {
return nil, fmt.Errorf("unexpected KDF type \"%s\"", params.KDF.Type)
}

pbkdfParams := &pbkdf2.Params{
Iterations: int32(params.KDF.Time),
HashAlg: crypto.Hash(params.KDF.Hash),
}
if !pbkdfParams.HashAlg.Available() {
return nil, fmt.Errorf("unavailable pbkdf2 digest algorithm %v", pbkdfParams.HashAlg)
}
return pbkdf2.Key(string(pin.Bytes()), params.KDF.Salt, pbkdfParams, int32(params.AuthKeySize)), nil
}

func (d *KeyData) updatePassphrase(payload, oldAuthKey []byte, passphrase string) error {
handler := handlers[d.data.PlatformName]
if handler == nil {
Expand Down Expand Up @@ -497,6 +539,26 @@ func (d *KeyData) updatePassphrase(payload, oldAuthKey []byte, passphrase string
return nil
}

func (d *KeyData) updatePIN(oldAuthKey []byte, pin PIN) error {
handler := handlers[d.data.PlatformName]
if handler == nil {
return ErrNoPlatformHandlerRegistered
}

authKey, err := d.derivePINAuthKey(pin)
if err != nil {
return err
}

handle, err := handler.ChangeAuthKey(d.platformKeyData(), oldAuthKey, authKey)
if err != nil {
return err
}

d.data.PlatformHandle = handle
return nil
}

func (d *KeyData) openWithPassphrase(passphrase string) (payload []byte, authKey []byte, err error) {
key, iv, authKey, err := d.derivePassphraseKeys(passphrase)
if err != nil {
Expand Down Expand Up @@ -589,6 +651,8 @@ func (d *KeyData) AuthMode() (out AuthMode) {
switch {
case d.data.PassphraseParams != nil:
return AuthModePassphrase
case d.data.PINParams != nil:
return AuthModePIN
default:
return AuthModeNone
}
Expand Down Expand Up @@ -678,6 +742,29 @@ func (d *KeyData) RecoverKeysWithPassphrase(passphrase string) (DiskUnlockKey, P
return d.recoverKeysCommon(c)
}

func (d *KeyData) RecoverKeysWithPIN(pin PIN) (DiskUnlockKey, PrimaryKey, error) {
if d.AuthMode() != AuthModePIN {
return nil, nil, errors.New("cannot recover key with PIN")
}

handler := handlers[d.data.PlatformName]
if handler == nil {
return nil, nil, ErrNoPlatformHandlerRegistered
}

key, err := d.derivePINAuthKey(pin)
if err != nil {
return nil, nil, err
}

c, err := handler.RecoverKeysWithAuthKey(d.platformKeyData(), d.data.EncryptedPayload, key)
if err != nil {
return nil, nil, processPlatformHandlerError(err)
}

return d.recoverKeysCommon(c)
}

// ChangePassphrase updates the passphrase used to recover the keys from this key data
// via the KeyData.RecoverKeysWithPassphrase API. This can only be called if a passhphrase
// has been set previously (KeyData.AuthMode returns AuthModePassphrase).
Expand All @@ -700,6 +787,23 @@ func (d *KeyData) ChangePassphrase(oldPassphrase, newPassphrase string) error {
return nil
}

func (d *KeyData) ChangePIN(oldPIN, newPIN PIN) error {
if d.AuthMode()&AuthModePIN == 0 {
return errors.New("cannot change PIN without setting an initial PIN")
}

oldKey, err := d.derivePINAuthKey(oldPIN)
if err != nil {
return err
}

if err := d.updatePIN(oldKey, newPIN); err != nil {
return processPlatformHandlerError(err)
}

return nil
}

// WriteAtomic saves this key data to the supplied KeyDataWriter.
func (d *KeyData) WriteAtomic(w KeyDataWriter) error {
enc := json.NewEncoder(w)
Expand Down Expand Up @@ -794,6 +898,47 @@ func NewKeyDataWithPassphrase(params *KeyWithPassphraseParams, passphrase string
return kd, nil
}

func NewKeyDataWithPIN(params *KeyWithPINParams, pin PIN) (*KeyData, error) {
kd, err := NewKeyData(&params.KeyParams)
if err != nil {
return nil, err
}

kdfOptions := params.KDFOptions
if kdfOptions == nil {
var defaultOptions PBKDF2Options
kdfOptions = &defaultOptions
}

if params.AuthKeySize < 0 {
return nil, errors.New("invalid auth key size")
}

kdfParams, err := kdfOptions.kdfParams(uint32(params.AuthKeySize))
if err != nil {
return nil, xerrors.Errorf("cannot derive KDF cost parameters: %w", err)
}

var salt [16]byte
if _, err := rand.Read(salt[:]); err != nil {
return nil, xerrors.Errorf("cannot read salt: %w", err)
}

kd.data.PINParams = &pinParams{
KDF: kdfData{
Salt: salt[:],
kdfParams: *kdfParams,
},
AuthKeySize: params.AuthKeySize,
}

if err := kd.updatePIN(make([]byte, params.AuthKeySize), pin); err != nil {
return nil, xerrors.Errorf("cannot set PIN: %w", err)
}

return kd, nil
}

// protectedKeys is used to pack a primary key and a unique value from which
// an unlock key is derived.
type protectedKeys struct {
Expand Down
57 changes: 57 additions & 0 deletions pin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2024 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 secboot

import (
"errors"
"fmt"
"math"
"math/big"
)

type PIN struct {
length uint8 // the length of the input PIN. This is *not* the length of the encoded binary number
value big.Int // the PIN value. This is encoded in big-endian form without leading zeroes.
}

func ParsePIN(s string) (PIN, error) {
l := len(s)
if l > math.MaxUint8 {
return PIN{}, errors.New("invalid PIN: too long")
}

val, ok := new(big.Int).SetString(s, 10)
if !ok {
return PIN{}, errors.New("invalid PIN")
}

return PIN{
length: uint8(l),
value: *val,
}, nil
}

func (p PIN) String() string {
return fmt.Sprintf("%0*s", p.length, p.value.String())
}

func (p PIN) Bytes() []byte {
return append([]byte{p.length}, p.value.Bytes()...)
}
36 changes: 36 additions & 0 deletions pin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package secboot_test

import (
. "github.com/snapcore/secboot"
"github.com/snapcore/secboot/internal/testutil"

. "gopkg.in/check.v1"
)

type pinSuite struct{}

var _ = Suite(&pinSuite{})

func (s *pinSuite) TestPIN(c *C) {
pin, err := ParsePIN("1234")
c.Assert(err, IsNil)

c.Check(pin.String(), Equals, "1234")
c.Check(pin.Bytes(), DeepEquals, testutil.DecodeHexString(c, "0404d2"))
}

func (s *pinSuite) TestPINZeroPaddedIsDifferent(c *C) {
pin, err := ParsePIN("00001234")
c.Assert(err, IsNil)

c.Check(pin.String(), Equals, "00001234")
c.Check(pin.Bytes(), DeepEquals, testutil.DecodeHexString(c, "0804d2"))
}

func (s *pinSuite) TestPIN2(c *C) {
pin, err := ParsePIN("12345678")
c.Assert(err, IsNil)

c.Check(pin.String(), Equals, "12345678")
c.Check(pin.Bytes(), DeepEquals, testutil.DecodeHexString(c, "08bc614e"))
}

0 comments on commit 0ba2bf4

Please sign in to comment.