// Copyright 2014 ISRG. All rights reserved // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package wfe import ( "bytes" "crypto/x509" "encoding/json" "fmt" "io/ioutil" "net/http" "regexp" "strconv" "strings" "time" "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd" jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose" "github.com/letsencrypt/boulder/core" blog "github.com/letsencrypt/boulder/log" ) // Paths are the ACME-spec identified URL path-segments for various methods 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" ) type WebFrontEndImpl struct { RA core.RegistrationAuthority SA core.StorageGetter stats statsd.Statter log *blog.AuditLogger // URL configuration parameters BaseURL string NewReg string RegBase string NewAuthz string AuthzBase string ChallengeBase string NewCert string CertBase string // JSON encoded endpoint directory DirectoryJSON []byte // Issuer certificate (DER) for /acme/issuer-cert IssuerCert []byte // URL to the current subscriber agreement (should contain some version identifier) SubscriberAgreementURL string // Register of anti-replay nonces nonceService core.NonceService // Cache settings CertCacheDuration time.Duration CertNoCacheExpirationWindow time.Duration IndexCacheDuration time.Duration IssuerCacheDuration time.Duration // CORS settings AllowOrigins []string // Graceful shutdown settings ShutdownStopTimeout time.Duration ShutdownKillTimeout time.Duration } func statusCodeFromError(err interface{}) int { // Populate these as needed. We probably should trim the error list in util.go switch err.(type) { case core.MalformedRequestError: return http.StatusBadRequest case core.NotSupportedError: return http.StatusNotImplemented case core.SyntaxError: return http.StatusBadRequest case core.UnauthorizedError: return http.StatusForbidden case core.NotFoundError: return http.StatusNotFound case core.LengthRequiredError: return http.StatusLengthRequired case core.SignatureValidationError: return http.StatusBadRequest case core.InternalServerError: return http.StatusInternalServerError case core.RateLimitedError: // net/http doesn't have a specific const for 'Too Many Requests' return 429 default: return http.StatusInternalServerError } } type requestEvent struct { ID string `json:",omitempty"` RealIP string `json:",omitempty"` ForwardedFor string `json:",omitempty"` Endpoint string `json:",omitempty"` Method string `json:",omitempty"` RequestTime time.Time `json:",omitempty"` ResponseTime time.Time `json:",omitempty"` Error string `json:",omitempty"` Requester int64 `json:",omitempty"` Contacts []*core.AcmeURL `json:",omitempty"` Extra map[string]interface{} `json:",omitempty"` } // NewWebFrontEndImpl constructs a web service for Boulder func NewWebFrontEndImpl(stats statsd.Statter) (WebFrontEndImpl, error) { logger := blog.GetAuditLogger() logger.Notice("Web Front End Starting") nonceService, err := core.NewNonceService() if err != nil { return WebFrontEndImpl{}, err } return WebFrontEndImpl{ log: logger, nonceService: nonceService, stats: stats, }, nil } // BodylessResponseWriter wraps http.ResponseWriter, discarding // anything written to the body. type BodylessResponseWriter struct { http.ResponseWriter } func (mrw BodylessResponseWriter) Write(buf []byte) (int, error) { return len(buf), 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. // // * 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 func(http.ResponseWriter, *http.Request), 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, ", ") mux.HandleFunc(pattern, func(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) } switch request.Method { case "HEAD": // Whether or not we're sending a 405 error, // we should comply with HTTP spec by not // sending a body. response = BodylessResponseWriter{response} case "OPTIONS": wfe.Options(response, request, methodsStr, methodsMap) return } if !methodsMap[request.Method] { logEvent := wfe.populateRequestEvent(request) defer wfe.logRequestDetails(&logEvent) logEvent.Error = "Method not allowed" response.Header().Set("Allow", methodsStr) wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed) return } wfe.setCORSHeaders(response, request, "") // Call the wrapped handler. h(response, request) }) } // Handler returns an http.Handler that uses various functions for // various ACME-specified paths. func (wfe *WebFrontEndImpl) Handler() (http.Handler, error) { wfe.NewReg = wfe.BaseURL + NewRegPath wfe.RegBase = wfe.BaseURL + RegPath wfe.NewAuthz = wfe.BaseURL + NewAuthzPath wfe.AuthzBase = wfe.BaseURL + AuthzPath wfe.ChallengeBase = wfe.BaseURL + ChallengePath wfe.NewCert = wfe.BaseURL + NewCertPath wfe.CertBase = wfe.BaseURL + CertPath // Only generate directory once directory := map[string]string{ "new-reg": wfe.NewReg, "new-authz": wfe.NewAuthz, "new-cert": wfe.NewCert, "revoke-cert": wfe.BaseURL + RevokeCertPath, } directoryJSON, err := json.Marshal(directory) if err != nil { return nil, err } wfe.DirectoryJSON = directoryJSON 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") 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") // 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.HandleFunc("/", wfe.Index) return m, nil } // Method implementations // Index serves a simple identification page. It is not part of the ACME spec. func (wfe *WebFrontEndImpl) Index(response http.ResponseWriter, request *http.Request) { logEvent := wfe.populateRequestEvent(request) defer wfe.logRequestDetails(&logEvent) // 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.Error = "Resource not found" http.NotFound(response, request) response.Header().Set("Content-Type", "application/problem+json") return } if request.Method != "GET" { logEvent.Error = "Bad method" response.Header().Set("Allow", "GET") response.WriteHeader(http.StatusMethodNotAllowed) return } response.Header().Set("Content-Type", "text/html") response.Write([]byte(fmt.Sprintf(` This is an ACME Certificate Authority running Boulder. JSON directory is available at %s. `, DirectoryPath, DirectoryPath))) addCacheHeader(response, wfe.IndexCacheDuration.Seconds()) } func addNoCacheHeader(w http.ResponseWriter) { w.Header().Add("Cache-Control", "public, max-age=0, no-cache") } func addCacheHeader(w http.ResponseWriter, age float64) { w.Header().Add("Cache-Control", fmt.Sprintf("public, max-age=%.f", age)) } func (wfe *WebFrontEndImpl) Directory(response http.ResponseWriter, request *http.Request) { response.Header().Set("Content-Type", "application/json") response.Write(wfe.DirectoryJSON) } // The ID is always the last slash-separated token in the path func parseIDFromPath(path string) string { re := regexp.MustCompile("^.*/") return re.ReplaceAllString(path, "") } const ( unknownKey = "No registration exists matching provided key" malformedJWS = "Unable to read/verify body" ) // 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. func (wfe *WebFrontEndImpl) verifyPOST(request *http.Request, regCheck bool, resource core.AcmeResource) ([]byte, *jose.JsonWebKey, core.Registration, error) { var err error // 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: -1} if _, ok := request.Header["Content-Length"]; !ok { err = core.LengthRequiredError("Content-Length header is required for POST.") wfe.log.Debug(err.Error()) return nil, nil, reg, err } // Read body if request.Body == nil { err = core.MalformedRequestError("No body on POST") wfe.log.Debug(err.Error()) return nil, nil, reg, err } bodyBytes, err := ioutil.ReadAll(request.Body) if err != nil { err = core.InternalServerError(err.Error()) wfe.log.Debug(err.Error()) return nil, nil, reg, err } body := string(bodyBytes) // Parse as JWS parsedJws, err := jose.ParseSigned(body) if err != nil { puberr := core.SignatureValidationError("Parse error reading JWS") wfe.log.Debug(fmt.Sprintf("%v :: %v", puberr.Error(), err.Error())) return nil, nil, reg, puberr } // 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. if len(parsedJws.Signatures) > 1 { err = core.SignatureValidationError("Too many signatures on POST") wfe.log.Debug(err.Error()) return nil, nil, reg, err } if len(parsedJws.Signatures) == 0 { err = core.SignatureValidationError("POST JWS not signed") wfe.log.Debug(err.Error()) return nil, nil, reg, err } submittedKey := parsedJws.Signatures[0].Header.JsonWebKey if submittedKey == nil { err = core.SignatureValidationError("No JWK in JWS header") wfe.log.Debug(err.Error()) return nil, nil, reg, err } var key *jose.JsonWebKey reg, err = wfe.SA.GetRegistrationByKey(*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 _, ok := err.(core.NoSuchRegistrationError); ok && !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 = core.GoodKey(submittedKey.Key); err != nil { return nil, nil, reg, err } key = submittedKey } else if err != nil { // For all other errors, or if regCheck is true, return error immediately. return nil, nil, reg, err } else { // If the lookup was successful, use that key. key = ®.Key } payload, header, err := parsedJws.Verify(key) if err != nil { puberr := core.SignatureValidationError("JWS verification error") wfe.log.Debug(string(body)) wfe.log.Debug(fmt.Sprintf("%v :: %v", puberr.Error(), err.Error())) return nil, nil, reg, puberr } // Check that the request has a known anti-replay nonce // i.e., Nonce is in protected header and if err != nil || len(header.Nonce) == 0 { err = core.SignatureValidationError("JWS has no anti-replay nonce") wfe.log.Debug(err.Error()) return nil, nil, reg, err } else if !wfe.nonceService.Valid(header.Nonce) { err = core.SignatureValidationError(fmt.Sprintf("JWS has invalid anti-replay nonce")) wfe.log.Debug(err.Error()) return nil, nil, reg, err } // 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 { puberr := core.SignatureValidationError("Request payload did not parse as JSON") wfe.log.Debug(fmt.Sprintf("%v :: %v", puberr.Error(), err.Error())) return nil, nil, reg, puberr } if parsedRequest.Resource == "" { err = core.MalformedRequestError("Request payload does not specify a resource") wfe.log.Debug(err.Error()) return nil, nil, reg, err } else if resource != core.AcmeResource(parsedRequest.Resource) { err = core.MalformedRequestError(fmt.Sprintf("Request payload has invalid resource: %s != %s", parsedRequest.Resource, resource)) wfe.log.Debug(err.Error()) return nil, nil, reg, err } return []byte(payload), key, reg, nil } // Notify the client of an error condition and log it for audit purposes. func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, msg string, detail interface{}, code int) { problem := core.ProblemDetails{Detail: msg} switch code { case http.StatusPreconditionFailed: fallthrough case http.StatusForbidden: problem.Type = core.UnauthorizedProblem case http.StatusConflict: fallthrough case http.StatusMethodNotAllowed: fallthrough case http.StatusNotFound: fallthrough case http.StatusBadRequest: fallthrough case http.StatusLengthRequired: problem.Type = core.MalformedProblem default: // Either http.StatusInternalServerError or an unexpected code problem.Type = core.ServerInternalProblem } // Only audit log internal errors so users cannot purposefully cause // auditable events. if problem.Type == core.ServerInternalProblem { // AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3 wfe.log.Audit(fmt.Sprintf("Internal error - %s - %s", msg, detail)) } else if statusCodeFromError(detail) != http.StatusInternalServerError { // If not an internal error and problem is a custom error type problem.Detail += fmt.Sprintf(" :: %s", detail) } problemDoc, err := json.Marshal(problem) if err != nil { // AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3 wfe.log.Audit(fmt.Sprintf("Could not marshal error message: %s - %+v", err, problem)) problemDoc = []byte("{\"detail\": \"Problem marshalling error message.\"}") } // Paraphrased from // https://golang.org/src/net/http/server.go#L1272 response.Header().Set("Content-Type", "application/problem+json") response.WriteHeader(code) response.Write(problemDoc) wfe.stats.Inc(fmt.Sprintf("WFE.HTTP.ErrorCodes.%d", code), 1, 1.0) problemSegments := strings.Split(string(problem.Type), ":") if len(problemSegments) > 0 { wfe.stats.Inc(fmt.Sprintf("WFE.HTTP.ProblemTypes.%s", problemSegments[len(problemSegments)-1]), 1, 1.0) } } 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(response http.ResponseWriter, request *http.Request) { logEvent := wfe.populateRequestEvent(request) defer wfe.logRequestDetails(&logEvent) body, key, _, err := wfe.verifyPOST(request, false, core.ResourceNewReg) if err != nil { logEvent.Error = err.Error() wfe.sendError(response, malformedJWS, err, statusCodeFromError(err)) return } if existingReg, err := wfe.SA.GetRegistrationByKey(*key); err == nil { logEvent.Error = "Registration key is already in use" response.Header().Set("Location", fmt.Sprintf("%s%d", wfe.RegBase, existingReg.ID)) wfe.sendError(response, logEvent.Error, nil, http.StatusConflict) return } var init core.Registration err = json.Unmarshal(body, &init) if err != nil { logEvent.Error = err.Error() wfe.sendError(response, "Error unmarshaling JSON", err, http.StatusBadRequest) return } if len(init.Agreement) > 0 && init.Agreement != wfe.SubscriberAgreementURL { logEvent.Error = fmt.Sprintf("Provided agreement URL [%s] does not match current agreement URL [%s]", init.Agreement, wfe.SubscriberAgreementURL) wfe.sendError(response, logEvent.Error, nil, http.StatusBadRequest) return } init.Key = *key reg, err := wfe.RA.NewRegistration(init) if err != nil { logEvent.Error = err.Error() wfe.sendError(response, "Error creating new registration", err, statusCodeFromError(err)) return } logEvent.Requester = 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. var id int64 = reg.ID regURL := fmt.Sprintf("%s%d", wfe.RegBase, id) responseBody, err := json.Marshal(reg) if err != nil { logEvent.Error = err.Error() // StatusInternalServerError because we just created this registration, it should be OK. wfe.sendError(response, "Error marshaling registration", err, http.StatusInternalServerError) return } response.Header().Add("Location", regURL) response.Header().Set("Content-Type", "application/json") response.Header().Add("Link", link(wfe.NewAuthz, "next")) if len(wfe.SubscriberAgreementURL) > 0 { response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service")) } response.WriteHeader(http.StatusCreated) response.Write(responseBody) } // NewAuthorization is used by clients to submit a new ID Authorization func (wfe *WebFrontEndImpl) NewAuthorization(response http.ResponseWriter, request *http.Request) { logEvent := wfe.populateRequestEvent(request) defer wfe.logRequestDetails(&logEvent) body, _, currReg, err := wfe.verifyPOST(request, true, core.ResourceNewAuthz) if err != nil { logEvent.Error = err.Error() respMsg := malformedJWS respCode := statusCodeFromError(err) if _, ok := err.(core.NoSuchRegistrationError); ok { respMsg = unknownKey respCode = http.StatusForbidden } wfe.sendError(response, respMsg, err, respCode) return } logEvent.Requester = currReg.ID logEvent.Contacts = currReg.Contact // 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 == "" { logEvent.Error = "Must agree to subscriber agreement before any further actions" wfe.sendError(response, logEvent.Error, nil, http.StatusForbidden) return } var init core.Authorization if err = json.Unmarshal(body, &init); err != nil { logEvent.Error = err.Error() wfe.sendError(response, "Error unmarshaling JSON", err, http.StatusBadRequest) return } logEvent.Extra["Identifier"] = init.Identifier // Create new authz and return authz, err := wfe.RA.NewAuthorization(init, currReg.ID) if err != nil { logEvent.Error = err.Error() wfe.sendError(response, "Error creating new authz", err, statusCodeFromError(err)) return } logEvent.Extra["AuthzID"] = authz.ID // Make a URL for this authz, then blow away the ID and RegID before serializing authzURL := wfe.AuthzBase + string(authz.ID) wfe.prepAuthorizationForDisplay(&authz) responseBody, err := json.Marshal(authz) if err != nil { logEvent.Error = err.Error() // StatusInternalServerError because we generated the authz, it should be OK wfe.sendError(response, "Error marshaling authz", err, http.StatusInternalServerError) return } response.Header().Add("Location", authzURL) response.Header().Add("Link", link(wfe.NewCert, "next")) response.Header().Set("Content-Type", "application/json") response.WriteHeader(http.StatusCreated) if _, err = response.Write(responseBody); err != nil { logEvent.Error = err.Error() wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) } } // RevokeCertificate is used by clients to request the revocation of a cert. func (wfe *WebFrontEndImpl) RevokeCertificate(response http.ResponseWriter, request *http.Request) { logEvent := wfe.populateRequestEvent(request) defer wfe.logRequestDetails(&logEvent) // We don't ask verifyPOST to verify there is a correponding registration, // because anyone with the right private key can revoke a certificate. body, requestKey, registration, err := wfe.verifyPOST(request, false, core.ResourceRevokeCert) if err != nil { logEvent.Error = err.Error() wfe.sendError(response, malformedJWS, err, statusCodeFromError(err)) return } logEvent.Requester = registration.ID logEvent.Contacts = registration.Contact type RevokeRequest struct { CertificateDER core.JSONBuffer `json:"certificate"` } var revokeRequest RevokeRequest if err = json.Unmarshal(body, &revokeRequest); err != nil { logEvent.Error = err.Error() wfe.log.Debug(fmt.Sprintf("Couldn't unmarshal in revoke request %s", string(body))) wfe.sendError(response, "Unable to read/verify body", err, http.StatusBadRequest) return } providedCert, err := x509.ParseCertificate(revokeRequest.CertificateDER) if err != nil { logEvent.Error = err.Error() wfe.log.Debug("Couldn't parse cert in revoke request.") wfe.sendError(response, "Unable to read/verify body", err, http.StatusBadRequest) return } serial := core.SerialToString(providedCert.SerialNumber) logEvent.Extra["ProvidedCertificateSerial"] = serial cert, err := wfe.SA.GetCertificate(serial) if err != nil || !bytes.Equal(cert.DER, revokeRequest.CertificateDER) { wfe.sendError(response, "No such certificate", err, http.StatusNotFound) return } parsedCertificate, err := x509.ParseCertificate(cert.DER) if err != nil { logEvent.Error = err.Error() // InternalServerError because this is a failure to decode from our DB. wfe.sendError(response, "Invalid certificate", err, http.StatusInternalServerError) 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(serial) if err != nil { logEvent.Error = err.Error() wfe.sendError(response, "Certificate status not yet available", err, http.StatusNotFound) return } logEvent.Extra["CertificateStatus"] = certStatus.Status if certStatus.Status == core.OCSPStatusRevoked { logEvent.Error = "Certificate already revoked" wfe.sendError(response, logEvent.Error, "", http.StatusConflict) return } // TODO: Implement method of revocation by authorizations on account. if !(core.KeyDigestEquals(requestKey, parsedCertificate.PublicKey) || registration.ID == cert.RegistrationID) { logEvent.Error = "Revocation request must be signed by private key of cert to be revoked, or by the account key of the account that issued it." wfe.log.Debug("Key mismatch for revoke") wfe.sendError(response, logEvent.Error, requestKey, http.StatusForbidden) return } // Use revocation code 0, meaning "unspecified" err = wfe.RA.RevokeCertificateWithReg(*parsedCertificate, 0, registration.ID) if err != nil { logEvent.Error = err.Error() wfe.sendError(response, "Failed to revoke certificate", err, statusCodeFromError(err)) } else { wfe.log.Debug(fmt.Sprintf("Revoked %v", serial)) response.WriteHeader(http.StatusOK) } } func (wfe *WebFrontEndImpl) logCsr(remoteAddr string, cr core.CertificateRequest, registration core.Registration) { var csrLog = struct { RemoteAddr string CsrBase64 []byte Registration core.Registration }{ RemoteAddr: remoteAddr, CsrBase64: cr.Bytes, Registration: registration, } 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(response http.ResponseWriter, request *http.Request) { logEvent := wfe.populateRequestEvent(request) defer wfe.logRequestDetails(&logEvent) body, _, reg, err := wfe.verifyPOST(request, true, core.ResourceNewCert) if err != nil { logEvent.Error = err.Error() respMsg := malformedJWS respCode := statusCodeFromError(err) if _, ok := err.(core.NoSuchRegistrationError); ok { respMsg = unknownKey respCode = http.StatusForbidden } wfe.sendError(response, respMsg, err, respCode) return } logEvent.Requester = reg.ID logEvent.Contacts = reg.Contact // 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 == "" { logEvent.Error = "Must agree to subscriber agreement before any further actions" wfe.sendError(response, logEvent.Error, nil, http.StatusForbidden) return } var certificateRequest core.CertificateRequest if err = json.Unmarshal(body, &certificateRequest); err != nil { logEvent.Error = err.Error() wfe.sendError(response, "Error unmarshaling certificate request", err, http.StatusBadRequest) return } wfe.logCsr(request.RemoteAddr, 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 = core.GoodKey(certificateRequest.CSR.PublicKey); err != nil { logEvent.Error = err.Error() wfe.sendError(response, "Invalid key in certificate request", err, http.StatusBadRequest) return } logEvent.Extra["CSRDNSNames"] = certificateRequest.CSR.DNSNames logEvent.Extra["CSREmailAddresses"] = certificateRequest.CSR.EmailAddresses logEvent.Extra["CSRIPAddresses"] = certificateRequest.CSR.IPAddresses // 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 rquest to the // RA for secondary validation. cert, err := wfe.RA.NewCertificate(certificateRequest, reg.ID) if err != nil { logEvent.Error = err.Error() wfe.sendError(response, "Error creating new cert", err, statusCodeFromError(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 { logEvent.Error = err.Error() wfe.sendError(response, "Error creating new cert", err, http.StatusBadRequest) return } serial := parsedCertificate.SerialNumber certURL := wfe.CertBase + core.SerialToString(serial) // TODO Content negotiation response.Header().Add("Location", certURL) response.Header().Add("Link", link(wfe.BaseURL+IssuerPath, "up")) response.Header().Set("Content-Type", "application/pkix-cert") response.WriteHeader(http.StatusCreated) if _, err = response.Write(cert.DER); err != nil { logEvent.Error = err.Error() wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) } } func (wfe *WebFrontEndImpl) Challenge( response http.ResponseWriter, request *http.Request) { logEvent := wfe.populateRequestEvent(request) defer wfe.logRequestDetails(&logEvent) notFound := func() { wfe.sendError(response, "No such registration", request.URL.Path, http.StatusNotFound) } // Challenge URIs are of the form /acme/challenge//. // Here we parse out the id components. TODO: Use a better tool to parse out // URL structure: https://github.com/letsencrypt/boulder/issues/437 slug := strings.Split(request.URL.Path[len(ChallengePath):], "/") if len(slug) != 2 { notFound() return } authorizationID := slug[0] challengeID, err := strconv.ParseInt(slug[1], 10, 64) if err != nil { notFound() return } logEvent.Extra["AuthorizationID"] = authorizationID logEvent.Extra["ChallengeID"] = challengeID authz, err := wfe.SA.GetAuthorization(authorizationID) if err != nil { notFound() return } // Check that the requested challenge exists within the authorization challengeIndex := authz.FindChallenge(challengeID) if challengeIndex == -1 { notFound() return } challenge := authz.Challenges[challengeIndex] logEvent.Extra["ChallengeType"] = challenge.Type logEvent.Extra["AuthorizationRegistrationID"] = authz.RegistrationID logEvent.Extra["AuthorizationIdentifier"] = authz.Identifier logEvent.Extra["AuthorizationStatus"] = authz.Status logEvent.Extra["AuthorizationExpires"] = authz.Expires switch request.Method { case "GET": wfe.getChallenge(response, request, authz, &challenge, &logEvent) case "POST": wfe.postChallenge(response, request, authz, challengeIndex, &logEvent) } } // prepChallengeForDisplay takes a core.Challenge and prepares it for display to // the client by filling in its URI field and clearing its AccountKey and ID // fields. // TODO: Come up with a cleaner way to do this. // https://github.com/letsencrypt/boulder/issues/761 func (wfe *WebFrontEndImpl) prepChallengeForDisplay(authz core.Authorization, challenge *core.Challenge) { challenge.URI = fmt.Sprintf("%s%s/%d", wfe.ChallengeBase, authz.ID, challenge.ID) challenge.AccountKey = nil // 0 is considered "empty" for the purpose of the JSON omitempty tag. challenge.ID = 0 } // prepAuthorizationForDisplay takes a core.Authorization and prepares it for // display to the client by clearing its ID and RegistrationID fields, and // preparing all its challenges. func (wfe *WebFrontEndImpl) prepAuthorizationForDisplay(authz *core.Authorization) { for i := range authz.Challenges { wfe.prepChallengeForDisplay(*authz, &authz.Challenges[i]) } authz.ID = "" authz.RegistrationID = 0 } func (wfe *WebFrontEndImpl) getChallenge( response http.ResponseWriter, request *http.Request, authz core.Authorization, challenge *core.Challenge, logEvent *requestEvent) { wfe.prepChallengeForDisplay(authz, challenge) jsonReply, err := json.Marshal(challenge) if err != nil { logEvent.Error = err.Error() // InternalServerError because this is a failure to decode data passed in // by the caller, which got it from the DB. wfe.sendError(response, "Failed to marshal challenge", err, http.StatusInternalServerError) return } authzURL := wfe.AuthzBase + string(authz.ID) response.Header().Add("Location", challenge.URI) response.Header().Set("Content-Type", "application/json") response.Header().Add("Link", link(authzURL, "up")) response.WriteHeader(http.StatusAccepted) if _, err := response.Write(jsonReply); err != nil { wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) logEvent.Error = err.Error() return } } func (wfe *WebFrontEndImpl) postChallenge( response http.ResponseWriter, request *http.Request, authz core.Authorization, challengeIndex int, logEvent *requestEvent) { body, _, currReg, err := wfe.verifyPOST(request, true, core.ResourceChallenge) if err != nil { logEvent.Error = err.Error() respMsg := malformedJWS respCode := http.StatusBadRequest if _, ok := err.(core.NoSuchRegistrationError); ok { respMsg = unknownKey respCode = http.StatusForbidden } wfe.sendError(response, respMsg, err, respCode) return } logEvent.Requester = currReg.ID logEvent.Contacts = currReg.Contact // 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 == "" { logEvent.Error = "Must agree to subscriber agreement before any further actions" wfe.sendError(response, logEvent.Error, nil, http.StatusForbidden) return } // Check that the registration ID matching the key used matches // the registration ID on the authz object if currReg.ID != authz.RegistrationID { logEvent.Error = fmt.Sprintf("User: %v != Authorization: %v", currReg.ID, authz.RegistrationID) wfe.sendError(response, "User registration ID doesn't match registration ID in authorization", logEvent.Error, http.StatusForbidden) return } var challengeUpdate core.Challenge if err = json.Unmarshal(body, &challengeUpdate); err != nil { logEvent.Error = err.Error() wfe.sendError(response, "Error unmarshaling challenge response", err, http.StatusBadRequest) return } // Ask the RA to update this authorization updatedAuthorization, err := wfe.RA.UpdateAuthorization(authz, challengeIndex, challengeUpdate) if err != nil { logEvent.Error = err.Error() wfe.sendError(response, "Unable to update challenge", err, statusCodeFromError(err)) return } // assumption: UpdateAuthorization does not modify order of challenges challenge := updatedAuthorization.Challenges[challengeIndex] wfe.prepChallengeForDisplay(authz, &challenge) jsonReply, err := json.Marshal(challenge) if err != nil { logEvent.Error = err.Error() // StatusInternalServerError because we made the challenges, they should be OK wfe.sendError(response, "Failed to marshal challenge", err, http.StatusInternalServerError) return } authzURL := wfe.AuthzBase + string(authz.ID) response.Header().Add("Location", challenge.URI) response.Header().Set("Content-Type", "application/json") response.Header().Add("Link", link(authzURL, "up")) response.WriteHeader(http.StatusAccepted) if _, err = response.Write(jsonReply); err != nil { logEvent.Error = err.Error() wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) return } } // Registration is used by a client to submit an update to their registration. func (wfe *WebFrontEndImpl) Registration(response http.ResponseWriter, request *http.Request) { logEvent := wfe.populateRequestEvent(request) defer wfe.logRequestDetails(&logEvent) body, _, currReg, err := wfe.verifyPOST(request, true, core.ResourceRegistration) if err != nil { logEvent.Error = err.Error() respMsg := malformedJWS respCode := statusCodeFromError(err) if _, ok := err.(core.NoSuchRegistrationError); ok { respMsg = unknownKey respCode = http.StatusForbidden } wfe.sendError(response, respMsg, err, respCode) return } logEvent.Requester = currReg.ID logEvent.Contacts = currReg.Contact // Requests to this handler should have a path that leads to a known // registration idStr := parseIDFromPath(request.URL.Path) id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { logEvent.Error = err.Error() wfe.sendError(response, "Registration ID must be an integer", err, http.StatusBadRequest) return } else if id <= 0 { logEvent.Error = "Registration ID must be a positive non-zero integer" wfe.sendError(response, logEvent.Error, id, http.StatusBadRequest) return } else if id != currReg.ID { logEvent.Error = "Request signing key did not match registration key" wfe.sendError(response, logEvent.Error, "", http.StatusForbidden) return } var update core.Registration err = json.Unmarshal(body, &update) if err != nil { logEvent.Error = err.Error() wfe.sendError(response, "Error unmarshaling registration", err, http.StatusBadRequest) return } if len(update.Agreement) > 0 && update.Agreement != wfe.SubscriberAgreementURL { logEvent.Error = fmt.Sprintf("Provided agreement URL [%s] does not match current agreement URL [%s]", update.Agreement, wfe.SubscriberAgreementURL) wfe.sendError(response, logEvent.Error, nil, http.StatusBadRequest) return } // Registration objects contain a JWK object, which must be non-nil. We know // the key of the updated registration object is going to be the same as the // key of the current one, so we set it here. This ensures we can cleanly // serialize the update as JSON to send via AMQP to the RA. update.Key = currReg.Key // Ask the RA to update this authorization. updatedReg, err := wfe.RA.UpdateRegistration(currReg, update) if err != nil { logEvent.Error = err.Error() wfe.sendError(response, "Unable to update registration", err, statusCodeFromError(err)) return } jsonReply, err := json.Marshal(updatedReg) if err != nil { logEvent.Error = err.Error() // StatusInternalServerError because we just generated the reg, it should be OK wfe.sendError(response, "Failed to marshal registration", err, http.StatusInternalServerError) return } response.Header().Set("Content-Type", "application/json") response.Header().Add("Link", link(wfe.NewAuthz, "next")) if len(wfe.SubscriberAgreementURL) > 0 { response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service")) } response.WriteHeader(http.StatusAccepted) response.Write(jsonReply) } // Authorization is used by clients to submit an update to one of their // authorizations. func (wfe *WebFrontEndImpl) Authorization(response http.ResponseWriter, request *http.Request) { logEvent := wfe.populateRequestEvent(request) defer wfe.logRequestDetails(&logEvent) // Requests to this handler should have a path that leads to a known authz id := parseIDFromPath(request.URL.Path) authz, err := wfe.SA.GetAuthorization(id) if err != nil { wfe.sendError(response, "Unable to find authorization", err, http.StatusNotFound) return } logEvent.Extra["AuthorizationID"] = authz.ID logEvent.Extra["AuthorizationRegistrationID"] = authz.RegistrationID logEvent.Extra["AuthorizationIdentifier"] = authz.Identifier logEvent.Extra["AuthorizationStatus"] = authz.Status logEvent.Extra["AuthorizationExpires"] = authz.Expires wfe.prepAuthorizationForDisplay(&authz) jsonReply, err := json.Marshal(authz) if err != nil { logEvent.Error = err.Error() // InternalServerError because this is a failure to decode from our DB. wfe.sendError(response, "Failed to marshal authz", err, http.StatusInternalServerError) return } response.Header().Add("Link", link(wfe.NewCert, "next")) response.Header().Set("Content-Type", "application/json") response.WriteHeader(http.StatusOK) if _, err = response.Write(jsonReply); err != nil { logEvent.Error = err.Error() wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) } } var allHex = regexp.MustCompile("^[0-9a-f]+$") // Certificate is used by clients to request a copy of their current certificate, or to // request a reissuance of the certificate. func (wfe *WebFrontEndImpl) Certificate(response http.ResponseWriter, request *http.Request) { logEvent := wfe.populateRequestEvent(request) defer wfe.logRequestDetails(&logEvent) path := request.URL.Path // Certificate paths consist of the CertBase path, plus exactly sixteen hex // digits. if !strings.HasPrefix(path, CertPath) { logEvent.Error = "Certificate not found" wfe.sendError(response, logEvent.Error, path, http.StatusNotFound) addNoCacheHeader(response) return } serial := path[len(CertPath):] if !core.ValidSerial(serial) { logEvent.Error = "Certificate not found" wfe.sendError(response, logEvent.Error, serial, http.StatusNotFound) addNoCacheHeader(response) return } wfe.log.Debug(fmt.Sprintf("Requested certificate ID %s", serial)) logEvent.Extra["RequestedSerial"] = serial cert, err := wfe.SA.GetCertificate(serial) if err != nil { logEvent.Error = err.Error() if strings.HasPrefix(err.Error(), "gorp: multiple rows returned") { wfe.sendError(response, "Multiple certificates with same short serial", err, http.StatusConflict) } else { addNoCacheHeader(response) wfe.sendError(response, "Certificate not found", err, http.StatusNotFound) } return } addCacheHeader(response, wfe.CertCacheDuration.Seconds()) // TODO Content negotiation response.Header().Set("Content-Type", "application/pkix-cert") response.Header().Add("Link", link(IssuerPath, "up")) response.WriteHeader(http.StatusOK) if _, err = response.Write(cert.DER); err != nil { logEvent.Error = err.Error() wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) } return } // Terms is used by the client to obtain the current Terms of Service / // Subscriber Agreement to which the subscriber must agree. func (wfe *WebFrontEndImpl) Terms(response http.ResponseWriter, request *http.Request) { logEvent := wfe.populateRequestEvent(request) defer wfe.logRequestDetails(&logEvent) http.Redirect(response, request, wfe.SubscriberAgreementURL, http.StatusFound) } // Issuer obtains the issuer certificate used by this instance of Boulder. func (wfe *WebFrontEndImpl) Issuer(response http.ResponseWriter, request *http.Request) { logEvent := wfe.populateRequestEvent(request) defer wfe.logRequestDetails(&logEvent) addCacheHeader(response, wfe.IssuerCacheDuration.Seconds()) // TODO Content negotiation response.Header().Set("Content-Type", "application/pkix-cert") response.WriteHeader(http.StatusOK) if _, err := response.Write(wfe.IssuerCert); err != nil { logEvent.Error = err.Error() wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) } } // BuildID tells the requestor what build we're running. func (wfe *WebFrontEndImpl) BuildID(response http.ResponseWriter, request *http.Request) { logEvent := wfe.populateRequestEvent(request) defer wfe.logRequestDetails(&logEvent) response.Header().Set("Content-Type", "text/plain") response.WriteHeader(http.StatusOK) detailsString := fmt.Sprintf("Boulder=(%s %s)", core.GetBuildID(), core.GetBuildTime()) if _, err := fmt.Fprintln(response, detailsString); err != nil { logEvent.Error = err.Error() wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) } } // Options responds to an HTTP OPTIONS request. func (wfe *WebFrontEndImpl) Options(response http.ResponseWriter, request *http.Request, methodsStr string, methodsMap map[string]bool) { // Every OPTIONS request gets an Allow header with a list of supported methods. response.Header().Set("Allow", methodsStr) // CORS preflight requests get additional headers. See // http://www.w3.org/TR/cors/#resource-preflight-requests reqMethod := request.Header.Get("Access-Control-Request-Method") if reqMethod == "" { reqMethod = "GET" } if methodsMap[reqMethod] { wfe.setCORSHeaders(response, request, methodsStr) } } // setCORSHeaders() tells the client that CORS is acceptable for this // request. If allowMethods == "" the request is assumed to be a CORS // actual request and no Access-Control-Allow-Methods header will be // sent. func (wfe *WebFrontEndImpl) setCORSHeaders(response http.ResponseWriter, request *http.Request, allowMethods string) { reqOrigin := request.Header.Get("Origin") if reqOrigin == "" { // This is not a CORS request. return } // Allow CORS if the current origin (or "*") is listed as an // allowed origin in config. Otherwise, disallow by returning // without setting any CORS headers. allow := false for _, ao := range wfe.AllowOrigins { if ao == "*" { response.Header().Set("Access-Control-Allow-Origin", "*") allow = true break } else if ao == reqOrigin { response.Header().Set("Vary", "Origin") response.Header().Set("Access-Control-Allow-Origin", ao) allow = true break } } if !allow { return } if allowMethods != "" { // For an OPTIONS request: allow all methods handled at this URL. response.Header().Set("Access-Control-Allow-Methods", allowMethods) } response.Header().Set("Access-Control-Expose-Headers", "Link, Replay-Nonce") response.Header().Set("Access-Control-Max-Age", "86400") } func (wfe *WebFrontEndImpl) logRequestDetails(logEvent *requestEvent) { logEvent.ResponseTime = time.Now() var msg string if logEvent.Error != "" { msg = "Terminated request" } else { msg = "Successful request" } wfe.log.InfoObject(msg, logEvent) } func (wfe *WebFrontEndImpl) populateRequestEvent(request *http.Request) (logEvent requestEvent) { logEvent = requestEvent{ ID: core.NewToken(), RealIP: request.Header.Get("X-Real-IP"), ForwardedFor: request.Header.Get("X-Forwarded-For"), Method: request.Method, RequestTime: time.Now(), Extra: make(map[string]interface{}, 0), } if request.URL != nil { logEvent.Endpoint = request.URL.String() } return }