1258 lines
		
	
	
		
			47 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			1258 lines
		
	
	
		
			47 KiB
		
	
	
	
		
			Go
		
	
	
	
package wfe2
 | 
						|
 | 
						|
import (
 | 
						|
	"crypto/x509"
 | 
						|
	"database/sql"
 | 
						|
	"encoding/hex"
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"net"
 | 
						|
	"net/http"
 | 
						|
	"net/url"
 | 
						|
	"path"
 | 
						|
	"regexp"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/jmhodges/clock"
 | 
						|
	"golang.org/x/net/context"
 | 
						|
 | 
						|
	"github.com/letsencrypt/boulder/core"
 | 
						|
	"github.com/letsencrypt/boulder/goodkey"
 | 
						|
	blog "github.com/letsencrypt/boulder/log"
 | 
						|
	"github.com/letsencrypt/boulder/metrics"
 | 
						|
	"github.com/letsencrypt/boulder/metrics/measured_http"
 | 
						|
	"github.com/letsencrypt/boulder/nonce"
 | 
						|
	"github.com/letsencrypt/boulder/probs"
 | 
						|
)
 | 
						|
 | 
						|
// Paths are the ACME-spec identified URL path-segments for various methods.
 | 
						|
// NOTE: In metrics/measured_http we make the assumption that these are all
 | 
						|
// lowercase plus hyphens. If you violate that assumption you should update
 | 
						|
// measured_http.
 | 
						|
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"
 | 
						|
	rolloverPath   = "/acme/key-change"
 | 
						|
)
 | 
						|
 | 
						|
// WebFrontEndImpl provides all the logic for Boulder's web-facing interface,
 | 
						|
// i.e., ACME.  Its members configure the paths for various ACME functions,
 | 
						|
// plus a few other data items used in ACME.  Its methods are primarily handlers
 | 
						|
// for HTTPS requests for the various ACME functions.
 | 
						|
type WebFrontEndImpl struct {
 | 
						|
	RA    core.RegistrationAuthority
 | 
						|
	SA    core.StorageGetter
 | 
						|
	stats metrics.Scope
 | 
						|
	log   blog.Logger
 | 
						|
	clk   clock.Clock
 | 
						|
 | 
						|
	// URL configuration parameters
 | 
						|
	BaseURL string
 | 
						|
 | 
						|
	// 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 *nonce.NonceService
 | 
						|
 | 
						|
	// Key policy.
 | 
						|
	keyPolicy goodkey.KeyPolicy
 | 
						|
 | 
						|
	// Cache settings
 | 
						|
	CertCacheDuration           time.Duration
 | 
						|
	CertNoCacheExpirationWindow time.Duration
 | 
						|
	IndexCacheDuration          time.Duration
 | 
						|
	IssuerCacheDuration         time.Duration
 | 
						|
 | 
						|
	// CORS settings
 | 
						|
	AllowOrigins []string
 | 
						|
 | 
						|
	// Maximum duration of a request
 | 
						|
	RequestTimeout time.Duration
 | 
						|
 | 
						|
	AcceptRevocationReason bool
 | 
						|
	AllowAuthzDeactivation bool
 | 
						|
}
 | 
						|
 | 
						|
// NewWebFrontEndImpl constructs a web service for Boulder
 | 
						|
func NewWebFrontEndImpl(
 | 
						|
	stats metrics.Scope,
 | 
						|
	clk clock.Clock,
 | 
						|
	keyPolicy goodkey.KeyPolicy,
 | 
						|
	logger blog.Logger,
 | 
						|
) (WebFrontEndImpl, error) {
 | 
						|
	nonceService, err := nonce.NewNonceService(stats)
 | 
						|
	if err != nil {
 | 
						|
		return WebFrontEndImpl{}, err
 | 
						|
	}
 | 
						|
 | 
						|
	return WebFrontEndImpl{
 | 
						|
		log:          logger,
 | 
						|
		clk:          clk,
 | 
						|
		nonceService: nonceService,
 | 
						|
		stats:        stats,
 | 
						|
		keyPolicy:    keyPolicy,
 | 
						|
	}, 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.
 | 
						|
//
 | 
						|
// * Set a no cache header
 | 
						|
//
 | 
						|
// * 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 wfeHandlerFunc, 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, ", ")
 | 
						|
	handler := http.StripPrefix(pattern, &topHandler{
 | 
						|
		log: wfe.log,
 | 
						|
		clk: clock.Default(),
 | 
						|
		wfe: wfeHandlerFunc(func(ctx context.Context, logEvent *requestEvent, 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)
 | 
						|
				logEvent.ResponseNonce = nonce
 | 
						|
			} else {
 | 
						|
				logEvent.AddError("unable to make nonce: %s", err)
 | 
						|
			}
 | 
						|
 | 
						|
			logEvent.Endpoint = pattern
 | 
						|
			if request.URL != nil {
 | 
						|
				logEvent.Endpoint = path.Join(logEvent.Endpoint, request.URL.Path)
 | 
						|
			}
 | 
						|
 | 
						|
			switch request.Method {
 | 
						|
			case "HEAD":
 | 
						|
				// Go's net/http (and httptest) servers will strip out the body
 | 
						|
				// of responses for us. This keeps the Content-Length for HEAD
 | 
						|
				// requests as the same as GET requests per the spec.
 | 
						|
			case "OPTIONS":
 | 
						|
				wfe.Options(response, request, methodsStr, methodsMap)
 | 
						|
				return
 | 
						|
			}
 | 
						|
 | 
						|
			// No cache header is set for all requests, succeed or fail.
 | 
						|
			addNoCacheHeader(response)
 | 
						|
 | 
						|
			if !methodsMap[request.Method] {
 | 
						|
				response.Header().Set("Allow", methodsStr)
 | 
						|
				wfe.sendError(response, logEvent, probs.MethodNotAllowed(), nil)
 | 
						|
				return
 | 
						|
			}
 | 
						|
 | 
						|
			wfe.setCORSHeaders(response, request, "")
 | 
						|
 | 
						|
			timeout := wfe.RequestTimeout
 | 
						|
			if timeout == 0 {
 | 
						|
				timeout = 5 * time.Minute
 | 
						|
			}
 | 
						|
			ctx, cancel := context.WithTimeout(ctx, timeout)
 | 
						|
			// TODO(riking): add request context using WithValue
 | 
						|
 | 
						|
			// Call the wrapped handler.
 | 
						|
			h(ctx, logEvent, response, request)
 | 
						|
			cancel()
 | 
						|
		}),
 | 
						|
	})
 | 
						|
	mux.Handle(pattern, handler)
 | 
						|
}
 | 
						|
 | 
						|
