From 6b1beddb187c9d6cb05bb9acbdc98151d38cbe6c Mon Sep 17 00:00:00 2001 From: Quantu <673216+Quantu@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:19:01 -0400 Subject: [PATCH] Add dynamic support for AES128/192/256 management keys, including setting them. Prefer AES192 over 3DES when possible. Update supportsVersion to use *version and bytes instead of Version and ints to eliminate a lot of unnecessary calls to .Version() --- README.md | 7 +- v2/piv/key.go | 14 ++-- v2/piv/key_test.go | 2 +- v2/piv/piv.go | 166 +++++++++++++++++++++++++++++++-------------- v2/piv/piv_test.go | 33 +++++---- 5 files changed, 142 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 059ce19..d19fb84 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ if err != nil { The PIV applet has three unique credentials: * Management key (3DES key) used to generate new keys on the YubiKey. + * YubiKey firmware 5.4.0+ adds support for AES128/192/256 keys * PIN (up to 8 digits, usually 6) used to access signing operations. * PUK (up to 8 digits) used to unblock the PIN. Usually set once and thrown away or managed by an administrator. @@ -103,7 +104,7 @@ newPIN := fmt.Sprintf("%06d", newPINInt) newPUK := fmt.Sprintf("%08d", newPUKInt) // Set all values to a new value. -if err := yk.SetManagementKey(piv.DefaultManagementKey, newKey); err != nil { +if err := yk.SetManagementKey(piv.DefaultManagementKey, newKey[:]); err != nil { // ... } if err := yk.SetPUK(piv.DefaultPUK, newPUK); err != nil { @@ -113,8 +114,8 @@ if err := yk.SetPIN(piv.DefaultPIN, newPIN); err != nil { // ... } // Store management key on the YubiKey. -m := piv.Metadata{ManagementKey: &newKey} -if err := yk.SetMetadata(newKey, m); err != nil { +m := piv.Metadata{ManagementKey: &newKey[:]} +if err := yk.SetMetadata(newKey[:], m); err != nil { // ... } diff --git a/v2/piv/key.go b/v2/piv/key.go index 7fbe053..aa82d74 100644 --- a/v2/piv/key.go +++ b/v2/piv/key.go @@ -743,8 +743,8 @@ 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 { - if err := ykAuthenticate(yk.tx, key, yk.rand); err != nil { +func (yk *YubiKey) SetCertificate(key []byte, slot Slot, cert *x509.Certificate) error { + if err := ykAuthenticate(yk.tx, key, yk.rand, yk.version); err != nil { return fmt.Errorf("authenticating with management key: %w", err) } return ykStoreCertificate(yk.tx, slot, cert) @@ -797,8 +797,8 @@ 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) { - if err := ykAuthenticate(yk.tx, key, yk.rand); err != nil { +func (yk *YubiKey) GenerateKey(key []byte, slot Slot, opts Key) (crypto.PublicKey, error) { + if err := ykAuthenticate(yk.tx, key, yk.rand, yk.version); err != nil { return nil, fmt.Errorf("authenticating with management key: %w", err) } return ykGenerateKey(yk.tx, slot, opts) @@ -926,7 +926,7 @@ func (k KeyAuth) do(yk *YubiKey, pp PINPolicy, f func(tx *scTx) ([]byte, error)) } func pinPolicy(yk *YubiKey, slot Slot) (PINPolicy, error) { - if supportsVersion(yk.Version(), 5, 3, 0) { + if supportsVersion(yk.version, 5, 3, 0) { info, err := yk.KeyInfo(slot) if err != nil { return 0, fmt.Errorf("get key info: %v", err) @@ -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 @@ -1073,7 +1073,7 @@ func (yk *YubiKey) SetPrivateKeyInsecure(key [24]byte, slot Slot, private crypto tags = append(tags, param...) } - if err := ykAuthenticate(yk.tx, key, yk.rand); err != nil { + if err := ykAuthenticate(yk.tx, key, yk.rand, yk.version); err != nil { return fmt.Errorf("authenticating with management key: %w", err) } diff --git a/v2/piv/key_test.go b/v2/piv/key_test.go index d366dae..fec6739 100644 --- a/v2/piv/key_test.go +++ b/v2/piv/key_test.go @@ -202,7 +202,7 @@ func TestPINPrompt(t *testing.T) { } func supportsAttestation(yk *YubiKey) bool { - return supportsVersion(yk.Version(), 4, 3, 0) + return supportsVersion(yk.version, 4, 3, 0) } func TestSlots(t *testing.T) { diff --git a/v2/piv/piv.go b/v2/piv/piv.go index a351665..e3356dd 100644 --- a/v2/piv/piv.go +++ b/v2/piv/piv.go @@ -16,6 +16,8 @@ package piv import ( "bytes" + "crypto/aes" + "crypto/cipher" "crypto/des" "crypto/rand" "encoding/asn1" @@ -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, @@ -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 @@ -348,8 +353,8 @@ type version struct { // certificates to slots. // // Use DefaultManagementKey if the management key hasn't been set. -func (yk *YubiKey) authManagementKey(key [24]byte) error { - return ykAuthenticate(yk.tx, key, yk.rand) +func (yk *YubiKey) authManagementKey(key []byte) error { + return ykAuthenticate(yk.tx, key, yk.rand, yk.version) } var ( @@ -364,14 +369,39 @@ 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, version *version) 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 + var managementKeyType byte + if supportsVersion(version, 5, 3, 0) { + // if yubikey version >= 5.3.0, determine management key type using slot metadata + 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 @@ -389,45 +419,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, } @@ -440,13 +480,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") } @@ -457,18 +497,21 @@ 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 { - if err := ykAuthenticate(yk.tx, oldKey, yk.rand); err != nil { +func (yk *YubiKey) SetManagementKey(oldKey, newKey []byte) error { + if err := ykAuthenticate(yk.tx, oldKey, yk.rand, yk.version); err != nil { return fmt.Errorf("authenticating with old key: %w", err) } - if err := ykSetManagementKey(yk.tx, newKey, false); err != nil { + if err := ykSetManagementKey(yk.tx, newKey, false, yk.version); err != nil { return err } return nil @@ -476,13 +519,32 @@ 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, version *version) error { + var managementKeyType byte + if supportsVersion(version, 5, 4, 0) { + // if yubikey version >= 5.4.0, set AES management key + switch len(key) { + case 16: + managementKeyType = algAES128 + case 24: + managementKeyType = algAES192 + case 32: + managementKeyType = algAES256 + default: + return fmt.Errorf("invalid new AES management key length: %d bytes (expected 16, 24, or 32)", len(key)) + } + } else if len(key) == 24 { + // if yubikey version < 5.4.0, set legacy 3DES management key + managementKeyType = alg3DES + } else { + return fmt.Errorf("invalid new 3DES management key length: %d bytes (expected 24)", len(key)) + } cmd := apdu{ instruction: insSetMGMKey, param1: 0xff, param2: 0xff, data: append([]byte{ - alg3DES, keyCardManagement, 24, + managementKeyType, keyCardManagement, byte(len(key)), }, key[:]...), } if touch { @@ -654,8 +716,8 @@ 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 { - return ykSetProtectedMetadata(yk.tx, key, m) +func (yk *YubiKey) SetMetadata(key []byte, m *Metadata) error { + return ykSetProtectedMetadata(yk.tx, key, m, yk.rand, yk.version) } // Metadata holds protected metadata. This is primarily used by YubiKey manager @@ -663,7 +725,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 @@ -679,7 +741,7 @@ func (m *Metadata) marshal() ([]byte, error) { 26, 0x89, 24, - }, m.ManagementKey[:]...), nil + }, *m.ManagementKey...), nil } if m.ManagementKey == nil { @@ -714,7 +776,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) } @@ -741,12 +803,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 } @@ -784,7 +846,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, rand io.Reader, version *version) error { data, err := m.marshal() if err != nil { return fmt.Errorf("encoding metadata: %v", err) @@ -796,29 +858,29 @@ func ykSetProtectedMetadata(tx *scTx, key [24]byte, m *Metadata) error { 0xc1, 0x09, }, marshalASN1(0x53, data)...) + // NOTE: for some reason this action requires the management key authenticated + // on the same transaction. It doesn't work otherwise. + if err := ykAuthenticate(tx, key, rand, version); err != nil { + return fmt.Errorf("authenticating with key: %w", err) + } cmd := apdu{ instruction: insPutData, param1: 0x3f, param2: 0xff, data: data, } - // NOTE: for some reason this action requires the management key authenticated - // on the same transaction. It doesn't work otherwise. - if err := ykAuthenticate(tx, key, rand.Reader); err != nil { - return fmt.Errorf("authenticating with key: %w", err) - } if _, err := tx.Transmit(cmd); err != nil { return fmt.Errorf("command failed: %w", err) } return nil } -func supportsVersion(v Version, major, minor, patch int) bool { - if v.Major != major { - return v.Major > major +func supportsVersion(v *version, major, minor, patch byte) bool { + if v.major != major { + return v.major > major } - if v.Minor != minor { - return v.Minor > minor + if v.minor != minor { + return v.minor > minor } - return v.Patch >= patch + return v.patch >= patch } diff --git a/v2/piv/piv_test.go b/v2/piv/piv_test.go index d6f1295..ee4ceed 100644 --- a/v2/piv/piv_test.go +++ b/v2/piv/piv_test.go @@ -49,10 +49,9 @@ func testGetVersion(t *testing.T, h *scHandle) { } } -func testRequiresVersion(t *testing.T, yk *YubiKey, major, minor, patch int) { - v := yk.Version() - if !supportsVersion(v, major, minor, patch) { - t.Skipf("test requires yubikey version %d.%d.%d: got %d.%d.%d", major, minor, patch, v.Major, v.Minor, v.Patch) +func testRequiresVersion(t *testing.T, yk *YubiKey, major, minor, patch byte) { + if !supportsVersion(yk.version, major, minor, patch) { + t.Skipf("test requires yubikey version %d.%d.%d: got %d.%d.%d", major, minor, patch, yk.version.major, yk.version.minor, yk.version.patch) } } @@ -212,13 +211,13 @@ func TestYubiKeySetManagementKey(t *testing.T) { t.Fatalf("generating management key: %v", err) } - if err := yk.SetManagementKey(DefaultManagementKey, mgmtKey); err != nil { + if err := yk.SetManagementKey(DefaultManagementKey, mgmtKey[:]); err != nil { t.Fatalf("setting management key: %v", err) } - if err := yk.authManagementKey(mgmtKey); err != nil { + if err := yk.authManagementKey(mgmtKey[:]); err != nil { t.Errorf("authenticating with new management key: %v", err) } - if err := yk.SetManagementKey(mgmtKey, DefaultManagementKey); err != nil { + if err := yk.SetManagementKey(mgmtKey[:], DefaultManagementKey); err != nil { t.Fatalf("resetting management key: %v", err) } } @@ -296,13 +295,13 @@ func TestChangeManagementKey(t *testing.T) { newKey[i] = b ^ 1 // flip least significant bit } } - if err := yk.SetManagementKey(newKey, newKey); err == nil { + if err := yk.SetManagementKey(newKey[:], newKey[:]); err == nil { t.Errorf("successfully changed management key with invalid key, expected error") } - if err := yk.SetManagementKey(DefaultManagementKey, newKey); err != nil { + if err := yk.SetManagementKey(DefaultManagementKey, newKey[:]); err != nil { t.Fatalf("changing management key: %v", err) } - if err := yk.SetManagementKey(newKey, DefaultManagementKey); err != nil { + if err := yk.SetManagementKey(newKey[:], DefaultManagementKey); err != nil { t.Fatalf("resetting management key: %v", err) } } @@ -328,7 +327,7 @@ func TestMetadata(t *testing.T) { t.Errorf("expected no management key set") } - wantKey := [24]byte{ + wantKey := []byte{ 0x09, 0xd9, 0x87, 0x81, 0xfb, 0xdc, 0xc9, 0xb6, 0x91, 0xa2, 0x05, 0x80, 0x6e, 0xc0, 0xba, 0x84, 0x31, 0xac, 0x0d, 0x9f, 0x59, 0xa5, 0x00, 0xad, @@ -345,14 +344,14 @@ func TestMetadata(t *testing.T) { } if got.ManagementKey == nil { t.Errorf("no management key") - } else if *got.ManagementKey != wantKey { + } else if !bytes.Equal(*got.ManagementKey, wantKey) { t.Errorf("wanted management key=0x%x, got=0x%x", wantKey, got.ManagementKey) } } func TestMetadataUnmarshal(t *testing.T) { data, _ := hex.DecodeString("881a891809d98781fbdcc9b691a205806ec0ba8431ac0d9f59a500ad") - wantKey := [24]byte{ + wantKey := []byte{ 0x09, 0xd9, 0x87, 0x81, 0xfb, 0xdc, 0xc9, 0xb6, 0x91, 0xa2, 0x05, 0x80, 0x6e, 0xc0, 0xba, 0x84, 0x31, 0xac, 0x0d, 0x9f, 0x59, 0xa5, 0x00, 0xad, @@ -365,13 +364,13 @@ func TestMetadataUnmarshal(t *testing.T) { t.Fatalf("no management key") } gotKey := *m.ManagementKey - if gotKey != wantKey { + if !bytes.Equal(gotKey, wantKey) { t.Errorf("(*Metadata).unmarshal, got key=0x%x, want key=0x%x", gotKey, wantKey) } } func TestMetadataMarshal(t *testing.T) { - key := [24]byte{ + key := []byte{ 0x09, 0xd9, 0x87, 0x81, 0xfb, 0xdc, 0xc9, 0xb6, 0x91, 0xa2, 0x05, 0x80, 0x6e, 0xc0, 0xba, 0x84, 0x31, 0xac, 0x0d, 0x9f, 0x59, 0xa5, 0x00, 0xad, @@ -395,7 +394,7 @@ func TestMetadataMarshal(t *testing.T) { } func TestMetadataUpdate(t *testing.T) { - key := [24]byte{ + key := []byte{ 0x09, 0xd9, 0x87, 0x81, 0xfb, 0xdc, 0xc9, 0xb6, 0x91, 0xa2, 0x05, 0x80, 0x6e, 0xc0, 0xba, 0x84, 0x31, 0xac, 0x0d, 0x9f, 0x59, 0xa5, 0x00, 0xad, @@ -428,7 +427,7 @@ func TestMetadataUpdate(t *testing.T) { } func TestMetadataAdditoinalFields(t *testing.T) { - key := [24]byte{ + key := []byte{ 0x09, 0xd9, 0x87, 0x81, 0xfb, 0xdc, 0xc9, 0xb6, 0x91, 0xa2, 0x05, 0x80, 0x6e, 0xc0, 0xba, 0x84, 0x31, 0xac, 0x0d, 0x9f, 0x59, 0xa5, 0x00, 0xad,