Skip to content

Commit 0a1ad49

Browse files
authored
Merge pull request #92 from smallstep/yubikey-policies
YubiKey pin and touch policies
2 parents 90c5c81 + 305a3e2 commit 0a1ad49

File tree

3 files changed

+232
-9
lines changed

3 files changed

+232
-9
lines changed

kms/apiv1/requests.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,35 @@ const (
1919
HSM
2020
)
2121

22+
// PINPolicy represents PIN requirements when signing or decrypting with an
23+
// asymmetric key in a given slot. PINPolicy is used by the YubiKey KMS.
24+
type PINPolicy int
25+
26+
// PIN policies supported by this package. The values must match the ones in
27+
// github.com/go-piv/piv-go/piv.
28+
//
29+
// Caching for PINPolicyOnce isn't supported on YubiKey
30+
// versions older than 4.3.0 due to issues with verifying if a PIN is needed.
31+
// If specified, a PIN will be required for every operation.
32+
const (
33+
PINPolicyNever PINPolicy = iota + 1
34+
PINPolicyOnce
35+
PINPolicyAlways
36+
)
37+
38+
// TouchPolicy represents proof-of-presence requirements when signing or
39+
// decrypting with asymmetric key in a given slot. TouchPolicy is used by the
40+
// YubiKey KMS.
41+
type TouchPolicy int
42+
43+
// Touch policies supported by this package. The values must match the ones in
44+
// github.com/go-piv/piv-go/piv.
45+
const (
46+
TouchPolicyNever TouchPolicy = iota + 1
47+
TouchPolicyAlways
48+
TouchPolicyCached
49+
)
50+
2251
// String returns a string representation of p.
2352
func (p ProtectionLevel) String() string {
2453
switch p {
@@ -118,6 +147,18 @@ type CreateKeyRequest struct {
118147
//
119148
// Used by: pkcs11
120149
Extractable bool
150+
151+
// PINPolicy defines PIN requirements when signing or decrypting with an
152+
// asymmetric key.
153+
//
154+
// Used by: yubikey
155+
PINPolicy PINPolicy
156+
157+
// TouchPolicy represents proof-of-presence requirements when signing or
158+
// decrypting with asymmetric key in a given slot.
159+
//
160+
// Used by: yubikey
161+
TouchPolicy TouchPolicy
121162
}
122163

123164
// CreateKeyResponse is the response value of the kms.CreateKey method.

kms/yubikey/yubikey.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,12 @@ func (k *YubiKey) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespon
164164
if err != nil {
165165
return nil, err
166166
}
167+
pinPolicy, touchPolicy := getPolicies(req)
167168

168169
pub, err := k.yk.GenerateKey(k.managementKey, slot, piv.Key{
169170
Algorithm: alg,
170-
PINPolicy: piv.PINPolicyAlways,
171-
TouchPolicy: piv.TouchPolicyNever,
171+
PINPolicy: pinPolicy,
172+
TouchPolicy: touchPolicy,
172173
})
173174
if err != nil {
174175
return nil, errors.Wrap(err, "error generating key")
@@ -380,3 +381,17 @@ func getSlotAndName(name string) (piv.Slot, string, error) {
380381
name = "yubikey:slot-id=" + url.QueryEscape(slotID)
381382
return s, name, nil
382383
}
384+
385+
// getPolicies returns the pin and touch policies from the request. If they are
386+
// not set the defaults are piv.PINPolicyAlways and piv.TouchPolicyNever.
387+
func getPolicies(req *apiv1.CreateKeyRequest) (piv.PINPolicy, piv.TouchPolicy) {
388+
pin := piv.PINPolicy(req.PINPolicy)
389+
touch := piv.TouchPolicy(req.TouchPolicy)
390+
if pin == 0 {
391+
pin = piv.PINPolicyAlways
392+
}
393+
if touch == 0 {
394+
touch = piv.TouchPolicyNever
395+
}
396+
return pin, touch
397+
}

kms/yubikey/yubikey_test.go

Lines changed: 174 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ import (
2424
)
2525

2626
type stubPivKey struct {
27-
attestCA *minica.CA
28-
userCA *minica.CA
29-
attestMap map[piv.Slot]*x509.Certificate
30-
certMap map[piv.Slot]*x509.Certificate
31-
signerMap map[piv.Slot]interface{}
27+
attestCA *minica.CA
28+
userCA *minica.CA
29+
attestMap map[piv.Slot]*x509.Certificate
30+
certMap map[piv.Slot]*x509.Certificate
31+
signerMap map[piv.Slot]interface{}
32+
keyOptionsMap map[piv.Slot]piv.Key
3233
}
3334

3435
//nolint:typecheck // ignore deadcode warnings
@@ -83,6 +84,7 @@ func newStubPivKey(t *testing.T) *stubPivKey {
8384
piv.SlotAuthentication: attSigner, // 9a
8485
piv.SlotSignature: userSigner, // 9c
8586
},
87+
keyOptionsMap: map[piv.Slot]piv.Key{},
8688
}
8789
}
8890

@@ -144,6 +146,7 @@ func (s *stubPivKey) GenerateKey(key [24]byte, slot piv.Slot, opts piv.Key) (cry
144146
}
145147

146148
s.signerMap[slot] = signer
149+
s.keyOptionsMap[slot] = opts
147150
return signer.Public(), nil
148151
}
149152

