From 66356cff4321f28df1572587df39e0e8dfc148a4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 14 Jul 2022 17:10:03 -0700 Subject: [PATCH] Add attestation certificate validation for Apple devices --- acme/api/order.go | 4 +- acme/challenge.go | 277 +++++++++++++++++++++------------------------- acme/errors.go | 8 -- acme/order.go | 79 ++++++------- 4 files changed, 164 insertions(+), 204 deletions(-) diff --git a/acme/api/order.go b/acme/api/order.go index e2666154..166db0e6 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -33,7 +33,7 @@ func (n *NewOrderRequest) Validate() error { return acme.NewError(acme.ErrorMalformedType, "identifiers list cannot be empty") } for _, id := range n.Identifiers { - if !(id.Type == acme.DNS || id.Type == acme.IP || id.Type == acme.PermanentIdentifier || id.Type == acme.CA) { + if !(id.Type == acme.DNS || id.Type == acme.IP || id.Type == acme.PermanentIdentifier) { return acme.NewError(acme.ErrorMalformedType, "identifier type unsupported: %s", id.Type) } if id.Type == acme.IP && net.ParseIP(id.Value) == nil { @@ -375,8 +375,6 @@ func challengeTypes(az *acme.Authorization) []acme.ChallengeType { } case acme.PermanentIdentifier: chTypes = []acme.ChallengeType{acme.DEVICEATTEST01} - case acme.CA: - chTypes = []acme.ChallengeType{acme.APPLEATTEST01} default: chTypes = []acme.ChallengeType{} } diff --git a/acme/challenge.go b/acme/challenge.go index 596c0b0b..7f5a55cd 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -1,14 +1,12 @@ package acme import ( - "bytes" "context" "crypto" "crypto/sha256" "crypto/subtle" "crypto/tls" "crypto/x509" - "crypto/x509/pkix" "encoding/asn1" "encoding/base64" "encoding/hex" @@ -25,11 +23,8 @@ import ( "time" "github.com/fxamacker/cbor/v2" - "github.com/google/go-attestation/attest" - "github.com/google/go-attestation/oid" - x509ext "github.com/google/go-attestation/x509" - "github.com/google/go-tpm/tpm2" "go.step.sm/crypto/jose" + "go.step.sm/crypto/pemutil" ) type ChallengeType string @@ -43,7 +38,6 @@ const ( TLSALPN01 ChallengeType = "tls-alpn-01" // DEVICEATTEST01 is the device-attest-01 ACME challenge type DEVICEATTEST01 ChallengeType = "device-attest-01" - APPLEATTEST01 ChallengeType = "client-01" ) // Challenge represents an ACME response Challenge type. @@ -87,8 +81,6 @@ func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, return tlsalpn01Validate(ctx, ch, db, jwk) case DEVICEATTEST01: return deviceAttest01Validate(ctx, ch, db, jwk, payload) - case APPLEATTEST01: - return appleAttest01Validate(ctx, ch, db, jwk, payload) default: return NewErrorISE("unexpected challenge type '%s'", ch.Type) } @@ -315,7 +307,8 @@ func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebK } type Payload struct { - AttStmt []byte `json:"attStmt"` + AttObj string `json:"attObj"` + Error string `json:"error"` } type AttestationObject struct { @@ -326,121 +319,11 @@ type AttestationObject struct { // TODO(bweeks): move attestation verification to a shared package. // TODO(bweeks): define new error type for failed attestation validation. func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, payload []byte) error { - // TODO(bweeks): investigate if the iOS implementation allows for proper - // platform detection. - { - var p ApplePayload - if err := json.Unmarshal(payload, &p); err == nil { - return appleAttest01Validate(ctx, ch, db, jwk, payload) - } - } - var p Payload if err := json.Unmarshal(payload, &p); err != nil { return WrapErrorISE(err, "error unmarshalling JSON") } - att := AttestationObject{} - if err := cbor.Unmarshal(p.AttStmt, &att); err != nil { - return WrapErrorISE(err, "error unmarshalling CBOR") - } - - // TODO(bweeks): move verification code to a shared package. - // begin TPM key certification verification - params := &attest.CertificationParameters{ - Public: att.AttStatement["pubArea"].([]byte), - CreateAttestation: att.AttStatement["certInfo"].([]byte), - CreateSignature: att.AttStatement["sig"].([]byte), - } - // end TPM key certification verification - - x5c, x509present := att.AttStatement["x5c"].([]interface{}) - if !x509present { - return errors.New("x5c not present") - } - - akCertBytes, valid := x5c[0].([]byte) - if !valid { - return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, - "error getting certificate from x5c cert chain")) - } - - akCert, err := x509.ParseCertificate(akCertBytes) - if err != nil { - return WrapErrorISE(err, "error parsing AK certificate") - } - - if err := params.Verify(attest.VerifyOpts{Public: akCert.PublicKey, Hash: crypto.SHA256}); err != nil { - return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, - "params.Verify failed: %v", err)) - } - - attData, err := tpm2.DecodeAttestationData(params.CreateAttestation) - if err != nil { - return WrapErrorISE(err, "error decoding attestation data") - } - - keyAuth, err := KeyAuthorization(ch.Token, jwk) - if err != nil { - return err - } - hashedKeyAuth := sha256.Sum256([]byte(keyAuth)) - - if !bytes.Equal(attData.ExtraData, hashedKeyAuth[:]) { - return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, - "key authorization mismatch")) - } - - var sanExt pkix.Extension - for _, ext := range akCert.Extensions { - if ext.Id.Equal(oid.SubjectAltName) { - sanExt = ext - } - } - if sanExt.Value == nil { - return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, - "akCert missing subjectAltName")) - } - - san, err := x509ext.ParseSubjectAltName(sanExt) - if err != nil { - return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, - "failed to parse subjectAltName")) - } - - if len(san.PermanentIdentifiers) != 1 { - return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, - "subjectAltName doesn't contain a PermanentIdentifier")) - } - - wantPermID := san.PermanentIdentifiers[0] - if wantPermID.IdentifierValue != ch.Value { - return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, - "identifier from certificate and challenge do not match")) - } - - // Update and store the challenge. - ch.Status = StatusValid - ch.Error = nil - ch.ValidatedAt = clock.Now().Format(time.RFC3339) - - if err := db.UpdateChallenge(ctx, ch); err != nil { - return WrapErrorISE(err, "error updating challenge") - } - return nil -} - -type ApplePayload struct { - AttObj string `json:"attObj"` - Error string `json:"error"` -} - -func appleAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, payload []byte) error { - var p ApplePayload - if err := json.Unmarshal(payload, &p); err != nil { - return WrapErrorISE(err, "error unmarshalling JSON") - } - if p.Error != "" { return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, "payload contained error: %v", p.Error)) @@ -456,39 +339,39 @@ func appleAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose. return WrapErrorISE(err, "error unmarshalling CBOR") } - if att.Format != "apple" { - return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatement, - "unexpected attestation object format")) - } + switch att.Format { + case "apple": + data, err := doAppleAttestationFormat(ctx, ch, db, &att) + if err != nil { + return err + } - x5c, x509present := att.AttStatement["x5c"].([]interface{}) - if !x509present { - return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatement, - "x5c not present")) - } + // Validate nonce with SHA-256 of the token + // + // TODO(mariano): validate this + if data.Nonce != "" { + sum := sha256.Sum256([]byte(ch.Token)) + if data.Nonce != hex.EncodeToString(sum[:]) { + return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatement, "challenge token does not match")) + } + } - if len(x5c) == 0 { - return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, - "x5c is empty")) - } + // Validate Apple's ClientIdentifier (Identifier.Value) with device + // identifiers. + // + // Note: We might want to use an external service for this. + if data.UDID != ch.Value && data.SerialNumber != ch.Value { + return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatement, "permanent identifier does not match")) + } - attCertBytes, valid := x5c[0].([]byte) - if !valid { - return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, - "error getting certificate from x5c cert chain")) + // TODO(mariano): debug - remove me + pem.Encode(os.Stderr, &pem.Block{ + Type: "CERTIFICATE", Bytes: data.Certificate.Raw, + }) + default: + return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatement, "unexpected attestation object format")) } - attCert, err := x509.ParseCertificate(attCertBytes) - if err != nil { - return WrapErrorISE(err, "error parsing AK certificate") - } - - b := &pem.Block{ - Type: "CERTIFICATE", - Bytes: attCert.Raw, - } - pem.Encode(os.Stderr, b) - // Update and store the challenge. ch.Status = StatusValid ch.Error = nil @@ -500,6 +383,104 @@ func appleAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose. return nil } +// Apple Enterprise Attestation Root CA from +// https://www.apple.com/certificateauthority/private/ +const appleEnterpriseAttestationRootCA = `-----BEGIN CERTIFICATE----- +MIICJDCCAamgAwIBAgIUQsDCuyxyfFxeq/bxpm8frF15hzcwCgYIKoZIzj0EAwMw +UTEtMCsGA1UEAwwkQXBwbGUgRW50ZXJwcmlzZSBBdHRlc3RhdGlvbiBSb290IENB +MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzAeFw0yMjAyMTYxOTAx +MjRaFw00NzAyMjAwMDAwMDBaMFExLTArBgNVBAMMJEFwcGxlIEVudGVycHJpc2Ug +QXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UE +BhMCVVMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT6Jigq+Ps9Q4CoT8t8q+UnOe2p +oT9nRaUfGhBTbgvqSGXPjVkbYlIWYO+1zPk2Sz9hQ5ozzmLrPmTBgEWRcHjA2/y7 +7GEicps9wn2tj+G89l3INNDKETdxSPPIZpPj8VmjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFPNqTQGd8muBpV5du+UIbVbi+d66MA4GA1UdDwEB/wQEAwIB +BjAKBggqhkjOPQQDAwNpADBmAjEA1xpWmTLSpr1VH4f8Ypk8f3jMUKYz4QPG8mL5 +8m9sX/b2+eXpTv2pH4RZgJjucnbcAjEA4ZSB6S45FlPuS/u4pTnzoz632rA+xW/T +ZwFEh9bhKjJ+5VQ9/Do1os0u3LEkgN/r +-----END CERTIFICATE-----` + +var ( + oidAppleSerialNumber = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 9, 1} + oidAppleUniqueDeviceIdentifier = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 9, 2} + oidAppleSecureEnclaveProcessorOSVersion = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 10, 2} + oidAppleNonce = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 11, 1} +) + +type appleAttestationData struct { + Nonce string + SerialNumber string + UDID string + SEPVersion string + Certificate *x509.Certificate +} + +func doAppleAttestationFormat(ctx context.Context, ch *Challenge, db DB, att *AttestationObject) (*appleAttestationData, error) { + root, err := pemutil.ParseCertificate([]byte(appleEnterpriseAttestationRootCA)) + 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")) + } + + 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 := &appleAttestationData{ + Certificate: leaf, + } + for _, ext := range leaf.Extensions { + switch { + case ext.Id.Equal(oidAppleSerialNumber): + data.SerialNumber = string(ext.Value) + case ext.Id.Equal(oidAppleUniqueDeviceIdentifier): + data.UDID = string(ext.Value) + case ext.Id.Equal(oidAppleSecureEnclaveProcessorOSVersion): + data.SEPVersion = string(ext.Value) + case ext.Id.Equal(oidAppleNonce): + data.Nonce = string(ext.Value) + } + } + + 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. diff --git a/acme/errors.go b/acme/errors.go index 6bee949c..416eaf01 100644 --- a/acme/errors.go +++ b/acme/errors.go @@ -4,8 +4,6 @@ import ( "encoding/json" "fmt" "net/http" - "os" - "runtime/debug" "github.com/pkg/errors" "github.com/smallstep/certificates/api/render" @@ -277,8 +275,6 @@ type Error struct { // NewError creates a new Error type. func NewError(pt ProblemType, msg string, args ...interface{}) *Error { - fmt.Fprintf(os.Stderr, msg+"\n", args...) - debug.PrintStack() return newError(pt, errors.Errorf(msg, args...)) } @@ -304,8 +300,6 @@ func newError(pt ProblemType, err error) *Error { // NewErrorISE creates a new ErrorServerInternalType Error. func NewErrorISE(msg string, args ...interface{}) *Error { - fmt.Fprintf(os.Stderr, msg+"\n", args...) - debug.PrintStack() return NewError(ErrorServerInternalType, msg, args...) } @@ -328,8 +322,6 @@ func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Err // WrapErrorISE shortcut to wrap an internal server error type. func WrapErrorISE(err error, msg string, args ...interface{}) *Error { - fmt.Fprintf(os.Stderr, msg+"\n", args...) - debug.PrintStack() return WrapError(ErrorServerInternalType, err, msg, args...) } diff --git a/acme/order.go b/acme/order.go index b880523e..e64cdc49 100644 --- a/acme/order.go +++ b/acme/order.go @@ -12,8 +12,6 @@ import ( "strings" "time" - "github.com/google/go-attestation/oid" - attest_x509 "github.com/google/go-attestation/x509" "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/crypto/x509util" ) @@ -25,9 +23,9 @@ const ( IP IdentifierType = "ip" // DNS is the ACME dns identifier type DNS IdentifierType = "dns" - // DNS is the ACME dns identifier type + // PermanentIdentifier is the ACME permanent-identifier identifier type + // defined in https://datatracker.ietf.org/doc/html/draft-bweeks-acme-device-attest-00 PermanentIdentifier IdentifierType = "permanent-identifier" - CA IdentifierType = "ca" ) // Identifier encodes the type that an order pertains to. @@ -131,6 +129,11 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error { // Finalize signs a certificate if the necessary conditions for Order completion // have been met. +// +// TODO(mariano): Here or in the challenge validation we should perform some +// external validation using the identifier value and the attestation data. From +// a validation service we can get the list of SANs to set in the final +// certificate. func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateRequest, auth CertificateAuthority, p Provisioner) error { if err := o.UpdateStatus(ctx, db); err != nil { return err @@ -149,24 +152,33 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques return NewErrorISE("unexpected status %s for order %s", o.Status, o.ID) } - b := &pem.Block{ - Type: "CERTIFICATE REQUEST", - Bytes: csr.Raw, - } - pem.Encode(os.Stderr, b) - // canonicalize the CSR to allow for comparison csr = canonicalize(csr) - // retrieve the requested SANs for the Order - sans, err := o.sans(csr) - if err != nil { - return err + // Template data + data := x509util.NewTemplateData() + data.SetCommonName(csr.Subject.CommonName) + + // TODO: support for multiple identifiers? + var permanentIdentifier string + for i := range o.Identifiers { + if o.Identifiers[i].Type == PermanentIdentifier { + permanentIdentifier = o.Identifiers[i].Value + break + } } - deviceIDs, err := o.deviceIDs(csr) - if err != nil { - return err + if permanentIdentifier != "" { + data.SetPermanentIdentifiers([]x509util.PermanentIdentifier{ + {Value: permanentIdentifier}, + }) + } else { + // retrieve the requested SANs for the Order + sans, err := o.sans(csr) + if err != nil { + return err + } + data.Set(x509util.SANsKey, sans) } // Get authorizations from the ACME provisioner. @@ -176,12 +188,6 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques return WrapErrorISE(err, "error retrieving authorization options from ACME provisioner") } - // Template data - data := x509util.NewTemplateData() - data.SetCommonName(csr.Subject.CommonName) - data.Set(x509util.SANsKey, sans) - data.SetPermanentIdentifiers(deviceIDs) - templateOptions, err := provisioner.TemplateOptions(p.GetOptions(), data) if err != nil { return WrapErrorISE(err, "error creating template options from ACME provisioner") @@ -206,6 +212,11 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques return WrapErrorISE(err, "error creating certificate for order %s", o.ID) } + // TODO(mariano): debug - remove me + pem.Encode(os.Stderr, &pem.Block{ + Type: "CERTIFICATE", Bytes: cert.Leaf.Raw, + }) + o.CertificateID = cert.ID o.Status = StatusValid if err = db.UpdateOrder(ctx, o); err != nil { @@ -215,9 +226,7 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques } func (o *Order) sans(csr *x509.CertificateRequest) ([]x509util.SubjectAlternativeName, error) { - var sans []x509util.SubjectAlternativeName - if len(csr.EmailAddresses) > 0 || len(csr.URIs) > 0 { return sans, NewError(ErrorBadCSRType, "Only DNS names and IP addresses are allowed") } @@ -238,7 +247,6 @@ func (o *Order) sans(csr *x509.CertificateRequest) ([]x509util.SubjectAlternativ case PermanentIdentifier: orderPIDs[indexPID] = n.Value indexPID++ - case CA: default: return sans, NewErrorISE("unsupported identifier type in order: %s", n.Type) } @@ -292,25 +300,6 @@ func (o *Order) sans(csr *x509.CertificateRequest) ([]x509util.SubjectAlternativ return sans, nil } -func (o *Order) deviceIDs(csr *x509.CertificateRequest) ([]x509util.PermanentIdentifier, error) { - var permIDs []x509util.PermanentIdentifier - for _, ext := range csr.Extensions { - if ext.Id.Equal(oid.SubjectAltName) { - san, err := attest_x509.ParseSubjectAltName(ext) - if err != nil { - return nil, err - } - for _, pi := range san.PermanentIdentifiers { - permIDs = append(permIDs, x509util.PermanentIdentifier{ - Value: pi.IdentifierValue, - Assigner: pi.Assigner, - }) - } - } - } - return permIDs, nil -} - // numberOfIdentifierType returns the number of Identifiers that // are of type typ. func numberOfIdentifierType(typ IdentifierType, ids []Identifier) int {