Repository: go-piv/piv-go (go)

Issue:
Support GET METADATA command and use it to derive default PIN policy
[`(*Yubikey).PrivateKey`](https://pkg.go.dev/github.com/go-piv/piv-go/piv#YubiKey.PrivateKey) requires you to give the PIN policy for the used slot, otherwise it derives a default from the attestation certificate. This fails if the key has not been generated on the hardware, as the Yubikey won't provide an attestation certificate in that case. So there is no robust way, currently, to set the correct PIN policy for such keys.

YubiKeys after 5.3 support a vendor-specific command to [get metadata about a given key slot](https://developers.yubico.com/PIV/Introduction/Yubico_extensions.html#_get_metadata). In particular, that metadata contains the PIN/Touch policy for the given key slot. There is reference code to [retrieve](https://github.com/Yubico/yubico-piv-tool/blob/yubico-piv-tool-2.3.1/lib/ykpiv.c#L916) and [parse the metadata](https://github.com/Yubico/yubico-piv-tool/blob/yubico-piv-tool-2.3.1/lib/util.c#L1529).

I would like to add support to a) retrieve that metadata, and b) use that to derive the default PIN policy in `PrivateKey`, if it is available. My proposal is to add some public API:

```go
// KeyInfo holds unprotected metadata about a key slot.
type KeyInfo struct {
	Algorithm   Algorithm
	PINPolicy   PINPolicy
	TouchPolicy TouchPolicy
	Origin      Origin
	PublicKey   crypto.PublicKey
}

func (yk *YubiKey) KeyInfo(slot Slot) (KeyInfo, error)

// Origin represents whether a key was generated on the hardware, or has been
// imported into it.
type Origin int

const (
	OriginGenerated Origin = iota + 1
	OriginImported
)
```

I chose the name `KeyInfo`, because `Metadata` (which seems a more canonical name, based on the command) is already taken for "PIN protected Metadata" (currently only the management key).

I have a PoC implemented and verified with my own YubiKey that it works. If this is wanted, I could clean it up, add some tests and send it as a PR.

Pull request:
piv: implement key metadata retrieval
Add support for a YubiKey vendor extension to retrieve public metadata (including the public key, PIN/Touch policies and whether or not the key was hardware generated) for a given key slot.

Also use this new method to determine the pin policy, for YubiKeys that support it. This is more general than using the attestation certificate, as it also works for keys that have been generated offline and imported into the hardware token.

Fixes #129

Patch:
diff --git a/piv/key.go b/piv/key.go
index 8e60571..180c215 100644
--- a/piv/key.go
+++ b/piv/key.go
@@ -476,6 +476,16 @@ const (
 	TouchPolicyCached
 )
 
