Skip to content

Commit f1fa996

Browse files
authored
Merge pull request #831 from smallstep/mariano/yubico-intermediates
Support for YubiKey 5.7.4+
2 parents f62e740 + 16684c2 commit f1fa996

File tree

2 files changed

+211
-1
lines changed

2 files changed

+211
-1
lines changed

kms/yubikey/yubikey.go

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"go.step.sm/crypto/kms/apiv1"
2525
"go.step.sm/crypto/kms/uri"
26+
"go.step.sm/crypto/pemutil"
2627
)
2728

2829
// Scheme is the scheme used in uris, the string "yubikey".
@@ -32,6 +33,95 @@ const Scheme = string(apiv1.YubiKey)
3233
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
3334
var oidYubicoSerialNumber = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 41482, 3, 7}
3435

36+
// A1 Intermediate certificates used in YubiKeys 5.7.4+
37+
// https://developers.yubico.com/PKI/yubico-intermediate.pem
38+
// https://developers.yubico.com/PKI/yubico-ca-certs.txt
39+
const yubicoPIVAttestationA1 = `
40+
-----BEGIN CERTIFICATE-----
41+
MIIDSTCCAjGgAwIBAgIUSiefkiKiicP9B63XwO7fKqevCkQwDQYJKoZIhvcNAQEL
42+
BQAwLjEsMCoGA1UEAwwjWXViaWNvIEF0dGVzdGF0aW9uIEludGVybWVkaWF0ZSBB
43+
IDEwIBcNMjQxMjAxMDAwMDAwWhgPOTk5OTEyMzEyMzU5NTlaMCUxIzAhBgNVBAMM
44+
Gll1YmljbyBQSVYgQXR0ZXN0YXRpb24gQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOC
45+
AQ8AMIIBCgKCAQEAyGCyrZjNrdPfChdDe4JWd+4TMLr8nbugcKJz12egglWi7oy5
46+
L9GT99/if9i1OrONdpEt0YrCa+qMb+dJJ0WUa8M5zXYnUDpn72vhFjH+Anb9P9+v
47+
+ZrRqaj/jnR/MYP7NpVpeLHiH2dRCe/PX/NH1XE41GvdUEncDtqUUGaXUea0DfDY
48+
McRDpPT2Qn5e8rn9FjzDA37SbOVuws5VlFTDzDdqR0FnqeWeIW0DFu17rzCqXcaB
49+
VRDnQLTc5EEPDTpiRrQE/Ag+7Wg9ieLrueos75YMQ1EIkfjL49OBVogU1A7kwRGv
50+
OnG8l7sYaY8LZ2b5FROe2hKqmsIy600qjn6b/QIDAQABo2YwZDAdBgNVHQ4EFgQU
51+
hAuLXXtpQVBkcsbqyFlj6LVAadgwHwYDVR0jBBgwFoAUIChQIRukWlvoU8udncXk
52+
/Gwveh8wEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI
53+
hvcNAQELBQADggEBAFxL/2oFjxkLh2KVnFKdhy7Nf7MmEfYXDDFSx1rFDn445jHO
54+
UP5kxQPbZc9r53jdvL5W0SQBqBjqA95PYh0r1CPMFsFJdiFXli8Hf3NQ0bTkeFSN
55+
G3LsQCOKMb+o2WjYU3vHkRVjKgKGLxysxxKxGfMUcXdJ0qM6ZVeRHehC2zy7XuI6
56+
TQn7/V0ZHXjk7So7dUV55xQde094/3cCTnh9Q3j2aqMjkGx6tDboCsz/+W+tne7W
57+
nMHG92ZiAAmOkP2bABjan461Qty/qBXPHomkfjqNbjUTluPXiMLYKCXHIyKwdkX6
58+
cphouSMU3QOTsb35Y2PeWNk54xu+Eds/3nhRMso=
59+
-----END CERTIFICATE-----
60+
-----BEGIN CERTIFICATE-----
61+
MIIDSDCCAjCgAwIBAgIUUcmMXzRIFOgGTK0Tb3gEuZYZkBIwDQYJKoZIhvcNAQEL
62+
BQAwJDEiMCAGA1UEAwwZWXViaWNvIEF0dGVzdGF0aW9uIFJvb3QgMTAgFw0yNDEy
63+
MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowLjEsMCoGA1UEAwwjWXViaWNvIEF0
64+
dGVzdGF0aW9uIEludGVybWVkaWF0ZSBBIDEwggEiMA0GCSqGSIb3DQEBAQUAA4IB
65+
DwAwggEKAoIBAQDm555bWY9WW+tOY0rIWHldh+aNanoCZCFh7Gk3YZrQmPUw0hkS
66+
G6qYHQtP+fZyS33VErvg+BQqnmumgNhfxFrkwEZELeidBcC8C4Ag4nqqiPWpzsvI
67+
17NcxYlInLNLFcZY/+gOiN6ZOTihO5/vBZMbj9riaAcqliYmNGJPgTcMGaEAyMzE
68+
MNy2nm6Ep+pjP5aF6gi21t/UQFsuJ1j2Rj/ynM/SdRt+ecal5OYotxHkFbL9vvv2
69+
A2Ov5ITZClw4bOS9npypQimOZ5QAYytmYaQpWl/pMYz6zSj8RqkVDNEJGqNfTKA2
70+
ivLYwX6lSttMPapg0J84l9X0voVN/FpS4VCVAgMBAAGjZjBkMB0GA1UdDgQWBBQg
71+
KFAhG6RaW+hTy52dxeT8bC96HzAfBgNVHSMEGDAWgBTS7u9aIo06bVwjlz3yhdUm
72+
8SV7kjASBgNVHRMBAf8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG
73+
9w0BAQsFAAOCAQEAYMzgLrJLIr0OovQnAZrRIGuabiHSUKSmbLRWpRkWeAtsChDE
74+
HpXcJ/bgDNKYWoHqQ8xRUjB4CyepYevc3YlrG8o7zHxpfVcaoL5SeuJkzHxKn4bT
75+
aSp9+Mvwamnp64kZMiNbFLknfP9kYKoRHkMWheRJ1UsP1z4ScmkCeILfsMs6vqov
76+
qjWClFsJpBcsluYHWF7bBJ1n4Rwg+ATEopY4IgGv6Zvwc+A9r+AT2hqpoSkYoAl+
77+
ANYwgslOf9sJe0V+TA9YY/UlaBmPPTd0//r9wvcePWZkPjKoAC/zUNhfDbh4LV8G
78+
Hs3lyX2XomL/LNc8JYzyIaDEhGQveoPhh/tr1g==
79+
-----END CERTIFICATE-----`
80+
81+
// B1 Intermediate certificates used in YubiKeys 5.7.4+
82+
// https://developers.yubico.com/PKI/yubico-intermediate.pem
83+
// https://developers.yubico.com/PKI/yubico-ca-certs.txt
84+
const yubicoPIVAttestationB1 = `-----BEGIN CERTIFICATE-----
85+
MIIDSTCCAjGgAwIBAgIUWVf2oJG+t1qP8t8TicWgJ2KYan4wDQYJKoZIhvcNAQEL
86+
BQAwLjEsMCoGA1UEAwwjWXViaWNvIEF0dGVzdGF0aW9uIEludGVybWVkaWF0ZSBC
87+
IDEwIBcNMjQxMjAxMDAwMDAwWhgPOTk5OTEyMzEyMzU5NTlaMCUxIzAhBgNVBAMM
88+
Gll1YmljbyBQSVYgQXR0ZXN0YXRpb24gQiAxMIIBIjANBgkqhkiG9w0BAQEFAAOC
89+
AQ8AMIIBCgKCAQEAv7WBL9/5AKxSpCMoL63183WqRtFrOHY7tdyuGtoidoYWQrxV
90+
aV9S+ZwH0aynh0IzD5A/PvCtuxdtL5w2cAI3tgsborOlEert4IZ904CZQfq3ooar
91+
1an/wssbtMpPOQkC3MQiqrUyHlFS2BTbuwbBXY66lSVX/tGRuUgnBdfBJtcQKS6M
92+
O4bU5ndPQqhGPyzcyY1LvlfzK7KJ1r/bixCRFqjhJRnPs0Czpg6rkRrFgC6cd5bK
93+
1UgTsJy+3wrIqkv4CeV3EhSVnhnQjZgIrdIcI5WZ8T1Oq3OhMlWmY0K0dy/oZdP/
94+
bpbG2qbyHLa6gprLT/qChQWLmffxn6D2DAB1zQIDAQABo2YwZDAdBgNVHQ4EFgQU
95+
M0Nt3QHo7eGzaKMZn2SmXT74vpcwHwYDVR0jBBgwFoAU6rdCkJ4Me2R621R8A7p8
96+
Tp/YoWEwEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI
97+
hvcNAQELBQADggEBAI0HwoS84fKMUyIof1LdUXvyeAMmEwW7+nVETvxNNlTMuwv7
98+
zPJ4XZAm9Fv95tz9CqZBj6l1PAPQn6Zht9LQA92OF7W7buuXuxuusBTgLM0C1iX2
99+
CGXqY/k/uSNvi3ZYfrpd44TIrfrr8bCG9ux7B5ZCRqb8adDUm92Yz3lK1aX2M6Cw
100+
jC9IZVTXQWhLyP8Ys3p7rb20CO2jJzV94deJ/+AsEb+bnCQImPat1GDKwrBosar+
101+
BxtU7k6kgkxZ0G384O59GFXqnwkbw2b5HhORvOsX7nhOUhePFufzi1vT1g8Tzbwr
102+
+TUfTwo2biKHHcI762KGtp8o6Bcv5y8WgExFuWY=
103+
-----END CERTIFICATE-----
104+
-----BEGIN CERTIFICATE-----
105+
MIIDSDCCAjCgAwIBAgIUDqERw+4RnGSggxgUewJFEPDRZ3YwDQYJKoZIhvcNAQEL
106+
BQAwJDEiMCAGA1UEAwwZWXViaWNvIEF0dGVzdGF0aW9uIFJvb3QgMTAgFw0yNDEy
107+
MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowLjEsMCoGA1UEAwwjWXViaWNvIEF0
108+
dGVzdGF0aW9uIEludGVybWVkaWF0ZSBCIDEwggEiMA0GCSqGSIb3DQEBAQUAA4IB
109+
DwAwggEKAoIBAQDI7XnH+ZvDwMCQU8M8ZeV5qscublvVYaaRt3Ybaxn9godLx5sw
110+
H0lXrdgjh5h7FpVgCgYYX7E4bl1vbzULemrMWT8N3WMGUe8QAJbBeioV7W/E+hTZ
111+
P/0SKJVa3ewKBo6ULeMnfQZDrVORAk8wTLq2v5Llj5vMj7JtOotKa9J7nHS8kLmz
112+
XXSaj0SwEPh5OAZUTNV4zs1bvoTAQQWrL4/J9QuKt6WCFE5nUNiRQcEbVF8mlqK2
113+
bx2z6okVltyDVLCxYbpUTELvY1usR3DTGPUoIClOm4crpwnDRLVHvjYePGBB//pE
114+
yzxA/gcScxjwaH1ZUw9bnSbHyurKqbTa1KvjAgMBAAGjZjBkMB0GA1UdDgQWBBTq
115+
t0KQngx7ZHrbVHwDunxOn9ihYTAfBgNVHSMEGDAWgBTS7u9aIo06bVwjlz3yhdUm
116+
8SV7kjASBgNVHRMBAf8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG
117+
9w0BAQsFAAOCAQEAqQaCWMxTGqVVX7Sk7kkJmUueTSYKuU6+KBBSgwIRnlw9K7He
118+
1IpxZ0hdwpPNikKjmcyFgFPzhImwHJgxxuT90Pw3vYOdcJJNktDg35PXOfzSn15c
119+
FAx1RO0mPTmIb8dXiEWOpzoXvdwXDM41ZaCDYMT7w4IQtMyvE7xUBZq2bjtAnq/N
120+
DUA7be4H8H3ipC+/+NKlUrcUh+j48K67WI0u1m6FeQueBA7n06j825rqDqsaLs9T
121+
b7KAHAw8PmrWaNPG2kjKerxPEfecivlFawp2RWZvxrVtn3TV2SBxyCJCkXsND05d
122+
CErVHSJIs+BdtTVNY9AwtyPmnyb0v4mSTzvWdw==
123+
-----END CERTIFICATE-----`
124+
35125
// YubiKey implements the KMS interface on a YubiKey.
36126
type YubiKey struct {
37127
yk pivKey
@@ -364,9 +454,27 @@ func (k *YubiKey) CreateAttestation(req *apiv1.CreateAttestationRequest) (*apiv1
364454
return nil, errors.Wrap(err, "error retrieving attestation certificate")
365455
}
366456

457+
chain := []*x509.Certificate{cert, intermediate}
458+
459+
// Append intermediates on YubiKeys 5.7.4+
460+
switch intermediate.Issuer.CommonName {
461+
case "Yubico PIV Attestation A 1":
462+
certs, err := pemutil.ParseCertificateBundle([]byte(yubicoPIVAttestationA1))
463+
if err != nil {
464+
return nil, fmt.Errorf("error parsing intermediate certificates: %w", err)
465+
}
466+
chain = append(chain, certs...)
467+
case "Yubico PIV Attestation B 1":
468+
certs, err := pemutil.ParseCertificateBundle([]byte(yubicoPIVAttestationB1))
469+
if err != nil {
470+
return nil, fmt.Errorf("error parsing intermediate certificates: %w", err)
471+
}
472+
chain = append(chain, certs...)
473+
}
474+
367475
return &apiv1.CreateAttestationResponse{
368476
Certificate: cert,
369-
CertificateChain: []*x509.Certificate{cert, intermediate},
477+
CertificateChain: chain,
370478
PublicKey: cert.PublicKey,
371479
PermanentIdentifier: getAttestedSerial(cert),
372480
}, nil

kms/yubikey/yubikey_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131

3232
"go.step.sm/crypto/kms/apiv1"
3333
"go.step.sm/crypto/minica"
34+
"go.step.sm/crypto/pemutil"
3435
"go.step.sm/crypto/randutil"
3536
)
3637