func marshalIndent(v interface{}) ([]byte, error) {
 | 
						|
	return json.MarshalIndent(v, "", "  ")
 | 
						|
}
 | 
						|
 | 
						|
func (wfe *WebFrontEndImpl) writeJsonResponse(response http.ResponseWriter, logEvent *requestEvent, status int, v interface{}) error {
 | 
						|
	jsonReply, err := marshalIndent(v)
 | 
						|
	if err != nil {
 | 
						|
		return err // All callers are responsible for handling this error
 | 
						|
	}
 | 
						|
 | 
						|
	response.Header().Set("Content-Type", "application/json")
 | 
						|
	response.WriteHeader(status)
 | 
						|
	_, err = response.Write(jsonReply)
 | 
						|
	if err != nil {
 | 
						|
		// Don't worry about returning this error because the caller will
 | 
						|
		// never handle it.
 | 
						|
		wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err))
 | 
						|
		logEvent.AddError(fmt.Sprintf("failed to write response: %s", err))
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// requestProto returns "http" for HTTP requests and "https" for HTTPS
 | 
						|
// requests. It supports the use of "X-Forwarded-Proto" to override the protocol.
 | 
						|
func requestProto(request *http.Request) string {
 | 
						|
	proto := "http"
 | 
						|
 | 
						|
	// If the request was received via TLS, use `https://` for the protocol
 | 
						|
	if request.TLS != nil {
 | 
						|
		proto = "https"
 | 
						|
	}
 | 
						|
 | 
						|
	// Allow upstream proxies  to specify the forwarded protocol. Allow this value
 | 
						|
	// to override our own guess.
 | 
						|
	if specifiedProto := request.Header.Get("X-Forwarded-Proto"); specifiedProto != "" {
 | 
						|
		proto = specifiedProto
 | 
						|
	}
 | 
						|
 | 
						|
	return proto
 | 
						|
}
 | 
						|
 | 
						|
func (wfe *WebFrontEndImpl) relativeEndpoint(request *http.Request, endpoint string) string {
 | 
						|
	var result string
 | 
						|
	host := request.Host
 | 
						|
	proto := requestProto(request)
 | 
						|
 | 
						|
	// Default to "localhost" when no request.Host is provided. Otherwise requests
 | 
						|
	// with an empty `Host` produce results like `http:///acme/new-authz`
 | 
						|
	if request.Host == "" {
 | 
						|
		host = "localhost"
 | 
						|
	}
 | 
						|
 | 
						|
	if wfe.BaseURL != "" {
 | 
						|
		result = fmt.Sprintf("%s%s", wfe.BaseURL, endpoint)
 | 
						|
	} else {
 | 
						|
		resultUrl := url.URL{Scheme: proto, Host: host, Path: endpoint}
 | 
						|
		result = resultUrl.String()
 | 
						|
	}
 | 
						|
 | 
						|
	return result
 | 
						|
}
 | 
						|
 | 
						|
const randomDirKeyExplanationLink = "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417"
 | 
						|
 | 
						|
