Skip to content
331 changes: 330 additions & 1 deletion acme/challenge.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"go.step.sm/crypto/x509util"
"golang.org/x/exp/slices"

"github.com/mbreban/attestation"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this is a new package, and not used in other projects (yet), it might be better to copy/fork its code over, and maintain it internally. Are you familiar with the author?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I'm not familiar with the author. We can manage to rewrite this lib. The lib support version 1 to 300. I saw there is version 400 now.

"github.com/smallstep/certificates/acme/wire"
"github.com/smallstep/certificates/authority/provisioner"
wireprovisioner "github.com/smallstep/certificates/authority/provisioner/wire"
Expand Down Expand Up @@ -834,7 +835,7 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
format := att.Format
prov := MustProvisionerFromContext(ctx)
if !prov.IsAttestationFormatEnabled(ctx, provisioner.ACMEAttestationFormat(format)) {
if format != "apple" && format != "step" && format != "tpm" {
if format != "apple" && format != "step" && format != "tpm" && format != "android-key" {
return storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, "unsupported attestation object format %q", format))
}

Expand All @@ -843,6 +844,36 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
}

switch format {
case "android-key":
data, err := doAndroidKeyAttestionFormat(ctx, prov, ch, jwk, &att)
if err != nil {
var acmeError *Error
if errors.As(err, &acmeError) {
if acmeError.Status == 500 {
return acmeError
}
return storeError(ctx, db, ch, true, acmeError)
}
return WrapErrorISE(err, "error validating attestation")
}

// 1. attestationSecurityLevel > 0 (TrustedEnvironment or Strongbox)
if data.Attestation.AttestationSecurityLevel < 1 {
return storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, "security level does not match"))
}

// 2. validate teeEnforced device identifier against permanent-identifier
if ch.Value != string(data.Attestation.TeeEnforced.AttestationIdSerial) {
subproblem := NewSubproblemWithIdentifier(
ErrorRejectedIdentifierType,
Identifier{Type: "permanent-identifier", Value: ch.Value},
"challenge identifier %q doesn't match any of the attested hardware identifiers %q", ch.Value, []string{string(data.Attestation.TeeEnforced.AttestationIdSerial)},
)
return storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, "permanent identifier does not match").AddSubproblems(subproblem))
}

