From 12c06a0a1e81d8c55df6194df70d3ae0413ab9ed Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 2 Oct 2024 15:56:14 -0500 Subject: [PATCH] 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 | 93 ++++++++++++++++++++++++++--- 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 | 77 ++++++++++++++++++++++++ 7 files changed, 163 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 d84a841..4666077 100644 --- a/v2/piv/key.go +++ b/v2/piv/key.go @@ -1006,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) } } @@ -1087,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)) @@ -1250,6 +1258,33 @@ func (k *ECDSAPrivateKey) ECDH(peer *ecdh.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 @@ -1335,6 +1370,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) { @@ -1432,6 +1499,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 1c32186..1eee41a 100644 --- a/v2/piv/key_test.go +++ b/v2/piv/key_test.go @@ -209,6 +209,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() @@ -1345,6 +1406,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, @@ -1394,6 +1462,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, @@ -1549,6 +1624,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) }