Skip to content

Commit

Permalink
Merge pull request #92 from smallstep/yubikey-policies
Browse files Browse the repository at this point in the history
YubiKey pin and touch policies
  • Loading branch information
maraino authored Oct 7, 2022
2 parents 90c5c81 + 305a3e2 commit 0a1ad49
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 9 deletions.
41 changes: 41 additions & 0 deletions kms/apiv1/requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,35 @@ const (
HSM
)

// PINPolicy represents PIN requirements when signing or decrypting with an
// asymmetric key in a given slot. PINPolicy is used by the YubiKey KMS.
type PINPolicy int

// PIN policies supported by this package. The values must match the ones in
// github.com/go-piv/piv-go/piv.
//
// Caching for PINPolicyOnce isn't supported on YubiKey
// versions older than 4.3.0 due to issues with verifying if a PIN is needed.
// If specified, a PIN will be required for every operation.
const (
PINPolicyNever PINPolicy = iota + 1
PINPolicyOnce
PINPolicyAlways
)

// TouchPolicy represents proof-of-presence requirements when signing or
// decrypting with asymmetric key in a given slot. TouchPolicy is used by the
// YubiKey KMS.
type TouchPolicy int

// Touch policies supported by this package. The values must match the ones in
// github.com/go-piv/piv-go/piv.
const (
TouchPolicyNever TouchPolicy = iota + 1
TouchPolicyAlways
TouchPolicyCached
)

// String returns a string representation of p.
func (p ProtectionLevel) String() string {
switch p {
Expand Down Expand Up @@ -118,6 +147,18 @@ type CreateKeyRequest struct {
//
// Used by: pkcs11
Extractable bool

// PINPolicy defines PIN requirements when signing or decrypting with an
// asymmetric key.
//
// Used by: yubikey
PINPolicy PINPolicy

// TouchPolicy represents proof-of-presence requirements when signing or
// decrypting with asymmetric key in a given slot.
//
// Used by: yubikey
TouchPolicy TouchPolicy
}

// CreateKeyResponse is the response value of the kms.CreateKey method.
Expand Down
19 changes: 17 additions & 2 deletions kms/yubikey/yubikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,12 @@ func (k *YubiKey) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespon
if err != nil {
return nil, err
}
pinPolicy, touchPolicy := getPolicies(req)

pub, err := k.yk.GenerateKey(k.managementKey, slot, piv.Key{
Algorithm: alg,
PINPolicy: piv.PINPolicyAlways,
TouchPolicy: piv.TouchPolicyNever,
PINPolicy: pinPolicy,
TouchPolicy: touchPolicy,
})
if err != nil {
return nil, errors.Wrap(err, "error generating key")
Expand Down Expand Up @@ -380,3 +381,17 @@ func getSlotAndName(name string) (piv.Slot, string, error) {
name = "yubikey:slot-id=" + url.QueryEscape(slotID)
return s, name, nil
}

// getPolicies returns the pin and touch policies from the request. If they are
// not set the defaults are piv.PINPolicyAlways and piv.TouchPolicyNever.
func getPolicies(req *apiv1.CreateKeyRequest) (piv.PINPolicy, piv.TouchPolicy) {
pin := piv.PINPolicy(req.PINPolicy)
touch := piv.TouchPolicy(req.TouchPolicy)
if pin == 0 {
pin = piv.PINPolicyAlways
}
if touch == 0 {
touch = piv.TouchPolicyNever
}
return pin, touch
}
181 changes: 174 additions & 7 deletions kms/yubikey/yubikey_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ import (
)

type stubPivKey struct {
attestCA *minica.CA
userCA *minica.CA
attestMap map[piv.Slot]*x509.Certificate
certMap map[piv.Slot]*x509.Certificate
signerMap map[piv.Slot]interface{}
attestCA *minica.CA
userCA *minica.CA
attestMap map[piv.Slot]*x509.Certificate
certMap map[piv.Slot]*x509.Certificate
signerMap map[piv.Slot]interface{}
keyOptionsMap map[piv.Slot]piv.Key
}

//nolint:typecheck // ignore deadcode warnings
Expand Down Expand Up @@ -83,6 +84,7 @@ func newStubPivKey(t *testing.T) *stubPivKey {
piv.SlotAuthentication: attSigner, // 9a
piv.SlotSignature: userSigner, // 9c
},
keyOptionsMap: map[piv.Slot]piv.Key{},
}
}

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

s.signerMap[slot] = signer
s.keyOptionsMap[slot] = opts
return signer.Public(), nil
}

Expand Down Expand Up @@ -539,6 +542,20 @@ func TestYubiKey_CreateKey(t *testing.T) {
},
}
}, false},
{"ok with policies", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
Name: "yubikey:slot-id=82",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
PINPolicy: apiv1.PINPolicyNever,
TouchPolicy: apiv1.TouchPolicyAlways,
}}, func() *apiv1.CreateKeyResponse {
return &apiv1.CreateKeyResponse{
Name: "yubikey:slot-id=82",
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "yubikey:slot-id=82",
},
}
}, false},
{"fail rsa 4096", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
Name: "yubikey:slot-id=82",
SignatureAlgorithm: apiv1.SHA256WithRSA,
Expand All @@ -559,9 +576,152 @@ func TestYubiKey_CreateKey(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.name == "fail getSlotAndName" {
t.Log(tt.name)
k := &YubiKey{
yk: tt.fields.yk,
pin: tt.fields.pin,
managementKey: tt.fields.managementKey,
}
got, err := k.CreateKey(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("YubiKey.CreateKey() error = %v, wantErr %v", err, tt.wantErr)
return
}
want := tt.wantFn()
if !reflect.DeepEqual(got, want) {
t.Errorf("YubiKey.CreateKey() = %v, want %v", got, want)
}
})
}
}

