package wfe import ( "bytes" "crypto/x509" "encoding/hex" "encoding/json" "errors" "fmt" "io/ioutil" "net" "net/http" "net/url" "regexp" "strconv" "strings" "time" "github.com/jmhodges/clock" "github.com/prometheus/client_golang/prometheus" "golang.org/x/net/context" jose "gopkg.in/square/go-jose.v2" "github.com/letsencrypt/boulder/core" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/goodkey" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/metrics/measured_http" "github.com/letsencrypt/boulder/nonce" "github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/revocation" "github.com/letsencrypt/boulder/web" ) // Paths are the ACME-spec identified URL path-segments for various methods. // NOTE: In metrics/measured_http we make the assumption that these are all // lowercase plus hyphens. If you violate that assumption you should update // measured_http. const ( directoryPath = "/directory" newRegPath = "/acme/new-reg" regPath = "/acme/reg/" newAuthzPath = "/acme/new-authz" authzPath = "/acme/authz/" challengePath = "/acme/challenge/" newCertPath = "/acme/new-cert" certPath = "/acme/cert/" revokeCertPath = "/acme/revoke-cert" termsPath = "/terms" issuerPath = "/acme/issuer-cert" buildIDPath = "/build" rolloverPath = "/acme/key-change" ) // WebFrontEndImpl provides all the logic for Boulder's web-facing interface, // i.e., ACME. Its members configure the paths for various ACME functions, // plus a few other data items used in ACME. Its methods are primarily handlers // for HTTPS requests for the various ACME functions. type WebFrontEndImpl struct { RA core.RegistrationAuthority SA core.StorageGetter stats metrics.Scope log blog.Logger clk clock.Clock // URL configuration parameters BaseURL string // Issuer certificate (DER) for /acme/issuer-cert IssuerCert []byte // URL to the current subscriber agreement (should contain some version identifier) SubscriberAgreementURL string // DirectoryCAAIdentity is used for the /directory response's "meta" // element's "caaIdentities" field. It should match the VA's issuerDomain // field value. DirectoryCAAIdentity string // DirectoryWebsite is used for the /directory response's "meta" element's // "website" field. DirectoryWebsite string // Register of anti-replay nonces nonceService *nonce.NonceService // Key policy. keyPolicy goodkey.KeyPolicy // CORS settings AllowOrigins []string // Maximum duration of a request RequestTimeout time.Duration AcceptRevocationReason bool AllowAuthzDeactivation bool csrSignatureAlgs *prometheus.CounterVec } // NewWebFrontEndImpl constructs a web service for Boulder func NewWebFrontEndImpl( stats metrics.Scope, clk clock.Clock, keyPolicy goodkey.KeyPolicy, logger blog.Logger, ) (WebFrontEndImpl, error) { nonceService, err := nonce.NewNonceService(stats) if err != nil { return WebFrontEndImpl{}, err } csrSignatureAlgs := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "csrSignatureAlgs", Help: "Number of CSR signatures by algorithm", }, []string{"type"}, ) stats.MustRegister(csrSignatureAlgs) return WebFrontEndImpl{ log: logger, clk: clk, nonceService: nonceService, stats: stats, keyPolicy: keyPolicy, csrSignatureAlgs: csrSignatureAlgs, }, nil } // HandleFunc registers a handler at the given path. It's // http.HandleFunc(), but with a wrapper around the handler that // provides some generic per-request functionality: // // * Set a Replay-Nonce header. // // * Respond to OPTIONS requests, including CORS preflight requests. // // * Set a no cache header // // * Respond http.StatusMethodNotAllowed for HTTP methods other than // those listed. // // * Set CORS headers when responding to CORS "actual" requests. // // * Never send a body in response to a HEAD request. Anything // written by the handler will be discarded if the method is HEAD. // Also, all handlers that accept GET automatically accept HEAD. func (wfe *WebFrontEndImpl) HandleFunc(mux *http.ServeMux, pattern string, h web.WFEHandlerFunc, methods ...string) { methodsMap := make(map[string]bool) for _, m := range methods { methodsMap[m] = true } if methodsMap["GET"] && !methodsMap["HEAD"] { // Allow HEAD for any resource that allows GET methods = append(methods, "HEAD") methodsMap["HEAD"] = true } methodsStr := strings.Join(methods, ", ") handler := http.StripPrefix(pattern, web.NewTopHandler(wfe.log, web.WFEHandlerFunc(func(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { // We do not propagate errors here, because (1) they should be // transient, and (2) they fail closed. nonce, err := wfe.nonceService.Nonce() if err == nil { response.Header().Set("Replay-Nonce", nonce) } else { logEvent.AddError("unable to make nonce: %s", err) } logEvent.Endpoint = pattern if request.URL != nil { logEvent.Slug = request.URL.Path } switch request.Method { case "HEAD": // Go's net/http (and httptest) servers will strip out the body // of responses for us. This keeps the Content-Length for HEAD // requests as the same as GET requests per the spec. case "OPTIONS": wfe.Options(response, request, methodsStr, methodsMap) return } // No cache header is set for all requests, succeed or fail. addNoCacheHeader(response) if !methodsMap[request.Method] { response.Header().Set("Allow", methodsStr) wfe.sendError(response, logEvent, probs.MethodNotAllowed(), nil) return } wfe.setCORSHeaders(response, request, "") timeout := wfe.RequestTimeout if timeout == 0 { timeout = 5 * time.Minute } ctx, cancel := context.WithTimeout(ctx, timeout) // TODO(riking): add request context using WithValue // Call the wrapped handler. h(ctx, logEvent, response, request) cancel() }), )) mux.Handle(pattern, handler) } func marshalIndent(v interface{}) ([]byte, error) { return json.MarshalIndent(v, "", " ") } func (wfe *WebFrontEndImpl) writeJsonResponse(response http.ResponseWriter, logEvent *web.RequestEvent, status int, v interface{}) error { jsonReply, err := marshalIndent(v) if err != nil { return err // All callers are responsible for handling this error } response.Header().Set("Content-Type", "application/json") response.WriteHeader(status) _, err = response.Write(jsonReply) if err != nil { // Don't worry about returning this error because the caller will // never handle it. wfe.log.Warningf("Could not write response: %s", err) logEvent.AddError(fmt.Sprintf("failed to write response: %s", err)) } return nil } const randomDirKeyExplanationLink = "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417" func (wfe *WebFrontEndImpl) relativeDirectory(request *http.Request, directory map[string]interface{}) ([]byte, error) { // Create an empty map sized equal to the provided directory to store the // relative-ized result relativeDir := make(map[string]interface{}, len(directory)) // Copy each entry of the provided directory into the new relative map, // prefixing it with the request protocol and host. for k, v := range directory { if v == randomDirKeyExplanationLink { relativeDir[k] = v continue } switch v := v.(type) { case string: // Only relative-ize top level string values, e.g. not the "meta" element relativeDir[k] = web.RelativeEndpoint(request, v) default: // If it isn't a string, put it into the results unmodified relativeDir[k] = v } } directoryJSON, err := marshalIndent(relativeDir) // This should never happen since we are just marshalling known strings if err != nil { return nil, err } return directoryJSON, nil } // Handler returns an http.Handler that uses various functions for // various ACME-specified paths. func (wfe *WebFrontEndImpl) Handler() http.Handler { m := http.NewServeMux() wfe.HandleFunc(m, directoryPath, wfe.Directory, "GET") wfe.HandleFunc(m, newRegPath, wfe.NewRegistration, "POST") wfe.HandleFunc(m, newAuthzPath, wfe.NewAuthorization, "POST") wfe.HandleFunc(m, newCertPath, wfe.NewCertificate, "POST") wfe.HandleFunc(m, regPath, wfe.Registration, "POST") wfe.HandleFunc(m, authzPath, wfe.Authorization, "GET", "POST") wfe.HandleFunc(m, challengePath, wfe.Challenge, "GET", "POST") wfe.HandleFunc(m, certPath, wfe.Certificate, "GET") wfe.HandleFunc(m, revokeCertPath, wfe.RevokeCertificate, "POST") wfe.HandleFunc(m, termsPath, wfe.Terms, "GET") wfe.HandleFunc(m, issuerPath, wfe.Issuer, "GET") wfe.HandleFunc(m, buildIDPath, wfe.BuildID, "GET") wfe.HandleFunc(m, rolloverPath, wfe.KeyRollover, "POST") // We don't use our special HandleFunc for "/" because it matches everything, // meaning we can wind up returning 405 when we mean to return 404. See // https://github.com/letsencrypt/boulder/issues/717 m.Handle("/", web.NewTopHandler(wfe.log, web.WFEHandlerFunc(wfe.Index))) return measured_http.New(m, wfe.clk, wfe.stats) } // Method implementations // Index serves a simple identification page. It is not part of the ACME spec. func (wfe *WebFrontEndImpl) Index(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { // http://golang.org/pkg/net/http/#example_ServeMux_Handle // The "/" pattern matches everything, so we need to check // that we're at the root here. if request.URL.Path != "/" { logEvent.AddError("Resource not found") http.NotFound(response, request) response.Header().Set("Content-Type", "application/problem+json") return } if request.Method != "GET" { logEvent.AddError("Bad method") response.Header().Set("Allow", "GET") response.WriteHeader(http.StatusMethodNotAllowed) return } addNoCacheHeader(response) response.Header().Set("Content-Type", "text/html") fmt.Fprintf(response, `
This is an ACME Certificate Authority running Boulder. JSON directory is available at %s. `, directoryPath, directoryPath) } func addNoCacheHeader(w http.ResponseWriter) { w.Header().Add("Cache-Control", "public, max-age=0, no-cache") } func addRequesterHeader(w http.ResponseWriter, requester int64) { if requester > 0 { w.Header().Set("Boulder-Requester", strconv.FormatInt(requester, 10)) } } // Directory is an HTTP request handler that provides the directory // object stored in the WFE's DirectoryEndpoints member with paths prefixed // using the `request.Host` of the HTTP request. func (wfe *WebFrontEndImpl) Directory(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { directoryEndpoints := map[string]interface{}{ "new-reg": newRegPath, "new-authz": newAuthzPath, "new-cert": newCertPath, "revoke-cert": revokeCertPath, } // Versions of Certbot pre-0.6.0 (named LetsEncryptPythonClient at the time) break when they // encounter a directory containing elements they don't expect so we gate // adding new directory fields for clients matching this UA. clientDirChangeIntolerant := strings.HasPrefix(request.UserAgent(), "LetsEncryptPythonClient") if !clientDirChangeIntolerant { directoryEndpoints["key-change"] = rolloverPath } if !clientDirChangeIntolerant { // Add a random key to the directory in order to make sure that clients don't hardcode an // expected set of keys. This ensures that we can properly extend the directory when we // need to add a new endpoint or meta element. directoryEndpoints[core.RandomString(8)] = randomDirKeyExplanationLink // ACME since draft-02 describes an optional "meta" directory entry. The // meta entry may optionally contain a "terms-of-service" URI for the // current ToS. metaMap := map[string]interface{}{ "terms-of-service": wfe.SubscriberAgreementURL, } // The "meta" directory entry may also include a []string of CAA identities if wfe.DirectoryCAAIdentity != "" { // The specification says caaIdentities is an array of strings. In // practice Boulder's VA only allows configuring ONE CAA identity. Given // that constraint it doesn't make sense to allow multiple directory CAA // identities so we use just the `wfe.DirectoryCAAIdentity` alone. metaMap["caaIdentities"] = []string{ wfe.DirectoryCAAIdentity, } } // The "meta" directory entry may also include a string with a website URL if wfe.DirectoryWebsite != "" { metaMap["website"] = wfe.DirectoryWebsite } directoryEndpoints["meta"] = metaMap } response.Header().Set("Content-Type", "application/json") relDir, err := wfe.relativeDirectory(request, directoryEndpoints) if err != nil { marshalProb := probs.ServerInternal("unable to marshal JSON directory") wfe.sendError(response, logEvent, marshalProb, nil) return } response.Write(relDir) } const ( unknownKey = "No registration exists matching provided key" ) func (wfe *WebFrontEndImpl) extractJWSKey(body string) (*jose.JSONWebKey, *jose.JSONWebSignature, error) { parsedJws, err := jose.ParseSigned(body) if err != nil { wfe.stats.Inc("Errors.UnableToParseJWS", 1) return nil, nil, errors.New("Parse error reading JWS") } if len(parsedJws.Signatures) > 1 { wfe.stats.Inc("Errors.TooManyJWSSignaturesInPOST", 1) return nil, nil, errors.New("Too many signatures in POST body") } if len(parsedJws.Signatures) == 0 { wfe.stats.Inc("Errors.JWSNotSignedInPOST", 1) return nil, nil, errors.New("POST JWS not signed") } key := parsedJws.Signatures[0].Header.JSONWebKey if key == nil { wfe.stats.Inc("Errors.NoJWKInJWSSignatureHeader", 1) return nil, nil, errors.New("No JWK in JWS header") } if !key.Valid() { wfe.stats.Inc("Errors.InvalidJWK", 1) return nil, nil, errors.New("Invalid JWK in JWS header") } return key, parsedJws, nil } // verifyPOST reads and parses the request body, looks up the Registration // corresponding to its JWK, verifies the JWS signature, checks that the // resource field is present and correct in the JWS protected header, and // returns the JWS payload bytes, the key used to verify, and the corresponding // Registration (or error). If regCheck is false, verifyPOST will still try to // look up a registration object, and will return it if found. However, if no // registration object is found, verifyPOST will attempt to verify the JWS using // the key in the JWS headers, and return the key plus a dummy registration if // successful. If a caller passes regCheck = false, it should plan on validating // the key itself. verifyPOST also appends its errors to web.RequestEvent.Errors so // code calling it does not need to if they immediately return a response to the // user. func (wfe *WebFrontEndImpl) verifyPOST(ctx context.Context, logEvent *web.RequestEvent, request *http.Request, regCheck bool, resource core.AcmeResource) ([]byte, *jose.JSONWebKey, core.Registration, *probs.ProblemDetails) { // TODO: We should return a pointer to a registration, which can be nil, // rather the a registration value with a sentinel value. // https://github.com/letsencrypt/boulder/issues/877 reg := core.Registration{ID: 0} if _, ok := request.Header["Content-Length"]; !ok { wfe.stats.Inc("HTTP.ClientErrors.LengthRequiredError", 1) return nil, nil, reg, probs.ContentLengthRequired() } // Read body if request.Body == nil { wfe.stats.Inc("Errors.NoPOSTBody", 1) return nil, nil, reg, probs.Malformed("No body on POST") } bodyBytes, err := ioutil.ReadAll(request.Body) if err != nil { wfe.stats.Inc("Errors.UnableToReadRequestBody", 1) return nil, nil, reg, probs.ServerInternal("unable to read request body") } body := string(bodyBytes) // Verify JWS // NOTE: It might seem insecure for the WFE to be trusted to verify // client requests, i.e., that the verification should be done at the // RA. However the WFE is the RA's only view of the outside world // *anyway*, so it could always lie about what key was used by faking // the signature itself. submittedKey, parsedJws, err := wfe.extractJWSKey(body) if err != nil { return nil, nil, reg, probs.Malformed(err.Error()) } var key *jose.JSONWebKey reg, err = wfe.SA.GetRegistrationByKey(ctx, submittedKey) // Special case: If no registration was found, but regCheck is false, use an // empty registration and the submitted key. The caller is expected to do some // validation on the returned key. if berrors.Is(err, berrors.NotFound) && !regCheck { // When looking up keys from the registrations DB, we can be confident they // are "good". But when we are verifying against any submitted key, we want // to check its quality before doing the verify. if err = wfe.keyPolicy.GoodKey(submittedKey.Key); err != nil { wfe.stats.Inc("Errors.JWKRejectedByGoodKey", 1) return nil, nil, reg, probs.Malformed(err.Error()) } key = submittedKey } else if err != nil { // For all other errors, or if regCheck is true, return error immediately. wfe.stats.Inc("Errors.UnableToGetRegistrationByKey", 1) logEvent.AddError("unable to fetch registration by the given JWK: %s", err) if berrors.Is(err, berrors.NotFound) { return nil, nil, reg, probs.Unauthorized(unknownKey) } return nil, nil, reg, probs.ServerInternal("Failed to get registration by key") } else { // If the lookup was successful, use that key. key = reg.Key logEvent.Requester = reg.ID logEvent.Contacts = reg.Contact } // Only check for validity if we are actually checking the registration if regCheck && reg.Status != core.StatusValid { return nil, nil, reg, probs.Unauthorized("Registration is not valid, has status '%s'", reg.Status) } if statName, err := checkAlgorithm(key, parsedJws); err != nil { wfe.stats.Inc(statName, 1) return nil, nil, reg, probs.Malformed(err.Error()) } payload, err := parsedJws.Verify(key) if err != nil { wfe.stats.Inc("Errors.JWSVerificationFailed", 1) n := len(body) if n > 100 { n = 100 } logEvent.AddError("verification of JWS with the JWK failed: %v; body: %s", err, body[:n]) return nil, nil, reg, probs.Malformed("JWS verification error") } logEvent.Payload = string(payload) // Check that the request has a known anti-replay nonce nonce := parsedJws.Signatures[0].Header.Nonce if len(nonce) == 0 { wfe.stats.Inc("Errors.JWSMissingNonce", 1) return nil, nil, reg, probs.BadNonce("JWS has no anti-replay nonce") } else if !wfe.nonceService.Valid(nonce) { wfe.stats.Inc("Errors.JWSInvalidNonce", 1) return nil, nil, reg, probs.BadNonce("JWS has invalid anti-replay nonce %s", nonce) } // Check that the "resource" field is present and has the correct value var parsedRequest struct { Resource string `json:"resource"` } err = json.Unmarshal([]byte(payload), &parsedRequest) if err != nil { wfe.stats.Inc("Errors.UnparseableJWSPayload", 1) return nil, nil, reg, probs.Malformed("Request payload did not parse as JSON") } if parsedRequest.Resource == "" { wfe.stats.Inc("Errors.NoResourceInJWSPayload", 1) return nil, nil, reg, probs.Malformed("Request payload does not specify a resource") } else if resource != core.AcmeResource(parsedRequest.Resource) { wfe.stats.Inc("Errors.MismatchedResourceInJWSPayload", 1) return nil, nil, reg, probs.Malformed("JWS resource payload does not match the HTTP resource: %s != %s", parsedRequest.Resource, resource) } return []byte(payload), key, reg, nil } // sendError wraps web.SendError func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, logEvent *web.RequestEvent, prob *probs.ProblemDetails, ierr error) { wfe.stats.Inc(fmt.Sprintf("HTTP.ProblemTypes.%s", prob.Type), 1) web.SendError(wfe.log, probs.V1ErrorNS, response, logEvent, prob, ierr) } func link(url, relation string) string { return fmt.Sprintf("<%s>;rel=\"%s\"", url, relation) } // NewRegistration is used by clients to submit a new registration/account func (wfe *WebFrontEndImpl) NewRegistration(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { body, key, _, prob := wfe.verifyPOST(ctx, logEvent, request, false, core.ResourceNewReg) addRequesterHeader(response, logEvent.Requester) if prob != nil { // verifyPOST handles its own setting of logEvent.Errors wfe.sendError(response, logEvent, prob, nil) return } if existingReg, err := wfe.SA.GetRegistrationByKey(ctx, key); err == nil { response.Header().Set("Location", web.RelativeEndpoint(request, fmt.Sprintf("%s%d", regPath, existingReg.ID))) // TODO(#595): check for missing registration err wfe.sendError(response, logEvent, probs.Conflict("Registration key is already in use"), err) return } var init core.Registration err := json.Unmarshal(body, &init) if err != nil { wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling JSON"), err) return } if len(init.Agreement) > 0 && init.Agreement != wfe.SubscriberAgreementURL { msg := fmt.Sprintf("Provided agreement URL [%s] does not match current agreement URL [%s]", init.Agreement, wfe.SubscriberAgreementURL) wfe.sendError(response, logEvent, probs.Malformed(msg), nil) return } init.Key = key init.InitialIP = net.ParseIP(request.Header.Get("X-Real-IP")) if init.InitialIP == nil { host, _, err := net.SplitHostPort(request.RemoteAddr) if err == nil { init.InitialIP = net.ParseIP(host) } else { wfe.sendError( response, logEvent, probs.ServerInternal("couldn't parse the remote (that is, the client's) address"), fmt.Errorf("Couldn't parse RemoteAddr: %s", request.RemoteAddr), ) return } } reg, err := wfe.RA.NewRegistration(ctx, init) if err != nil { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Error creating new registration"), err) return } logEvent.Requester = reg.ID addRequesterHeader(response, reg.ID) logEvent.Contacts = reg.Contact // Use an explicitly typed variable. Otherwise `go vet' incorrectly complains // that reg.ID is a string being passed to %d. regURL := web.RelativeEndpoint(request, fmt.Sprintf("%s%d", regPath, reg.ID)) response.Header().Add("Location", regURL) response.Header().Add("Link", link(web.RelativeEndpoint(request, newAuthzPath), "next")) if len(wfe.SubscriberAgreementURL) > 0 { response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service")) } err = wfe.writeJsonResponse(response, logEvent, http.StatusCreated, reg) if err != nil { // ServerInternal because we just created this registration, and it // should be OK. wfe.sendError(response, logEvent, probs.ServerInternal("Error marshaling registration"), err) return } } // NewAuthorization is used by clients to submit a new ID Authorization func (wfe *WebFrontEndImpl) NewAuthorization(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { body, _, currReg, prob := wfe.verifyPOST(ctx, logEvent, request, true, core.ResourceNewAuthz) addRequesterHeader(response, logEvent.Requester) if prob != nil { // verifyPOST handles its own setting of logEvent.Errors wfe.sendError(response, logEvent, prob, nil) return } // Any version of the agreement is acceptable here. Version match is enforced in // wfe.Registration when agreeing the first time. Agreement updates happen // by mailing subscribers and don't require a registration update. if currReg.Agreement == "" { wfe.sendError(response, logEvent, probs.Unauthorized("Must agree to subscriber agreement before any further actions"), nil) return } var init core.Authorization if err := json.Unmarshal(body, &init); err != nil { wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling JSON"), err) return } logEvent.Extra["Identifier"] = init.Identifier // Create new authz and return authz, err := wfe.RA.NewAuthorization(ctx, init, currReg.ID) if err != nil { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Error creating new authz"), err) return } logEvent.Extra["AuthzID"] = authz.ID // Make a URL for this authz, then blow away the ID and RegID before serializing authzURL := web.RelativeEndpoint(request, authzPath+string(authz.ID)) wfe.prepAuthorizationForDisplay(request, &authz) response.Header().Add("Location", authzURL) response.Header().Add("Link", link(web.RelativeEndpoint(request, newCertPath), "next")) err = wfe.writeJsonResponse(response, logEvent, http.StatusCreated, authz) if err != nil { // ServerInternal because we generated the authz, it should be OK wfe.sendError(response, logEvent, probs.ServerInternal("Error marshaling authz"), err) return } } func (wfe *WebFrontEndImpl) regHoldsAuthorizations(ctx context.Context, regID int64, names []string) (bool, error) { authz, err := wfe.SA.GetValidAuthorizations(ctx, regID, names, wfe.clk.Now()) if err != nil { return false, err } if len(names) != len(authz) { return false, nil } missingNames := false for _, name := range names { if _, present := authz[name]; !present { missingNames = true } } return !missingNames, nil } // RevokeCertificate is used by clients to request the revocation of a cert. func (wfe *WebFrontEndImpl) RevokeCertificate(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { // We don't ask verifyPOST to verify there is a corresponding registration, // because anyone with the right private key can revoke a certificate. body, requestKey, registration, prob := wfe.verifyPOST(ctx, logEvent, request, false, core.ResourceRevokeCert) addRequesterHeader(response, logEvent.Requester) if prob != nil { // verifyPOST handles its own setting of logEvent.Errors wfe.sendError(response, logEvent, prob, nil) return } type RevokeRequest struct { CertificateDER core.JSONBuffer `json:"certificate"` Reason *revocation.Reason `json:"reason"` } var revokeRequest RevokeRequest if err := json.Unmarshal(body, &revokeRequest); err != nil { wfe.sendError(response, logEvent, probs.Malformed("Unable to JSON parse revoke request"), err) return } providedCert, err := x509.ParseCertificate(revokeRequest.CertificateDER) if err != nil { wfe.sendError(response, logEvent, probs.Malformed("Unable to parse revoke certificate DER"), err) return } serial := core.SerialToString(providedCert.SerialNumber) logEvent.Extra["ProvidedCertificateSerial"] = serial cert, err := wfe.SA.GetCertificate(ctx, serial) // TODO(#991): handle db errors better if err != nil || !bytes.Equal(cert.DER, revokeRequest.CertificateDER) { wfe.sendError(response, logEvent, probs.NotFound("No such certificate"), err) return } parsedCertificate, err := x509.ParseCertificate(cert.DER) if err != nil { // InternalServerError because this is a failure to decode from our DB. wfe.sendError(response, logEvent, probs.ServerInternal("invalid parse of stored certificate"), err) return } logEvent.Extra["RetrievedCertificateSerial"] = core.SerialToString(parsedCertificate.SerialNumber) logEvent.Extra["RetrievedCertificateDNSNames"] = parsedCertificate.DNSNames logEvent.Extra["RetrievedCertificateEmailAddresses"] = parsedCertificate.EmailAddresses logEvent.Extra["RetrievedCertificateIPAddresses"] = parsedCertificate.IPAddresses certStatus, err := wfe.SA.GetCertificateStatus(ctx, serial) if err != nil { // TODO(#991): handle db errors wfe.sendError(response, logEvent, probs.NotFound("Certificate status not yet available"), err) return } logEvent.Extra["CertificateStatus"] = certStatus.Status if certStatus.Status == core.OCSPStatusRevoked { wfe.sendError(response, logEvent, probs.Conflict("Certificate already revoked"), nil) return } if !(core.KeyDigestEquals(requestKey, parsedCertificate.PublicKey) || registration.ID == cert.RegistrationID) { valid, err := wfe.regHoldsAuthorizations(ctx, registration.ID, parsedCertificate.DNSNames) if err != nil { wfe.sendError(response, logEvent, probs.ServerInternal("Failed to retrieve authorizations for names in certificate"), err) return } if !valid { wfe.sendError(response, logEvent, probs.Unauthorized("Revocation request must be signed by private key of cert to be revoked, by the "+ "account key of the account that issued it, or by the account key of an account that holds valid "+ "authorizations for all names in the certificate."), nil) return } } reason := revocation.Reason(0) if revokeRequest.Reason != nil && wfe.AcceptRevocationReason { if _, present := revocation.UserAllowedReasons[*revokeRequest.Reason]; !present { wfe.sendError(response, logEvent, probs.Malformed("unsupported revocation reason code provided"), nil) return } reason = *revokeRequest.Reason } err = wfe.RA.RevokeCertificateWithReg(ctx, *parsedCertificate, reason, registration.ID) if err != nil { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Failed to revoke certificate"), err) } else { wfe.log.Debugf("Revoked %v", serial) response.WriteHeader(http.StatusOK) } } func (wfe *WebFrontEndImpl) logCsr(request *http.Request, cr core.CertificateRequest, registration core.Registration) { var csrLog = struct { ClientAddr string CSR string Requester int64 }{ ClientAddr: web.GetClientAddr(request), CSR: hex.EncodeToString(cr.Bytes), Requester: registration.ID, } wfe.log.AuditObject("Certificate request", csrLog) } // NewCertificate is used by clients to request the issuance of a cert for an // authorized identifier. func (wfe *WebFrontEndImpl) NewCertificate(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { body, _, reg, prob := wfe.verifyPOST(ctx, logEvent, request, true, core.ResourceNewCert) addRequesterHeader(response, logEvent.Requester) if prob != nil { // verifyPOST handles its own setting of logEvent.Errors wfe.sendError(response, logEvent, prob, nil) return } // Any version of the agreement is acceptable here. Version match is enforced in // wfe.Registration when agreeing the first time. Agreement updates happen // by mailing subscribers and don't require a registration update. if reg.Agreement == "" { wfe.sendError(response, logEvent, probs.Unauthorized("Must agree to subscriber agreement before any further actions"), nil) return } var rawCSR core.RawCertificateRequest err := json.Unmarshal(body, &rawCSR) if err != nil { wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling certificate request"), err) return } // Assuming a properly formatted CSR there should be two four byte SEQUENCE // declarations then a two byte integer declaration which defines the version // of the CSR. If those two bytes (at offset 8 and 9) and equal to 2 and 0 // then the CSR was generated by a pre-1.0.2 version of OpenSSL with a client // which didn't explicitly set the version causing the integer to be malformed // and encoding/asn1 will refuse to parse it. If this is the case exit early // with a more useful error message. if len(rawCSR.CSR) >= 10 && rawCSR.CSR[8] == 2 && rawCSR.CSR[9] == 0 { wfe.sendError( response, logEvent, probs.Malformed("CSR generated using a pre-1.0.2 OpenSSL with a client that doesn't properly specify the CSR version. See https://community.letsencrypt.org/t/openssl-bug-information/19591"), nil, ) return } certificateRequest := core.CertificateRequest{Bytes: rawCSR.CSR} certificateRequest.CSR, err = x509.ParseCertificateRequest(rawCSR.CSR) if err != nil { wfe.sendError(response, logEvent, probs.Malformed("Error parsing certificate request: %s", err), err) return } wfe.logCsr(request, certificateRequest, reg) // Check that the key in the CSR is good. This will also be checked in the CA // component, but we want to discard CSRs with bad keys as early as possible // because (a) it's an easy check and we can save unnecessary requests and // bytes on the wire, and (b) the CA logs all rejections as audit events, but // a bad key from the client is just a malformed request and doesn't need to // be audited. if err := wfe.keyPolicy.GoodKey(certificateRequest.CSR.PublicKey); err != nil { wfe.sendError(response, logEvent, probs.Malformed("Invalid key in certificate request :: %s", err), err) return } logEvent.Extra["CSRDNSNames"] = certificateRequest.CSR.DNSNames logEvent.Extra["CSREmailAddresses"] = certificateRequest.CSR.EmailAddresses logEvent.Extra["CSRIPAddresses"] = certificateRequest.CSR.IPAddresses // Inc CSR signature algorithm counter wfe.csrSignatureAlgs.With(prometheus.Labels{"type": certificateRequest.CSR.SignatureAlgorithm.String()}).Inc() // Create new certificate and return // TODO IMPORTANT: The RA trusts the WFE to provide the correct key. If the // WFE is compromised, *and* the attacker knows the public key of an account // authorized for target site, they could cause issuance for that site by // lying to the RA. We should probably pass a copy of the whole request to the // RA for secondary validation. cert, err := wfe.RA.NewCertificate(ctx, certificateRequest, reg.ID) if err != nil { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Error creating new cert"), err) return } // Make a URL for this certificate. // We use only the sequential part of the serial number, because it should // uniquely identify the certificate, and this makes it easy for anybody to // enumerate and mirror our certificates. parsedCertificate, err := x509.ParseCertificate([]byte(cert.DER)) if err != nil { wfe.sendError(response, logEvent, probs.ServerInternal("Unable to parse certificate"), err) return } serial := parsedCertificate.SerialNumber certURL := web.RelativeEndpoint(request, certPath+core.SerialToString(serial)) // TODO Content negotiation response.Header().Add("Location", certURL) relativeIssuerPath := web.RelativeEndpoint(request, issuerPath) response.Header().Add("Link", link(relativeIssuerPath, "up")) response.Header().Set("Content-Type", "application/pkix-cert") response.WriteHeader(http.StatusCreated) if _, err = response.Write(cert.DER); err != nil { wfe.log.Warningf("Could not write response: %s", err) } } // Challenge handles POST requests to challenge URLs. Such requests are clients' // responses to the server's challenges. func (wfe *WebFrontEndImpl) Challenge( ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { notFound := func() { wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil) } // Challenge URIs are of the form /acme/challenge/