From dcad6c4fbc7d13083eb602a9f880a69426e8293b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 23 Sep 2024 16:59:46 -0700 Subject: [PATCH 1/3] 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 aa82d74..c6c51b1 100644 --- a/v2/piv/key.go +++ b/v2/piv/key.go @@ -448,6 +448,9 @@ const ( AlgorithmEd25519 AlgorithmRSA1024 AlgorithmRSA2048 + AlgorithmRSA3072 + AlgorithmRSA4096 + AlgorithmX25519 ) // PINPolicy represents PIN requirements when signing or decrypting with an @@ -531,6 +534,9 @@ var algorithmsMap = map[Algorithm]byte{ AlgorithmEd25519: algEd25519, AlgorithmRSA1024: algRSA1024, AlgorithmRSA2048: algRSA2048, + AlgorithmRSA3072: algRSA3072, + AlgorithmRSA4096: algRSA4096, + AlgorithmX25519: algX25519, } var algorithmsMapInv = map[byte]Algorithm{ @@ -539,6 +545,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 @@ -846,7 +855,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) @@ -862,6 +871,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") } @@ -991,7 +1006,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) } } @@ -1024,6 +1039,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 } @@ -1055,9 +1076,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)) @@ -1382,6 +1416,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 fec6739..0f36987 100644 --- a/v2/piv/key_test.go +++ b/v2/piv/key_test.go @@ -143,6 +143,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 @@ -290,12 +332,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) { @@ -304,6 +349,7 @@ func TestYubiKeySignRSA(t *testing.T) { } yk, close := newTestYubiKey(t) defer close() + testRequiresVersion(t, yk, test.version) slot := SlotAuthentication key := Key{ Algorithm: test.alg, @@ -340,12 +386,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) { @@ -354,6 +403,7 @@ func TestYubiKeySignRSAPSS(t *testing.T) { } yk, close := newTestYubiKey(t) defer close() + testRequiresVersion(t, yk, test.version) slot := SlotAuthentication key := Key{ Algorithm: test.alg, @@ -513,12 +563,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) { @@ -527,6 +580,7 @@ func TestYubiKeyDecryptRSA(t *testing.T) { } yk, close := newTestYubiKey(t) defer close() + testRequiresVersion(t, yk, test.version) slot := SlotAuthentication key := Key{ Algorithm: test.alg, @@ -576,7 +630,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 { @@ -690,18 +744,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", @@ -712,6 +768,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) { @@ -720,10 +799,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, @@ -888,12 +964,6 @@ func TestSetRSAPrivateKey(t *testing.T) { slot: SlotCardAuthentication, wantErr: nil, }, - { - name: "rsa 4096", - bits: 4096, - slot: SlotAuthentication, - wantErr: errUnsupportedKeySize, - }, { name: "rsa 512", bits: 512, @@ -1145,7 +1215,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) @@ -1157,102 +1227,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, @@ -1295,7 +1428,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) @@ -1346,6 +1479,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) From 2837a9998965b0b347de4d5e6b985e7b2ea8b5e5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 24 Sep 2024 12:18:31 -0700 Subject: [PATCH 2/3] Fix suggestions from code review --- v2/piv/key.go | 7 ++++--- v2/piv/key_go120.go | 11 +++++------ v2/piv/key_go120_test.go | 2 +- v2/piv/key_legacy.go | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/v2/piv/key.go b/v2/piv/key.go index c6c51b1..45bfa72 100644 --- a/v2/piv/key.go +++ b/v2/piv/key.go @@ -1006,7 +1006,8 @@ func (yk *YubiKey) PrivateKey(slot Slot, public crypto.PublicKey, auth KeyAuth) case *rsa.PublicKey: return &keyRSA{yk, slot, pub, auth, pp}, nil default: - return yk.privateKey(slot, public, auth, pp) + // Add support for X25519 keys using build tags + return yk.tryX25519PrivateKey(slot, public, auth, pp) } } @@ -1086,9 +1087,9 @@ func (yk *YubiKey) SetPrivateKeyInsecure(key []byte, slot Slot, private crypto.P copy(privateKey, priv[:32]) params = append(params, privateKey) default: - // Add support for ecdh.PrivateKey using build tags + // Add support for X25519 keys using build tags var err error - params, paramTag, elemLen, err = yk.setPrivateKeyInsecure(private) + params, paramTag, elemLen, err = yk.tryX22519PrivateKeyInsecure(private) if err != nil { return err } diff --git a/v2/piv/key_go120.go b/v2/piv/key_go120.go index 2c50a6a..e01d4a2 100644 --- a/v2/piv/key_go120.go +++ b/v2/piv/key_go120.go @@ -20,7 +20,6 @@ package piv import ( "crypto" "crypto/ecdh" - "errors" "fmt" ) @@ -46,7 +45,7 @@ func (k *X25519PrivateKey) SharedKey(peer *ecdh.PublicKey) ([]byte, error) { }) } -func (yk *YubiKey) privateKey(slot Slot, public crypto.PublicKey, auth KeyAuth, pp PINPolicy) (crypto.PrivateKey, error) { +func (yk *YubiKey) tryX25519PrivateKey(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() { @@ -58,18 +57,18 @@ func (yk *YubiKey) privateKey(slot Slot, public crypto.PublicKey, auth KeyAuth, } } -func (yk *YubiKey) setPrivateKeyInsecure(private crypto.PrivateKey) ([][]byte, byte, int, error) { +func (yk *YubiKey) tryX22519PrivateKeyInsecure(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") + if crv := priv.Curve(); crv != ecdh.X25519() { + return nil, 0, 0, fmt.Errorf("unsupported ecdh curve: %v", crv) } // 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") + return nil, 0, 0, fmt.Errorf("unsupported private key type: %T", private) } } diff --git a/v2/piv/key_go120_test.go b/v2/piv/key_go120_test.go index 220d885..9785f7a 100644 --- a/v2/piv/key_go120_test.go +++ b/v2/piv/key_go120_test.go @@ -29,7 +29,7 @@ import ( func TestYubiKeyX25519ImportKey(t *testing.T) { importKey, err := ecdh.X25519().GenerateKey(rand.Reader) if err != nil { - t.Fatalf("error geneating X25519 key: %v", err) + t.Fatalf("error generating X25519 key: %v", err) } yk, close := newTestYubiKey(t) diff --git a/v2/piv/key_legacy.go b/v2/piv/key_legacy.go index 8e9768c..d3b0f47 100644 --- a/v2/piv/key_legacy.go +++ b/v2/piv/key_legacy.go @@ -23,12 +23,12 @@ import ( "fmt" ) -func (yk *YubiKey) privateKey(slot Slot, public crypto.PublicKey, auth KeyAuth, pp PINPolicy) (crypto.PrivateKey, error) { +func (yk *YubiKey) tryX25519PrivateKey(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 (yk *YubiKey) tryX22519PrivateKeyInsecure(private crypto.PrivateKey) ([][]byte, byte, int, error) { + return nil, 0, 0, errors.New("unsupported private key type: %T", private) } func decodeX25519Public(b []byte) (crypto.PublicKey, error) { From cfec690808806e07f77f25d4b7b199b767d5de21 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 2 Oct 2024 15:56:14 -0500 Subject: [PATCH 3/3] Drop support for versions of Go lower than 1.20 This commit removes the code to support Go 1.16 to 1.19, requiring now Go 1.20. With this requirement we can remove the build tags. It also renames the X25519 SharedKey to ECDH. --- v2/go.mod | 2 +- v2/piv/key.go | 94 ++++++++++++++++++++++++++--- v2/piv/key_go120.go | 115 ------------------------------------ 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 | 78 +++++++++++++++++++++++++ 7 files changed, 165 insertions(+), 328 deletions(-) delete mode 100644 v2/piv/key_go120.go delete mode 100644 v2/piv/key_go120_test.go delete mode 100644 v2/piv/key_legacy.go delete mode 100644 v2/piv/key_legacy_test.go diff --git a/v2/go.mod b/v2/go.mod index 6925451..8c4dcbb 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -1,3 +1,3 @@ module github.com/go-piv/piv-go/v2 -go 1.16 +go 1.20 diff --git a/v2/piv/key.go b/v2/piv/key.go index 45bfa72..ae1f17d 100644 --- a/v2/piv/key.go +++ b/v2/piv/key.go @@ -17,6 +17,7 @@ package piv import ( "bytes" "crypto" + "crypto/ecdh" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" @@ -1005,9 +1006,13 @@ func (yk *YubiKey) PrivateKey(slot Slot, public crypto.PublicKey, auth KeyAuth) return &keyEd25519{yk, slot, pub, auth, pp}, nil case *rsa.PublicKey: return &keyRSA{yk, slot, pub, auth, pp}, nil + 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: - // Add support for X25519 keys using build tags - return yk.tryX25519PrivateKey(slot, public, auth, pp) + return nil, fmt.Errorf("unsupported public key type: %T", public) } } @@ -1086,13 +1091,17 @@ func (yk *YubiKey) SetPrivateKeyInsecure(key []byte, slot Slot, private crypto.P privateKey := make([]byte, elemLen) copy(privateKey, priv[:32]) params = append(params, privateKey) - default: - // Add support for X25519 keys using build tags - var err error - params, paramTag, elemLen, err = yk.tryX22519PrivateKeyInsecure(private) - if err != nil { - return err + case *ecdh.PrivateKey: + if crv := priv.Curve(); crv != ecdh.X25519() { + return fmt.Errorf("unsupported ecdh curve: %v", crv) } + paramTag = 0x08 + elemLen = 32 + + // seed + params = append(params, priv.Bytes()) + default: + return fmt.Errorf("unsupported private key type: %T", private) } elemLenASN1 := marshalASN1Length(uint64(elemLen)) @@ -1228,6 +1237,33 @@ func (k *ECDSAPrivateKey) SharedKey(peer *ecdsa.PublicKey) ([]byte, error) { }) } +// X25519PrivateKey is a crypto.PrivateKey implementation for X25519 keys. It +// implements the method ECDH to perform Diffie-Hellman key agreements. +// +// Keys returned by YubiKey.PrivateKey() may be type asserted to +// *X25519PrivateKey, if the slot contains an X25519 key. +type X25519PrivateKey struct { + yk *YubiKey + slot Slot + pub *ecdh.PublicKey + auth KeyAuth + pp PINPolicy +} + +func (k *X25519PrivateKey) Public() crypto.PublicKey { + return k.pub +} + +// ECDH 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) ECDH(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) + }) +} + type keyEd25519 struct { yk *YubiKey slot Slot @@ -1313,6 +1349,38 @@ func ykSignECDSA(tx *scTx, slot Slot, pub *ecdsa.PublicKey, digest []byte) ([]by return rs, nil } +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 +} + // This function only works on SoloKeys prototypes and other PIV devices that choose // to implement Ed25519 signatures under alg 0x22. func skSignEd25519(tx *scTx, slot Slot, pub ed25519.PublicKey, digest []byte) ([]byte, error) { @@ -1410,6 +1478,16 @@ func decodeRSAPublic(b []byte) (*rsa.PublicKey, error) { return &rsa.PublicKey{N: &n, E: int(e.Int64())}, nil } +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 rsaAlg(pub *rsa.PublicKey) (byte, error) { size := pub.N.BitLen() switch size { diff --git a/v2/piv/key_go120.go b/v2/piv/key_go120.go deleted file mode 100644 index e01d4a2..0000000 --- a/v2/piv/key_go120.go +++ /dev/null @@ -1,115 +0,0 @@ -//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" - "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) tryX25519PrivateKey(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) tryX22519PrivateKeyInsecure(private crypto.PrivateKey) ([][]byte, byte, int, error) { - switch priv := private.(type) { - case *ecdh.PrivateKey: - if crv := priv.Curve(); crv != ecdh.X25519() { - return nil, 0, 0, fmt.Errorf("unsupported ecdh curve: %v", crv) - } - // seed - params := make([][]byte, 0) - params = append(params, priv.Bytes()) - return params, 0x08, 32, nil - default: - return nil, 0, 0, fmt.Errorf("unsupported private key type: %T", private) - } -} - -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 deleted file mode 100644 index 9785f7a..0000000 --- a/v2/piv/key_go120_test.go +++ /dev/null @@ -1,120 +0,0 @@ -//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 generating 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 deleted file mode 100644 index d3b0f47..0000000 --- a/v2/piv/key_legacy.go +++ /dev/null @@ -1,36 +0,0 @@ -//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) tryX25519PrivateKey(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) tryX22519PrivateKeyInsecure(private crypto.PrivateKey) ([][]byte, byte, int, error) { - return nil, 0, 0, errors.New("unsupported private key type: %T", private) -} - -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 deleted file mode 100644 index ed79e09..0000000 --- a/v2/piv/key_legacy_test.go +++ /dev/null @@ -1,48 +0,0 @@ -//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 0f36987..7cdb377 100644 --- a/v2/piv/key_test.go +++ b/v2/piv/key_test.go @@ -17,6 +17,7 @@ package piv import ( "bytes" "crypto" + "crypto/ecdh" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" @@ -143,6 +144,67 @@ func TestYubiKeyECDSASharedKey(t *testing.T) { }) } +func TestYubiKeyX25519ECDH(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.ECDH(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.ECDH(peer.PublicKey()) + if !errors.Is(err, errMismatchingAlgorithms) { + t.Fatalf("unexpected error value: wanted errMismatchingAlgorithms: %v", err) + } + }) + }) +} + func TestYubiKeySignEd25519(t *testing.T) { yk, close := newTestYubiKey(t) defer close() @@ -1279,6 +1341,13 @@ func TestKeyInfo(t *testing.T) { Key{AlgorithmEd25519, PINPolicyNever, TouchPolicyNever}, false, version57, }, + { + "Generated x25517", + SlotAuthentication, + nil, + Key{AlgorithmEd25519, PINPolicyNever, TouchPolicyNever}, + false, version57, + }, { "Imported ec_256", SlotAuthentication, @@ -1328,6 +1397,13 @@ func TestKeyInfo(t *testing.T) { Key{AlgorithmEd25519, PINPolicyNever, TouchPolicyNever}, false, version57, }, + { + "Imported x25519", + SlotAuthentication, + ephemeralKey(t, AlgorithmX25519), + Key{AlgorithmX25519, PINPolicyNever, TouchPolicyNever}, + false, version57, + }, { "PINPolicyOnce", SlotAuthentication, @@ -1483,6 +1559,8 @@ func ephemeralKey(t *testing.T, alg Algorithm) privateKey { key, err = rsa.GenerateKey(rand.Reader, 3072) case AlgorithmRSA4096: key, err = rsa.GenerateKey(rand.Reader, 4096) + case AlgorithmX25519: + key, err = ecdh.X25519().GenerateKey(rand.Reader) default: t.Fatalf("ephemeral key: unknown algorithm %d", alg) }