Skip to content

Commit 4ef2678

Browse files
Hayley Jamesericchiang
Hayley James
authored andcommitted
Implement import key functionality
1 parent 47b85fc commit 4ef2678

File tree

2 files changed

+304
-8
lines changed

2 files changed

+304
-8
lines changed

piv/key.go

Lines changed: 145 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ import (
3535
// is given keys using different algorithms.
3636
var errMismatchingAlgorithms = errors.New("mismatching key algorithms")
3737

38+
// errUnsupportedKeySize is returned when a key has an unsupported size
39+
var errUnsupportedKeySize = errors.New("unsupported key size")
40+
41+
// unsupportedCurveError is used when a key has an unsupported curve
42+
type unsupportedCurveError struct {
43+
curve int
44+
}
45+
46+
func (e unsupportedCurveError) Error() string {
47+
return fmt.Sprintf("unsupported curve: %d", e.curve)
48+
}
49+
3850
// Slot is a private key and certificate combination managed by the security key.
3951
type Slot struct {
4052
// Key is a reference for a key type.
@@ -445,19 +457,25 @@ func (yk *YubiKey) Certificate(slot Slot) (*x509.Certificate, error) {
445457
return cert, nil
446458
}
447459

448-
// marshalASN1 encodes a tag, length and data.
449-
//
450-
// TODO: clean this up and maybe switch to cryptobyte?
451-
func marshalASN1(tag byte, data []byte) []byte {
460+
// marshalASN1Length encodes the length.
461+
func marshalASN1Length(n uint64) []byte {
452462
var l []byte
453-
n := uint64(len(data))
454463
if n < 0x80 {
455464
l = []byte{byte(n)}
456-
} else if len(data) < 0x100 {
465+
} else if n < 0x100 {
457466
l = []byte{0x81, byte(n)}
458467
} else {
459468
l = []byte{0x82, byte(n >> 8), byte(n)}
460469
}
470+
471+
return l
472+
}
473+
474+
// marshalASN1 encodes a tag, length and data.
475+
//
476+
// TODO: clean this up and maybe switch to cryptobyte?
477+
func marshalASN1(tag byte, data []byte) []byte {
478+
l := marshalASN1Length(uint64(len(data)))
461479
d := append([]byte{tag}, l...)
462480
return append(d, data...)
463481
}
@@ -709,6 +727,125 @@ func (yk *YubiKey) PrivateKey(slot Slot, public crypto.PublicKey, auth KeyAuth)
709727
}
710728
}
711729

730+
// SetPrivateKeyInsecure is an insecure method which imports a private key into the slot.
731+
// Users should almost always use GeneratePrivateKey() instead.
732+
//
733+
// Importing a private key breaks functionality provided by this package, including
734+
// AttestationCertificate() and Attest(). There are no stability guarantees for other
735+
// methods for imported private keys.
736+
//
737+
// Keys generated outside of the YubiKey should not be considered hardware-backed,
738+
// as there's no way to prove the key wasn't copied, exfiltrated, or replaced with malicious
739+
// material before being imported.
740+
func (yk *YubiKey) SetPrivateKeyInsecure(key [24]byte, slot Slot, private crypto.PrivateKey, policy Key) error {
741+
// Reference implementation
742+
// https://github.com/Yubico/yubico-piv-tool/blob/671a5740ef09d6c5d9d33f6e5575450750b58bde/lib/ykpiv.c#L1812
743+
744+
params := make([][]byte, 0)
745+
746+
var paramTag byte
747+
var elemLen int
748+
749+
switch priv := private.(type) {
750+
case *rsa.PrivateKey:
751+
paramTag = 0x01
752+
switch priv.N.BitLen() {
753+
case 1024:
754+
policy.Algorithm = AlgorithmRSA1024
755+
elemLen = 64
756+
case 2048:
757+
policy.Algorithm = AlgorithmRSA2048
758+
elemLen = 128
759+
default:
760+
return errUnsupportedKeySize
761+
}
762+
763+
priv.Precompute()
764+
765+
params = append(params, priv.Primes[0].Bytes()) // P
766+
params = append(params, priv.Primes[1].Bytes()) // Q
767+
params = append(params, priv.Precomputed.Dp.Bytes()) // dP
768+
params = append(params, priv.Precomputed.Dq.Bytes()) // dQ
769+
params = append(params, priv.Precomputed.Qinv.Bytes()) // Qinv
770+
case *ecdsa.PrivateKey:
771+
paramTag = 0x6
772+
size := priv.PublicKey.Params().BitSize
773+
switch size {
774+
case 256:
775+
policy.Algorithm = AlgorithmEC256
776+
elemLen = 32
777+
case 384:
778+
policy.Algorithm = AlgorithmEC384
779+
elemLen = 48
780+
default:
781+
return unsupportedCurveError{curve: size}
782+
}
783+
784+
// S value
785+
privateKey := make([]byte, elemLen)
786+
valueBytes := priv.D.Bytes()
787+
padding := len(privateKey) - len(valueBytes)
788+
copy(privateKey[padding:], valueBytes)
789+
790+
params = append(params, privateKey)
791+
default:
792+
return errors.New("unsupported private key type")
793+
}
794+
795+
elemLenASN1 := marshalASN1Length(uint64(elemLen))
796+
797+
tags := make([]byte, 0)
798+
for i, param := range params {
799+
tag := paramTag + byte(i)
800+
tags = append(tags, tag)
801+
tags = append(tags, elemLenASN1...)
802+
803+
padding := elemLen - len(param)
804+
param = append(make([]byte, padding), param...)
805+
tags = append(tags, param...)
806+
}
807+
808+
if err := ykAuthenticate(yk.tx, key, yk.rand); err != nil {
809+
return fmt.Errorf("authenticating with management key: %w", err)
810+
}
811+
812+
return ykImportKey(yk.tx, tags, slot, policy)
813+
}
814+
815+
func ykImportKey(tx *scTx, tags []byte, slot Slot, o Key) error {
816+
alg, ok := algorithmsMap[o.Algorithm]
817+
if !ok {
818+
return fmt.Errorf("unsupported algorithm")
819+
820+
}
821+
tp, ok := touchPolicyMap[o.TouchPolicy]
822+
if !ok {
823+
return fmt.Errorf("unsupported touch policy")
824+
}
825+
pp, ok := pinPolicyMap[o.PINPolicy]
826+
if !ok {
827+
return fmt.Errorf("unsupported pin policy")
828+
}
829+
830+
// This command is a Yubico PIV extension.
831+
// https://developers.yubico.com/PIV/Introduction/Yubico_extensions.html
832+
cmd := apdu{
833+
instruction: insImportKey,
834+
param1: alg,
835+
param2: byte(slot.Key),
836+
data: append(tags, []byte{
837+
tagPINPolicy, 0x01, pp,
838+
tagTouchPolicy, 0x01, tp,
839+
}...),
840+
}
841+
842+
if _, err := tx.Transmit(cmd); err != nil {
843+
return fmt.Errorf("command failed: %w", err)
844+
}
845+
846+
return nil
847+
}
848+
712849
// ECDSAPrivateKey is a crypto.PrivateKey implementation for ECDSA
713850
// keys. It implements crypto.Signer and the method SharedKey performs
714851
// Diffie-Hellman key agreements.
@@ -760,7 +897,7 @@ func (k *ECDSAPrivateKey) SharedKey(peer *ecdsa.PublicKey) ([]byte, error) {
760897
case 384:
761898
alg = algECCP384
762899
default:
763-
return nil, fmt.Errorf("unsupported curve: %d", size)
900+
return nil, unsupportedCurveError{curve: size}
764901
}
765902

766903
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf#page=118
@@ -840,7 +977,7 @@ func ykSignECDSA(tx *scTx, slot Slot, pub *ecdsa.PublicKey, digest []byte) ([]by
840977
case 384:
841978
alg = algECCP384
842979
default:
843-
return nil, fmt.Errorf("unsupported curve: %d", size)
980+
return nil, unsupportedCurveError{curve: size}
844981
}
845982

846983
// Same as the standard library

piv/key_test.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,3 +683,162 @@ func TestRetiredKeyManagementSlot(t *testing.T) {
683683
})
684684
}
685685
}
686+
687+
func TestSetRSAPrivateKey(t *testing.T) {
688+
tests := []struct {
689+
name string
690+
bits int
691+
slot Slot
692+
wantErr error
693+
}{
694+
695+
{
696+
name: "rsa 1024",
697+
bits: 1024,
698+
slot: SlotSignature,
699+
wantErr: nil,
700+
},
701+
{
702+
name: "rsa 2048",
703+
bits: 2048,
704+
slot: SlotCardAuthentication,
705+
wantErr: nil,
706+
},
707+
{
708+
name: "rsa 4096",
709+
bits: 4096,
710+
slot: SlotAuthentication,
711+
wantErr: errUnsupportedKeySize,
712+
},
713+
{
714+
name: "rsa 512",
715+
bits: 512,
716+
slot: SlotKeyManagement,
717+
wantErr: errUnsupportedKeySize,
718+
},
719+
}
720+
721+
for _, tt := range tests {
722+
t.Run(tt.name, func(t *testing.T) {
723+
yk, close := newTestYubiKey(t)
724+
defer close()
725+
726+
generated, err := rsa.GenerateKey(rand.Reader, tt.bits)
727+
if err != nil {
728+
t.Fatalf("generating private key: %v", err)
729+
}
730+
731+
err = yk.SetPrivateKeyInsecure(DefaultManagementKey, tt.slot, generated, Key{
732+
PINPolicy: PINPolicyNever,
733+
TouchPolicy: TouchPolicyNever,
734+
})
735+
if err != tt.wantErr {
736+
t.Fatalf("SetPrivateKeyInsecure(): wantErr=%v, got err=%v", tt.wantErr, err)
737+
}
738+
if err != nil {
739+
return
740+
}
741+
742+
priv, err := yk.PrivateKey(tt.slot, &generated.PublicKey, KeyAuth{})
743+
if err != nil {
744+
t.Fatalf("getting private key: %v", err)
745+
}
746+
747+
data := []byte("Test data that we will encrypt")
748+
749+
// Encrypt the data using our generated key
750+
encrypted, err := rsa.EncryptPKCS1v15(rand.Reader, &generated.PublicKey, data)
751+
if err != nil {
752+
t.Fatalf("encrypting data: %v", err)
753+
}
754+
755+
deviceDecrypter := priv.(crypto.Decrypter)
756+
757+
// Decrypt the data on the device
758+
decrypted, err := deviceDecrypter.Decrypt(rand.Reader, encrypted, nil)
759+
if err != nil {
760+
t.Fatalf("decrypting data: %v", err)
761+
}
762+
763+
if bytes.Compare(data, decrypted) != 0 {
764+
t.Fatalf("decrypted data is different to the source data")
765+
}
766+
})
767+
}
768+
}
769+
770+
func TestSetECDSAPrivateKey(t *testing.T) {
771+
tests := []struct {
772+
name string
773+
curve elliptic.Curve
774+
slot Slot
775+
wantErr error
776+
}{
777+
{
778+
name: "ecdsa P256",
779+
curve: elliptic.P256(),
780+
slot: SlotSignature,
781+
wantErr: nil,
782+
},
783+
{
784+
name: "ecdsa P384",
785+
curve: elliptic.P384(),
786+
slot: SlotCardAuthentication,
787+
wantErr: nil,
788+
},
789+
{
790+
name: "ecdsa P224",
791+
curve: elliptic.P224(),
792+
slot: SlotAuthentication,
793+
wantErr: unsupportedCurveError{curve: 224},
794+
},
795+
{
796+
name: "ecdsa P521",
797+
curve: elliptic.P521(),
798+
slot: SlotKeyManagement,
799+
wantErr: unsupportedCurveError{curve: 521},
800+
},
801+
}
802+
803+
for _, tt := range tests {
804+
t.Run(tt.name, func(t *testing.T) {
805+
yk, close := newTestYubiKey(t)
806+
defer close()
807+
808+
generated, err := ecdsa.GenerateKey(tt.curve, rand.Reader)
809+
if err != nil {
810+
t.Fatalf("generating private key: %v", err)
811+
}
812+
813+
err = yk.SetPrivateKeyInsecure(DefaultManagementKey, tt.slot, generated, Key{
814+
PINPolicy: PINPolicyNever,
815+
TouchPolicy: TouchPolicyNever,
816+
})
817+
if err != tt.wantErr {
818+
t.Fatalf("SetPrivateKeyInsecure(): wantErr=%v, got err=%v", tt.wantErr, err)
819+
}
820+
if err != nil {
821+
return
822+
}
823+
824+
priv, err := yk.PrivateKey(tt.slot, &generated.PublicKey, KeyAuth{})
825+
if err != nil {
826+
t.Fatalf("getting private key: %v", err)
827+
}
828+
829+
deviceSigner := priv.(crypto.Signer)
830+
831+
hash := []byte("Test data to sign")
832+
// Sign the data on the device
833+
sig, err := deviceSigner.Sign(rand.Reader, hash, nil)
834+
if err != nil {
835+
t.Fatalf("signing data: %v", err)
836+
}
837+
838+
// Verify the signature using the generated key
839+
if !ecdsa.VerifyASN1(&generated.PublicKey, hash, sig) {
840+
t.Fatal("Failed to verify signed data")
841+
}
842+
})
843+
}
844+
}

0 commit comments

Comments
 (0)