// Update attestation key fingerprint to compare against the CSR
az.Fingerprint = data.Fingerprint
case "apple":
data, err := doAppleAttestationFormat(ctx, prov, ch, &att)
if err != nil {
Expand Down Expand Up @@ -1367,6 +1398,304 @@ func doAppleAttestationFormat(_ context.Context, prov Provisioner, _ *Challenge,
return data, nil
}

// Android Root CA for RSA
// https://developer.android.com/privacy-and-security/security-key-attestation#root_certificate
// Note: Update your attestation processes to trust both the new and existing root certificates. Older devices with factory-provisioned keys don't support key rotation and continue to use the old root.
const OldAndroidRootCARSA = `-----BEGIN CERTIFICATE-----
MIIFYDCCA0igAwIBAgIJAOj6GWMU0voYMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTYwNTI2MTYyODUyWhcNMjYwNTI0MTYy
ODUyWjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
AGMCAwEAAaOBpjCBozAdBgNVHQ4EFgQUNmHhAHyIBQlRi0RsR/8aTMnqTxIwHwYD
VR0jBBgwFoAUNmHhAHyIBQlRi0RsR/8aTMnqTxIwDwYDVR0TAQH/BAUwAwEB/zAO
BgNVHQ8BAf8EBAMCAYYwQAYDVR0fBDkwNzA1oDOgMYYvaHR0cHM6Ly9hbmRyb2lk
Lmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC8wDQYJKoZIhvcNAQELBQAD
ggIBACDIw41L3KlXG0aMiS//cqrG+EShHUGo8HNsw30W1kJtjn6UBwRM6jnmiwfB
Pb8VA91chb2vssAtX2zbTvqBJ9+LBPGCdw/E53Rbf86qhxKaiAHOjpvAy5Y3m00m
qC0w/Zwvju1twb4vhLaJ5NkUJYsUS7rmJKHHBnETLi8GFqiEsqTWpG/6ibYCv7rY
DBJDcR9W62BW9jfIoBQcxUCUJouMPH25lLNcDc1ssqvC2v7iUgI9LeoM1sNovqPm
QUiG9rHli1vXxzCyaMTjwftkJLkf6724DFhuKug2jITV0QkXvaJWF4nUaHOTNA4u
JU9WDvZLI1j83A+/xnAJUucIv/zGJ1AMH2boHqF8CY16LpsYgBt6tKxxWH00XcyD
CdW2KlBCeqbQPcsFmWyWugxdcekhYsAWyoSf818NUsZdBWBaR/OukXrNLfkQ79Iy
ZohZbvabO/X+MVT3rriAoKc8oE2Uws6DF+60PV7/WIPjNvXySdqspImSN78mflxD
qwLqRBYkA3I75qppLGG9rp7UCdRjxMl8ZDBld+7yvHVgt1cVzJx9xnyGCC23Uaic
MDSXYrB4I4WHXPGjxhZuCuPBLTdOLU8YRvMYdEvYebWHMpvwGCF6bAx3JBpIeOQ1
wDB5y0USicV3YgYGmi+NZfhA4URSh77Yd6uuJOJENRaNVTzk
-----END CERTIFICATE-----`

const AndroidRootCARSA = `-----BEGIN CERTIFICATE-----
MIIFHDCCAwSgAwIBAgIJAPHBcqaZ6vUdMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMjIwMzIwMTgwNzQ4WhcNNDIwMzE1MTgw
NzQ4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
AGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud
IwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD
VR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQB8cMqTllHc8U+qCrOlg3H7
174lmaCsbo/bJ0C17JEgMLb4kvrqsXZs01U3mB/qABg/1t5Pd5AORHARs1hhqGIC
W/nKMav574f9rZN4PC2ZlufGXb7sIdJpGiO9ctRhiLuYuly10JccUZGEHpHSYM2G
tkgYbZba6lsCPYAAP83cyDV+1aOkTf1RCp/lM0PKvmxYN10RYsK631jrleGdcdkx
oSK//mSQbgcWnmAEZrzHoF1/0gso1HZgIn0YLzVhLSA/iXCX4QT2h3J5z3znluKG
1nv8NQdxei2DIIhASWfu804CA96cQKTTlaae2fweqXjdN1/v2nqOhngNyz1361mF
mr4XmaKH/ItTwOe72NI9ZcwS1lVaCvsIkTDCEXdm9rCNPAY10iTunIHFXRh+7KPz
lHGewCq/8TOohBRn0/NNfh7uRslOSZ/xKbN9tMBtw37Z8d2vvnXq/YWdsm1+JLVw
n6yYD/yacNJBlwpddla8eaVMjsF6nBnIgQOf9zKSe06nSTqvgwUHosgOECZJZ1Eu
zbH4yswbt02tKtKEFhx+v+OTge/06V+jGsqTWLsfrOCNLuA8H++z+pUENmpqnnHo
vaI47gC+TNpkgYGkkBT6B/m/U01BuOBBTzhIlMEZq9qkDWuM2cA5kW5V3FJUcfHn
w1IdYIg2Wxg7yHcQZemFQg==
-----END CERTIFICATE-----`

// Android Root CA for ECDSA (starting feb 26)
// https://developer.android.com/privacy-and-security/security-key-attestation#root_certificate
const AndroidRootCAECDSA = `-----BEGIN CERTIFICATE-----
MIICIjCCAaigAwIBAgIRAISp0Cl7DrWK5/8OgN52BgUwCgYIKoZIzj0EAwMwUjEc
MBoGA1UEAwwTS2V5IEF0dGVzdGF0aW9uIENBMTEQMA4GA1UECwwHQW5kcm9pZDET
MBEGA1UECgwKR29vZ2xlIExMQzELMAkGA1UEBhMCVVMwHhcNMjUwNzE3MjIzMjE4
WhcNMzUwNzE1MjIzMjE4WjBSMRwwGgYDVQQDDBNLZXkgQXR0ZXN0YXRpb24gQ0Ex
MRAwDgYDVQQLDAdBbmRyb2lkMRMwEQYDVQQKDApHb29nbGUgTExDMQswCQYDVQQG
EwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABCPaI3FO3z5bBQo8cuiEas4HjqCt
G/mLFfRT0MsIssPBEEU5Cfbt6sH5yOAxqEi5QagpU1yX4HwnGb7OtBYpDTB57uH5
Eczm34A5FNijV3s0/f0UPl7zbJcTx6xwqMIRq6NCMEAwDwYDVR0TAQH/BAUwAwEB
/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFFIyuyz7RkOb3NaBqQ5lZuA0QepA
MAoGCCqGSM49BAMDA2gAMGUCMETfjPO/HwqReR2CS7p0ZWoD/LHs6hDi422opifH
EUaYLxwGlT9SLdjkVpz0UUOR5wIxAIoGyxGKRHVTpqpGRFiJtQEOOTp/+s1GcxeY
uR2zh/80lQyu9vAFCj6E4AXc+osmRg==`

// OID for the Android attestation extension
// https://source.android.com/docs/security/features/keystore/attestation#id-attestation
var oidAndroidAttestation = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 1, 17}
var oidAndroidProvisionningAttestation = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 1, 30}

