// 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 ( "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "net/url" "regexp" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/jose" blog "github.com/letsencrypt/boulder/log" ) type WebFrontEndImpl struct { RA core.RegistrationAuthority SA core.StorageGetter log *blog.AuditLogger // URL configuration parameters NewReg string RegBase string NewAuthz string AuthzBase string NewCert string CertBase string SubscriberAgreementURL string } func NewWebFrontEndImpl(logger *blog.AuditLogger) WebFrontEndImpl { logger.Notice("Web Front End Starting") return WebFrontEndImpl{log: logger} } // Method implementations func verifyPOST(request *http.Request) ([]byte, jose.JsonWebKey, error) { zeroKey := jose.JsonWebKey{} // Read body if request.Body == nil { return nil, zeroKey, errors.New("No body on POST") } body, err := ioutil.ReadAll(request.Body) if err != nil { return nil, zeroKey, err } // Parse as JWS var jws jose.JsonWebSignature if err = json.Unmarshal(body, &jws); err != nil { return nil, zeroKey, err } // 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 err = jws.Verify(); err != nil { return nil, zeroKey, err } // TODO Return JWS body return []byte(jws.Payload), jws.Header.Key, nil } // The ID is always the last slash-separated token in the path func parseIDFromPath(path string) string { re := regexp.MustCompile("^.*/") return re.ReplaceAllString(path, "") } // Problem objects represent problem documents, which are // returned with HTTP error responses // https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00 type problem struct { Type string `json:"type,omitempty"` Detail string `json:"detail,omitempty"` Instance string `json:"instance,omitempty"` } func sendError(response http.ResponseWriter, message string, code int) { problem := problem{Detail: message} problemDoc, err := json.Marshal(problem) if err != nil { return } // 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) } func link(url, relation string) string { return fmt.Sprintf("<%s>;rel=\"%s\"", url, relation) } func (wfe *WebFrontEndImpl) NewRegistration(response http.ResponseWriter, request *http.Request) { if request.Method != "POST" { sendError(response, "Method not allowed", http.StatusMethodNotAllowed) return } body, key, err := verifyPOST(request) if err != nil { sendError(response, fmt.Sprintf("Unable to read/verify body: %v", err), http.StatusBadRequest) return } var init core.Registration err = json.Unmarshal(body, &init) if err != nil { sendError(response, "Error unmarshaling JSON", http.StatusBadRequest) return } reg, err := wfe.RA.NewRegistration(init, key) if err != nil { sendError(response, fmt.Sprintf("Error creating new registration: %+v", err), http.StatusInternalServerError) } regURL := wfe.RegBase + string(reg.ID) reg.ID = "" responseBody, err := json.Marshal(reg) if err != nil { sendError(response, "Error marshaling authz", 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) } func (wfe *WebFrontEndImpl) NewAuthorization(response http.ResponseWriter, request *http.Request) { if request.Method != "POST" { sendError(response, "Method not allowed", http.StatusMethodNotAllowed) return } body, key, err := verifyPOST(request) if err != nil { sendError(response, "Unable to read/verify body", http.StatusBadRequest) return } var init core.Authorization if err = json.Unmarshal(body, &init); err != nil { sendError(response, "Error unmarshaling JSON", http.StatusBadRequest) return } // Create new authz and return authz, err := wfe.RA.NewAuthorization(init, key) if err != nil { sendError(response, fmt.Sprintf("Error creating new authz: %+v", err), http.StatusInternalServerError) return } // Make a URL for this authz, then blow away the ID before serializing authzURL := wfe.AuthzBase + string(authz.ID) authz.ID = "" responseBody, err := json.Marshal(authz) if err != nil { sendError(response, "Error marshaling authz", 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 { wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) } } func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request *http.Request) { if request.Method != "POST" { sendError(response, "Method not allowed", http.StatusMethodNotAllowed) return } body, key, err := verifyPOST(request) if err != nil { sendError(response, "Unable to read/verify body", http.StatusBadRequest) return } var init core.CertificateRequest if err = json.Unmarshal(body, &init); err != nil { fmt.Println(err) sendError(response, "Error unmarshaling certificate request", http.StatusBadRequest) return } wfe.log.Notice(fmt.Sprintf("Client requested new certificate: %v %v %v", request.RemoteAddr, init, key)) // 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(init, key) if err != nil { sendError(response, fmt.Sprintf("Error creating new cert: %+v", err), http.StatusBadRequest) return } // Make a URL for this authz certURL := wfe.CertBase + string(cert.ID) // TODO The spec says this should 201 over to /cert, not reply with the // certificate at this point... fix will need to land in boulder and client // simultaneously. // TODO The spec says a client should send an Accept: application/pkix-cert // header; either explicitly insist or tolerate response.Header().Add("Location", certURL) response.Header().Set("Content-Type", "application/pkix-cert") response.WriteHeader(http.StatusCreated) if _, err = response.Write(cert.DER); err != nil { wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) } } func (wfe *WebFrontEndImpl) Challenge(authz core.Authorization, response http.ResponseWriter, request *http.Request) { // Check that the requested challenge exists within the authorization found := false var challengeIndex int for i, challenge := range authz.Challenges { tempURL := url.URL(challenge.URI) if tempURL.Path == request.URL.Path && tempURL.RawQuery == request.URL.RawQuery { found = true challengeIndex = i break } } if !found { sendError(response, fmt.Sprintf("Unable to find challenge"), http.StatusNotFound) return } switch request.Method { default: sendError(response, "Method not allowed", http.StatusMethodNotAllowed) return case "POST": body, key, err := verifyPOST(request) if err != nil { sendError(response, "Unable to read/verify body", http.StatusBadRequest) return } var challengeResponse core.Challenge if err = json.Unmarshal(body, &challengeResponse); err != nil { sendError(response, "Error unmarshaling authorization", http.StatusBadRequest) return } // Check that the signing key is the right key if !key.Equals(authz.Key) { sendError(response, "Signing key does not match key in authorization", http.StatusForbidden) return } // Ask the RA to update this authorization updatedAuthz, err := wfe.RA.UpdateAuthorization(authz, challengeIndex, challengeResponse) if err != nil { sendError(response, "Unable to update authorization", http.StatusInternalServerError) return } jsonReply, err := json.Marshal(updatedAuthz) if err != nil { sendError(response, "Failed to marshal authz", http.StatusInternalServerError) return } response.Header().Set("Content-Type", "application/json") response.WriteHeader(http.StatusAccepted) if _, err = response.Write(jsonReply); err != nil { wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) } } } func (wfe *WebFrontEndImpl) Registration(response http.ResponseWriter, request *http.Request) { // Requests to this handler should have a path that leads to a known authz id := parseIDFromPath(request.URL.Path) reg, err := wfe.SA.GetRegistration(id) if err != nil { sendError(response, fmt.Sprintf("Unable to find registration: %+v", err), http.StatusNotFound) return } reg.ID = id switch request.Method { default: sendError(response, "Method not allowed", http.StatusMethodNotAllowed) return case "GET": jsonReply, err := json.Marshal(reg) if err != nil { sendError(response, "Failed to marshal authz", http.StatusInternalServerError) return } response.Header().Set("Content-Type", "application/json") response.WriteHeader(http.StatusOK) response.Write(jsonReply) case "POST": body, key, err := verifyPOST(request) if err != nil { sendError(response, "Unable to read/verify body", http.StatusBadRequest) return } var update core.Registration err = json.Unmarshal(body, &update) if err != nil { sendError(response, "Error unmarshaling registration", http.StatusBadRequest) return } // Check that the signing key is the right key if !key.Equals(reg.Key) { sendError(response, "Signing key does not match key in registration", http.StatusForbidden) return } // Ask the RA to update this authorization updatedReg, err := wfe.RA.UpdateRegistration(reg, update) if err != nil { fmt.Println(err) sendError(response, "Unable to update registration", http.StatusInternalServerError) return } jsonReply, err := json.Marshal(updatedReg) if err != nil { sendError(response, "Failed to marshal authz", http.StatusInternalServerError) return } response.Header().Set("Content-Type", "application/json") response.WriteHeader(http.StatusAccepted) response.Write(jsonReply) } } func (wfe *WebFrontEndImpl) Authorization(response http.ResponseWriter, request *http.Request) { // 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 { sendError(response, fmt.Sprintf("Unable to find authorization: %+v", err), http.StatusNotFound) return } // If there is a fragment, then this is actually a request to a challenge URI if len(request.URL.RawQuery) != 0 { wfe.Challenge(authz, response, request) return } switch request.Method { default: sendError(response, "Method not allowed", http.StatusMethodNotAllowed) return case "GET": jsonReply, err := json.Marshal(authz) if err != nil { sendError(response, "Failed to marshal authz", http.StatusInternalServerError) return } response.Header().Set("Content-Type", "application/json") response.WriteHeader(http.StatusOK) if _, err = response.Write(jsonReply); err != nil { wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) } } } func (wfe *WebFrontEndImpl) Certificate(response http.ResponseWriter, request *http.Request) { switch request.Method { default: sendError(response, "Method not allowed", http.StatusMethodNotAllowed) return case "GET": id := parseIDFromPath(request.URL.Path) wfe.log.Notice(fmt.Sprintf("Requested certificate ID %s", id)) cert, err := wfe.SA.GetCertificate(id) if err != nil { sendError(response, "Not found", http.StatusNotFound) return } // TODO: Content negotiation // TODO: Link header response.Header().Set("Content-Type", "application/pkix-cert") response.WriteHeader(http.StatusOK) if _, err = response.Write(cert); err != nil { wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err)) } case "POST": // TODO: Handle revocation in POST } }