@@ -1080,9 +1081,20 @@ func TestYubiKey_CreateDecrypter(t *testing.T) {
10801081
func TestYubiKey_CreateAttestation(t *testing.T) {
10811082
yk := newStubPivKey(t, ECDSA)
10821083

1084+
ykA1 := newStubPivKey(t, ECDSA)
1085+
ykA1.attestCA.Intermediate.Issuer.CommonName = "Yubico PIV Attestation A 1"
1086+
1087+
ykB1 := newStubPivKey(t, ECDSA)
1088+
ykB1.attestCA.Intermediate.Issuer.CommonName = "Yubico PIV Attestation B 1"
1089+
10831090
ykFail := newStubPivKey(t, ECDSA)
10841091
delete(ykFail.certMap, slotAttestation)
10851092

1093+
a1Certs, err := pemutil.ParseCertificateBundle([]byte(yubicoPIVAttestationA1))
1094+
require.NoError(t, err)
1095+
b1Certs, err := pemutil.ParseCertificateBundle([]byte(yubicoPIVAttestationB1))
1096+
require.NoError(t, err)
1097+
10861098
type fields struct {
10871099
yk pivKey
10881100
pin string
@@ -1106,6 +1118,28 @@ func TestYubiKey_CreateAttestation(t *testing.T) {
11061118
PublicKey: yk.attestMap[piv.SlotAuthentication].PublicKey,
11071119
PermanentIdentifier: "112233",
11081120
}, false},
1121+
{"ok A1", fields{ykA1, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateAttestationRequest{
1122+
Name: "yubikey:slot-id=9a",
1123+
}}, &apiv1.CreateAttestationResponse{
1124+
Certificate: ykA1.attestMap[piv.SlotAuthentication],
1125+
CertificateChain: []*x509.Certificate{
1126+
ykA1.attestMap[piv.SlotAuthentication], ykA1.attestCA.Intermediate,
1127+
a1Certs[0], a1Certs[1],
1128+
},
1129+
PublicKey: ykA1.attestMap[piv.SlotAuthentication].PublicKey,
1130+
PermanentIdentifier: "112233",
1131+
}, false},
1132+
{"ok B1", fields{ykB1, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateAttestationRequest{
1133+
Name: "yubikey:slot-id=9a",
1134+
}}, &apiv1.CreateAttestationResponse{
1135+
Certificate: ykB1.attestMap[piv.SlotAuthentication],
1136+
CertificateChain: []*x509.Certificate{
1137+
ykB1.attestMap[piv.SlotAuthentication], ykB1.attestCA.Intermediate,
1138+
b1Certs[0], b1Certs[1],
1139+
},
1140+
PublicKey: ykB1.attestMap[piv.SlotAuthentication].PublicKey,
1141+
PermanentIdentifier: "112233",
1142+
}, false},
11091143
{"fail getSlot", fields{yk, "123456", piv.DefaultManagementKey}, args{&apiv1.CreateAttestationRequest{
11101144
Name: "yubikey://:slot-id=9a",
11111145
}}, nil, true},
@@ -1340,3 +1374,71 @@ func Test_syncDecrypter_Decrypt(t *testing.T) {
13401374
assert.NoError(t, err)
13411375
assert.Equal(t, data, plain)
13421376
}
1377+
1378+
func TestYubicoNewRoots(t *testing.T) {
1379+
const rootPEM = `-----BEGIN CERTIFICATE-----
1380+
MIIDPjCCAiagAwIBAgIUXzeiEDJEOTt14F5n0o6Zf/bBwiUwDQYJKoZIhvcNAQEN
1381+
BQAwJDEiMCAGA1UEAwwZWXViaWNvIEF0dGVzdGF0aW9uIFJvb3QgMTAgFw0yNDEy
1382+
MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowJDEiMCAGA1UEAwwZWXViaWNvIEF0
1383+
dGVzdGF0aW9uIFJvb3QgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
1384+
AMZ6/TxM8rIT+EaoPvG81ontMOo/2mQ2RBwJHS0QZcxVaNXvl12LUhBZ5LmiBScI
1385+
Zd1Rnx1od585h+/dhK7hEm7JAALkKKts1fO53KGNLZujz5h3wGncr4hyKF0G74b/
1386+
U3K9hE5mGND6zqYchCRAHfrYMYRDF4YL0X4D5nGdxvppAy6nkEmtWmMnwO3i0TAu
1387+
csrbE485HvGM4r0VpgVdJpvgQjiTJCTIq+D35hwtT8QDIv+nGvpcyi5wcIfCkzyC
1388+
imJukhYy6KoqNMKQEdpNiSOvWyDMTMt1bwCvEzpw91u+msUt4rj0efnO9s0ZOwdw
1389+
MRDnH4xgUl5ZLwrrPkfC1/0CAwEAAaNmMGQwHQYDVR0OBBYEFNLu71oijTptXCOX
1390+
PfKF1SbxJXuSMB8GA1UdIwQYMBaAFNLu71oijTptXCOXPfKF1SbxJXuSMBIGA1Ud
1391+
EwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBDQUAA4IB
1392+
AQC3IW/sgB9pZ8apJNjxuGoX+FkILks0wMNrdXL/coUvsrhzsvl6mePMrbGJByJ1
1393+
XnquB5sgcRENFxdQFma3mio8Upf1owM1ZreXrJ0mADG2BplqbJnxiyYa+R11reIF
1394+
TWeIhMNcZKsDZrFAyPuFjCWSQvJmNWe9mFRYFgNhXJKkXIb5H1XgEDlwiedYRM7V
1395+
olBNlld6pRFKlX8ust6OTMOeADl2xNF0m1LThSdeuXvDyC1g9+ILfz3S6OIYgc3i
1396+
roRcFD354g7rKfu67qFAw9gC4yi0xBTPrY95rh4/HqaUYCA/L8ldRk6H7Xk35D+W
1397+
Vpmq2Sh/xT5HiFuhf4wJb0bK
1398+
-----END CERTIFICATE-----`
1399+
1400+
root, err := pemutil.ParseCertificate([]byte(rootPEM))
1401+
require.NoError(t, err)
1402+
1403+
a1Certs, err := pemutil.ParseCertificateBundle([]byte(yubicoPIVAttestationA1))
1404+
require.NoError(t, err)
1405+
require.Len(t, a1Certs, 2)
1406+
1407+
b1Certs, err := pemutil.ParseCertificateBundle([]byte(yubicoPIVAttestationB1))
1408+
require.NoError(t, err)
1409+
require.Len(t, b1Certs, 2)
1410+
1411+
assert.Equal(t, "Yubico PIV Attestation A 1", a1Certs[0].Subject.CommonName)
1412+
assert.Equal(t, "Yubico Attestation Intermediate A 1", a1Certs[1].Subject.CommonName)
1413+
1414+
assert.Equal(t, "Yubico PIV Attestation B 1", b1Certs[0].Subject.CommonName)
1415+
assert.Equal(t, "Yubico Attestation Intermediate B 1", b1Certs[1].Subject.CommonName)
1416+
1417+
assert.True(t, a1Certs[0].BasicConstraintsValid)
1418+
assert.True(t, a1Certs[0].IsCA)
1419+
assert.True(t, a1Certs[0].MaxPathLen == 1)
1420+
assert.True(t, a1Certs[1].BasicConstraintsValid)
1421+
assert.True(t, a1Certs[1].IsCA)
1422+
assert.True(t, a1Certs[1].MaxPathLen == 2)
1423+
1424+
assert.True(t, b1Certs[0].BasicConstraintsValid)
1425+
assert.True(t, b1Certs[0].IsCA)
1426+
assert.True(t, b1Certs[0].MaxPathLen == 1)
1427+
assert.True(t, b1Certs[1].BasicConstraintsValid)
1428+
assert.True(t, b1Certs[1].IsCA)
1429+
assert.True(t, b1Certs[1].MaxPathLen == 2)
1430+
1431+
rootPool := x509.NewCertPool()
1432+
rootPool.AddCert(root)
1433+
for _, chain := range [][]*x509.Certificate{a1Certs, b1Certs} {
1434+
intPool := x509.NewCertPool()
1435+
intPool.AddCert(chain[1])
1436+
1437+
_, err := chain[0].Verify(x509.VerifyOptions{
1438+
Roots: rootPool,
1439+
Intermediates: intPool,
1440+
})
1441+
assert.NoError(t, err)
1442+
}
1443+
1444+
}

0 commit comments

Comments
 (0)