Skip to content

Commit 7d1550b

Browse files
authored
Merge pull request #63 from smallstep/attest
YubiKey Attestation
2 parents 707ad4c + a7834b8 commit 7d1550b

File tree

7 files changed

+903
-7
lines changed

7 files changed

+903
-7
lines changed

kms/apiv1/options.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ type NameValidator interface {
3636
ValidateName(s string) error
3737
}
3838

39+
// Attester is the interface implemented by the KMS that can respond with an
40+
// attestation certificate or key.
41+
//
42+
// # Experimental
43+
//
44+
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later
45+
// release.
46+
type Attester interface {
47+
CreateAttestation(req *CreateAttestationRequest) (*CreateAttestationResponse, error)
48+
}
49+
3950
// ErrNotImplemented is the type of error returned if an operation is not
4051
// implemented.
4152
type ErrNotImplemented struct {

kms/apiv1/requests.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,27 @@ type StoreCertificateRequest struct {
165165
// Used by: pkcs11
166166
Extractable bool
167167
}
168+
169+
// CreateAttestationRequest is the parameter used in the kms.CreateAttestation
170+
// method.
171+
//
172+
// # Experimental
173+
//
174+
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later
175+
// release.
176+
type CreateAttestationRequest struct {
177+
Name string
178+
}
179+
180+
// CreateAttestationResponse is the response value of the kms.CreateAttestation
181+
// method.
182+
//
183+
// # Experimental
184+
//
185+
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later
186+
// release.
187+
type CreateAttestationResponse struct {
188+
Certificate *x509.Certificate
189+
CertificateChain []*x509.Certificate
190+
PublicKey crypto.PublicKey
191+
}

kms/kms.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ type KeyManager = apiv1.KeyManager
1717
// store x509.Certificates.
1818
type CertificateManager = apiv1.CertificateManager
1919

20+
// Attester is the interface implemented by the KMS that can respond with an
21+
// attestation certificate or key.
22+
//
23+
// # Experimental
24+
//
25+
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later
26+
// release.
27+
type Attester = apiv1.Attester
28+
2029
// Options are the KMS options. They represent the kms object in the ca.json.
2130
type Options = apiv1.Options
2231

kms/yubikey/yubikey.go

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,30 @@ const Scheme = "yubikey"
2222

2323
// YubiKey implements the KMS interface on a YubiKey.
2424
type YubiKey struct {
25-
yk *piv.YubiKey
25+
yk pivKey
2626
pin string
2727
managementKey [24]byte
2828
}
2929

30+
type pivKey interface {
31+
Certificate(slot piv.Slot) (*x509.Certificate, error)
32+
SetCertificate(key [24]byte, slot piv.Slot, cert *x509.Certificate) error
33+
GenerateKey(key [24]byte, slot piv.Slot, opts piv.Key) (crypto.PublicKey, error)
34+
PrivateKey(slot piv.Slot, public crypto.PublicKey, auth piv.KeyAuth) (crypto.PrivateKey, error)
35+
Attest(slot piv.Slot) (*x509.Certificate, error)
36+
Close() error
37+
}
38+
39+
var pivCards = piv.Cards
40+
41+
var pivOpen = func(card string) (pivKey, error) {
42+
return piv.Open(card)
43+
}
44+
3045
// New initializes a new YubiKey.
3146
// TODO(mariano): only one card is currently supported.
3247
func New(ctx context.Context, opts apiv1.Options) (*YubiKey, error) {
48+
pin := "123456"
3349
managementKey := piv.DefaultManagementKey
3450

3551
if opts.URI != "" {
@@ -57,22 +73,26 @@ func New(ctx context.Context, opts apiv1.Options) (*YubiKey, error) {
5773
copy(managementKey[:], b[:24])
5874
}
5975

60-
cards, err := piv.Cards()
76+
if opts.Pin != "" {
77+
pin = opts.Pin
78+
}
79+
80+
cards, err := pivCards()
6181
if err != nil {
6282
return nil, err
6383
}
6484
if len(cards) == 0 {
6585
return nil, errors.New("error detecting yubikey: try removing and reconnecting the device")
6686
}
6787

68-
yk, err := piv.Open(cards[0])
88+
yk, err := pivOpen(cards[0])
6989
if err != nil {
7090
return nil, errors.Wrap(err, "error opening yubikey")
7191
}
7292

7393
return &YubiKey{
7494
yk: yk,
75-
pin: opts.Pin,
95+
pin: pin,
7696
managementKey: managementKey,
7797
}, nil
7898
}
@@ -170,13 +190,21 @@ func (k *YubiKey) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, e
170190
return nil, err
171191
}
172192

193+
pin := k.pin
194+
if pin == "" {
195+
// Attempt to get the pin from the uri
196+
if u, err := uri.ParseWithScheme(Scheme, req.SigningKey); err == nil {
197+
pin = u.Pin()
198+
}
199+
}
200+
173201
pub, err := k.getPublicKey(slot)
174202
if err != nil {
175203
return nil, err
176204
}
177205

178206
priv, err := k.yk.PrivateKey(slot, pub, piv.KeyAuth{
179-
PIN: k.pin,
207+
PIN: pin,
180208
PINPolicy: piv.PINPolicyAlways,
181209
})
182210
if err != nil {
@@ -190,6 +218,35 @@ func (k *YubiKey) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, e
190218
return signer, nil
191219
}
192220

221+
// CreateAttestation creates an attestation certificate from a YubiKey slot.
222+
//
223+
// # Experimental
224+
//
225+
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later
226+
// release.
227+
func (k *YubiKey) CreateAttestation(req *apiv1.CreateAttestationRequest) (*apiv1.CreateAttestationResponse, error) {
228+
slot, err := getSlot(req.Name)
229+
if err != nil {
230+
return nil, err
231+
}
232+
233+
cert, err := k.yk.Attest(slot)
234+
if err != nil {
235+
return nil, errors.Wrap(err, "error attesting slot")
236+
}
237+
238+
intermediate, err := k.yk.Certificate(slotAttestation)
239+
if err != nil {
240+
return nil, errors.Wrap(err, "error retrieving attestation certificate")
241+
}
242+
243+
return &apiv1.CreateAttestationResponse{
244+
Certificate: cert,
245+
CertificateChain: []*x509.Certificate{intermediate},
246+
PublicKey: cert.PublicKey,
247+
}, nil
248+
}
249+
193250
// Close releases the connection to the YubiKey.
194251
func (k *YubiKey) Close() error {
195252
return errors.Wrap(k.yk.Close(), "error closing yubikey")
@@ -258,6 +315,8 @@ func getSignatureAlgorithm(alg apiv1.SignatureAlgorithm, bits int) (piv.Algorith
258315
}
259316
}
260317

318+
var slotAttestation = piv.Slot{Key: 0xf9, Object: 0x5fff01}
319+
261320
var slotMapping = map[string]piv.Slot{
262321
"9a": piv.SlotAuthentication,
263322
"9c": piv.SlotSignature,
@@ -307,7 +366,7 @@ func getSlotAndName(name string) (piv.Slot, string, error) {
307366
return piv.Slot{}, "", errors.Wrapf(err, "error parsing '%s'", name)
308367
}
309368
if slotID = v.Get("slot-id"); slotID == "" {
310-
return piv.Slot{}, "", errors.Wrapf(err, "error parsing '%s': slot-id is missing", name)
369+
return piv.Slot{}, "", errors.Errorf("error parsing '%s': slot-id is missing", name)
311370
}
312371
} else {
313372
slotID = name

0 commit comments

Comments
 (0)