diff --git a/acme/challenge.go b/acme/challenge.go index 3efcf32d..d66b28a0 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -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.