type androidKeyAttestationData struct {
Certificate *x509.Certificate
Fingerprint string
Attestation *attestation.KeyDescription
}
Comment on lines +1491 to +1495
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make this a "plainer" struct? For example, for the apple format we extract just the properties that we act on. In this case, it would be nice to have (a subset of) the properties from the KeyDescription struct at the top level.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine it is possible. The difference between Android Attestation and Apple Attestation, if I understand it, is that in Apple each data is a separate OID when Android is a structure in one OID. Parsing this value to the attestation KeyDescription object coming from the lib introduce in this implementation. We may still extract value necessary then discard the whole object.


func hasAndroidAttestation(cert *x509.Certificate) bool {
for _, ext := range cert.Extensions {
if ext.Id.Equal(oidAndroidAttestation) {
return true
}
}
return false
}

// https://developer.android.com/privacy-and-security/security-key-attestation
// 3. Verify that the root public certificate is trustworthy and that each certificate signs the next certificate in the chain.
// 4. Check each certificate's revocation status to ensure that none of the certificates have been revoked.
// 5. Optionally, inspect the provisioning information certificate extension that is only present in newer certificate chains.
// Obtain a reference to the CBOR parser library that is most appropriate for your toolset. Find the nearest certificate to the root that contains the provisioning information certificate extension. Use the parser to extract the provisioning information certificate extension data from that certificate.
// See the section about the provisioning information extension for more details.
// 6. Find the nearest certificate to the root that contains the key attestation certificate extension. If the provisioning information certificate extension was present, the key attestation certificate extension must be in the immediately subsequent certificate. Use the parser to extract the key attestation certificate extension data from that certificate.
// 7. Check the extension data that you've retrieved in the previous steps for consistency and compare with the set of values that you expect the hardware-backed key to contain.

func doAndroidKeyAttestionFormat(_ context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*androidKeyAttestationData, error) {
// Extract x5c and verify certificate
acme := prov.(*provisioner.ACME)
certs := []*x509.Certificate{}
x5c, ok := att.AttStatement["x5c"].([]any)
if !ok {
return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c not present")
}
if len(x5c) == 0 {
return nil, NewDetailedError(ErrorRejectedIdentifierType, "x5c is empty")
}
if len(x5c) < 3 {
return nil, NewDetailedError(ErrorBadAttestationStatementType, "at least 3 certificates are required")
}
der, ok := x5c[0].([]byte)
if !ok {
return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c is malformed")
}
leaf, err := x509.ParseCertificate(der)
if err != nil {
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "failed parsing leaf certificate")
}
if !hasAndroidAttestation(leaf) {
return nil, NewDetailedError(ErrorBadAttestationStatementType, "leaf certificate do not contains attestation")
}

certs = append(certs, leaf)

// Parse intermediates and root
intermediates := x509.NewCertPool()
var root *x509.Certificate
for i, v := range x5c[1:] {
der, ok := v.([]byte)
if !ok {
return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c is malformed")
}
cert, err := x509.ParseCertificate(der)
if err != nil {
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "failed parsing certificate in chain")
}
// Verify CRL
if acme.IsCertificateRevoked(cert.SerialNumber.String()) {
return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c element contain a revoked certificate")
}
if i == len(x5c)-2 {
// Last cert = root
certs = append(certs, cert)
root = cert
} else {
certs = append(certs, cert)
intermediates.AddCert(cert)
}
}

if root == nil {
return nil, NewDetailedError(ErrorBadAttestationStatementType, "missing root certificate in x5c chain")
}
Comment on lines +1578 to +1580
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the root cert part of the attestation chain? That's surprising to see. I'd expect to only contain intermediates. It seems to be in line with the Google docs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the root is part of the chain.

check that the attestation certificate chain contains a root certificate that is signed with the Google attestation root key and that the attestationSecurityLevel element within the key description data structure is set to the TrustedEnvironment security level or to the StrongBox security level.


