Add support for yubikey attestation

pull/1040/head
Mariano Cano 2 years ago
parent ebce40e9b6
commit 735c9d49b0

@ -19,6 +19,7 @@ import (
"net/url"
"os"
"reflect"
"strconv"
"strings"
"time"
@ -361,11 +362,19 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
if data.UDID != ch.Value && data.SerialNumber != ch.Value {
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatement, "permanent identifier does not match"))
}
case "step":
data, err := doStepAttestationFormat(ctx, ch, db, &att)
if err != nil {
return err
}
// TODO(mariano): debug - remove me
pem.Encode(os.Stderr, &pem.Block{
Type: "CERTIFICATE", Bytes: data.Certificate.Raw,
})
// Validate Apple's ClientIdentifier (Identifier.Value) with device
// identifiers.
//
// Note: We might want to use an external service for this.
if data.SerialNumber != ch.Value {
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatement, "permanent identifier does not match"))
}
default:
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatement, "unexpected attestation object format"))
}
@ -479,6 +488,106 @@ func doAppleAttestationFormat(ctx context.Context, ch *Challenge, db DB, att *At
return data, nil
}
// Yubico PIV Root CA Serial 263751
// https://developers.yubico.com/PIV/Introduction/piv-attestation-ca.pem
const yubicoPIVRootCA = `-----BEGIN CERTIFICATE-----
MIIDFzCCAf+gAwIBAgIDBAZHMA0GCSqGSIb3DQEBCwUAMCsxKTAnBgNVBAMMIFl1
YmljbyBQSVYgUm9vdCBDQSBTZXJpYWwgMjYzNzUxMCAXDTE2MDMxNDAwMDAwMFoY
DzIwNTIwNDE3MDAwMDAwWjArMSkwJwYDVQQDDCBZdWJpY28gUElWIFJvb3QgQ0Eg
U2VyaWFsIDI2Mzc1MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMN2
cMTNR6YCdcTFRxuPy31PabRn5m6pJ+nSE0HRWpoaM8fc8wHC+Tmb98jmNvhWNE2E
ilU85uYKfEFP9d6Q2GmytqBnxZsAa3KqZiCCx2LwQ4iYEOb1llgotVr/whEpdVOq
joU0P5e1j1y7OfwOvky/+AXIN/9Xp0VFlYRk2tQ9GcdYKDmqU+db9iKwpAzid4oH
BVLIhmD3pvkWaRA2H3DA9t7H/HNq5v3OiO1jyLZeKqZoMbPObrxqDg+9fOdShzgf
wCqgT3XVmTeiwvBSTctyi9mHQfYd2DwkaqxRnLbNVyK9zl+DzjSGp9IhVPiVtGet
X02dxhQnGS7K6BO0Qe8CAwEAAaNCMEAwHQYDVR0OBBYEFMpfyvLEojGc6SJf8ez0
1d8Cv4O/MA8GA1UdEwQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3
DQEBCwUAA4IBAQBc7Ih8Bc1fkC+FyN1fhjWioBCMr3vjneh7MLbA6kSoyWF70N3s
XhbXvT4eRh0hvxqvMZNjPU/VlRn6gLVtoEikDLrYFXN6Hh6Wmyy1GTnspnOvMvz2
lLKuym9KYdYLDgnj3BeAvzIhVzzYSeU77/Cupofj093OuAswW0jYvXsGTyix6B3d
bW5yWvyS9zNXaqGaUmP3U9/b6DlHdDogMLu3VLpBB9bm5bjaKWWJYgWltCVgUbFq
Fqyi4+JE014cSgR57Jcu3dZiehB6UtAPgad9L5cNvua/IWRmm+ANy3O2LH++Pyl8
SREzU8onbBsjMg9QDiSf5oJLKvd/Ren+zGY7
-----END CERTIFICATE-----`
// Serial number of the YubiKey, encoded as an integer.
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
var oidYubicoSerialNumber = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 41482, 3, 7}
type stepAttestationData struct {
Certificate *x509.Certificate
SerialNumber string
}
func doStepAttestationFormat(ctx context.Context, ch *Challenge, db DB, att *AttestationObject) (*stepAttestationData, error) {
root, err := pemutil.ParseCertificate([]byte(yubicoPIVRootCA))
if err != nil {
return nil, WrapErrorISE(err, "error parsing apple enterprise ca")
}
roots := x509.NewCertPool()
roots.AddCert(root)
x5c, ok := att.AttStatement["x5c"].([]interface{})
if !ok {
return nil, storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatement, "x5c not present"))
}
if len(x5c) == 0 {
return nil, storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, "x5c is empty"))
}
der, ok := x5c[0].([]byte)
if !ok {
return nil, storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatement, "x5c is malformed"))
}
leaf, err := x509.ParseCertificate(der)
if err != nil {
return nil, storeError(ctx, db, ch, true, WrapError(ErrorBadAttestationStatement, err, "x5c is malformed"))
}
pem.Encode(os.Stderr, &pem.Block{
Type: "CERTIFICATE", Bytes: leaf.Raw,
})
intermediates := x509.NewCertPool()
for _, v := range x5c[1:] {
der, ok = v.([]byte)
if !ok {
return nil, storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatement, "x5c is malformed"))
}
cert, err := x509.ParseCertificate(der)
if err != nil {
return nil, storeError(ctx, db, ch, true, WrapError(ErrorBadAttestationStatement, err, "x5c is malformed"))
}
intermediates.AddCert(cert)
}
if _, err := leaf.Verify(x509.VerifyOptions{
Intermediates: intermediates,
Roots: roots,
CurrentTime: time.Now().Truncate(time.Second),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}); err != nil {
return nil, storeError(ctx, db, ch, true, WrapError(ErrorBadAttestationStatement, err, "x5c is not valid"))
}
data := &stepAttestationData{
Certificate: leaf,
}
for _, ext := range leaf.Extensions {
switch {
case ext.Id.Equal(oidYubicoSerialNumber):
var serialNumber int
rest, err := asn1.Unmarshal(ext.Value, &serialNumber)
if err != nil || len(rest) > 0 {
return nil, storeError(ctx, db, ch, true, WrapError(ErrorBadAttestationStatement, err, "error parsing serial number"))
}
data.SerialNumber = strconv.Itoa(serialNumber)
}
}
return data, nil
}
// serverName determines the SNI HostName to set based on an acme.Challenge
// for TLS-ALPN-01 challenges RFC8738 states that, if HostName is an IP, it
// should be the ARPA address https://datatracker.ietf.org/doc/html/rfc8738#section-6.

Loading…
Cancel
Save