449 lines
13 KiB
Go
449 lines
13 KiB
Go
// 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
|
|
}
|
|
}
|