@@ -539,6 +542,20 @@ func TestYubiKey_CreateKey(t *testing.T) {
539542
},
540543
}
541544
}, false},
545+
{"ok with policies", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
546+
Name: "yubikey:slot-id=82",
547+
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
548+
PINPolicy: apiv1.PINPolicyNever,
549+
TouchPolicy: apiv1.TouchPolicyAlways,
550+
}}, func() *apiv1.CreateKeyResponse {
551+
return &apiv1.CreateKeyResponse{
552+
Name: "yubikey:slot-id=82",
553+
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
554+
CreateSignerRequest: apiv1.CreateSignerRequest{
555+
SigningKey: "yubikey:slot-id=82",
556+
},
557+
}
558+
}, false},
542559
{"fail rsa 4096", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
543560
Name: "yubikey:slot-id=82",
544561
SignatureAlgorithm: apiv1.SHA256WithRSA,
@@ -559,9 +576,152 @@ func TestYubiKey_CreateKey(t *testing.T) {
559576
}
560577
for _, tt := range tests {
561578
t.Run(tt.name, func(t *testing.T) {
562-
if tt.name == "fail getSlotAndName" {
563-
t.Log(tt.name)
579+
k := &YubiKey{
580+
yk: tt.fields.yk,
581+
pin: tt.fields.pin,
582+
managementKey: tt.fields.managementKey,
583+
}
584+
got, err := k.CreateKey(tt.args.req)
585+
if (err != nil) != tt.wantErr {
586+
t.Errorf("YubiKey.CreateKey() error = %v, wantErr %v", err, tt.wantErr)
587+
return
588+
}
589+
want := tt.wantFn()
590+
if !reflect.DeepEqual(got, want) {
591+
t.Errorf("YubiKey.CreateKey() = %v, want %v", got, want)
592+
}
593+
})
594+
}
595+
}
596+
597+
func TestYubiKey_CreateKey_policies(t *testing.T) {
598+
yk := newStubPivKey(t)
599+
600+
type fields struct {
601+
yk pivKey
602+
pin string
603+
managementKey [24]byte
604+
}
605+
type args struct {
606+
req *apiv1.CreateKeyRequest
607+
}
608+
tests := []struct {
609+
name string
610+
fields fields
611+
args args
612+
wantSlot piv.Slot
613+
wantPinPolicy piv.PINPolicy
614+
wantTouchPolicy piv.TouchPolicy
615+
wantFn func() *apiv1.CreateKeyResponse
616+
wantErr bool
617+
}{
618+
{"ok", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
619+
Name: "yubikey:slot-id=82",
620+
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
621+
}}, slotMapping["82"], piv.PINPolicyAlways, piv.TouchPolicyNever, func() *apiv1.CreateKeyResponse {
622+
return &apiv1.CreateKeyResponse{
623+
Name: "yubikey:slot-id=82",
624+
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
625+
CreateSignerRequest: apiv1.CreateSignerRequest{
626+
SigningKey: "yubikey:slot-id=82",
627+
},
628+
}
629+
}, false},
630+
{"ok PINPolicyNever", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
631+
Name: "yubikey:slot-id=82",
632+
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
633+
PINPolicy: apiv1.PINPolicyNever,
634+
}}, slotMapping["82"], piv.PINPolicyNever, piv.TouchPolicyNever, func() *apiv1.CreateKeyResponse {
635+
return &apiv1.CreateKeyResponse{
636+
Name: "yubikey:slot-id=82",
637+
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
638+
CreateSignerRequest: apiv1.CreateSignerRequest{
639+
SigningKey: "yubikey:slot-id=82",
640+
},
564641
}
642+
}, false},
643+
{"ok PINPolicyOnce", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
644+
Name: "yubikey:slot-id=82",
645+
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
646+
PINPolicy: apiv1.PINPolicyOnce,
647+
}}, slotMapping["82"], piv.PINPolicyOnce, piv.TouchPolicyNever, func() *apiv1.CreateKeyResponse {
648+
return &apiv1.CreateKeyResponse{
649+
Name: "yubikey:slot-id=82",
650+
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
651+
CreateSignerRequest: apiv1.CreateSignerRequest{
652+
SigningKey: "yubikey:slot-id=82",
653+
},
654+
}
655+
}, false},
656+
{"ok PINPolicyAlways", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
657+
Name: "yubikey:slot-id=82",
658+
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
659+
PINPolicy: apiv1.PINPolicyAlways,
660+
}}, slotMapping["82"], piv.PINPolicyAlways, piv.TouchPolicyNever, func() *apiv1.CreateKeyResponse {
661+
return &apiv1.CreateKeyResponse{
662+
Name: "yubikey:slot-id=82",
663+
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
664+
CreateSignerRequest: apiv1.CreateSignerRequest{
665+
SigningKey: "yubikey:slot-id=82",
666+
},
667+
}
668+
}, false},
669+
{"ok TouchPolicyNever", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
670+
Name: "yubikey:slot-id=82",
671+
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
672+
TouchPolicy: apiv1.TouchPolicyNever,
673+
}}, slotMapping["82"], piv.PINPolicyAlways, piv.TouchPolicyNever, func() *apiv1.CreateKeyResponse {
674+
return &apiv1.CreateKeyResponse{
675+
Name: "yubikey:slot-id=82",
676+
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
677+
CreateSignerRequest: apiv1.CreateSignerRequest{
678+
SigningKey: "yubikey:slot-id=82",
679+
},
680+
}
681+
}, false},
682+
{"ok TouchPolicyAlways", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
683+
Name: "yubikey:slot-id=82",
684+
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
685+
TouchPolicy: apiv1.TouchPolicyAlways,
686+
}}, slotMapping["82"], piv.PINPolicyAlways, piv.TouchPolicyAlways, func() *apiv1.CreateKeyResponse {
687+
return &apiv1.CreateKeyResponse{
688+
Name: "yubikey:slot-id=82",
689+
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
690+
CreateSignerRequest: apiv1.CreateSignerRequest{
691+
SigningKey: "yubikey:slot-id=82",
692+
},
693+
}
694+
}, false},
695+
{"ok TouchPolicyCached", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
696+
Name: "yubikey:slot-id=82",
697+
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
698+
TouchPolicy: apiv1.TouchPolicyCached,
699+
}}, slotMapping["82"], piv.PINPolicyAlways, piv.TouchPolicyCached, func() *apiv1.CreateKeyResponse {
700+
return &apiv1.CreateKeyResponse{
701+
Name: "yubikey:slot-id=82",
702+
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
703+
CreateSignerRequest: apiv1.CreateSignerRequest{
704+
SigningKey: "yubikey:slot-id=82",
705+
},
706+
}
707+
}, false},
708+
{"ok both policies", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
709+
Name: "yubikey:slot-id=82",
710+
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
711+
PINPolicy: apiv1.PINPolicyNever,
712+
TouchPolicy: apiv1.TouchPolicyAlways,
713+
}}, slotMapping["82"], piv.PINPolicyNever, piv.TouchPolicyAlways, func() *apiv1.CreateKeyResponse {
714+
return &apiv1.CreateKeyResponse{
715+
Name: "yubikey:slot-id=82",
716+
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
717+
CreateSignerRequest: apiv1.CreateSignerRequest{
718+
SigningKey: "yubikey:slot-id=82",
719+
},
720+
}
721+
}, false},
722+
}
723+
for _, tt := range tests {
724+
t.Run(tt.name, func(t *testing.T) {
565725
k := &YubiKey{
566726
yk: tt.fields.yk,
567727
pin: tt.fields.pin,
@@ -572,10 +732,17 @@ func TestYubiKey_CreateKey(t *testing.T) {
572732
t.Errorf("YubiKey.CreateKey() error = %v, wantErr %v", err, tt.wantErr)
573733
return
574734
}
735+
if v := yk.keyOptionsMap[tt.wantSlot].PINPolicy; !reflect.DeepEqual(v, tt.wantPinPolicy) {
736+
t.Errorf("YubiKey.CreateKey() PINPolicy = %v, want %v", v, tt.wantPinPolicy)
737+
}
738+
if v := yk.keyOptionsMap[tt.wantSlot].TouchPolicy; !reflect.DeepEqual(v, tt.wantTouchPolicy) {
739+
t.Errorf("YubiKey.CreateKey() TouchPolicy = %v, want %v", v, tt.wantTouchPolicy)
740+
}
575741
want := tt.wantFn()
576742
if !reflect.DeepEqual(got, want) {
577743
t.Errorf("YubiKey.CreateKey() = %v, want %v", got, want)
578744
}
745+
579746
})
580747
}
581748
}

0 commit comments

Comments
 (0)