Skip to content

Commit

Permalink
Add dynamic support for AES128/192/256 management keys, including set…
Browse files Browse the repository at this point in the history
…ting them. Prefer AES192 over 3DES when possible.
  • Loading branch information
Quantu committed Jul 1, 2024
1 parent 5a76b44 commit e4fec72
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 56 deletions.
6 changes: 3 additions & 3 deletions piv/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ func marshalASN1(tag byte, data []byte) []byte {
// SetCertificate stores a certificate object in the provided slot. Setting a
// certificate isn't required to use the associated key for signing or
// decryption.
func (yk *YubiKey) SetCertificate(key [24]byte, slot Slot, cert *x509.Certificate) error {
func (yk *YubiKey) SetCertificate(key []byte, slot Slot, cert *x509.Certificate) error {
if err := ykAuthenticate(yk.tx, key, yk.rand); err != nil {
return fmt.Errorf("authenticating with management key: %w", err)
}
Expand Down Expand Up @@ -797,7 +797,7 @@ type Key struct {

// GenerateKey generates an asymmetric key on the card, returning the key's
// public key.
func (yk *YubiKey) GenerateKey(key [24]byte, slot Slot, opts Key) (crypto.PublicKey, error) {
func (yk *YubiKey) GenerateKey(key []byte, slot Slot, opts Key) (crypto.PublicKey, error) {
if err := ykAuthenticate(yk.tx, key, yk.rand); err != nil {
return nil, fmt.Errorf("authenticating with management key: %w", err)
}
Expand Down Expand Up @@ -1005,7 +1005,7 @@ func (yk *YubiKey) PrivateKey(slot Slot, public crypto.PublicKey, auth KeyAuth)
// Keys generated outside of the YubiKey should not be considered hardware-backed,
// as there's no way to prove the key wasn't copied, exfiltrated, or replaced with malicious
// material before being imported.
func (yk *YubiKey) SetPrivateKeyInsecure(key [24]byte, slot Slot, private crypto.PrivateKey, policy Key) error {
func (yk *YubiKey) SetPrivateKeyInsecure(key []byte, slot Slot, private crypto.PrivateKey, policy Key) error {
// Reference implementation
// https://github.com/Yubico/yubico-piv-tool/blob/671a5740ef09d6c5d9d33f6e5575450750b58bde/lib/ykpiv.c#L1812

Expand Down
150 changes: 110 additions & 40 deletions piv/piv.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ package piv

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/des"
"crypto/rand"
"encoding/asn1"
Expand All @@ -36,7 +38,7 @@ var (
// DefaultManagementKey for the PIV applet. The Management Key is a Triple-DES
// key required for slot actions such as generating keys, setting certificates,
// and signing.
DefaultManagementKey = [24]byte{
DefaultManagementKey = []byte{
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
Expand All @@ -59,6 +61,9 @@ const (
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-78-4.pdf#page=17
algTag = 0x80
alg3DES = 0x03
algAES128 = 0x08
algAES192 = 0x0a
algAES256 = 0x0c
algRSA1024 = 0x06
algRSA2048 = 0x07
algECCP256 = 0x11
Expand Down Expand Up @@ -348,7 +353,7 @@ type version struct {
// certificates to slots.
//
// Use DefaultManagementKey if the management key hasn't been set.
func (yk *YubiKey) authManagementKey(key [24]byte) error {
func (yk *YubiKey) authManagementKey(key []byte) error {
return ykAuthenticate(yk.tx, key, yk.rand)
}

Expand All @@ -364,14 +369,37 @@ var (
aidYubiKey = [...]byte{0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01}
)

func ykAuthenticate(tx *scTx, key [24]byte, rand io.Reader) error {
func ykAuthenticate(tx *scTx, key []byte, rand io.Reader) error {
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=92
// https://tsapps.nist.gov/publication/get_pdf.cfm?pub_id=918402#page=114

// request a witness
// determine management key type, this will fail on older keys and that's fine
var managementKeyType byte
cmd := apdu{
instruction: insGetMetadata,
param1: 0x00,
param2: keyCardManagement,
}
resp, err := tx.Transmit(cmd)
if err == nil {
managementKeyType = resp[2:][0]
}

// set challengeLength based on managementKeyType
var challengeLength byte
switch managementKeyType {
case algAES128, algAES192, algAES256:
challengeLength = 16
default:
// default fallback to 3DES
managementKeyType = alg3DES
challengeLength = 8
}

// request a witness
cmd = apdu{
instruction: insAuthenticate,
param1: alg3DES,
param1: managementKeyType,
param2: keyCardManagement,
data: []byte{
0x7c, // Dynamic Authentication Template tag
Expand All @@ -380,7 +408,7 @@ func ykAuthenticate(tx *scTx, key [24]byte, rand io.Reader) error {
0x00, // Return encrypted random
},
}
resp, err := tx.Transmit(cmd)
resp, err = tx.Transmit(cmd)
if err != nil {
return fmt.Errorf("get auth challenge: %w", err)
}
Expand All @@ -389,45 +417,55 @@ func ykAuthenticate(tx *scTx, key [24]byte, rand io.Reader) error {
}
if !bytes.Equal(resp[:4], []byte{
0x7c,
0x0a,
0x80, // 'Witness'
0x08, // Tag length
challengeLength + 2,
0x80, // 'Witness'
challengeLength, // Tag length
}) {
return fmt.Errorf("invalid authentication object header: %x", resp[:4])
}

cardChallenge := resp[4 : 4+8]
cardResponse := make([]byte, 8)

block, err := des.NewTripleDESCipher(key[:])
if err != nil {
return fmt.Errorf("creating triple des block cipher: %v", err)
var block cipher.Block
switch managementKeyType {
case algAES128, algAES192, algAES256:
block, err = aes.NewCipher(key[:])
if err != nil {
return fmt.Errorf("creating aes block cipher: %v", err)
}
default:
block, err = des.NewTripleDESCipher(key[:])
if err != nil {
return fmt.Errorf("creating des block cipher: %v", err)
}
}

cardChallenge := resp[4 : 4+challengeLength]
cardResponse := make([]byte, challengeLength)

block.Decrypt(cardResponse, cardChallenge)

challenge := make([]byte, 8)
challenge := make([]byte, challengeLength)
if _, err := io.ReadFull(rand, challenge); err != nil {
return fmt.Errorf("reading rand data: %v", err)
}
response := make([]byte, 8)
response := make([]byte, challengeLength)
block.Encrypt(response, challenge)

data := []byte{
0x7c, // Dynamic Authentication Template tag
20, // 2+8+2+8
0x80, // 'Witness'
0x08, // Tag length
(challengeLength + 2) * 2,
0x80, // 'Witness'
challengeLength, // Tag length
}
data = append(data, cardResponse...)
data = append(data,
0x81, // 'Challenge'
0x08, // Tag length
0x81, // 'Challenge'
challengeLength, // Tag length
)
data = append(data, challenge...)

cmd = apdu{
instruction: insAuthenticate,
param1: alg3DES,
param1: managementKeyType,
param2: keyCardManagement,
data: data,
}
Expand All @@ -440,13 +478,13 @@ func ykAuthenticate(tx *scTx, key [24]byte, rand io.Reader) error {
}
if !bytes.Equal(resp[:4], []byte{
0x7c,
0x0a,
challengeLength + 2,
0x82, // 'Response'
0x08,
challengeLength,
}) {
return fmt.Errorf("response invalid authentication object header: %x", resp[:4])
}
if !bytes.Equal(resp[4:4+8], response) {
if !bytes.Equal(resp[4:4+challengeLength], response) {
return fmt.Errorf("challenge failed")
}

Expand All @@ -457,14 +495,17 @@ func ykAuthenticate(tx *scTx, key [24]byte, rand io.Reader) error {
// are triple-des keys, however padding isn't verified. To generate a new key,
// generate 24 random bytes.
//
// Note: Yubikeys also support aes128, aes192, and aes256 management keys,
// which are 16, 24, and 32 bytes, respectively.
//
// var newKey [24]byte
// if _, err := io.ReadFull(rand.Reader, newKey[:]); err != nil {
// // ...
// }
// if err := yk.SetManagementKey(piv.DefaultManagementKey, newKey); err != nil {
// if err := yk.SetManagementKey(piv.DefaultManagementKey, newKey[:]); err != nil {
// // ...
// }
func (yk *YubiKey) SetManagementKey(oldKey, newKey [24]byte) error {
func (yk *YubiKey) SetManagementKey(oldKey, newKey []byte) error {
if err := ykAuthenticate(yk.tx, oldKey, yk.rand); err != nil {
return fmt.Errorf("authenticating with old key: %w", err)
}
Expand All @@ -476,19 +517,48 @@ func (yk *YubiKey) SetManagementKey(oldKey, newKey [24]byte) error {

// ykSetManagementKey updates the management key to a new key. This requires
// authenticating with the existing management key.
func ykSetManagementKey(tx *scTx, key [24]byte, touch bool) error {
func ykSetManagementKey(tx *scTx, key []byte, touch bool) error {
var managementKeyType byte
switch len(key) {
case 16:
managementKeyType = algAES128
case 24:
// could also be 3DES key, but AES192 is preferred so try that first
managementKeyType = algAES192
case 32:
managementKeyType = algAES256
default:
return fmt.Errorf("invalid new management key length: %d", len(key))
}
cmd := apdu{
instruction: insSetMGMKey,
param1: 0xff,
param2: 0xff,
data: append([]byte{
alg3DES, keyCardManagement, 24,
managementKeyType, keyCardManagement, byte(len(key)),
}, key[:]...),
}
if touch {
cmd.param2 = 0xfe
}
if _, err := tx.Transmit(cmd); err != nil {
_, err := tx.Transmit(cmd)
if err != nil && len(key) == 24 {
// if setting AES192 managment key failed, fallback to 3DES
managementKeyType = alg3DES
cmd := apdu{
instruction: insSetMGMKey,
param1: 0xff,
param2: 0xff,
data: append([]byte{
managementKeyType, keyCardManagement, byte(len(key)),
}, key[:]...),
}
if touch {
cmd.param2 = 0xfe
}
_, err = tx.Transmit(cmd)
}
if err != nil {
return fmt.Errorf("command failed: %w", err)
}
return nil
Expand Down Expand Up @@ -654,7 +724,7 @@ func (yk *YubiKey) Metadata(pin string) (*Metadata, error) {
// SetMetadata sets PIN protected metadata on the key. This is primarily to
// store the management key on the smart card instead of managing the PIN and
// management key seperately.
func (yk *YubiKey) SetMetadata(key [24]byte, m *Metadata) error {
func (yk *YubiKey) SetMetadata(key []byte, m *Metadata) error {
return ykSetProtectedMetadata(yk.tx, key, m)
}

Expand All @@ -663,7 +733,7 @@ func (yk *YubiKey) SetMetadata(key [24]byte, m *Metadata) error {
// guarded by the PIN.
type Metadata struct {
// ManagementKey is the management key stored directly on the YubiKey.
ManagementKey *[24]byte
ManagementKey *[]byte

// raw, if not nil, is the full bytes
raw []byte
Expand All @@ -679,7 +749,7 @@ func (m *Metadata) marshal() ([]byte, error) {
26,
0x89,
24,
}, m.ManagementKey[:]...), nil
}, *m.ManagementKey...), nil
}

if m.ManagementKey == nil {
Expand Down Expand Up @@ -714,7 +784,7 @@ func (m *Metadata) marshal() ([]byte, error) {
metadata.Bytes = append(metadata.Bytes, v.FullBytes...)
}
metadata.Bytes = append(metadata.Bytes, 0x89, 24)
metadata.Bytes = append(metadata.Bytes, m.ManagementKey[:]...)
metadata.Bytes = append(metadata.Bytes, *m.ManagementKey...)
return asn1.Marshal(metadata)
}

Expand All @@ -741,12 +811,12 @@ func (m *Metadata) unmarshal(b []byte) error {
continue
}
// 0x89 indicates key
if len(v.Bytes) != 24 {
switch len(v.Bytes) {
case 16, 24, 32:
default:
return fmt.Errorf("invalid management key length: %d", len(v.Bytes))
}
var key [24]byte
copy(key[:], v.Bytes)
m.ManagementKey = &key
m.ManagementKey = &v.Bytes
}
return nil
}
Expand Down Expand Up @@ -784,7 +854,7 @@ func ykGetProtectedMetadata(tx *scTx, pin string) (*Metadata, error) {
return &m, nil
}

func ykSetProtectedMetadata(tx *scTx, key [24]byte, m *Metadata) error {
func ykSetProtectedMetadata(tx *scTx, key []byte, m *Metadata) error {
data, err := m.marshal()
if err != nil {
return fmt.Errorf("encoding metadata: %v", err)
Expand Down
Loading

0 comments on commit e4fec72

Please sign in to comment.