// Verify attestation root signature
// Use configured or default attestation roots if none is configured.
attestationRoots, attOk := prov.GetAttestationRoots()
if !attOk {
attestationRoots = x509.NewCertPool()
switch root.PublicKey.(type) {
case *rsa.PublicKey:
rsaRoot, err := pemutil.ParseCertificate([]byte(AndroidRootCARSA))
oldRsaRoot, err := pemutil.ParseCertificate([]byte(OldAndroidRootCARSA))
if err != nil {
return nil, WrapErrorISE(err, "error parsing root ca")
}
// 1. verify public key
if !root.PublicKey.(*rsa.PublicKey).Equal(rsaRoot.PublicKey) {
return nil, NewDetailedError(ErrorBadAttestationStatementType, "root certificate not signed by Google")
}
attestationRoots.AddCert(rsaRoot)
attestationRoots.AddCert(oldRsaRoot)
case *ecdsa.PublicKey:
ecdsaRoot, err := pemutil.ParseCertificate([]byte(AndroidRootCAECDSA))
if err != nil {
return nil, WrapErrorISE(err, "error parsing root ca")
}
if !root.PublicKey.(*rsa.PublicKey).Equal(ecdsaRoot.PublicKey) {
return nil, NewDetailedError(ErrorBadAttestationStatementType, "root certificate not signed by Google")
}
attestationRoots.AddCert(ecdsaRoot)
default:
return nil, NewDetailedError(ErrorBadAttestationStatementType, "invalid root certificate key type")
}
// 2. validate root certificate
if _, err := root.Verify(x509.VerifyOptions{
Roots: attestationRoots,
CurrentTime: time.Now(),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}); err != nil {
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "root certificate not signed by Google")
}
} else {
if _, err := root.Verify(x509.VerifyOptions{
Roots: attestationRoots,
CurrentTime: time.Now(),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}); err != nil {
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "root certificate not signed by provided roots")
}
}

// Validate the full chain including root as trust anchor
roots := x509.NewCertPool()
roots.AddCert(root)

if _, err := leaf.Verify(x509.VerifyOptions{
Intermediates: intermediates,
Roots: roots,
CurrentTime: time.Now(),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}); err != nil {
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "x5c chain verification failed")
}

// Get signature
sig, ok := att.AttStatement["sig"].([]byte)
if !ok {
return nil, NewDetailedError(ErrorBadAttestationStatementType, "sig not present")
}

keyAuth, err := KeyAuthorization(ch.Token, jwk)
if err != nil {
return nil, err
}

// Parse attestation data:
// find the attestation certificate
// attCert, err := findAndroidAttestationCert(certs)
// if err != nil {
// return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "")
// }
attCert := leaf
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually this is not safe. Documentation alert say:
Caution: Don't assume that the key attestation certificate extension is in the leaf certificate of the chain. Only the first occurrence of the extension in the chain can be trusted. Any further instances of the extension have not been issued by the secure hardware and might have been issued by an attacker extending the chain while attempting to create fake attestations for untrusted keys.


switch pub := attCert.PublicKey.(type) {
case *ecdsa.PublicKey:
if pub.Curve != elliptic.P256() {
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "unsupported elliptic curve %s", pub.Curve)
}
sum := sha256.Sum256([]byte(keyAuth))
if !ecdsa.VerifyASN1(pub, sum[:], sig) {
return nil, NewDetailedError(ErrorBadAttestationStatementType, "failed to validate signature")
}
case *rsa.PublicKey:
sum := sha256.Sum256([]byte(keyAuth))
if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, sum[:], sig); err != nil {
return nil, NewDetailedError(ErrorBadAttestationStatementType, "failed to validate signature")
}
case ed25519.PublicKey:
if !ed25519.Verify(pub, []byte(keyAuth), sig) {
return nil, NewDetailedError(ErrorBadAttestationStatementType, "failed to validate signature")
}
default:
return nil, NewDetailedError(ErrorBadAttestationStatementType, "unsupported public key type %T", pub)
}

data := &androidKeyAttestationData{
Certificate: attCert,
}
if data.Fingerprint, err = keyutil.Fingerprint(attCert.PublicKey); err != nil {
return nil, WrapErrorISE(err, "error calculating key fingerprint")
}

for _, ext := range attCert.Extensions {
if !ext.Id.Equal(oidAndroidAttestation) {
continue
}
keyDesc, err := attestation.ParseExtension(ext.Value)
if err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "error parsing attestation")
}
data.Attestation = keyDesc
break
}

// validate challenge
if subtle.ConstantTimeCompare([]byte(keyAuth), data.Attestation.AttestationChallenge) != 1 {
return nil, NewDetailedError(ErrorBadAttestationStatementType, fmt.Sprintf("challenge mismatch; expected %q, got %q", keyAuth, string(data.Attestation.AttestationChallenge)))
}

return data, nil
}

// Yubico PIV Root CA Serial 263751
// https://developers.yubico.com/PIV/Introduction/piv-attestation-ca.pem
const yubicoPIVRootCA = `-----BEGIN CERTIFICATE-----
Expand Down
Loading