func (wfe *WebFrontEndImpl) relativeDirectory(request *http.Request, directory map[string]interface{}) ([]byte, error) {
 | 
						|
	// Create an empty map sized equal to the provided directory to store the
 | 
						|
	// relative-ized result
 | 
						|
	relativeDir := make(map[string]interface{}, len(directory))
 | 
						|
 | 
						|
	// Copy each entry of the provided directory into the new relative map. If
 | 
						|
	// `wfe.BaseURL` != "", use the old behaviour and prefix each endpoint with
 | 
						|
	// the `BaseURL`. Otherwise, prefix each endpoint using the request protocol
 | 
						|
	// & host.
 | 
						|
	for k, v := range directory {
 | 
						|
		if v == randomDirKeyExplanationLink {
 | 
						|
			relativeDir[k] = v
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		switch v := v.(type) {
 | 
						|
		case string:
 | 
						|
			// Only relative-ize top level string values, e.g. not the "meta" element
 | 
						|
			relativeDir[k] = wfe.relativeEndpoint(request, v)
 | 
						|
		default:
 | 
						|
			// If it isn't a string, put it into the results unmodified
 | 
						|
			relativeDir[k] = v
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	directoryJSON, err := marshalIndent(relativeDir)
 | 
						|
	// This should never happen since we are just marshalling known strings
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return directoryJSON, nil
 | 
						|
}
 | 
						|
 | 
						|
// Handler returns an http.Handler that uses various functions for
 | 
						|
// various ACME-specified paths.
 | 
						|
func (wfe *WebFrontEndImpl) Handler() http.Handler {
 | 
						|
	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", "POST")
 | 
						|
	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")
 | 
						|
	wfe.HandleFunc(m, rolloverPath, wfe.KeyRollover, "POST")
 | 
						|
	// 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.Handle("/", &topHandler{
 | 
						|
		log: wfe.log,
 | 
						|
		clk: clock.Default(),
 | 
						|
		wfe: wfeHandlerFunc(wfe.Index),
 | 
						|
	})
 | 
						|
	return measured_http.New(m, wfe.clk)
 | 
						|
}
 | 
						|
 | 
						|
// Method implementations
 | 
						|
 | 
						|
// Index serves a simple identification page. It is not part of the ACME spec.
 | 
						|
func (wfe *WebFrontEndImpl) Index(ctx context.Context, logEvent *requestEvent, 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 != "/" {
 | 
						|
		logEvent.AddError("Resource not found")
 | 
						|
		http.NotFound(response, request)
 | 
						|
		response.Header().Set("Content-Type", "application/problem+json")
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if request.Method != "GET" {
 | 
						|
		logEvent.AddError("Bad method")
 | 
						|
		response.Header().Set("Allow", "GET")
 | 
						|
		wfe.sendError(response, logEvent, probs.MethodNotAllowed(), nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	addNoCacheHeader(response)
 | 
						|
	response.Header().Set("Content-Type", "text/html")
 | 
						|
	response.Write([]byte(fmt.Sprintf(`<html>
 | 
						|
		<body>
 | 
						|
			This is an <a href="https://github.com/ietf-wg-acme/acme/">ACME</a>
 | 
						|
			Certificate Authority running <a href="https://github.com/letsencrypt/boulder">Boulder</a>.
 | 
						|
			JSON directory is available at <a href="%s">%s</a>.
 | 
						|
		</body>
 | 
						|
	</html>
 | 
						|
	`, directoryPath, directoryPath)))
 | 
						|
}
 | 
						|
 | 
						|
func addNoCacheHeader(w http.ResponseWriter) {
 | 
						|
	w.Header().Add("Cache-Control", "public, max-age=0, no-cache")
 | 
						|
}
 | 
						|
 | 
						|
func addRequesterHeader(w http.ResponseWriter, requester int64) {
 | 
						|
	if requester > 0 {
 | 
						|
		w.Header().Set("Boulder-Requester", fmt.Sprintf("%d", requester))
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Directory is an HTTP request handler that provides the directory
 | 
						|
// object stored in the WFE's DirectoryEndpoints member with paths prefixed
 | 
						|
// using the `request.Host` of the HTTP request.
 | 
						|
func (wfe *WebFrontEndImpl) Directory(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
 | 
						|
	directoryEndpoints := map[string]interface{}{
 | 
						|
		"new-reg":     newRegPath,
 | 
						|
		"new-authz":   newAuthzPath,
 | 
						|
		"new-cert":    newCertPath,
 | 
						|
		"revoke-cert": revokeCertPath,
 | 
						|
	}
 | 
						|
 | 
						|
	directoryEndpoints["key-change"] = rolloverPath
 | 
						|
 | 
						|
	// Add a random key to the directory in order to make sure that clients don't hardcode an
 | 
						|
	// expected set of keys. This ensures that we can properly extend the directory when we
 | 
						|
	// need to add a new endpoint or meta element.
 | 
						|
	directoryEndpoints[core.RandomString(8)] = randomDirKeyExplanationLink
 | 
						|
 | 
						|
	// ACME since draft-02 describes an optional "meta" directory entry. The
 | 
						|
	// meta entry may optionally contain a "terms-of-service" URI for the
 | 
						|
	// current ToS.
 | 
						|
	directoryEndpoints["meta"] = map[string]string{
 | 
						|
		"terms-of-service": wfe.SubscriberAgreementURL,
 | 
						|
	}
 | 
						|
 | 
						|
	response.Header().Set("Content-Type", "application/json")
 | 
						|
 | 
						|
	relDir, err := wfe.relativeDirectory(request, directoryEndpoints)
 | 
						|
	if err != nil {
 | 
						|
		marshalProb := probs.ServerInternal("unable to marshal JSON directory")
 | 
						|
		wfe.sendError(response, logEvent, marshalProb, nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	response.Write(relDir)
 | 
						|
}
 | 
						|
 | 
						|
// sendError sends an error response represented by the given ProblemDetails,
 | 
						|
// and, if the ProblemDetails.Type is ServerInternalProblem, audit logs the
 | 
						|
// internal ierr.
 | 
						|
func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, logEvent *requestEvent, prob *probs.ProblemDetails, ierr error) {
 | 
						|
	code := probs.ProblemDetailsToStatusCode(prob)
 | 
						|
 | 
						|
	// Record details to the log event
 | 
						|
	logEvent.AddError(fmt.Sprintf("%d :: %s :: %s", prob.HTTPStatus, prob.Type, prob.Detail))
 | 
						|
 | 
						|
	// Only audit log internal errors so users cannot purposefully cause
 | 
						|
	// auditable events.
 | 
						|
	if prob.Type == probs.ServerInternalProblem {
 | 
						|
		if ierr != nil {
 | 
						|
			wfe.log.AuditErr(fmt.Sprintf("Internal error - %s - %s", prob.Detail, ierr))
 | 
						|
		} else {
 | 
						|
			wfe.log.AuditErr(fmt.Sprintf("Internal error - %s", prob.Detail))
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	problemDoc, err := marshalIndent(prob)
 | 
						|
	if err != nil {
 | 
						|
		wfe.log.AuditErr(fmt.Sprintf("Could not marshal error message: %s - %+v", err, prob))
 | 
						|
		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)
 | 
						|
 | 
						|
	problemSegments := strings.Split(string(prob.Type), ":")
 | 
						|
	if len(problemSegments) > 0 {
 | 
						|
		wfe.stats.Inc(fmt.Sprintf("HTTP.ProblemTypes.%s", problemSegments[len(problemSegments)-1]), 1)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
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(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
 | 
						|
 | 
						|
	// NewRegistration uses `validSelfAuthenticatedPOST` instead of
 | 
						|
	// `validPOSTforAccount` because there is no account to authenticate against
 | 
						|
	// until after it is created!
 | 
						|
	body, key, prob := wfe.validSelfAuthenticatedPOST(request, logEvent)
 | 
						|
	if prob != nil {
 | 
						|
		// validSelfAuthenticatedPOST handles its own setting of logEvent.Errors
 | 
						|
		wfe.sendError(response, logEvent, prob, nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if existingReg, err := wfe.SA.GetRegistrationByKey(ctx, key); err == nil {
 | 
						|
		response.Header().Set("Location", wfe.relativeEndpoint(request, fmt.Sprintf("%s%d", regPath, existingReg.ID)))
 | 
						|
		// TODO(#595): check for missing registration err
 | 
						|
		wfe.sendError(response, logEvent, probs.Conflict("Registration key is already in use"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	var init core.Registration
 | 
						|
	err := json.Unmarshal(body, &init)
 | 
						|
	if err != nil {
 | 
						|
		wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling JSON"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if len(init.Agreement) > 0 && init.Agreement != wfe.SubscriberAgreementURL {
 | 
						|
		msg := fmt.Sprintf("Provided agreement URL [%s] does not match current agreement URL [%s]", init.Agreement, wfe.SubscriberAgreementURL)
 | 
						|
		wfe.sendError(response, logEvent, probs.Malformed(msg), nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	init.Key = key
 | 
						|
	init.InitialIP = net.ParseIP(request.Header.Get("X-Real-IP"))
 | 
						|
	if init.InitialIP == nil {
 | 
						|
		host, _, err := net.SplitHostPort(request.RemoteAddr)
 | 
						|
		if err == nil {
 | 
						|
			init.InitialIP = net.ParseIP(host)
 | 
						|
		} else {
 | 
						|
			logEvent.AddError("Couldn't parse RemoteAddr: %s", request.RemoteAddr)
 | 
						|
			wfe.sendError(response, logEvent, probs.ServerInternal("couldn't parse the remote (that is, the client's) address"), nil)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	reg, err := wfe.RA.NewRegistration(ctx, init)
 | 
						|
	if err != nil {
 | 
						|
		logEvent.AddError("unable to create new registration: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, problemDetailsForError(err, "Error creating new registration"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	logEvent.Requester = reg.ID
 | 
						|
	addRequesterHeader(response, 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.
 | 
						|
	regURL := wfe.relativeEndpoint(request, fmt.Sprintf("%s%d", regPath, reg.ID))
 | 
						|
 | 
						|
	response.Header().Add("Location", regURL)
 | 
						|
	response.Header().Add("Link", link(wfe.relativeEndpoint(request, newAuthzPath), "next"))
 | 
						|
	if len(wfe.SubscriberAgreementURL) > 0 {
 | 
						|
		response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service"))
 | 
						|
	}
 | 
						|
 | 
						|
	err = wfe.writeJsonResponse(response, logEvent, http.StatusCreated, reg)
 | 
						|
	if err != nil {
 | 
						|
		// ServerInternal because we just created this registration, and it
 | 
						|
		// should be OK.
 | 
						|
		logEvent.AddError("unable to marshal registration: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.ServerInternal("Error marshaling registration"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// NewAuthorization is used by clients to submit a new ID Authorization
 | 
						|
func (wfe *WebFrontEndImpl) NewAuthorization(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
 | 
						|
	body, currReg, prob := wfe.validPOSTForAccount(request, ctx, logEvent)
 | 
						|
	addRequesterHeader(response, logEvent.Requester)
 | 
						|
	if prob != nil {
 | 
						|
		// validPOSTforAccount handles its own setting of logEvent.Errors
 | 
						|
		wfe.sendError(response, logEvent, prob, nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	// 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 == "" {
 | 
						|
		wfe.sendError(response, logEvent, probs.Unauthorized("Must agree to subscriber agreement before any further actions"), nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	var init core.Authorization
 | 
						|
	if err := json.Unmarshal(body, &init); err != nil {
 | 
						|
		logEvent.AddError("unable to JSON unmarshal Authorization: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling JSON"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	logEvent.Extra["Identifier"] = init.Identifier
 | 
						|
 | 
						|
	// Create new authz and return
 | 
						|
	authz, err := wfe.RA.NewAuthorization(ctx, init, currReg.ID)
 | 
						|
	if err != nil {
 | 
						|
		logEvent.AddError("unable to create new authz: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, problemDetailsForError(err, "Error creating new authz"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	logEvent.Extra["AuthzID"] = authz.ID
 | 
						|
 | 
						|
	// Make a URL for this authz, then blow away the ID and RegID before serializing
 | 
						|
	authzURL := wfe.relativeEndpoint(request, authzPath+string(authz.ID))
 | 
						|
	wfe.prepAuthorizationForDisplay(request, &authz)
 | 
						|
 | 
						|
	response.Header().Add("Location", authzURL)
 | 
						|
	response.Header().Add("Link", link(wfe.relativeEndpoint(request, newCertPath), "next"))
 | 
						|
 | 
						|
	err = wfe.writeJsonResponse(response, logEvent, http.StatusCreated, authz)
 | 
						|
	if err != nil {
 | 
						|
		// ServerInternal because we generated the authz, it should be OK
 | 
						|
		wfe.sendError(response, logEvent, probs.ServerInternal("Error marshaling authz"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (wfe *WebFrontEndImpl) regHoldsAuthorizations(ctx context.Context, regID int64, names []string) (bool, error) {
 | 
						|
	authz, err := wfe.SA.GetValidAuthorizations(ctx, regID, names, wfe.clk.Now())
 | 
						|
	if err != nil {
 | 
						|
		return false, err
 | 
						|
	}
 | 
						|
	if len(names) != len(authz) {
 | 
						|
		return false, nil
 | 
						|
	}
 | 
						|
	missingNames := false
 | 
						|
	for _, name := range names {
 | 
						|
		if _, present := authz[name]; !present {
 | 
						|
			missingNames = true
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return !missingNames, nil
 | 
						|
}
 | 
						|
 | 
						|
// RevokeCertificate is used by clients to request the revocation of a cert.
 | 
						|
func (wfe *WebFrontEndImpl) RevokeCertificate(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
 | 
						|
	// RevokeCertificate is a NOP for WFEv2 at the present time. It needs unique JWS
 | 
						|
	// validation compared to other ACME v2 endpoints.
 | 
						|
	wfe.sendError(response, logEvent,
 | 
						|
		probs.ServerInternal("RevokeCertificate is not presently implemented for ACME v2"), nil)
 | 
						|
	return
 | 
						|
}
 | 
						|
 | 
						|
func (wfe *WebFrontEndImpl) logCsr(request *http.Request, cr core.CertificateRequest, registration core.Registration) {
 | 
						|
	var csrLog = struct {
 | 
						|
		ClientAddr   string
 | 
						|
		CSR          string
 | 
						|
		Registration core.Registration
 | 
						|
	}{
 | 
						|
		ClientAddr:   getClientAddr(request),
 | 
						|
		CSR:          hex.EncodeToString(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(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
 | 
						|
	body, reg, prob := wfe.validPOSTForAccount(request, ctx, logEvent)
 | 
						|
	addRequesterHeader(response, logEvent.Requester)
 | 
						|
	if prob != nil {
 | 
						|
		// validPOSTForAccount handles its own setting of logEvent.Errors
 | 
						|
		wfe.sendError(response, logEvent, prob, nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	// 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 == "" {
 | 
						|
		wfe.sendError(response, logEvent, probs.Unauthorized("Must agree to subscriber agreement before any further actions"), nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	var rawCSR core.RawCertificateRequest
 | 
						|
	err := json.Unmarshal(body, &rawCSR)
 | 
						|
	if err != nil {
 | 
						|
		logEvent.AddError("unable to JSON unmarshal CertificateRequest: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling certificate request"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	// Assuming a properly formatted CSR there should be two four byte SEQUENCE
 | 
						|
	// declarations then a two byte integer declaration which defines the version
 | 
						|
	// of the CSR. If those two bytes (at offset 8 and 9) and equal to 2 and 0
 | 
						|
	// then the CSR was generated by a pre-1.0.2 version of OpenSSL with a client
 | 
						|
	// which didn't explicitly set the version causing the integer to be malformed
 | 
						|
	// and encoding/asn1 will refuse to parse it. If this is the case exit early
 | 
						|
	// with a more useful error message.
 | 
						|
	if len(rawCSR.CSR) >= 10 && rawCSR.CSR[8] == 2 && rawCSR.CSR[9] == 0 {
 | 
						|
		logEvent.AddError("Pre-1.0.2 OpenSSL malformed CSR")
 | 
						|
		wfe.sendError(
 | 
						|
			response,
 | 
						|
			logEvent,
 | 
						|
			probs.Malformed("CSR generated using a pre-1.0.2 OpenSSL with a client that doesn't properly specify the CSR version. See https://community.letsencrypt.org/t/openssl-bug-information/19591"),
 | 
						|
			nil,
 | 
						|
		)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	certificateRequest := core.CertificateRequest{Bytes: rawCSR.CSR}
 | 
						|
	certificateRequest.CSR, err = x509.ParseCertificateRequest(rawCSR.CSR)
 | 
						|
	if err != nil {
 | 
						|
		logEvent.AddError("unable to parse certificate request: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.Malformed("Error parsing certificate request: %s", err), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	wfe.logCsr(request, 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 := wfe.keyPolicy.GoodKey(certificateRequest.CSR.PublicKey); err != nil {
 | 
						|
		logEvent.AddError("CSR public key failed GoodKey: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.Malformed("Invalid key in certificate request :: %s", err), err)
 | 
						|
		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 request to the
 | 
						|
	// RA for secondary validation.
 | 
						|
	cert, err := wfe.RA.NewCertificate(ctx, certificateRequest, reg.ID)
 | 
						|
	if err != nil {
 | 
						|
		logEvent.AddError("unable to create new cert: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, problemDetailsForError(err, "Error creating new cert"), 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.AddError("unable to parse certificate: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.ServerInternal("Unable to parse certificate"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	serial := parsedCertificate.SerialNumber
 | 
						|
	certURL := wfe.relativeEndpoint(request, certPath+core.SerialToString(serial))
 | 
						|
 | 
						|
	// TODO Content negotiation
 | 
						|
	response.Header().Add("Location", certURL)
 | 
						|
	if err = wfe.addIssuingCertificateURLs(response, parsedCertificate.IssuingCertificateURL); err != nil {
 | 
						|
		logEvent.AddError("unable to parse IssuingCertificateURL: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.ServerInternal("unable to parse IssuingCertificateURL"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	response.Header().Set("Content-Type", "application/pkix-cert")
 | 
						|
	response.WriteHeader(http.StatusCreated)
 | 
						|
	if _, err = response.Write(cert.DER); err != nil {
 | 
						|
		logEvent.AddError(err.Error())
 | 
						|
		wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err))
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Challenge handles POST requests to challenge URLs.  Such requests are clients'
 | 
						|
// responses to the server's challenges.
 | 
						|
func (wfe *WebFrontEndImpl) Challenge(
 | 
						|
	ctx context.Context,
 | 
						|
	logEvent *requestEvent,
 | 
						|
	response http.ResponseWriter,
 | 
						|
	request *http.Request) {
 | 
						|
 | 
						|
	notFound := func() {
 | 
						|
		wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil)
 | 
						|
	}
 | 
						|
 | 
						|
	// Challenge URIs are of the form /acme/challenge/<auth id>/<challenge id>.
 | 
						|
	// Here we parse out the id components.
 | 
						|
	slug := strings.Split(request.URL.Path, "/")
 | 
						|
	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(ctx, authorizationID)
 | 
						|
	if err != nil {
 | 
						|
		if err == sql.ErrNoRows {
 | 
						|
			notFound()
 | 
						|
		} else {
 | 
						|
			wfe.sendError(response, logEvent, probs.ServerInternal("Problem getting authorization"), err)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// After expiring, challenges are inaccessible
 | 
						|
	if authz.Expires == nil || authz.Expires.Before(wfe.clk.Now()) {
 | 
						|
		logEvent.AddError("Authorization %v expired in the past (%v)", authz.ID, *authz.Expires)
 | 
						|
		wfe.sendError(response, logEvent, probs.NotFound("Expired authorization"), nil)
 | 
						|
		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", "HEAD":
 | 
						|
		wfe.getChallenge(ctx, response, request, authz, &challenge, logEvent)
 | 
						|
 | 
						|
	case "POST":
 | 
						|
		wfe.postChallenge(ctx, 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 ID field.
 | 
						|
// TODO: Come up with a cleaner way to do this.
 | 
						|
// https://github.com/letsencrypt/boulder/issues/761
 | 
						|
func (wfe *WebFrontEndImpl) prepChallengeForDisplay(request *http.Request, authz core.Authorization, challenge *core.Challenge) {
 | 
						|
	challenge.URI = wfe.relativeEndpoint(request, fmt.Sprintf("%s%s/%d", challengePath, authz.ID, challenge.ID))
 | 
						|
	// 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(request *http.Request, authz *core.Authorization) {
 | 
						|
	for i := range authz.Challenges {
 | 
						|
		wfe.prepChallengeForDisplay(request, *authz, &authz.Challenges[i])
 | 
						|
	}
 | 
						|
	authz.ID = ""
 | 
						|
	authz.RegistrationID = 0
 | 
						|
}
 | 
						|
 | 
						|
func (wfe *WebFrontEndImpl) getChallenge(
 | 
						|
	ctx context.Context,
 | 
						|
	response http.ResponseWriter,
 | 
						|
	request *http.Request,
 | 
						|
	authz core.Authorization,
 | 
						|
	challenge *core.Challenge,
 | 
						|
	logEvent *requestEvent) {
 | 
						|
 | 
						|
	wfe.prepChallengeForDisplay(request, authz, challenge)
 | 
						|
 | 
						|
	authzURL := wfe.relativeEndpoint(request, authzPath+string(authz.ID))
 | 
						|
	response.Header().Add("Location", challenge.URI)
 | 
						|
	response.Header().Add("Link", link(authzURL, "up"))
 | 
						|
 | 
						|
	err := wfe.writeJsonResponse(response, logEvent, http.StatusAccepted, challenge)
 | 
						|
	if err != nil {
 | 
						|
		// InternalServerError because this is a failure to decode data passed in
 | 
						|
		// by the caller, which got it from the DB.
 | 
						|
		logEvent.AddError("unable to marshal challenge: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.ServerInternal("Failed to marshal challenge"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (wfe *WebFrontEndImpl) postChallenge(
 | 
						|
	ctx context.Context,
 | 
						|
	response http.ResponseWriter,
 | 
						|
	request *http.Request,
 | 
						|
	authz core.Authorization,
 | 
						|
	challengeIndex int,
 | 
						|
	logEvent *requestEvent) {
 | 
						|
	body, currReg, prob := wfe.validPOSTForAccount(request, ctx, logEvent)
 | 
						|
	addRequesterHeader(response, logEvent.Requester)
 | 
						|
	if prob != nil {
 | 
						|
		// validPOSTForAccount handles its own setting of logEvent.Errors
 | 
						|
		wfe.sendError(response, logEvent, prob, nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	// 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 == "" {
 | 
						|
		wfe.sendError(response, logEvent, probs.Unauthorized("Registration didn't agree to subscriber agreement before any further actions"), nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// Check that the registration ID matching the key used matches
 | 
						|
	// the registration ID on the authz object
 | 
						|
	if currReg.ID != authz.RegistrationID {
 | 
						|
		logEvent.AddError("User registration id: %d != Authorization registration id: %v", currReg.ID, authz.RegistrationID)
 | 
						|
		wfe.sendError(response,
 | 
						|
			logEvent,
 | 
						|
			probs.Unauthorized("User registration ID doesn't match registration ID in authorization"),
 | 
						|
			nil,
 | 
						|
		)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	var challengeUpdate core.Challenge
 | 
						|
	if err := json.Unmarshal(body, &challengeUpdate); err != nil {
 | 
						|
		logEvent.AddError("error JSON unmarshaling challenge response: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling challenge response"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// Ask the RA to update this authorization
 | 
						|
	updatedAuthorization, err := wfe.RA.UpdateAuthorization(ctx, authz, challengeIndex, challengeUpdate)
 | 
						|
	if err != nil {
 | 
						|
		logEvent.AddError("unable to update challenge: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, problemDetailsForError(err, "Unable to update challenge"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// assumption: UpdateAuthorization does not modify order of challenges
 | 
						|
	challenge := updatedAuthorization.Challenges[challengeIndex]
 | 
						|
	wfe.prepChallengeForDisplay(request, authz, &challenge)
 | 
						|
 | 
						|
	authzURL := wfe.relativeEndpoint(request, authzPath+string(authz.ID))
 | 
						|
	response.Header().Add("Location", challenge.URI)
 | 
						|
	response.Header().Add("Link", link(authzURL, "up"))
 | 
						|
 | 
						|
	err = wfe.writeJsonResponse(response, logEvent, http.StatusAccepted, challenge)
 | 
						|
	if err != nil {
 | 
						|
		// ServerInternal because we made the challenges, they should be OK
 | 
						|
		logEvent.AddError("failed to marshal challenge: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.ServerInternal("Failed to marshal challenge"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Registration is used by a client to submit an update to their registration.
 | 
						|
func (wfe *WebFrontEndImpl) Registration(
 | 
						|
	ctx context.Context,
 | 
						|
	logEvent *requestEvent,
 | 
						|
	response http.ResponseWriter,
 | 
						|
	request *http.Request) {
 | 
						|
	body, currReg, prob := wfe.validPOSTForAccount(request, ctx, logEvent)
 | 
						|
	addRequesterHeader(response, logEvent.Requester)
 | 
						|
	if prob != nil {
 | 
						|
		// validPOSTForAccount handles its own setting of logEvent.Errors
 | 
						|
		wfe.sendError(response, logEvent, prob, nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// Requests to this handler should have a path that leads to a known
 | 
						|
	// registration
 | 
						|
	idStr := request.URL.Path
 | 
						|
	id, err := strconv.ParseInt(idStr, 10, 64)
 | 
						|
	if err != nil {
 | 
						|
		logEvent.AddError("registration ID must be an integer, was %#v", idStr)
 | 
						|
		wfe.sendError(response, logEvent, probs.Malformed("Registration ID must be an integer"), err)
 | 
						|
		return
 | 
						|
	} else if id <= 0 {
 | 
						|
		msg := fmt.Sprintf("Registration ID must be a positive non-zero integer, was %d", id)
 | 
						|
		logEvent.AddError(msg)
 | 
						|
		wfe.sendError(response, logEvent, probs.Malformed(msg), nil)
 | 
						|
		return
 | 
						|
	} else if id != currReg.ID {
 | 
						|
		logEvent.AddError("Request signing key did not match registration key: %d != %d", id, currReg.ID)
 | 
						|
		wfe.sendError(response, logEvent, probs.Unauthorized("Request signing key did not match registration key"), nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	var update core.Registration
 | 
						|
	err = json.Unmarshal(body, &update)
 | 
						|
	if err != nil {
 | 
						|
		logEvent.AddError("unable to JSON parse registration: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling registration"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// People *will* POST their full registrations to this endpoint, including
 | 
						|
	// the 'valid' status, to avoid always failing out when that happens only
 | 
						|
	// attempt to deactivate if the provided status is different from their current
 | 
						|
	// status.
 | 
						|
	//
 | 
						|
	// If a user tries to send both a deactivation request and an update to their
 | 
						|
	// contacts or subscriber agreement URL the deactivation will take place and
 | 
						|
	// return before an update would be performed.
 | 
						|
	if update.Status != "" && update.Status != currReg.Status {
 | 
						|
		if update.Status != core.StatusDeactivated {
 | 
						|
			wfe.sendError(response, logEvent, probs.Malformed("Invalid value provided for status field"), nil)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		wfe.deactivateRegistration(ctx, *currReg, response, request, logEvent)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// If a user POSTs their registration object including a previously valid
 | 
						|
	// agreement URL but that URL has since changed we will fail out here
 | 
						|
	// since the update agreement URL doesn't match the current URL. To fix that we
 | 
						|
	// only fail if the sent URL doesn't match the currently valid agreement URL
 | 
						|
	// and it doesn't match the URL currently stored in the registration
 | 
						|
	// in the database. The RA understands the user isn't actually trying to
 | 
						|
	// update the agreement but since we do an early check here in order to prevent
 | 
						|
	// extraneous requests to the RA we have to add this bypass.
 | 
						|
	if len(update.Agreement) > 0 && update.Agreement != currReg.Agreement &&
 | 
						|
		update.Agreement != wfe.SubscriberAgreementURL {
 | 
						|
		msg := fmt.Sprintf("Provided agreement URL [%s] does not match current agreement URL [%s]", update.Agreement, wfe.SubscriberAgreementURL)
 | 
						|
		logEvent.AddError(msg)
 | 
						|
		wfe.sendError(response, logEvent, probs.Malformed(msg), nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// Registration objects contain a JWK object which are merged in UpdateRegistration
 | 
						|
	// if it is different from the existing registration key. Since this isn't how you
 | 
						|
	// update the key we just copy the existing one into the update object here. This
 | 
						|
	// ensures the key isn't changed and that we can cleanly serialize the update as
 | 
						|
	// JSON to send via RPC to the RA.
 | 
						|
	update.Key = currReg.Key
 | 
						|
 | 
						|
	updatedReg, err := wfe.RA.UpdateRegistration(ctx, *currReg, update)
 | 
						|
	if err != nil {
 | 
						|
		logEvent.AddError("unable to update registration: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, problemDetailsForError(err, "Unable to update registration"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	response.Header().Add("Link", link(wfe.relativeEndpoint(request, newAuthzPath), "next"))
 | 
						|
	if len(wfe.SubscriberAgreementURL) > 0 {
 | 
						|
		response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service"))
 | 
						|
	}
 | 
						|
 | 
						|
	err = wfe.writeJsonResponse(response, logEvent, http.StatusAccepted, updatedReg)
 | 
						|
	if err != nil {
 | 
						|
		// ServerInternal because we just generated the reg, it should be OK
 | 
						|
		logEvent.AddError("unable to marshal updated registration: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.ServerInternal("Failed to marshal registration"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (wfe *WebFrontEndImpl) deactivateAuthorization(
 | 
						|
	ctx context.Context,
 | 
						|
	authz *core.Authorization,
 | 
						|
	logEvent *requestEvent,
 | 
						|
	response http.ResponseWriter,
 | 
						|
	request *http.Request) bool {
 | 
						|
	body, reg, prob := wfe.validPOSTForAccount(request, ctx, logEvent)
 | 
						|
	addRequesterHeader(response, logEvent.Requester)
 | 
						|
	if prob != nil {
 | 
						|
		wfe.sendError(response, logEvent, prob, nil)
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	if reg.ID != authz.RegistrationID {
 | 
						|
		logEvent.AddError("registration ID doesn't match ID for authorization")
 | 
						|
		wfe.sendError(response, logEvent, probs.Unauthorized("Registration ID doesn't match ID for authorization"), nil)
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	var req struct {
 | 
						|
		Status core.AcmeStatus
 | 
						|
	}
 | 
						|
	err := json.Unmarshal(body, &req)
 | 
						|
	if err != nil {
 | 
						|
		wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling JSON"), err)
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	if req.Status != core.StatusDeactivated {
 | 
						|
		logEvent.AddError("invalid status value")
 | 
						|
		wfe.sendError(response, logEvent, probs.Malformed("Invalid status value"), err)
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	err = wfe.RA.DeactivateAuthorization(ctx, *authz)
 | 
						|
	if err != nil {
 | 
						|
		logEvent.AddError("unable to deactivate authorization", err)
 | 
						|
		wfe.sendError(response, logEvent, problemDetailsForError(err, "Error deactivating authorization"), err)
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	// Since the authorization passed to DeactivateAuthorization isn't
 | 
						|
	// mutated locally by the function we must manually set the status
 | 
						|
	// here before displaying the authorization to the user
 | 
						|
	authz.Status = core.StatusDeactivated
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
// Authorization is used by clients to submit an update to one of their
 | 
						|
// authorizations.
 | 
						|
func (wfe *WebFrontEndImpl) Authorization(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
 | 
						|
	// Requests to this handler should have a path that leads to a known authz
 | 
						|
	id := request.URL.Path
 | 
						|
	authz, err := wfe.SA.GetAuthorization(ctx, id)
 | 
						|
	if err != nil {
 | 
						|
		logEvent.AddError("No such authorization at id %s", id)
 | 
						|
		// TODO(#1199): handle db errors
 | 
						|
		wfe.sendError(response, logEvent, probs.NotFound("Unable to find authorization"), err)
 | 
						|
		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
 | 
						|
 | 
						|
	// After expiring, authorizations are inaccessible
 | 
						|
	if authz.Expires == nil || authz.Expires.Before(wfe.clk.Now()) {
 | 
						|
		msg := fmt.Sprintf("Authorization %v expired in the past (%v)", authz.ID, *authz.Expires)
 | 
						|
		logEvent.AddError(msg)
 | 
						|
		wfe.sendError(response, logEvent, probs.NotFound("Expired authorization"), nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if wfe.AllowAuthzDeactivation && request.Method == "POST" {
 | 
						|
		// If the deactivation fails return early as errors and return codes
 | 
						|
		// have already been set. Otherwise continue so that the user gets
 | 
						|
		// sent the deactivated authorization.
 | 
						|
		if !wfe.deactivateAuthorization(ctx, &authz, logEvent, response, request) {
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	wfe.prepAuthorizationForDisplay(request, &authz)
 | 
						|
 | 
						|
	response.Header().Add("Link", link(wfe.relativeEndpoint(request, newCertPath), "next"))
 | 
						|
 | 
						|
	err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, authz)
 | 
						|
	if err != nil {
 | 
						|
		// InternalServerError because this is a failure to decode from our DB.
 | 
						|
		logEvent.AddError("Failed to JSON marshal authz: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.ServerInternal("Failed to JSON marshal authz"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
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(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
 | 
						|
 | 
						|
	serial := request.URL.Path
 | 
						|
	// Certificate paths consist of the CertBase path, plus exactly sixteen hex
 | 
						|
	// digits.
 | 
						|
	if !core.ValidSerial(serial) {
 | 
						|
		logEvent.AddError("certificate serial provided was not valid: %s", serial)
 | 
						|
		wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	logEvent.Extra["RequestedSerial"] = serial
 | 
						|
 | 
						|
	cert, err := wfe.SA.GetCertificate(ctx, serial)
 | 
						|
	// TODO(#991): handle db errors
 | 
						|
	if err != nil {
 | 
						|
		logEvent.AddError("unable to get certificate by serial id %#v: %s", serial, err)
 | 
						|
		if strings.HasPrefix(err.Error(), "gorp: multiple rows returned") {
 | 
						|
			wfe.sendError(response, logEvent, probs.Conflict("Multiple certificates with same short serial"), err)
 | 
						|
		} else {
 | 
						|
			wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), err)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// TODO Content negotiation
 | 
						|
	response.Header().Set("Content-Type", "application/pkix-cert")
 | 
						|
	parsedCertificate, err := x509.ParseCertificate([]byte(cert.DER))
 | 
						|
	if err != nil {
 | 
						|
		logEvent.AddError("unable to parse certificate: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.ServerInternal("Unable to parse certificate"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if err = wfe.addIssuingCertificateURLs(response, parsedCertificate.IssuingCertificateURL); err != nil {
 | 
						|
		logEvent.AddError("unable to parse IssuingCertificateURL: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.ServerInternal("unable to parse IssuingCertificateURL"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	response.WriteHeader(http.StatusOK)
 | 
						|
	if _, err = response.Write(cert.DER); err != nil {
 | 
						|
		logEvent.AddError(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(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
 | 
						|
	http.Redirect(response, request, wfe.SubscriberAgreementURL, http.StatusFound)
 | 
						|
}
 | 
						|
 | 
						|
// Issuer obtains the issuer certificate used by this instance of Boulder.
 | 
						|
func (wfe *WebFrontEndImpl) Issuer(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
 | 
						|
	// TODO Content negotiation
 | 
						|
	response.Header().Set("Content-Type", "application/pkix-cert")
 | 
						|
	response.WriteHeader(http.StatusOK)
 | 
						|
	if _, err := response.Write(wfe.IssuerCert); err != nil {
 | 
						|
		logEvent.AddError("unable to write issuer certificate response: %s", err)
 | 
						|
		wfe.log.Warning(fmt.Sprintf("Could not write response: %s", err))
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// BuildID tells the requestor what build we're running.
 | 
						|
func (wfe *WebFrontEndImpl) BuildID(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
 | 
						|
	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.AddError("unable to print build information: %s", err)
 | 
						|
		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")
 | 
						|
}
 | 
						|
 | 
						|
// KeyRollover allows a user to change their signing key
 | 
						|
func (wfe *WebFrontEndImpl) KeyRollover(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
 | 
						|
	// KeyRollover is a NOP for WFEv2 at the present time. It needs unique JWS
 | 
						|
	// validation compared to other ACME v2 endpoints.
 | 
						|
	wfe.sendError(response, logEvent,
 | 
						|
		probs.ServerInternal("KeyRollover is not presently implemented for ACME v2"), nil)
 | 
						|
	return
 | 
						|
}
 | 
						|
 | 
						|
func (wfe *WebFrontEndImpl) deactivateRegistration(ctx context.Context, reg core.Registration, response http.ResponseWriter, request *http.Request, logEvent *requestEvent) {
 | 
						|
	err := wfe.RA.DeactivateRegistration(ctx, reg)
 | 
						|
	if err != nil {
 | 
						|
		logEvent.AddError("unable to deactivate registration", err)
 | 
						|
		wfe.sendError(response, logEvent, problemDetailsForError(err, "Error deactivating registration"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	reg.Status = core.StatusDeactivated
 | 
						|
 | 
						|
	err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, reg)
 | 
						|
	if err != nil {
 | 
						|
		// ServerInternal because registration is from DB and should be fine
 | 
						|
		logEvent.AddError("unable to marshal updated registration: %s", err)
 | 
						|
		wfe.sendError(response, logEvent, probs.ServerInternal("Failed to marshal registration"), err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// addIssuingCertificateURLs() adds Issuing Certificate URLs (AIA) from a
 | 
						|
// X.509 certificate to the HTTP response. If the IssuingCertificateURL
 | 
						|
// in a certificate is not https://, it will be upgraded to https://
 | 
						|
func (wfe *WebFrontEndImpl) addIssuingCertificateURLs(response http.ResponseWriter, issuingCertificateURL []string) error {
 | 
						|
	for _, rawURL := range issuingCertificateURL {
 | 
						|
		parsedURI, err := url.ParseRequestURI(rawURL)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		parsedURI.Scheme = "https"
 | 
						|
		response.Header().Add("Link", link(parsedURI.String(), "up"))
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 |