Skip to content

Commit e6548dd

Browse files
nickrayericchiang
authored andcommitted
Add custom Ed25519 signature support with alg 0x22
1 parent c329a41 commit e6548dd

File tree

2 files changed

+79
-0
lines changed

2 files changed

+79
-0
lines changed

piv/key.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"bytes"
1919
"crypto"
2020
"crypto/ecdsa"
21+
"crypto/ed25519"
2122
"crypto/elliptic"
2223
"crypto/rsa"
2324
"crypto/x509"
@@ -250,10 +251,13 @@ type Algorithm int
250251
// Algorithms supported by this package. Note that not all cards will support
251252
// every algorithm.
252253
//
254+
// AlgorithmEd25519 is currently only implemented by SoloKeys.
255+
//
253256
// For algorithm discovery, see: https://github.com/ericchiang/piv-go/issues/1
254257
const (
255258
AlgorithmEC256 Algorithm = iota + 1
256259
AlgorithmEC384
260+
AlgorithmEd25519
257261
AlgorithmRSA1024
258262
AlgorithmRSA2048
259263
)
@@ -304,6 +308,7 @@ var touchPolicyMap = map[TouchPolicy]byte{
304308
var algorithmsMap = map[Algorithm]byte{
305309
AlgorithmEC256: algECCP256,
306310
AlgorithmEC384: algECCP384,
311+
AlgorithmEd25519: algEd25519,
307312
AlgorithmRSA1024: algRSA1024,
308313
AlgorithmRSA2048: algRSA2048,
309314
}
@@ -522,6 +527,12 @@ func ykGenerateKey(tx *scTx, slot Slot, o Key) (crypto.PublicKey, error) {
522527
curve = elliptic.P256()
523528
case AlgorithmEC384:
524529
curve = elliptic.P384()
530+
case AlgorithmEd25519:
531+
pub, err := decodeEd25519Public(resp)
532+
if err != nil {
533+
return nil, fmt.Errorf("decoding ed25519 public key: %v", err)
534+
}
535+
return pub, nil
525536
default:
526537
return nil, fmt.Errorf("unsupported algorithm")
527538
}
@@ -647,6 +658,8 @@ func (yk *YubiKey) PrivateKey(slot Slot, public crypto.PublicKey, auth KeyAuth)
647658
switch pub := public.(type) {
648659
case *ecdsa.PublicKey:
649660
return &keyECDSA{yk, slot, pub, auth, pp}, nil
661+
case ed25519.PublicKey:
662+
return &keyEd25519{yk, slot, pub, auth, pp}, nil
650663
case *rsa.PublicKey:
651664
return &keyRSA{yk, slot, pub, auth, pp}, nil
652665
default:
@@ -672,6 +685,24 @@ func (k *keyECDSA) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (
672685
})
673686
}
674687

688+
type keyEd25519 struct {
689+
yk *YubiKey
690+
slot Slot
691+
pub ed25519.PublicKey
692+
auth KeyAuth
693+
pp PINPolicy
694+
}
695+
696+
func (k *keyEd25519) Public() crypto.PublicKey {
697+
return k.pub
698+
}
699+
700+
func (k *keyEd25519) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
701+
return k.auth.do(k.yk, k.pp, func(tx *scTx) ([]byte, error) {
702+
return skSignEd25519(tx, k.slot, k.pub, digest)
703+
})
704+
}
705+
675706
type keyRSA struct {
676707
yk *YubiKey
677708
slot Slot
@@ -739,6 +770,34 @@ func ykSignECDSA(tx *scTx, slot Slot, pub *ecdsa.PublicKey, digest []byte) ([]by
739770
return rs, nil
740771
}
741772

773+
// This function only works on SoloKeys prototypes and other PIV devices that choose
774+
// to implement Ed25519 signatures under alg 0x22.
775+
func skSignEd25519(tx *scTx, slot Slot, pub ed25519.PublicKey, digest []byte) ([]byte, error) {
776+
// Adaptation of
777+
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=118
778+
cmd := apdu{
779+
instruction: insAuthenticate,
780+
param1: algEd25519,
781+
param2: byte(slot.Key),
782+
data: marshalASN1(0x7c,
783+
append([]byte{0x82, 0x00},
784+
marshalASN1(0x81, digest)...)),
785+
}
786+
resp, err := tx.Transmit(cmd)
787+
if err != nil {
788+
return nil, fmt.Errorf("command failed: %w", err)
789+
}
790+
sig, _, err := unmarshalASN1(resp, 1, 0x1c) // 0x7c
791+
if err != nil {
792+
return nil, fmt.Errorf("unmarshal response: %v", err)
793+
}
794+
rs, _, err := unmarshalASN1(sig, 2, 0x02) // 0x82
795+
if err != nil {
796+
return nil, fmt.Errorf("unmarshal response signature: %v", err)
797+
}
798+
return rs, nil
799+
}
800+
742801
func unmarshalASN1(b []byte, class, tag int) (obj, rest []byte, err error) {
743802
var v asn1.RawValue
744803
rest, err = asn1.Unmarshal(b, &v)
@@ -780,6 +839,23 @@ func decodeECPublic(b []byte, curve elliptic.Curve) (*ecdsa.PublicKey, error) {
780839
return &ecdsa.PublicKey{Curve: curve, X: &x, Y: &y}, nil
781840
}
782841

842+
func decodeEd25519Public(b []byte) (ed25519.PublicKey, error) {
843+
// Adaptation of
844+
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=95
845+
r, _, err := unmarshalASN1(b, 1, 0x49)
846+
if err != nil {
847+
return nil, fmt.Errorf("unmarshal response: %v", err)
848+
}
849+
p, _, err := unmarshalASN1(r, 2, 0x06)
850+
if err != nil {
851+
return nil, fmt.Errorf("unmarshal points: %v", err)
852+
}
853+
if len(p) != ed25519.PublicKeySize {
854+
return nil, fmt.Errorf("unexpected points length: %d", len(p))
855+
}
856+
return ed25519.PublicKey(p), nil
857+
}
858+
783859
func decodeRSAPublic(b []byte) (*rsa.PublicKey, error) {
784860
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=95
785861
r, _, err := unmarshalASN1(b, 1, 0x49)

piv/piv.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ const (
6363
algRSA2048 = 0x07
6464
algECCP256 = 0x11
6565
algECCP384 = 0x14
66+
// non-standard; as implemented by SoloKeys. Chosen for low probability of eventual
67+
// clashes, if and when PIV standard adds Ed25519 support
68+
algEd25519 = 0x22
6669

6770
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-78-4.pdf#page=16
6871
keyAuthentication = 0x9a

0 commit comments

Comments
 (0)