-
Notifications
You must be signed in to change notification settings - Fork 1
/
attestation.go
207 lines (181 loc) · 6.31 KB
/
attestation.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// SPDX-FileCopyrightText: 2020 Google LLC
// SPDX-License-Identifier: Apache-2.0
package piv
import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
iso "cunicu.li/go-iso7816"
)
// Prefix in the x509 Subject Common Name for YubiKey attestations
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
const yubikeySubjectCNPrefix = "YubiKey PIV Attestation "
// Attestation returns additional information about a key attested to be generated
// on a card. See https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
// for more information.
type Attestation struct {
// Version of the YubiKey's firmware.
Version iso.Version
// Serial is the YubiKey's serial number.
Serial uint32
// FormFactor indicates the physical type of the YubiKey.
//
// FormFactor may be empty FormFactor(0) for some YubiKeys.
FormFactor FormFactor
// PINPolicy set on the slot.
PINPolicy PINPolicy
// TouchPolicy set on the slot.
TouchPolicy TouchPolicy
// Slot is the inferred slot the attested key resides in based on the
// common name in the attestation. If the slot cannot be determined,
// this field will be an empty struct.
Slot Slot
}
func (a *Attestation) addExt(e pkix.Extension) error {
switch {
case e.Id.Equal(extIDFirmwareVersion):
if len(e.Value) != 3 {
return fmt.Errorf("%w for firmware version, got=%dB, want=3B", errUnexpectedLength, len(e.Value))
}
a.Version = iso.Version{
Major: int(e.Value[0]),
Minor: int(e.Value[1]),
Patch: int(e.Value[2]),
}
case e.Id.Equal(extIDSerialNumber):
var serial int64
if _, err := asn1.Unmarshal(e.Value, &serial); err != nil {
return fmt.Errorf("failed to parse serial number: %w", err)
}
if serial < 0 {
return fmt.Errorf("%w: is negative %d", errInvalidSerialNumber, serial)
}
a.Serial = uint32(serial) //nolint:gosec
case e.Id.Equal(extIDKeyPolicy):
if len(e.Value) != 2 {
return fmt.Errorf("%w for key policy: got=%dB, want=2B", errUnexpectedLength, len(e.Value))
}
switch e.Value[0] {
case 0x01:
a.PINPolicy = PINPolicyNever
case 0x02:
a.PINPolicy = PINPolicyOnce
case 0x03:
a.PINPolicy = PINPolicyAlways
default:
return fmt.Errorf("%w: 0x%x", errUnsupportedPinPolicy, e.Value[0])
}
switch e.Value[1] {
case 0x01:
a.TouchPolicy = TouchPolicyNever
case 0x02:
a.TouchPolicy = TouchPolicyAlways
case 0x03:
a.TouchPolicy = TouchPolicyCached
default:
return fmt.Errorf("%w: 0x%x", errUnsupportedTouchPolicy, e.Value[1])
}
case e.Id.Equal(extIDFormFactor):
if len(e.Value) != 1 {
return fmt.Errorf("%w: expected 1 byte for form factor, got=%d", errUnexpectedLength, len(e.Value))
}
a.FormFactor = FormFactor(e.Value[0])
}
return nil
}
// Verify proves that a key was generated on a YubiKey. It ensures the slot and
// YubiKey certificate chains up to the Yubico CA, parsing additional information
// out of the slot certificate, such as the touch and PIN policies of a key.
func Verify(attestationCert, slotCert *x509.Certificate) (*Attestation, error) {
var v Verifier
return v.Verify(attestationCert, slotCert)
}
// Verifier allows specifying options when verifying attestations produced by
// YubiKeys.
type Verifier struct {
// Root certificates to use to validate challenges. If nil, this defaults to Yubico's
// CA bundle.
//
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
// https://developers.yubico.com/PIV/Introduction/piv-attestation-ca.pem
// https://developers.yubico.com/U2F/yubico-u2f-ca-certs.txt
Roots *x509.CertPool
}
// Verify proves that a key was generated on a YubiKey.
//
// As opposed to the package level [Verify], it uses any options enabled on the [Verifier].
func (v *Verifier) Verify(attestationCert, slotCert *x509.Certificate) (*Attestation, error) {
o := x509.VerifyOptions{KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}}
o.Roots = v.Roots
if o.Roots == nil {
cas, err := yubicoCAs()
if err != nil {
return nil, fmt.Errorf("failed to load yubico CAs: %w", err)
}
o.Roots = cas
}
o.Intermediates = x509.NewCertPool()
// The attestation cert in some YubiKey 4 does not encode X509v3 Basic Constraints.
// This isn't valid as per https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9
// (fourth paragraph) and thus makes x509.go validation fail.
// Work around this by setting this constraint here.
if !attestationCert.BasicConstraintsValid {
attestationCert.BasicConstraintsValid = true
attestationCert.IsCA = true
}
o.Intermediates.AddCert(attestationCert)
if _, err := slotCert.Verify(o); err != nil {
return nil, fmt.Errorf("failed to verify attestation certificate: %w", err)
}
return parseAttestation(slotCert)
}
func parseAttestation(slotCert *x509.Certificate) (*Attestation, error) {
var a Attestation
for _, ext := range slotCert.Extensions {
if err := a.addExt(ext); err != nil {
return nil, fmt.Errorf("failed to parse extension: %w", err)
}
}
slot, ok := parseSlot(slotCert.Subject.CommonName)
if ok {
a.Slot = slot
}
return &a, nil
}
// AttestationCertificate returns the YubiKey's attestation certificate, which
// is unique to the key and signed by Yubico.
func (c *Card) AttestationCertificate() (*x509.Certificate, error) {
return c.Certificate(SlotAttestation)
}
// Attest generates a certificate for a key, signed by the YubiKey's attestation
// certificate. This can be used to prove a key was generate on a specific
// YubiKey.
//
// This method is only supported for YubiKey versions >= 4.3.0.
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
//
// Certificates returned by this method MUST NOT be used for anything other than
// attestation or determining the slots public key. For example, the certificate
// is NOT suitable for TLS.
//
// If the slot doesn't have a key, the returned error wraps ErrNotFound.
func (c *Card) Attest(slot Slot) (cert *x509.Certificate, err error) {
resp, err := send(c.tx, insAttest, slot.Key, 0, nil)
if err != nil {
return nil, fmt.Errorf("failed to execute command: %w", err)
}
var e *iso.Code
if errors.As(err, &e) && *e == iso.ErrIncorrectData {
return nil, ErrNotFound
}
if cert, err = x509.ParseCertificate(resp); err != nil {
return nil, fmt.Errorf("%w: %w", errParseCert, err)
}
return cert, nil
}
func (c *Card) SupportsAttestation() bool {
v, _ := iso.ParseVersion("4.3.0")
return !c.version.Less(v)
}