diff --git a/acme/api/order.go b/acme/api/order.go index 166db0e6..e2666154 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) { + if !(id.Type == acme.DNS || id.Type == acme.IP || id.Type == acme.PermanentIdentifier || id.Type == acme.CA) { return acme.NewError(acme.ErrorMalformedType, "identifier type unsupported: %s", id.Type) } if id.Type == acme.IP && net.ParseIP(id.Value) == nil { @@ -375,6 +375,8 @@ 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 5153dc26..56b4ebaf 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -13,11 +13,13 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" + "encoding/pem" "errors" "fmt" "io" "net" "net/url" + "os" "reflect" "strings" "time" @@ -41,6 +43,7 @@ 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. @@ -84,6 +87,8 @@ 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) } @@ -416,6 +421,68 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose 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") + } + + fmt.Fprintf(os.Stderr, "p.AttObj: %v\n", p.AttObj) + + attObj, err := base64.RawURLEncoding.DecodeString(p.AttObj) + if err != nil { + return WrapErrorISE(err, "error base64 decoding attObj") + } + + att := AttestationObject{} + if err := cbor.Unmarshal(attObj, &att); err != nil { + return WrapErrorISE(err, "error unmarshalling CBOR") + } + + if att.Format != "apple" { + return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatement, + "unexpected attestation object format")) + } + + x5c, x509present := att.AttStatement["x5c"].([]interface{}) + if !x509present { + return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatement, + "x5c not present")) + } + + attCertBytes, valid := x5c[0].([]byte) + if !valid { + return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, + "error getting certificate from x5c cert chain")) + } + + 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 + ch.ValidatedAt = clock.Now().Format(time.RFC3339) + + if err := db.UpdateChallenge(ctx, ch); err != nil { + return WrapErrorISE(err, "error updating challenge") + } + return 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 e9122aa1..6bee949c 100644 --- a/acme/errors.go +++ b/acme/errors.go @@ -19,6 +19,8 @@ const ( ErrorAccountDoesNotExistType ProblemType = iota // ErrorAlreadyRevokedType request specified a certificate to be revoked that has already been revoked ErrorAlreadyRevokedType + // ErrorBadAttestation WebAuthn attestation statement could not be verified + ErrorBadAttestationStatement // ErrorBadCSRType CSR is unacceptable (e.g., due to a short key) ErrorBadCSRType // ErrorBadNonceType client sent an unacceptable anti-replay nonce diff --git a/acme/order.go b/acme/order.go index 4150e2ce..b880523e 100644 --- a/acme/order.go +++ b/acme/order.go @@ -5,7 +5,9 @@ import ( "context" "crypto/x509" "encoding/json" + "encoding/pem" "net" + "os" "sort" "strings" "time" @@ -25,6 +27,7 @@ const ( DNS IdentifierType = "dns" // DNS is the ACME dns identifier type PermanentIdentifier IdentifierType = "permanent-identifier" + CA IdentifierType = "ca" ) // Identifier encodes the type that an order pertains to. @@ -146,6 +149,12 @@ 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) @@ -229,6 +238,7 @@ 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) }