boulder/wfe/web-front-end.go

567 lines
17 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"
"html/template"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"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
Stats statsd.Statter
log *blog.AuditLogger
// URL configuration parameters
BaseURL string
NewReg string
NewRegPath string
RegBase string
RegPath string
NewAuthz string
NewAuthzPath string
AuthzBase string
AuthzPath string
NewCert string
NewCertPath string
CertBase string
CertPath string
TermsPath string
IssuerPath string
// Issuer certificate (DER) for /acme/issuer-cert
IssuerCert []byte
}
func NewWebFrontEndImpl() WebFrontEndImpl {
logger := blog.GetAuditLogger()
logger.Notice("Web Front End Starting")
return WebFrontEndImpl{
log: logger,
NewRegPath: "/acme/new-reg",
RegPath: "/acme/reg/",
NewAuthzPath: "/acme/new-authz",
AuthzPath: "/acme/authz/",
NewCertPath: "/acme/new-cert",
CertPath: "/acme/cert/",
TermsPath: "/terms",
IssuerPath: "/acme/issuer-cert",
}
}
func (wfe *WebFrontEndImpl) HandlePaths() {
wfe.NewReg = wfe.BaseURL + wfe.NewRegPath
wfe.RegBase = wfe.BaseURL + wfe.RegPath
wfe.NewAuthz = wfe.BaseURL + wfe.NewAuthzPath
wfe.AuthzBase = wfe.BaseURL + wfe.AuthzPath
wfe.NewCert = wfe.BaseURL + wfe.NewCertPath
wfe.CertBase = wfe.BaseURL + wfe.CertPath
http.HandleFunc("/", wfe.Index)
http.HandleFunc(wfe.NewRegPath, wfe.NewRegistration)
http.HandleFunc(wfe.NewAuthzPath, wfe.NewAuthorization)
http.HandleFunc(wfe.NewCertPath, wfe.NewCertificate)
http.HandleFunc(wfe.RegPath, wfe.Registration)
http.HandleFunc(wfe.AuthzPath, wfe.Authorization)
http.HandleFunc(wfe.CertPath, wfe.Certificate)
http.HandleFunc(wfe.TermsPath, wfe.Terms)
http.HandleFunc(wfe.IssuerPath, wfe.Issuer)
}
// Method implementations
func (wfe *WebFrontEndImpl) Index(response http.ResponseWriter, request *http.Request) {
// http://golang.org/pkg/net/http/#example_ServeMux_Handle
// The "/" pattern matches everything, so we need to check
// that we're at the root here.
if request.URL.Path != "/" {
http.NotFound(response, request)
return
}
tmpl := template.Must(template.New("body").Parse(`<html>
<body>
<a href="https://letsencrypt.org/">Let's Encrypt</a> Certificate Authority
running <a href="https://github.com/letsencrypt/boulder">Boulder</a>,
a reference <a href="https://letsencrypt.github.io/acme-spec/">ACME</a>
server implementation. New registration is available at
<a href="{{.NewReg}}">{{.NewReg}}</a>.
</body>
</html>
`))
tmpl.Execute(response, wfe)
response.Header().Set("Content-Type", "text/html")
}
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 (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, message string, code int) {
problem := problem{Detail: message}
problemDoc, err := json.Marshal(problem)
if err != nil {
problemDoc = []byte("{\"detail\": \"Problem marshalling error message.\"}")
}
wfe.log.Debug("Sending error to client: " + string(problemDoc))
// 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" {
wfe.sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, key, err := verifyPOST(request)
if err != nil {
wfe.sendError(response, "Unable to read/verify body", http.StatusBadRequest)
return
}
var init core.Registration
err = json.Unmarshal(body, &init)
if err != nil {
wfe.sendError(response, "Error unmarshaling JSON", http.StatusBadRequest)
return
}
reg, err := wfe.RA.NewRegistration(init, key)
if err != nil {
wfe.sendError(response,
fmt.Sprintf("Error creating new registration: %+v", err),
http.StatusInternalServerError)
return
}
regURL := wfe.RegBase + string(reg.ID)
reg.ID = ""
responseBody, err := json.Marshal(reg)
if err != nil {
wfe.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.TermsPath) > 0 {
response.Header().Add("Link", link(wfe.BaseURL+wfe.TermsPath, "terms-of-service"))
}
response.WriteHeader(http.StatusCreated)
response.Write(responseBody)
// incr reg stat
wfe.Stats.Inc("Registrations", 1, 1.0)
}
func (wfe *WebFrontEndImpl) NewAuthorization(response http.ResponseWriter, request *http.Request) {
if request.Method != "POST" {
wfe.sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, key, err := verifyPOST(request)
if err != nil {
wfe.sendError(response, "Unable to read/verify body", http.StatusBadRequest)
return
}
var init core.Authorization
if err = json.Unmarshal(body, &init); err != nil {
wfe.sendError(response, "Error unmarshaling JSON", http.StatusBadRequest)
return
}
// Create new authz and return
authz, err := wfe.RA.NewAuthorization(init, key)
if err != nil {
wfe.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 {
wfe.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))
}
// incr pending auth stat (?)
wfe.Stats.Inc("PendingAuthorizations", 1, 1.0)
}
func (wfe *WebFrontEndImpl) NewCertificate(response http.ResponseWriter, request *http.Request) {
if request.Method != "POST" {
wfe.sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, key, err := verifyPOST(request)
if err != nil {
wfe.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)
wfe.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 {
wfe.sendError(response,
fmt.Sprintf("Error creating new cert: %+v", err),
http.StatusBadRequest)
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.
serial := cert.ParsedCertificate.SerialNumber
certURL := fmt.Sprintf("%s%016x", wfe.CertBase, serial.Rsh(serial, 64))
// 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().Add("Link", link(wfe.IssuerPath, "up"))
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))
}
// incr cert stat
wfe.Stats.Inc("Certificates", 1, 1.0)
}
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 {
wfe.sendError(response,
fmt.Sprintf("Unable to find challenge"),
http.StatusNotFound)
return
}
switch request.Method {
default:
wfe.sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
return
case "POST":
body, key, err := verifyPOST(request)
if err != nil {
wfe.sendError(response, "Unable to read/verify body", http.StatusBadRequest)
return
}
var challengeResponse core.Challenge
if err = json.Unmarshal(body, &challengeResponse); err != nil {
wfe.sendError(response, "Error unmarshaling authorization", http.StatusBadRequest)
return
}
// Check that the signing key is the right key
if !key.Equals(authz.Key) {
wfe.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 {
wfe.sendError(response, "Unable to update authorization", http.StatusInternalServerError)
return
}
challenge := updatedAuthz.Challenges[challengeIndex]
// assumption: UpdateAuthorization does not modify order of challenges
jsonReply, err := json.Marshal(challenge)
if err != nil {
wfe.sendError(response, "Failed to marshal challenge", http.StatusInternalServerError)
return
}
authzURL := wfe.AuthzBase + string(authz.ID)
challengeURL := url.URL(challenge.URI)
response.Header().Add("Location", challengeURL.String())
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))
}
}
}
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 {
wfe.sendError(response,
fmt.Sprintf("Unable to find registration: %+v", err),
http.StatusNotFound)
return
}
reg.ID = id
switch request.Method {
default:
wfe.sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
return
case "GET":
jsonReply, err := json.Marshal(reg)
if err != nil {
wfe.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 {
wfe.sendError(response, "Unable to read/verify body", http.StatusBadRequest)
return
}
var update core.Registration
err = json.Unmarshal(body, &update)
if err != nil {
wfe.sendError(response, "Error unmarshaling registration", http.StatusBadRequest)
return
}
// Check that the signing key is the right key
if !key.Equals(reg.Key) {
wfe.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)
wfe.sendError(response, "Unable to update registration", http.StatusInternalServerError)
return
}
jsonReply, err := json.Marshal(updatedReg)
if err != nil {
wfe.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 {
wfe.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:
wfe.sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
return
case "GET":
jsonReply, err := json.Marshal(authz)
if err != nil {
wfe.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))
}
}
}
var allHex = regexp.MustCompile("^[0-9a-f]+$")
func (wfe *WebFrontEndImpl) notFound(response http.ResponseWriter) {
wfe.sendError(response, "Not found", http.StatusNotFound)
}
func (wfe *WebFrontEndImpl) Certificate(response http.ResponseWriter, request *http.Request) {
path := request.URL.Path
switch request.Method {
default:
wfe.sendError(response, "Method not allowed", http.StatusMethodNotAllowed)
return
case "GET":
// Certificate paths consist of the CertBase path, plus exactly sixteen hex
// digits.
if !strings.HasPrefix(path, wfe.CertPath) {
wfe.notFound(response)
return
}
serial := path[len(wfe.CertPath):]
if len(serial) != 16 || !allHex.Match([]byte(serial)) {
wfe.notFound(response)
return
}
wfe.log.Notice(fmt.Sprintf("Requested certificate ID %s", serial))
cert, err := wfe.SA.GetCertificateByShortSerial(serial)
if err != nil {
wfe.notFound(response)
return
}
// TODO: Content negotiation
response.Header().Set("Content-Type", "application/pkix-cert")
response.Header().Add("Link", link(wfe.IssuerPath, "up"))
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
// incr revoked cert stat
wfe.Stats.Inc("RevokedCertificates", 1, 1.0)
}
}
func (wfe *WebFrontEndImpl) Terms(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "You agree to do the right thing")
}
func (wfe *WebFrontEndImpl) Issuer(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Location", wfe.IssuerPath)
w.Header().Set("Content-Type", "application/pkix-cert")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(wfe.IssuerCert); err != nil {
wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err))
}
}