From 33d0d4635d7bed78500f8948e485633578a8b372 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 23 Sep 2024 16:59:46 -0700 Subject: [PATCH] Add new algorithms supported in firmware 5.7.x This commit supports the new algorithms supported on YubiKeys with a 5.7.x firmware. It adds support for RSA-3072, RSA-4096, Ed25519, and X25519. Generating or importing X25519 keys is only supported with Go 1.20+, which adds support for the crypto/ecdh package. --- v2/piv/key.go | 44 +++++++- v2/piv/key_go120.go | 116 +++++++++++++++++++++ v2/piv/key_go120_test.go | 120 ++++++++++++++++++++++ v2/piv/key_legacy.go | 36 +++++++ v2/piv/key_legacy_test.go | 48 +++++++++ v2/piv/key_test.go | 205 +++++++++++++++++++++++++++++++------- v2/piv/piv.go | 17 ++-- v2/piv/piv_test.go | 27 ++++- 8 files changed, 565 insertions(+), 48 deletions(-) create mode 100644 v2/piv/key_go120.go create mode 100644 v2/piv/key_go120_test.go create mode 100644 v2/piv/key_legacy.go create mode 100644 v2/piv/key_legacy_test.go diff --git a/v2/piv/key.go b/v2/piv/key.go index 69eeecb..41167df 100644 --- a/v2/piv/key.go +++ b/v2/piv/key.go @@ -449,6 +449,9 @@ const ( AlgorithmEd25519 AlgorithmRSA1024 AlgorithmRSA2048 + AlgorithmRSA3072 + AlgorithmRSA4096 + AlgorithmX25519 ) // PINPolicy represents PIN requirements when signing or decrypting with an @@ -532,6 +535,9 @@ var algorithmsMap = map[Algorithm]byte{ AlgorithmEd25519: algEd25519, AlgorithmRSA1024: algRSA1024, AlgorithmRSA2048: algRSA2048, + AlgorithmRSA3072: algRSA3072, + AlgorithmRSA4096: algRSA4096, + AlgorithmX25519: algX25519, } var algorithmsMapInv = map[byte]Algorithm{ @@ -540,6 +546,9 @@ var algorithmsMapInv = map[byte]Algorithm{ algEd25519: AlgorithmEd25519, algRSA1024: AlgorithmRSA1024, algRSA2048: AlgorithmRSA2048, + algRSA3072: AlgorithmRSA3072, + algRSA4096: AlgorithmRSA4096, + algX25519: AlgorithmX25519, } // AttestationCertificate returns the YubiKey's attestation certificate, which @@ -847,7 +856,7 @@ func ykGenerateKey(tx *scTx, slot Slot, o Key) (crypto.PublicKey, error) { func decodePublic(b []byte, alg Algorithm) (crypto.PublicKey, error) { var curve elliptic.Curve switch alg { - case AlgorithmRSA1024, AlgorithmRSA2048: + case AlgorithmRSA1024, AlgorithmRSA2048, AlgorithmRSA3072, AlgorithmRSA4096: pub, err := decodeRSAPublic(b) if err != nil { return nil, fmt.Errorf("decoding rsa public key: %v", err) @@ -863,6 +872,12 @@ func decodePublic(b []byte, alg Algorithm) (crypto.PublicKey, error) { return nil, fmt.Errorf("decoding ed25519 public key: %v", err) } return pub, nil + case AlgorithmX25519: + pub, err := decodeX25519Public(b) + if err != nil { + return nil, fmt.Errorf("decoding X25519 public key: %v", err) + } + return pub, nil default: return nil, fmt.Errorf("unsupported algorithm") } @@ -992,7 +1007,7 @@ func (yk *YubiKey) PrivateKey(slot Slot, public crypto.PublicKey, auth KeyAuth) case *rsa.PublicKey: return &keyRSA{yk, slot, pub, auth, pp}, nil default: - return nil, fmt.Errorf("unsupported public key type: %T", public) + return yk.privateKey(slot, public, auth, pp) } } @@ -1025,6 +1040,12 @@ func (yk *YubiKey) SetPrivateKeyInsecure(key []byte, slot Slot, private crypto.P case 2048: policy.Algorithm = AlgorithmRSA2048 elemLen = 128 + case 3072: + policy.Algorithm = AlgorithmRSA3072 + elemLen = 192 + case 4096: + policy.Algorithm = AlgorithmRSA4096 + elemLen = 256 default: return errUnsupportedKeySize } @@ -1056,9 +1077,22 @@ func (yk *YubiKey) SetPrivateKeyInsecure(key []byte, slot Slot, private crypto.P padding := len(privateKey) - len(valueBytes) copy(privateKey[padding:], valueBytes) + params = append(params, privateKey) + case ed25519.PrivateKey: + paramTag = 0x07 + elemLen = ed25519.SeedSize + + // seed + privateKey := make([]byte, elemLen) + copy(privateKey, priv[:32]) params = append(params, privateKey) default: - return errors.New("unsupported private key type") + // Add support for ecdh.PrivateKey using build tags + var err error + params, paramTag, elemLen, err = yk.setPrivateKeyInsecure(private) + if err != nil { + return err + } } elemLenASN1 := marshalASN1Length(uint64(elemLen)) @@ -1404,6 +1438,10 @@ func rsaAlg(pub *rsa.PublicKey) (byte, error) { return algRSA1024, nil case 2048: return algRSA2048, nil + case 3072: + return algRSA3072, nil + case 4096: + return algRSA4096, nil default: return 0, fmt.Errorf("unsupported rsa key size: %d", size) } diff --git a/v2/piv/key_go120.go b/v2/piv/key_go120.go new file mode 100644 index 0000000..2c50a6a --- /dev/null +++ b/v2/piv/key_go120.go @@ -0,0 +1,116 @@ +//go:build go1.20 +// +build go1.20 + +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package piv + +import ( + "crypto" + "crypto/ecdh" + "errors" + "fmt" +) + +type X25519PrivateKey struct { + yk *YubiKey + slot Slot + pub *ecdh.PublicKey + auth KeyAuth + pp PINPolicy +} + +func (k *X25519PrivateKey) Public() crypto.PublicKey { + return k.pub +} + +// SharedKey performs an ECDH exchange and returns the shared secret. +// +// Peer's public key must use the same algorithm as the key in this slot, or an +// error will be returned. +func (k *X25519PrivateKey) SharedKey(peer *ecdh.PublicKey) ([]byte, error) { + return k.auth.do(k.yk, k.pp, func(tx *scTx) ([]byte, error) { + return ykECDHX25519(tx, k.slot, k.pub, peer) + }) +} + +func (yk *YubiKey) privateKey(slot Slot, public crypto.PublicKey, auth KeyAuth, pp PINPolicy) (crypto.PrivateKey, error) { + switch pub := public.(type) { + case *ecdh.PublicKey: + if crv := pub.Curve(); crv != ecdh.X25519() { + return nil, fmt.Errorf("unsupported ecdh curve: %v", crv) + } + return &X25519PrivateKey{yk, slot, pub, auth, pp}, nil + default: + return nil, fmt.Errorf("unsupported public key type: %T", public) + } +} + +func (yk *YubiKey) setPrivateKeyInsecure(private crypto.PrivateKey) ([][]byte, byte, int, error) { + switch priv := private.(type) { + case *ecdh.PrivateKey: + if priv.Curve() != ecdh.X25519() { + return nil, 0, 0, errors.New("unsupported private key type") + } + // seed + params := make([][]byte, 0) + params = append(params, priv.Bytes()) + return params, 0x08, 32, nil + default: + return nil, 0, 0, errors.New("unsupported private key type") + } +} + +func decodeX25519Public(b []byte) (*ecdh.PublicKey, error) { + // Adaptation of + // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=95 + p, _, err := unmarshalASN1(b, 2, 0x06) + if err != nil { + return nil, fmt.Errorf("unmarshal points: %v", err) + } + return ecdh.X25519().NewPublicKey(p) +} + +func ykECDHX25519(tx *scTx, slot Slot, pub *ecdh.PublicKey, peer *ecdh.PublicKey) ([]byte, error) { + if crv := pub.Curve(); crv != ecdh.X25519() { + return nil, fmt.Errorf("unsupported ecdh curve: %v", crv) + } + if pub.Curve() != peer.Curve() { + return nil, errMismatchingAlgorithms + } + cmd := apdu{ + instruction: insAuthenticate, + param1: algX25519, + param2: byte(slot.Key), + data: marshalASN1(0x7c, + append([]byte{0x82, 0x00}, + marshalASN1(0x85, peer.Bytes())...)), + } + resp, err := tx.Transmit(cmd) + if err != nil { + return nil, fmt.Errorf("command failed: %w", err) + } + + sig, _, err := unmarshalASN1(resp, 1, 0x1c) // 0x7c + if err != nil { + return nil, fmt.Errorf("unmarshal response: %v", err) + } + sharedSecret, _, err := unmarshalASN1(sig, 2, 0x02) // 0x82 + if err != nil { + return nil, fmt.Errorf("unmarshal response signature: %v", err) + } + + return sharedSecret, nil +} diff --git a/v2/piv/key_go120_test.go b/v2/piv/key_go120_test.go new file mode 100644 index 0000000..220d885 --- /dev/null +++ b/v2/piv/key_go120_test.go @@ -0,0 +1,120 @@ +//go:build go1.20 +// +build go1.20 + +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package piv + +import ( + "bytes" + "crypto/ecdh" + "crypto/rand" + "errors" + "reflect" + "testing" +) + +func TestYubiKeyX25519ImportKey(t *testing.T) { + importKey, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("error geneating X25519 key: %v", err) + } + + yk, close := newTestYubiKey(t) + defer close() + + slot := SlotAuthentication + + err = yk.SetPrivateKeyInsecure(DefaultManagementKey, slot, importKey, Key{AlgorithmX25519, PINPolicyNever, TouchPolicyNever}) + if err != nil { + t.Fatalf("error importing key: %v", err) + } + want := KeyInfo{ + Algorithm: AlgorithmX25519, + PINPolicy: PINPolicyNever, + TouchPolicy: TouchPolicyNever, + Origin: OriginImported, + PublicKey: importKey.Public(), + } + + got, err := yk.KeyInfo(slot) + if err != nil { + t.Fatalf("KeyInfo() = _, %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("KeyInfo() = %#v, want %#v", got, want) + } +} + +func TestYubiKeyX25519SharedKey(t *testing.T) { + yk, close := newTestYubiKey(t) + defer close() + + slot := SlotAuthentication + + key := Key{ + Algorithm: AlgorithmX25519, + TouchPolicy: TouchPolicyNever, + PINPolicy: PINPolicyNever, + } + pubKey, err := yk.GenerateKey(DefaultManagementKey, slot, key) + if err != nil { + t.Fatalf("generating key: %v", err) + } + pub, ok := pubKey.(*ecdh.PublicKey) + if !ok { + t.Fatalf("public key is not an ecdh key") + } + priv, err := yk.PrivateKey(slot, pub, KeyAuth{}) + if err != nil { + t.Fatalf("getting private key: %v", err) + } + privX25519, ok := priv.(*X25519PrivateKey) + if !ok { + t.Fatalf("expected private key to be X25519 private key") + } + + t.Run("good", func(t *testing.T) { + peer, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("cannot generate key: %v", err) + } + + secret1, err := privX25519.SharedKey(peer.PublicKey()) + if err != nil { + t.Fatalf("key agreement failed: %v", err) + } + secret2, err := peer.ECDH(pub) + if err != nil { + t.Fatalf("key agreement failed: %v", err) + } + if !bytes.Equal(secret1, secret2) { + t.Errorf("key agreement didn't match") + } + }) + + t.Run("bad", func(t *testing.T) { + t.Run("curve", func(t *testing.T) { + peer, err := ecdh.P256().GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("cannot generate key: %v", err) + } + _, err = privX25519.SharedKey(peer.PublicKey()) + if !errors.Is(err, errMismatchingAlgorithms) { + t.Fatalf("unexpected error value: wanted errMismatchingAlgorithms: %v", err) + } + }) + }) +} diff --git a/v2/piv/key_legacy.go b/v2/piv/key_legacy.go new file mode 100644 index 0000000..8e9768c --- /dev/null +++ b/v2/piv/key_legacy.go @@ -0,0 +1,36 @@ +//go:build !go1.20 +// +build !go1.20 + +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package piv + +import ( + "crypto" + "errors" + "fmt" +) + +func (yk *YubiKey) privateKey(slot Slot, public crypto.PublicKey, auth KeyAuth, pp PINPolicy) (crypto.PrivateKey, error) { + return nil, fmt.Errorf("unsupported public key type: %T", public) +} + +func (yk *YubiKey) setPrivateKeyInsecure(private crypto.PrivateKey) ([][]byte, byte, int, error) { + return nil, 0, 0, errors.New("unsupported private key type") +} + +func decodeX25519Public(b []byte) (crypto.PublicKey, error) { + return nil, fmt.Errorf("unsupported algorithm") +} diff --git a/v2/piv/key_legacy_test.go b/v2/piv/key_legacy_test.go new file mode 100644 index 0000000..ed79e09 --- /dev/null +++ b/v2/piv/key_legacy_test.go @@ -0,0 +1,48 @@ +//go:build !go1.20 +// +build !go1.20 + +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package piv + +import "testing" + +func TestYubiKeyX25519Legacy(t *testing.T) { + yk, close := newTestYubiKey(t) + defer close() + + slot := SlotAuthentication + + key := Key{ + Algorithm: AlgorithmX25519, + TouchPolicy: TouchPolicyNever, + PINPolicy: PINPolicyNever, + } + _, err := yk.GenerateKey(DefaultManagementKey, slot, key) + if err == nil { + t.Error("expected error with legacy Go") + } + + importKey := []byte{ + 0x6b, 0x66, 0x8f, 0xbe, 0xad, 0x61, 0x9d, 0x9f, + 0xb5, 0x4b, 0x14, 0xa7, 0x34, 0x03, 0xb7, 0x21, + 0xde, 0x9a, 0x0c, 0xa4, 0x79, 0x83, 0x2c, 0xee, + 0x76, 0x78, 0xe1, 0x9c, 0xe3, 0x06, 0xa7, 0x38, + } + err = yk.SetPrivateKeyInsecure(DefaultManagementKey, slot, importKey, Key{AlgorithmX25519, PINPolicyNever, TouchPolicyNever}) + if err == nil { + t.Error("expected error with legacy Go") + } +} diff --git a/v2/piv/key_test.go b/v2/piv/key_test.go index 35d4a43..1c32186 100644 --- a/v2/piv/key_test.go +++ b/v2/piv/key_test.go @@ -209,6 +209,48 @@ func TestYubiKeyECDSASharedKey(t *testing.T) { }) } +func TestYubiKeySignEd25519(t *testing.T) { + yk, close := newTestYubiKey(t) + defer close() + testRequiresVersion(t, yk, version57) + + if err := yk.Reset(); err != nil { + t.Fatalf("reset yubikey: %v", err) + } + + slot := SlotAuthentication + + key := Key{ + Algorithm: AlgorithmEd25519, + TouchPolicy: TouchPolicyNever, + PINPolicy: PINPolicyNever, + } + pubKey, err := yk.GenerateKey(DefaultManagementKey, slot, key) + if err != nil { + t.Fatalf("generating key: %v", err) + } + pub, ok := pubKey.(ed25519.PublicKey) + if !ok { + t.Fatalf("public key is not an ecdsa key") + } + data := []byte("hello") + priv, err := yk.PrivateKey(slot, pub, KeyAuth{}) + if err != nil { + t.Fatalf("getting private key: %v", err) + } + s, ok := priv.(crypto.Signer) + if !ok { + t.Fatalf("expected private key to implement crypto.Signer") + } + sig, err := s.Sign(rand.Reader, data, crypto.Hash(0)) + if err != nil { + t.Fatalf("signing failed: %v", err) + } + if !ed25519.Verify(pub, data, sig) { + t.Errorf("signature didn't match") + } +} + func TestPINPrompt(t *testing.T) { tests := []struct { name string @@ -356,12 +398,15 @@ func TestSlots(t *testing.T) { func TestYubiKeySignRSA(t *testing.T) { tests := []struct { - name string - alg Algorithm - long bool + name string + alg Algorithm + long bool + version version }{ - {"rsa1024", AlgorithmRSA1024, false}, - {"rsa2048", AlgorithmRSA2048, true}, + {"rsa1024", AlgorithmRSA1024, false, version{}}, + {"rsa2048", AlgorithmRSA2048, true, version{}}, + {"rsa3072", AlgorithmRSA3072, true, version57}, + {"rsa4096", AlgorithmRSA4096, true, version57}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -370,6 +415,7 @@ func TestYubiKeySignRSA(t *testing.T) { } yk, close := newTestYubiKey(t) defer close() + testRequiresVersion(t, yk, test.version) slot := SlotAuthentication key := Key{ Algorithm: test.alg, @@ -406,12 +452,15 @@ func TestYubiKeySignRSA(t *testing.T) { func TestYubiKeySignRSAPSS(t *testing.T) { tests := []struct { - name string - alg Algorithm - long bool + name string + alg Algorithm + long bool + version version }{ - {"rsa1024", AlgorithmRSA1024, false}, - {"rsa2048", AlgorithmRSA2048, true}, + {"rsa1024", AlgorithmRSA1024, false, version{}}, + {"rsa2048", AlgorithmRSA2048, true, version{}}, + {"rsa3072", AlgorithmRSA3072, true, version57}, + {"rsa4096", AlgorithmRSA4096, true, version57}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -420,6 +469,7 @@ func TestYubiKeySignRSAPSS(t *testing.T) { } yk, close := newTestYubiKey(t) defer close() + testRequiresVersion(t, yk, test.version) slot := SlotAuthentication key := Key{ Algorithm: test.alg, @@ -579,12 +629,15 @@ func TestTLS13(t *testing.T) { func TestYubiKeyDecryptRSA(t *testing.T) { tests := []struct { - name string - alg Algorithm - long bool + name string + alg Algorithm + long bool + version version }{ - {"rsa1024", AlgorithmRSA1024, false}, - {"rsa2048", AlgorithmRSA2048, true}, + {"rsa1024", AlgorithmRSA1024, false, version{}}, + {"rsa2048", AlgorithmRSA2048, true, version{}}, + {"rsa3072", AlgorithmRSA3072, true, version57}, + {"rsa4096", AlgorithmRSA4096, true, version57}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -593,6 +646,7 @@ func TestYubiKeyDecryptRSA(t *testing.T) { } yk, close := newTestYubiKey(t) defer close() + testRequiresVersion(t, yk, test.version) slot := SlotAuthentication key := Key{ Algorithm: test.alg, @@ -642,7 +696,7 @@ func TestYubiKeyAttestation(t *testing.T) { TouchPolicy: TouchPolicyNever, } - testRequiresVersion(t, yk, 4, 3, 0) + testRequiresVersion(t, yk, version43) cert, err := yk.AttestationCertificate() if err != nil { @@ -756,18 +810,20 @@ func TestYubiKeyStoreCertificate(t *testing.T) { func TestYubiKeyGenerateKey(t *testing.T) { tests := []struct { - name string - alg Algorithm - bits int - long bool // Does the key generation take a long time? + name string + alg Algorithm + bits int + long bool // Does the key generation take a long time? + version version }{ { name: "ec_256", alg: AlgorithmEC256, }, { - name: "ec_384", - alg: AlgorithmEC384, + name: "ec_384", + alg: AlgorithmEC384, + version: version43, }, { name: "rsa_1024", @@ -778,6 +834,29 @@ func TestYubiKeyGenerateKey(t *testing.T) { alg: AlgorithmRSA2048, long: true, }, + { + name: "rsa_2048", + alg: AlgorithmRSA2048, + long: true, + }, + { + name: "rsa_3072", + alg: AlgorithmRSA3072, + long: true, + version: version57, + }, + { + name: "rsa_4096", + alg: AlgorithmRSA4096, + long: true, + version: version57, + }, + { + name: "ed25519", + alg: AlgorithmEd25519, + long: false, + version: version57, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -786,10 +865,7 @@ func TestYubiKeyGenerateKey(t *testing.T) { } yk, close := newTestYubiKey(t) defer close() - if test.alg == AlgorithmEC384 { - testRequiresVersion(t, yk, 4, 3, 0) - } - + testRequiresVersion(t, yk, test.version) key := Key{ Algorithm: test.alg, TouchPolicy: TouchPolicyNever, @@ -954,12 +1030,6 @@ func TestSetRSAPrivateKey(t *testing.T) { slot: SlotCardAuthentication, wantErr: nil, }, - { - name: "rsa 4096", - bits: 4096, - slot: SlotAuthentication, - wantErr: errUnsupportedKeySize, - }, { name: "rsa 512", bits: 512, @@ -1211,7 +1281,7 @@ func TestKeyInfo(t *testing.T) { yk, close := newTestYubiKey(t) defer close() - testRequiresVersion(t, yk, 5, 3, 0) + testRequiresVersion(t, yk, version53) if err := yk.Reset(); err != nil { t.Fatalf("resetting key: %v", err) @@ -1223,102 +1293,165 @@ func TestKeyInfo(t *testing.T) { slot Slot importKey privateKey policy Key + long bool + version version }{ { "Generated ec_256", SlotAuthentication, nil, Key{AlgorithmEC256, PINPolicyNever, TouchPolicyNever}, + false, version{}, }, { "Generated ec_384", SlotAuthentication, nil, Key{AlgorithmEC384, PINPolicyNever, TouchPolicyNever}, + false, version43, }, { "Generated rsa_1024", SlotAuthentication, nil, Key{AlgorithmRSA1024, PINPolicyNever, TouchPolicyNever}, + false, version{}, }, { "Generated rsa_2048", SlotAuthentication, nil, Key{AlgorithmRSA2048, PINPolicyNever, TouchPolicyNever}, + true, version{}, + }, + { + "Generated rsa_3072", + SlotAuthentication, + nil, + Key{AlgorithmRSA3072, PINPolicyNever, TouchPolicyNever}, + true, version57, + }, + { + "Generated rsa_4096", + SlotAuthentication, + nil, + Key{AlgorithmRSA4096, PINPolicyNever, TouchPolicyNever}, + true, version57, + }, + { + "Generated ed25517", + SlotAuthentication, + nil, + Key{AlgorithmEd25519, PINPolicyNever, TouchPolicyNever}, + false, version57, }, { "Imported ec_256", SlotAuthentication, ephemeralKey(t, AlgorithmEC256), Key{AlgorithmEC256, PINPolicyNever, TouchPolicyNever}, + false, version{}, }, { "Imported ec_384", SlotAuthentication, ephemeralKey(t, AlgorithmEC384), Key{AlgorithmEC384, PINPolicyNever, TouchPolicyNever}, + false, version43, }, { "Imported rsa_1024", SlotAuthentication, ephemeralKey(t, AlgorithmRSA1024), Key{AlgorithmRSA1024, PINPolicyNever, TouchPolicyNever}, + false, version{}, }, { "Imported rsa_2048", SlotAuthentication, ephemeralKey(t, AlgorithmRSA2048), Key{AlgorithmRSA2048, PINPolicyNever, TouchPolicyNever}, + false, version{}, + }, + { + "Imported rsa_3072", + SlotAuthentication, + ephemeralKey(t, AlgorithmRSA3072), + Key{AlgorithmRSA3072, PINPolicyNever, TouchPolicyNever}, + false, version57, + }, + { + "Imported rsa_4096", + SlotAuthentication, + ephemeralKey(t, AlgorithmRSA4096), + Key{AlgorithmRSA4096, PINPolicyNever, TouchPolicyNever}, + false, version57, + }, + { + "Imported ed25519", + SlotAuthentication, + ephemeralKey(t, AlgorithmEd25519), + Key{AlgorithmEd25519, PINPolicyNever, TouchPolicyNever}, + false, version57, }, { "PINPolicyOnce", SlotAuthentication, nil, Key{AlgorithmEC256, PINPolicyOnce, TouchPolicyNever}, + false, version{}, }, { "PINPolicyAlways", SlotAuthentication, nil, Key{AlgorithmEC256, PINPolicyAlways, TouchPolicyNever}, + false, version{}, }, { "TouchPolicyAlways", SlotAuthentication, nil, Key{AlgorithmEC256, PINPolicyNever, TouchPolicyAlways}, + false, version{}, }, { "TouchPolicyCached", SlotAuthentication, nil, Key{AlgorithmEC256, PINPolicyNever, TouchPolicyCached}, + false, version{}, }, { "SlotSignature", SlotSignature, nil, Key{AlgorithmEC256, PINPolicyNever, TouchPolicyCached}, + false, version{}, }, { "SlotCardAuthentication", SlotCardAuthentication, nil, Key{AlgorithmEC256, PINPolicyNever, TouchPolicyCached}, + false, version{}, }, { "SlotKeyManagement", SlotKeyManagement, nil, Key{AlgorithmEC256, PINPolicyNever, TouchPolicyCached}, + false, version{}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { + if test.long && testing.Short() { + t.Skip("skipping test in short mode") + } yk, close := newTestYubiKey(t) defer close() + testRequiresVersion(t, yk, test.version) want := KeyInfo{ Algorithm: test.policy.Algorithm, @@ -1361,7 +1494,7 @@ func TestPINPolicy(t *testing.T) { yk, close := newTestYubiKey(t) defer close() - testRequiresVersion(t, yk, 5, 3, 0) + testRequiresVersion(t, yk, version53) if err := yk.Reset(); err != nil { t.Fatalf("resetting key: %v", err) @@ -1412,6 +1545,10 @@ func ephemeralKey(t *testing.T, alg Algorithm) privateKey { key, err = rsa.GenerateKey(rand.Reader, 1024) case AlgorithmRSA2048: key, err = rsa.GenerateKey(rand.Reader, 2048) + case AlgorithmRSA3072: + key, err = rsa.GenerateKey(rand.Reader, 3072) + case AlgorithmRSA4096: + key, err = rsa.GenerateKey(rand.Reader, 4096) default: t.Fatalf("ephemeral key: unknown algorithm %d", alg) } diff --git a/v2/piv/piv.go b/v2/piv/piv.go index 47669c2..a4d884e 100644 --- a/v2/piv/piv.go +++ b/v2/piv/piv.go @@ -66,11 +66,14 @@ const ( algAES256 = 0x0c algRSA1024 = 0x06 algRSA2048 = 0x07 + algRSA3072 = 0x05 + algRSA4096 = 0x16 algECCP256 = 0x11 algECCP384 = 0x14 - // non-standard; as implemented by SoloKeys. Chosen for low probability of eventual - // clashes, if and when PIV standard adds Ed25519 support - algEd25519 = 0x22 + // non-standard; implemented by YubiKey 5.7.x. Previous versions supported + // Ed25519 on SoloKeys with the value 0x22 + algEd25519 = 0xE0 + algX25519 = 0xE1 // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-78-4.pdf#page=16 keyAuthentication = 0x9a @@ -910,10 +913,10 @@ func ykSetProtectedMetadata(tx *scTx, key []byte, m *Metadata, rand io.Reader, v 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) + // 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, diff --git a/v2/piv/piv_test.go b/v2/piv/piv_test.go index 02bfa33..36fb859 100644 --- a/v2/piv/piv_test.go +++ b/v2/piv/piv_test.go @@ -30,6 +30,12 @@ import ( // destroying data on YubiKeys connected to the system. var canModifyYubiKey bool +var ( + version43 = version{4, 3, 0} // EC384 and Attestation + version53 = version{5, 3, 0} // PINPolicy and KeyInfo + version57 = version{5, 7, 0} // RSA3072, RSA4096, Ed25519, and X25519 +) + func init() { flag.BoolVar(&canModifyYubiKey, "wipe-yubikey", false, "Flag required to run tests that access the yubikey") @@ -49,9 +55,9 @@ func testGetVersion(t *testing.T, h *scHandle) { } } -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) +func testRequiresVersion(t *testing.T, yk *YubiKey, v version) { + if !supportsVersion(yk.version, v.major, v.minor, v.patch) { + t.Skipf("test requires yubikey version %d.%d.%d: got %d.%d.%d", v.major, v.minor, v.patch, yk.version.major, yk.version.minor, yk.version.patch) } } @@ -145,7 +151,7 @@ func TestYubiKeyLoginNeeded(t *testing.T) { yk, close := newTestYubiKey(t) defer close() - testRequiresVersion(t, yk, 4, 3, 0) + testRequiresVersion(t, yk, version43) if !ykLoginNeeded(yk.tx) { t.Errorf("expected login needed") @@ -159,8 +165,21 @@ func TestYubiKeyLoginNeeded(t *testing.T) { } func TestYubiKeyPINRetries(t *testing.T) { + // The call to Retries may fail after performing the login in + // TestYubiKeyLoginNeeded. It appears that the YubiKey doesn’t close the + // connection immediately, leading to test failures. To prevent this, we + // will reset the YubiKey before running the test. + func() { + yk, close := newTestYubiKey(t) + defer close() + if err := yk.Reset(); err != nil { + t.Fatalf("resetting key: %v", err) + } + }() + yk, close := newTestYubiKey(t) defer close() + retries, err := yk.Retries() if err != nil { t.Fatalf("getting retries: %v", err)