// 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/