+// Origin represents whether a key was generated on the hardware, or has been
+// imported into it.
+type Origin int
+
+// Origins supported by this package.
+const (
+	OriginGenerated Origin = iota + 1
+	OriginImported
+)
+
 const (
 	tagPINPolicy   = 0xaa
 	tagTouchPolicy = 0xab
@@ -487,12 +497,34 @@ var pinPolicyMap = map[PINPolicy]byte{
 	PINPolicyAlways: 0x03,
 }
 
+var pinPolicyMapInv = map[byte]PINPolicy{
+	0x01: PINPolicyNever,
+	0x02: PINPolicyOnce,
+	0x03: PINPolicyAlways,
+}
+
 var touchPolicyMap = map[TouchPolicy]byte{
 	TouchPolicyNever:  0x01,
 	TouchPolicyAlways: 0x02,
 	TouchPolicyCached: 0x03,
 }
 
+var touchPolicyMapInv = map[byte]TouchPolicy{
+	0x01: TouchPolicyNever,
+	0x02: TouchPolicyAlways,
+	0x03: TouchPolicyCached,
+}
+
+var originMap = map[Origin]byte{
+	OriginGenerated: 0x01,
+	OriginImported:  0x02,
+}
+
+var originMapInv = map[byte]Origin{
+	0x01: OriginGenerated,
+	0x02: OriginImported,
+}
+
 var algorithmsMap = map[Algorithm]byte{
 	AlgorithmEC256:   algECCP256,
 	AlgorithmEC384:   algECCP384,
@@ -501,6 +533,14 @@ var algorithmsMap = map[Algorithm]byte{
 	AlgorithmRSA2048: algRSA2048,
 }
 
+var algorithmsMapInv = map[byte]Algorithm{
+	algECCP256: AlgorithmEC256,
+	algECCP384: AlgorithmEC384,
+	algEd25519: AlgorithmEd25519,
+	algRSA1024: AlgorithmRSA1024,
+	algRSA2048: AlgorithmRSA2048,
+}
+
 // AttestationCertificate returns the YubiKey's attestation certificate, which
 // is unique to the key and signed by Yubico.
 func (yk *YubiKey) AttestationCertificate() (*x509.Certificate, error) {
@@ -554,6 +594,92 @@ func ykAttest(tx *scTx, slot Slot) (*x509.Certificate, error) {
 	return cert, nil
 }
 
+// KeyInfo holds unprotected metadata about a key slot.
+type KeyInfo struct {
+	Algorithm   Algorithm
+	PINPolicy   PINPolicy
+	TouchPolicy TouchPolicy
+	Origin      Origin
+	PublicKey   crypto.PublicKey
+}
+
+func (ki *KeyInfo) unmarshal(b []byte) error {
+	for len(b) > 0 {
+		var v asn1.RawValue
+		rest, err := asn1.Unmarshal(b, &v)
+		if err != nil {
+			return err
+		}
+		b = rest
+		if v.Class != 0 || v.IsCompound {
+			continue
+		}
+		var ok bool
+		switch v.Tag {
+		case 1:
+			if len(v.Bytes) != 1 {
+				return errors.New("invalid algorithm in response")
+			}
+			if ki.Algorithm, ok = algorithmsMapInv[v.Bytes[0]]; !ok {
+				return errors.New("unknown algorithm in response")
+			}
+		case 2:
+			if len(v.Bytes) != 2 {
+				return errors.New("invalid policy in response")
+			}
+			if ki.PINPolicy, ok = pinPolicyMapInv[v.Bytes[0]]; !ok {
+				return errors.New("unknown PIN policy in response")
+			}
+			if ki.TouchPolicy, ok = touchPolicyMapInv[v.Bytes[1]]; !ok {
+				return errors.New("unknown touch policy in response")
+			}
+		case 3:
+			if len(v.Bytes) != 1 {
+				return errors.New("invalid origin in response")
+			}
+			if ki.Origin, ok = originMapInv[v.Bytes[0]]; !ok {
+				return errors.New("unknown origin in response")
+			}
+		case 4:
+			ki.PublicKey, err = decodePublic(v.Bytes, ki.Algorithm)
+			if err != nil {
+				return fmt.Errorf("parse public key: %w", err)
+			}
+		default:
+			// TODO: According to the Yubico website, we get two more fields,
+			// if we pass 0x80 or 0x81 as slots:
+			//     1. Default value (for PIN/PUK and management key): Whether the
+			//        default value is used.
+			//     2. Retries (for PIN/PUK): The number of retries remaining
+			// However, it seems the reference implementation does not expect
+			// these and can not parse them out:
+			// https://github.com/Yubico/yubico-piv-tool/blob/yubico-piv-tool-2.3.1/lib/util.c#L1529
+			// For now, we just ignore them.
+		}
+	}
+	return nil
+}
+
+// KeyInfo returns public information about the given key slot. It is only
+// supported by YubiKeys with a version >= 5.3.0.
+func (yk *YubiKey) KeyInfo(slot Slot) (KeyInfo, error) {
+	// https://developers.yubico.com/PIV/Introduction/Yubico_extensions.html#_get_metadata
+	cmd := apdu{
+		instruction: insGetMetadata,
+		param1:      0x00,
+		param2:      byte(slot.Key),
+	}
+	resp, err := yk.tx.Transmit(cmd)
+	if err != nil {
+		return KeyInfo{}, fmt.Errorf("command failed: %w", err)
+	}
+	var ki KeyInfo
+	if err := ki.unmarshal(resp); err != nil {
+		return KeyInfo{}, err
+	}
+	return ki, nil
+}
+
 // Certificate returns the certifiate object stored in a given slot.
 //
 // If a certificate hasn't been set in the provided slot, the returned error
@@ -709,10 +835,20 @@ func ykGenerateKey(tx *scTx, slot Slot, o Key) (crypto.PublicKey, error) {
 		return nil, fmt.Errorf("command failed: %w", err)
 	}
 
+	// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=95
+	obj, _, err := unmarshalASN1(resp, 1, 0x49)
+	if err != nil {
+		return nil, fmt.Errorf("unmarshal response: %v", err)
+	}
+
+	return decodePublic(obj, o.Algorithm)
+}
+
+func decodePublic(b []byte, alg Algorithm) (crypto.PublicKey, error) {
 	var curve elliptic.Curve
-	switch o.Algorithm {
+	switch alg {
 	case AlgorithmRSA1024, AlgorithmRSA2048:
-		pub, err := decodeRSAPublic(resp)
+		pub, err := decodeRSAPublic(b)
 		if err != nil {
 			return nil, fmt.Errorf("decoding rsa public key: %v", err)
 		}
@@ -722,7 +858,7 @@ func ykGenerateKey(tx *scTx, slot Slot, o Key) (crypto.PublicKey, error) {
 	case AlgorithmEC384:
 		curve = elliptic.P384()
 	case AlgorithmEd25519:
-		pub, err := decodeEd25519Public(resp)
+		pub, err := decodeEd25519Public(b)
 		if err != nil {
 			return nil, fmt.Errorf("decoding ed25519 public key: %v", err)
 		}
@@ -730,7 +866,7 @@ func ykGenerateKey(tx *scTx, slot Slot, o Key) (crypto.PublicKey, error) {
 	default:
 		return nil, fmt.Errorf("unsupported algorithm")
 	}
-	pub, err := decodeECPublic(resp, curve)
+	pub, err := decodeECPublic(b, curve)
 	if err != nil {
 		return nil, fmt.Errorf("decoding ec public key: %v", err)
 	}
@@ -791,6 +927,13 @@ func (k KeyAuth) do(yk *YubiKey, pp PINPolicy, f func(tx *scTx) ([]byte, error))
 }
 
 func pinPolicy(yk *YubiKey, slot Slot) (PINPolicy, error) {
+	if supportsVersion(yk.Version(), 5, 3, 0) {
+		info, err := yk.KeyInfo(slot)
+		if err != nil {
+			return 0, fmt.Errorf("get key info: %v", err)
+		}
+		return info.PINPolicy, nil
+	}
 	cert, err := yk.Attest(slot)
 	if err != nil {
 		var e *apduErr
@@ -1180,11 +1323,7 @@ func unmarshalASN1(b []byte, class, tag int) (obj, rest []byte, err error) {
 
 func decodeECPublic(b []byte, curve elliptic.Curve) (*ecdsa.PublicKey, error) {
 	// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=95
-	r, _, err := unmarshalASN1(b, 1, 0x49)
-	if err != nil {
-		return nil, fmt.Errorf("unmarshal response: %v", err)
-	}
-	p, _, err := unmarshalASN1(r, 2, 0x06)
+	p, _, err := unmarshalASN1(b, 2, 0x06)
 	if err != nil {
 		return nil, fmt.Errorf("unmarshal points: %v", err)
 	}
@@ -1210,11 +1349,7 @@ func decodeECPublic(b []byte, curve elliptic.Curve) (*ecdsa.PublicKey, error) {
 func decodeEd25519Public(b []byte) (ed25519.PublicKey, error) {
 	// Adaptation of
 	// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=95
-	r, _, err := unmarshalASN1(b, 1, 0x49)
-	if err != nil {
-		return nil, fmt.Errorf("unmarshal response: %v", err)
-	}
-	p, _, err := unmarshalASN1(r, 2, 0x06)
+	p, _, err := unmarshalASN1(b, 2, 0x06)
 	if err != nil {
 		return nil, fmt.Errorf("unmarshal points: %v", err)
 	}
@@ -1226,11 +1361,7 @@ func decodeEd25519Public(b []byte) (ed25519.PublicKey, error) {
 
 func decodeRSAPublic(b []byte) (*rsa.PublicKey, error) {
 	// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=95
-	r, _, err := unmarshalASN1(b, 1, 0x49)
-	if err != nil {
-		return nil, fmt.Errorf("unmarshal response: %v", err)
-	}
-	mod, r, err := unmarshalASN1(r, 2, 0x01)
+	mod, r, err := unmarshalASN1(b, 2, 0x01)
 	if err != nil {
 		return nil, fmt.Errorf("unmarshal modulus: %v", err)
 	}
diff --git a/piv/piv.go b/piv/piv.go
index 2f2ed6c..11a04b1 100644
--- a/piv/piv.go
+++ b/piv/piv.go
@@ -93,6 +93,7 @@ const (
 	insSetPINRetries = 0xfa
 	insAttest        = 0xf9
 	insGetSerial     = 0xf8
+	insGetMetadata   = 0xf7
 )
 
 // YubiKey is an exclusive open connection to a YubiKey smart card. While open,
@@ -808,3 +809,13 @@ func ykSetProtectedMetadata(tx *scTx, key [24]byte, m *Metadata) error {
 	}
 	return nil
 }
+
+func supportsVersion(v Version, major, minor, patch int) bool {
+	if v.Major != major {
+		return v.Major > major
+	}
+	if v.Minor != minor {
+		return v.Minor > minor
+	}
+	return v.Patch >= patch
+}


Test patch:
diff --git a/piv/key_test.go b/piv/key_test.go
index f8ebf55..b38f72a 100644
--- a/piv/key_test.go
+++ b/piv/key_test.go
@@ -18,6 +18,7 @@ import (
 	"bytes"
 	"crypto"
 	"crypto/ecdsa"
+	"crypto/ed25519"
 	"crypto/elliptic"
 	"crypto/rand"
 	"crypto/rsa"
@@ -31,6 +32,7 @@ import (
 	"fmt"
 	"io"
 	"math/big"
+	"reflect"
 	"testing"
 	"time"
 )
@@ -1138,3 +1140,218 @@ func TestVerify(t *testing.T) {
 		})
 	}
 }
+
+func TestKeyInfo(t *testing.T) {
+	func() {
+		yk, close := newTestYubiKey(t)
+		defer close()
+
+		testRequiresVersion(t, yk, 5, 3, 0)
+
+		if err := yk.Reset(); err != nil {
+			t.Fatalf("resetting key: %v", err)
+		}
+	}()
+
+	tests := []struct {
+		name      string
+		slot      Slot
+		importKey privateKey
+		policy    Key
+	}{
+		{
+			"Generated ec_256",
+			SlotAuthentication,
+			nil,
+			Key{AlgorithmEC256, PINPolicyNever, TouchPolicyNever},
+		},
+		{
+			"Generated ec_384",
+			SlotAuthentication,
+			nil,
+			Key{AlgorithmEC384, PINPolicyNever, TouchPolicyNever},
+		},
+		{
+			"Generated rsa_1024",
+			SlotAuthentication,
+			nil,
+			Key{AlgorithmRSA1024, PINPolicyNever, TouchPolicyNever},
+		},
+		{
+			"Generated rsa_2048",
+			SlotAuthentication,
+			nil,
+			Key{AlgorithmRSA2048, PINPolicyNever, TouchPolicyNever},
+		},
+		{
+			"Imported ec_256",
+			SlotAuthentication,
+			ephemeralKey(t, AlgorithmEC256),
+			Key{AlgorithmEC256, PINPolicyNever, TouchPolicyNever},
+		},
+		{
+			"Imported ec_384",
+			SlotAuthentication,
+			ephemeralKey(t, AlgorithmEC384),
+			Key{AlgorithmEC384, PINPolicyNever, TouchPolicyNever},
+		},
+		{
+			"Imported rsa_1024",
+			SlotAuthentication,
+			ephemeralKey(t, AlgorithmRSA1024),
+			Key{AlgorithmRSA1024, PINPolicyNever, TouchPolicyNever},
+		},
+		{
+			"Imported rsa_2048",
+			SlotAuthentication,
+			ephemeralKey(t, AlgorithmRSA2048),
+			Key{AlgorithmRSA2048, PINPolicyNever, TouchPolicyNever},
+		},
+		{
+			"PINPolicyOnce",
+			SlotAuthentication,
+			nil,
+			Key{AlgorithmEC256, PINPolicyOnce, TouchPolicyNever},
+		},
+		{
+			"PINPolicyAlways",
+			SlotAuthentication,
+			nil,
+			Key{AlgorithmEC256, PINPolicyAlways, TouchPolicyNever},
+		},
+		{
+			"TouchPolicyAlways",
+			SlotAuthentication,
+			nil,
+			Key{AlgorithmEC256, PINPolicyNever, TouchPolicyAlways},
+		},
+		{
+			"TouchPolicyCached",
+			SlotAuthentication,
+			nil,
+			Key{AlgorithmEC256, PINPolicyNever, TouchPolicyCached},
+		},
+		{
+			"SlotSignature",
+			SlotSignature,
+			nil,
+			Key{AlgorithmEC256, PINPolicyNever, TouchPolicyCached},
+		},
+		{
+			"SlotCardAuthentication",
+			SlotCardAuthentication,
+			nil,
+			Key{AlgorithmEC256, PINPolicyNever, TouchPolicyCached},
+		},
+		{
+			"SlotKeyManagement",
+			SlotKeyManagement,
+			nil,
+			Key{AlgorithmEC256, PINPolicyNever, TouchPolicyCached},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			yk, close := newTestYubiKey(t)
+			defer close()
+
+			want := KeyInfo{
+				Algorithm:   test.policy.Algorithm,
+				PINPolicy:   test.policy.PINPolicy,
+				TouchPolicy: test.policy.TouchPolicy,
+			}
+
+			if test.importKey == nil {
+				pub, err := yk.GenerateKey(DefaultManagementKey, test.slot, test.policy)
+				if err != nil {
+					t.Fatalf("generating key: %v", err)
+				}
+				want.Origin = OriginGenerated
+				want.PublicKey = pub
+			} else {
+				err := yk.SetPrivateKeyInsecure(DefaultManagementKey, test.slot, test.importKey, test.policy)
+				if err != nil {
+					t.Fatalf("importing key: %v", err)
+				}
+				want.Origin = OriginImported
+				want.PublicKey = test.importKey.Public()
+			}
+
+			got, err := yk.KeyInfo(test.slot)
+			if err != nil {
+				t.Fatalf("KeyInfo() = _, %v", err)
+			}
+			if !reflect.DeepEqual(got, want) {
+				t.Errorf("KeyInfo() = %#v, want %#v", got, want)
+			}
+		})
+	}
+}
+
+// TestDerivePINPolicy checks that Yubikeys with version >= 5.3.0 use the
+// KeyInfo method to determine the pin policy, instead of the attestation
+// certificate.
+func TestPINPolicy(t *testing.T) {
+	func() {
+		yk, close := newTestYubiKey(t)
+		defer close()
+
+		testRequiresVersion(t, yk, 5, 3, 0)
+
+		if err := yk.Reset(); err != nil {
+			t.Fatalf("resetting key: %v", err)
+		}
+	}()
+
+	yk, close := newTestYubiKey(t)
+	defer close()
+
+	// for imported keys, using the attestation certificate to derive the PIN
+	// policy fails. So we check that pinPolicy succeeds with imported keys.
+	priv := ephemeralKey(t, AlgorithmEC256)
+	err := yk.SetPrivateKeyInsecure(DefaultManagementKey, SlotAuthentication, priv, Key{
+		Algorithm:   AlgorithmEC256,
+		PINPolicy:   PINPolicyNever,
+		TouchPolicy: TouchPolicyNever,
+	})
+	if err != nil {
+		t.Fatalf("import key: %v", err)
+	}
+	if got, err := pinPolicy(yk, SlotAuthentication); err != nil || got != PINPolicyNever {
+		t.Fatalf("pinPolicy() = %v, %v, want %v, <nil>", got, err, PINPolicyNever)
+	}
+}
+
+// privateKey is an interface with the optional (but always supported) methods
+// of crypto.PrivateKey.
+type privateKey interface {
+	Equal(crypto.PrivateKey) bool
+	Public() crypto.PublicKey
+}
+
+// ephemeralKey generates an ephemeral key for the given algorithm.
+func ephemeralKey(t *testing.T, alg Algorithm) privateKey {
+	t.Helper()
+	var (
+		key privateKey
+		err error
+	)
+	switch alg {
+	case AlgorithmEC256:
+		key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	case AlgorithmEC384:
+		key, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+	case AlgorithmEd25519:
+		_, key, err = ed25519.GenerateKey(rand.Reader)
+	case AlgorithmRSA1024:
+		key, err = rsa.GenerateKey(rand.Reader, 1024)
+	case AlgorithmRSA2048:
+		key, err = rsa.GenerateKey(rand.Reader, 2048)
+	default:
+		t.Fatalf("ephemeral key: unknown algorithm %d", alg)
+	}
+	if err != nil {
+		t.Fatalf("ephemeral key: %v", err)
+	}
+	return key
+}
diff --git a/piv/piv_test.go b/piv/piv_test.go
index 2672697..d6f1295 100644
--- a/piv/piv_test.go
+++ b/piv/piv_test.go
@@ -49,16 +49,6 @@ func testGetVersion(t *testing.T, h *scHandle) {
 	}
 }
 
-func supportsVersion(v Version, major, minor, patch int) bool {
-	if v.Major != major {
-		return v.Major > major
-	}
-	if v.Minor != minor {
-		return v.Minor > minor
-	}
-	return v.Patch >= patch
-}
-
 func testRequiresVersion(t *testing.T, yk *YubiKey, major, minor, patch int) {
 	v := yk.Version()
 	if !supportsVersion(v, major, minor, patch) {