func TestYubiKey_CreateKey_policies(t *testing.T) {
yk := newStubPivKey(t)

type fields struct {
yk pivKey
pin string
managementKey [24]byte
}
type args struct {
req *apiv1.CreateKeyRequest
}
tests := []struct {
name string
fields fields
args args
wantSlot piv.Slot
wantPinPolicy piv.PINPolicy
wantTouchPolicy piv.TouchPolicy
wantFn func() *apiv1.CreateKeyResponse
wantErr bool
}{
{"ok", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
Name: "yubikey:slot-id=82",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
}}, slotMapping["82"], piv.PINPolicyAlways, piv.TouchPolicyNever, func() *apiv1.CreateKeyResponse {
return &apiv1.CreateKeyResponse{
Name: "yubikey:slot-id=82",
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "yubikey:slot-id=82",
},
}
}, false},
{"ok PINPolicyNever", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
Name: "yubikey:slot-id=82",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
PINPolicy: apiv1.PINPolicyNever,
}}, slotMapping["82"], piv.PINPolicyNever, piv.TouchPolicyNever, func() *apiv1.CreateKeyResponse {
return &apiv1.CreateKeyResponse{
Name: "yubikey:slot-id=82",
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "yubikey:slot-id=82",
},
}
}, false},
{"ok PINPolicyOnce", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
Name: "yubikey:slot-id=82",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
PINPolicy: apiv1.PINPolicyOnce,
}}, slotMapping["82"], piv.PINPolicyOnce, piv.TouchPolicyNever, func() *apiv1.CreateKeyResponse {
return &apiv1.CreateKeyResponse{
Name: "yubikey:slot-id=82",
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "yubikey:slot-id=82",
},
}
}, false},
{"ok PINPolicyAlways", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
Name: "yubikey:slot-id=82",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
PINPolicy: apiv1.PINPolicyAlways,
}}, slotMapping["82"], piv.PINPolicyAlways, piv.TouchPolicyNever, func() *apiv1.CreateKeyResponse {
return &apiv1.CreateKeyResponse{
Name: "yubikey:slot-id=82",
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "yubikey:slot-id=82",
},
}
}, false},
{"ok TouchPolicyNever", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
Name: "yubikey:slot-id=82",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
TouchPolicy: apiv1.TouchPolicyNever,
}}, slotMapping["82"], piv.PINPolicyAlways, piv.TouchPolicyNever, func() *apiv1.CreateKeyResponse {
return &apiv1.CreateKeyResponse{
Name: "yubikey:slot-id=82",
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "yubikey:slot-id=82",
},
}
}, false},
{"ok TouchPolicyAlways", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
Name: "yubikey:slot-id=82",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
TouchPolicy: apiv1.TouchPolicyAlways,
}}, slotMapping["82"], piv.PINPolicyAlways, piv.TouchPolicyAlways, func() *apiv1.CreateKeyResponse {
return &apiv1.CreateKeyResponse{
Name: "yubikey:slot-id=82",
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "yubikey:slot-id=82",
},
}
}, false},
{"ok TouchPolicyCached", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
Name: "yubikey:slot-id=82",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
TouchPolicy: apiv1.TouchPolicyCached,
}}, slotMapping["82"], piv.PINPolicyAlways, piv.TouchPolicyCached, func() *apiv1.CreateKeyResponse {
return &apiv1.CreateKeyResponse{
Name: "yubikey:slot-id=82",
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "yubikey:slot-id=82",
},
}
}, false},
{"ok both policies", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateKeyRequest{
Name: "yubikey:slot-id=82",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
PINPolicy: apiv1.PINPolicyNever,
TouchPolicy: apiv1.TouchPolicyAlways,
}}, slotMapping["82"], piv.PINPolicyNever, piv.TouchPolicyAlways, func() *apiv1.CreateKeyResponse {
return &apiv1.CreateKeyResponse{
Name: "yubikey:slot-id=82",
PublicKey: yk.signerMap[slotMapping["82"]].(crypto.Signer).Public(),
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "yubikey:slot-id=82",
},
}
}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
k := &YubiKey{
yk: tt.fields.yk,
pin: tt.fields.pin,
Expand All @@ -572,10 +732,17 @@ func TestYubiKey_CreateKey(t *testing.T) {
t.Errorf("YubiKey.CreateKey() error = %v, wantErr %v", err, tt.wantErr)
return
}
if v := yk.keyOptionsMap[tt.wantSlot].PINPolicy; !reflect.DeepEqual(v, tt.wantPinPolicy) {
t.Errorf("YubiKey.CreateKey() PINPolicy = %v, want %v", v, tt.wantPinPolicy)
}
if v := yk.keyOptionsMap[tt.wantSlot].TouchPolicy; !reflect.DeepEqual(v, tt.wantTouchPolicy) {
t.Errorf("YubiKey.CreateKey() TouchPolicy = %v, want %v", v, tt.wantTouchPolicy)
}
want := tt.wantFn()
if !reflect.DeepEqual(got, want) {
t.Errorf("YubiKey.CreateKey() = %v, want %v", got, want)
}

})
}
}
Expand Down

0 comments on commit 0a1ad49

Please sign in to comment.