use ProblemDetails inside of wfe

This uses ProblemDetails throughout the wfe. This is the last step in
allowing the backend services to pass ProblemDetails from RPCs through
to the user.

Updates #1153.

Fixes #1161.
This commit is contained in:
Jeff Hodges 2015-11-23 13:58:10 -08:00
parent a63a88f7a7
commit 6b0e53b8e0
7 changed files with 352 additions and 263 deletions

View File

@ -28,6 +28,7 @@ import (
"io/ioutil" "io/ioutil"
"math/big" "math/big"
mrand "math/rand" mrand "math/rand"
"net/http"
"net/url" "net/url"
"regexp" "regexp"
"strings" "strings"
@ -35,6 +36,7 @@ import (
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose" jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/probs"
) )
// Package Variables Variables // Package Variables Variables
@ -123,6 +125,51 @@ func (e TooManyRPCRequestsError) Error() string { return string(e) }
func (e ServiceUnavailableError) Error() string { return string(e) } func (e ServiceUnavailableError) Error() string { return string(e) }
func (e BadNonceError) Error() string { return string(e) } func (e BadNonceError) Error() string { return string(e) }
// statusTooManyRequests is the HTTP status code meant for rate limiting
// errors. It's not currently in the net/http library so we add it here.
const statusTooManyRequests = 429
// ProblemDetailsForError turns an error into a ProblemDetails with the special
// case of returning the same error back if its already a ProblemDetails. If the
// error is of an type unknown to ProblemDetailsForError, it will return a
// ServerInternal ProblemDetails.
func ProblemDetailsForError(err error, msg string) *probs.ProblemDetails {
switch e := err.(type) {
case *probs.ProblemDetails:
return e
case MalformedRequestError, SyntaxError:
return probs.Malformed(fmt.Sprintf("%s :: %s", msg, err))
case NotSupportedError:
return &probs.ProblemDetails{
Type: probs.ServerInternalProblem,
Detail: fmt.Sprintf("%s :: %s", msg, err),
HTTPStatus: http.StatusNotImplemented,
}
case UnauthorizedError:
return probs.Unauthorized(fmt.Sprintf("%s :: %s", msg, err))
case NotFoundError:
return probs.NotFound(fmt.Sprintf("%s :: %s", msg, err))
case LengthRequiredError:
prob := probs.Malformed("missing Content-Length header")
prob.HTTPStatus = http.StatusLengthRequired
return prob
case SignatureValidationError:
return probs.Malformed(fmt.Sprintf("%s :: %s", msg, err))
case RateLimitedError:
return &probs.ProblemDetails{
Type: probs.RateLimitedProblem,
Detail: fmt.Sprintf("%s :: %s", msg, err),
HTTPStatus: statusTooManyRequests,
}
case BadNonceError:
return probs.BadNonce(fmt.Sprintf("%s :: %s", msg, err))
default:
// Internal server error messages may include sensitive data, so we do
// not include it.
return probs.ServerInternal(msg)
}
}
// Base64 functions // Base64 functions
func pad(x string) string { func pad(x string) string {

View File

@ -11,10 +11,12 @@ import (
"math" "math"
"math/big" "math/big"
"net/url" "net/url"
"reflect"
"sort" "sort"
"testing" "testing"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose" "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/test" "github.com/letsencrypt/boulder/test"
) )
@ -128,3 +130,39 @@ func TestUnmarshalAcmeURL(t *testing.T) {
t.Errorf("Expected error parsing ':', but got nil err.") t.Errorf("Expected error parsing ':', but got nil err.")
} }
} }
func TestProblemDetailsFromError(t *testing.T) {
testCases := []struct {
err error
statusCode int
problem probs.ProblemType
}{
{InternalServerError("foo"), 500, probs.ServerInternalProblem},
{NotSupportedError("foo"), 501, probs.ServerInternalProblem},
{MalformedRequestError("foo"), 400, probs.MalformedProblem},
{UnauthorizedError("foo"), 403, probs.UnauthorizedProblem},
{NotFoundError("foo"), 404, probs.MalformedProblem},
{SyntaxError("foo"), 400, probs.MalformedProblem},
{SignatureValidationError("foo"), 400, probs.MalformedProblem},
{RateLimitedError("foo"), 429, probs.RateLimitedProblem},
{LengthRequiredError("foo"), 411, probs.MalformedProblem},
{BadNonceError("foo"), 400, probs.BadNonceProblem},
}
for _, c := range testCases {
p := ProblemDetailsForError(c.err, "k")
if p.HTTPStatus != c.statusCode {
t.Errorf("Incorrect status code for %s. Expected %d, got %d", reflect.TypeOf(c.err).Name(), c.statusCode, p.HTTPStatus)
}
if probs.ProblemType(p.Type) != c.problem {
t.Errorf("Expected problem urn %#v, got %#v", c.problem, p.Type)
}
}
expected := &probs.ProblemDetails{
Type: probs.MalformedProblem,
HTTPStatus: 200,
Detail: "gotcha",
}
p := ProblemDetailsForError(expected, "k")
test.AssertDeepEquals(t, expected, p)
}

View File

@ -1,6 +1,9 @@
package probs package probs
import "fmt" import (
"fmt"
"net/http"
)
// Error types that can be used in ACME payloads // Error types that can be used in ACME payloads
const ( const (
@ -20,11 +23,121 @@ type ProblemType string
// ProblemDetails objects represent problem documents // ProblemDetails objects represent problem documents
// https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00 // https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
type ProblemDetails struct { type ProblemDetails struct {
Type ProblemType `json:"type,omitempty"` Type ProblemType `json:"type,omitempty"`
Detail string `json:"detail,omitempty"` Detail string `json:"detail,omitempty"`
HTTPStatus int `json:"status,omitempty"` // HTTPStatus is the HTTP status code the ProblemDetails should probably be sent
// as.
HTTPStatus int `json:"status,omitempty"`
} }
func (pd *ProblemDetails) Error() string { func (pd *ProblemDetails) Error() string {
return fmt.Sprintf("%s :: %s", pd.Type, pd.Detail) return fmt.Sprintf("%s :: %s", pd.Type, pd.Detail)
} }
// statusTooManyRequests is the HTTP status code meant for rate limiting
// errors. It's not currently in the net/http library so we add it here.
const statusTooManyRequests = 429
// ProblemDetailsToStatusCode inspects the given ProblemDetails to figure out
// what HTTP status code it should represent. It should only be used by the WFE
// but is included in this package because of its reliance on ProblemTypes.
func ProblemDetailsToStatusCode(prob *ProblemDetails) int {
if prob.HTTPStatus != 0 {
return prob.HTTPStatus
}
switch prob.Type {
case ConnectionProblem, MalformedProblem, TLSProblem, UnknownHostProblem, BadNonceProblem:
return http.StatusBadRequest
case ServerInternalProblem:
return http.StatusInternalServerError
case UnauthorizedProblem:
return http.StatusForbidden
case RateLimitedProblem:
return statusTooManyRequests
default:
return http.StatusInternalServerError
}
}
// BadNonce returns a ProblemDetails with a BadNonceProblem and a 400 Bad
// Request status code.
func BadNonce(detail string) *ProblemDetails {
return &ProblemDetails{
Type: BadNonceProblem,
Detail: detail,
HTTPStatus: http.StatusBadRequest,
}
}
// Conflict returns a ProblemDetails with a MalformedProblem and a 409 Conflict
// status code.
func Conflict(detail string) *ProblemDetails {
return &ProblemDetails{
Type: MalformedProblem,
Detail: detail,
HTTPStatus: http.StatusConflict,
}
}
// Malformed returns a ProblemDetails with a MalformedProblem and a 400 Bad
// Request status code.
func Malformed(detail string, args ...interface{}) *ProblemDetails {
if len(args) > 0 {
detail = fmt.Sprintf(detail, args...)
}
return &ProblemDetails{
Type: MalformedProblem,
Detail: detail,
HTTPStatus: http.StatusBadRequest,
}
}
// NotFound returns a ProblemDetails with a MalformedProblem and a 404 Not Found
// status code.
func NotFound(detail string) *ProblemDetails {
return &ProblemDetails{
Type: MalformedProblem,
Detail: detail,
HTTPStatus: http.StatusNotFound,
}
}
// ServerInternal returns a ProblemDetails with a ServerInternalProblem and a
// 500 Internal Server Failure status code.
func ServerInternal(detail string) *ProblemDetails {
return &ProblemDetails{
Type: ServerInternalProblem,
Detail: detail,
HTTPStatus: http.StatusInternalServerError,
}
}
// Unauthorized returns a ProblemDetails with an UnauthorizedProblem and a 403
// Forbidden status code.
func Unauthorized(detail string) *ProblemDetails {
return &ProblemDetails{
Type: UnauthorizedProblem,
Detail: detail,
HTTPStatus: http.StatusForbidden,
}
}
// MethodNotAllowed returns a ProblemDetails representing a disallowed HTTP
// method error.
func MethodNotAllowed() *ProblemDetails {
return &ProblemDetails{
Type: MalformedProblem,
Detail: "Method not allowed",
HTTPStatus: http.StatusMethodNotAllowed,
}
}
// ContentLengthRequired returns a ProblemDetails representing a missing
// Content-Length header error
func ContentLengthRequired() *ProblemDetails {
return &ProblemDetails{
Type: MalformedProblem,
Detail: "missing Content-Length header",
HTTPStatus: http.StatusLengthRequired,
}
}

View File

@ -8,7 +8,9 @@ import (
func TestProblemDetails(t *testing.T) { func TestProblemDetails(t *testing.T) {
pd := &ProblemDetails{ pd := &ProblemDetails{
Type: MalformedProblem, Type: MalformedProblem,
Detail: "Wat? o.O"} Detail: "Wat? o.O",
HTTPStatus: 403,
}
test.AssertEquals(t, pd.Error(), "urn:acme:error:malformed :: Wat? o.O") test.AssertEquals(t, pd.Error(), "urn:acme:error:malformed :: Wat? o.O")
} }

View File

@ -10,7 +10,7 @@ import (
func TestRejectsNone(t *testing.T) { func TestRejectsNone(t *testing.T) {
wfe, _ := setupWFE(t) wfe, _ := setupWFE(t)
_, _, _, err := wfe.verifyPOST(newRequestEvent(), makePostRequest(` _, _, _, prob := wfe.verifyPOST(newRequestEvent(), makePostRequest(`
{ {
"header": { "header": {
"alg": "none", "alg": "none",
@ -24,17 +24,17 @@ func TestRejectsNone(t *testing.T) {
"signature": "" "signature": ""
} }
`), true, "foo") `), true, "foo")
if err == nil { if prob == nil {
t.Fatalf("verifyPOST did not reject JWS with alg: 'none'") t.Fatalf("verifyPOST did not reject JWS with alg: 'none'")
} }
if err.Error() != "algorithm 'none' in JWS header not acceptable" { if prob.Detail != "algorithm 'none' in JWS header not acceptable" {
t.Fatalf("verifyPOST rejected JWS with alg: 'none', but for wrong reason: %s", err) t.Fatalf("verifyPOST rejected JWS with alg: 'none', but for wrong reason: %#v", prob)
} }
} }
func TestRejectsHS256(t *testing.T) { func TestRejectsHS256(t *testing.T) {
wfe, _ := setupWFE(t) wfe, _ := setupWFE(t)
_, _, _, err := wfe.verifyPOST(newRequestEvent(), makePostRequest(` _, _, _, prob := wfe.verifyPOST(newRequestEvent(), makePostRequest(`
{ {
"header": { "header": {
"alg": "HS256", "alg": "HS256",
@ -48,12 +48,12 @@ func TestRejectsHS256(t *testing.T) {
"signature": "" "signature": ""
} }
`), true, "foo") `), true, "foo")
if err == nil { if prob == nil {
t.Fatalf("verifyPOST did not reject JWS with alg: 'HS256'") t.Fatalf("verifyPOST did not reject JWS with alg: 'HS256'")
} }
expected := "algorithm 'HS256' in JWS header not acceptable" expected := "algorithm 'HS256' in JWS header not acceptable"
if err.Error() != expected { if prob.Detail != expected {
t.Fatalf("verifyPOST rejected JWS with alg: 'none', but for wrong reason: got '%s', wanted %s", err, expected) t.Fatalf("verifyPOST rejected JWS with alg: 'none', but for wrong reason: got '%s', wanted %s", prob, expected)
} }
} }

View File

@ -40,12 +40,6 @@ const (
TermsPath = "/terms" TermsPath = "/terms"
IssuerPath = "/acme/issuer-cert" IssuerPath = "/acme/issuer-cert"
BuildIDPath = "/build" BuildIDPath = "/build"
// StatusRateLimited is not in net/http
StatusRateLimited = 429
// statusBadNonce is used as a sentinel value to force sendError to send
// the proper error type
statusBadNonce = 1400
) )
// WebFrontEndImpl provides all the logic for Boulder's web-facing interface, // WebFrontEndImpl provides all the logic for Boulder's web-facing interface,
@ -95,34 +89,6 @@ type WebFrontEndImpl struct {
ShutdownKillTimeout time.Duration ShutdownKillTimeout time.Duration
} }
func statusCodeFromError(err error) int {
// Populate these as needed. We probably should trim the error list in util.go
switch err.(type) {
case core.MalformedRequestError:
return http.StatusBadRequest
case core.NotSupportedError:
return http.StatusNotImplemented
case core.SyntaxError:
return http.StatusBadRequest
case core.UnauthorizedError:
return http.StatusForbidden
case core.NotFoundError:
return http.StatusNotFound
case core.LengthRequiredError:
return http.StatusLengthRequired
case core.SignatureValidationError:
return http.StatusBadRequest
case core.InternalServerError:
return http.StatusInternalServerError
case core.RateLimitedError:
return StatusRateLimited
case core.BadNonceError:
return statusBadNonce
default:
return http.StatusInternalServerError
}
}
// NewWebFrontEndImpl constructs a web service for Boulder // NewWebFrontEndImpl constructs a web service for Boulder
func NewWebFrontEndImpl(stats statsd.Statter, clk clock.Clock) (WebFrontEndImpl, error) { func NewWebFrontEndImpl(stats statsd.Statter, clk clock.Clock) (WebFrontEndImpl, error) {
logger := blog.GetAuditLogger() logger := blog.GetAuditLogger()
@ -201,9 +167,8 @@ func (wfe *WebFrontEndImpl) HandleFunc(mux *http.ServeMux, pattern string, h wfe
} }
if !methodsMap[request.Method] { if !methodsMap[request.Method] {
msg := "Method not allowed"
response.Header().Set("Allow", methodsStr) response.Header().Set("Allow", methodsStr)
wfe.sendError(response, logEvent, msg, nil, http.StatusMethodNotAllowed) wfe.sendError(response, logEvent, probs.MethodNotAllowed(), nil)
return return
} }
@ -318,8 +283,7 @@ func parseIDFromPath(path string) string {
} }
const ( const (
unknownKey = "No registration exists matching provided key" unknownKey = "No registration exists matching provided key"
malformedJWS = "Unable to read/verify body"
) )
// verifyPOST reads and parses the request body, looks up the Registration // verifyPOST reads and parses the request body, looks up the Registration
@ -334,44 +298,39 @@ const (
// the key itself. verifyPOST also appends its errors to requestEvent.Errors so // the key itself. verifyPOST also appends its errors to requestEvent.Errors so
// code calling it does not need to if they imediately return a response to the // code calling it does not need to if they imediately return a response to the
// user. // user.
func (wfe *WebFrontEndImpl) verifyPOST(logEvent *requestEvent, request *http.Request, regCheck bool, resource core.AcmeResource) ([]byte, *jose.JsonWebKey, core.Registration, error) { func (wfe *WebFrontEndImpl) verifyPOST(logEvent *requestEvent, request *http.Request, regCheck bool, resource core.AcmeResource) ([]byte, *jose.JsonWebKey, core.Registration, *probs.ProblemDetails) {
var err error
// TODO: We should return a pointer to a registration, which can be nil, // TODO: We should return a pointer to a registration, which can be nil,
// rather the a registration value with a sentinel value. // rather the a registration value with a sentinel value.
// https://github.com/letsencrypt/boulder/issues/877 // https://github.com/letsencrypt/boulder/issues/877
reg := core.Registration{ID: 0} reg := core.Registration{ID: 0}
if _, ok := request.Header["Content-Length"]; !ok { if _, ok := request.Header["Content-Length"]; !ok {
err = core.LengthRequiredError("Content-Length header is required for POST.")
wfe.stats.Inc("WFE.HTTP.ClientErrors.LengthRequiredError", 1, 1.0) wfe.stats.Inc("WFE.HTTP.ClientErrors.LengthRequiredError", 1, 1.0)
logEvent.AddError("missing Content-Length header on POST") logEvent.AddError("missing Content-Length header on POST")
return nil, nil, reg, err return nil, nil, reg, probs.ContentLengthRequired()
} }
// Read body // Read body
if request.Body == nil { if request.Body == nil {
err = core.MalformedRequestError("No body on POST")
wfe.stats.Inc("WFE.Errors.NoPOSTBody", 1, 1.0) wfe.stats.Inc("WFE.Errors.NoPOSTBody", 1, 1.0)
logEvent.AddError("no body on POST") logEvent.AddError("no body on POST")
return nil, nil, reg, err return nil, nil, reg, probs.Malformed("No body on POST")
} }
bodyBytes, err := ioutil.ReadAll(request.Body) bodyBytes, err := ioutil.ReadAll(request.Body)
if err != nil { if err != nil {
err = core.InternalServerError("unable to read request body")
wfe.stats.Inc("WFE.Errors.UnableToReadRequestBody", 1, 1.0) wfe.stats.Inc("WFE.Errors.UnableToReadRequestBody", 1, 1.0)
logEvent.AddError("unable to read request body") logEvent.AddError("unable to read request body")
return nil, nil, reg, err return nil, nil, reg, probs.ServerInternal("unable to read request body")
} }
body := string(bodyBytes) body := string(bodyBytes)
// Parse as JWS // Parse as JWS
parsedJws, err := jose.ParseSigned(body) parsedJws, err := jose.ParseSigned(body)
if err != nil { if err != nil {
puberr := core.SignatureValidationError("Parse error reading JWS")
wfe.stats.Inc("WFE.Errors.UnableToParseJWS", 1, 1.0) wfe.stats.Inc("WFE.Errors.UnableToParseJWS", 1, 1.0)
logEvent.AddError("could not JSON parse body into JWS: %s", err) logEvent.AddError("could not JSON parse body into JWS: %s", err)
return nil, nil, reg, puberr return nil, nil, reg, probs.Malformed("Parse error reading JWS")
} }
// Verify JWS // Verify JWS
@ -381,24 +340,21 @@ func (wfe *WebFrontEndImpl) verifyPOST(logEvent *requestEvent, request *http.Req
// *anyway*, so it could always lie about what key was used by faking // *anyway*, so it could always lie about what key was used by faking
// the signature itself. // the signature itself.
if len(parsedJws.Signatures) > 1 { if len(parsedJws.Signatures) > 1 {
err = core.SignatureValidationError("Too many signatures in POST body")
wfe.stats.Inc("WFE.Errors.TooManyJWSSignaturesInPOST", 1, 1.0) wfe.stats.Inc("WFE.Errors.TooManyJWSSignaturesInPOST", 1, 1.0)
logEvent.AddError("too many signatures in POST body: %d", len(parsedJws.Signatures)) logEvent.AddError("too many signatures in POST body: %d", len(parsedJws.Signatures))
return nil, nil, reg, err return nil, nil, reg, probs.Malformed("Too many signatures in POST body")
} }
if len(parsedJws.Signatures) == 0 { if len(parsedJws.Signatures) == 0 {
err = core.SignatureValidationError("POST JWS not signed")
wfe.stats.Inc("WFE.Errors.JWSNotSignedInPOST", 1, 1.0) wfe.stats.Inc("WFE.Errors.JWSNotSignedInPOST", 1, 1.0)
logEvent.AddError("no signatures in POST body") logEvent.AddError("no signatures in POST body")
return nil, nil, reg, err return nil, nil, reg, probs.Malformed("POST JWS not signed")
} }
submittedKey := parsedJws.Signatures[0].Header.JsonWebKey submittedKey := parsedJws.Signatures[0].Header.JsonWebKey
if submittedKey == nil { if submittedKey == nil {
err = core.SignatureValidationError("No JWK in JWS header")
wfe.stats.Inc("WFE.Errors.NoJWKInJWSSignatureHeader", 1, 1.0) wfe.stats.Inc("WFE.Errors.NoJWKInJWSSignatureHeader", 1, 1.0)
logEvent.AddError("no JWK in JWS signature header in POST body") logEvent.AddError("no JWK in JWS signature header in POST body")
return nil, nil, reg, err return nil, nil, reg, probs.Malformed("No JWK in JWS header")
} }
var key *jose.JsonWebKey var key *jose.JsonWebKey
@ -413,14 +369,18 @@ func (wfe *WebFrontEndImpl) verifyPOST(logEvent *requestEvent, request *http.Req
if err = core.GoodKey(submittedKey.Key); err != nil { if err = core.GoodKey(submittedKey.Key); err != nil {
wfe.stats.Inc("WFE.Errors.JWKRejectedByGoodKey", 1, 1.0) wfe.stats.Inc("WFE.Errors.JWKRejectedByGoodKey", 1, 1.0)
logEvent.AddError("JWK in request was rejected by GoodKey: %s", err) logEvent.AddError("JWK in request was rejected by GoodKey: %s", err)
return nil, nil, reg, err return nil, nil, reg, probs.Malformed(err.Error())
} }
key = submittedKey key = submittedKey
} else if err != nil { } else if err != nil {
// For all other errors, or if regCheck is true, return error immediately. // For all other errors, or if regCheck is true, return error immediately.
wfe.stats.Inc("WFE.Errors.UnableToGetRegistrationByKey", 1, 1.0) wfe.stats.Inc("WFE.Errors.UnableToGetRegistrationByKey", 1, 1.0)
logEvent.AddError("unable to fetch registration by the given JWK: %s", err) logEvent.AddError("unable to fetch registration by the given JWK: %s", err)
return nil, nil, reg, err if _, ok := err.(core.NoSuchRegistrationError); ok {
return nil, nil, reg, probs.Unauthorized(unknownKey)
}
return nil, nil, reg, core.ProblemDetailsForError(err, "")
} else { } else {
// If the lookup was successful, use that key. // If the lookup was successful, use that key.
key = &reg.Key key = &reg.Key
@ -430,19 +390,18 @@ func (wfe *WebFrontEndImpl) verifyPOST(logEvent *requestEvent, request *http.Req
if statName, err := checkAlgorithm(key, parsedJws); err != nil { if statName, err := checkAlgorithm(key, parsedJws); err != nil {
wfe.stats.Inc(statName, 1, 1.0) wfe.stats.Inc(statName, 1, 1.0)
return nil, nil, reg, err return nil, nil, reg, probs.Malformed(err.Error())
} }
payload, err := parsedJws.Verify(key) payload, err := parsedJws.Verify(key)
if err != nil { if err != nil {
puberr := core.SignatureValidationError("JWS verification error")
wfe.stats.Inc("WFE.Errors.JWSVerificationFailed", 1, 1.0) wfe.stats.Inc("WFE.Errors.JWSVerificationFailed", 1, 1.0)
n := len(body) n := len(body)
if n > 100 { if n > 100 {
n = 100 n = 100
} }
logEvent.AddError("verification of JWS with the JWK failed: %v; body: %s", err, body[:n]) logEvent.AddError("verification of JWS with the JWK failed: %v; body: %s", err, body[:n])
return nil, nil, reg, puberr return nil, nil, reg, probs.Malformed("JWS verification error")
} }
// Check that the request has a known anti-replay nonce // Check that the request has a known anti-replay nonce
@ -450,13 +409,11 @@ func (wfe *WebFrontEndImpl) verifyPOST(logEvent *requestEvent, request *http.Req
if err != nil || len(nonce) == 0 { if err != nil || len(nonce) == 0 {
wfe.stats.Inc("WFE.Errors.JWSMissingNonce", 1, 1.0) wfe.stats.Inc("WFE.Errors.JWSMissingNonce", 1, 1.0)
logEvent.AddError("JWS is missing an anti-replay nonce") logEvent.AddError("JWS is missing an anti-replay nonce")
err = core.BadNonceError("JWS has no anti-replay nonce") return nil, nil, reg, probs.BadNonce("JWS has no anti-replay nonce")
return nil, nil, reg, err
} else if !wfe.nonceService.Valid(nonce) { } else if !wfe.nonceService.Valid(nonce) {
wfe.stats.Inc("WFE.Errors.JWSInvalidNonce", 1, 1.0) wfe.stats.Inc("WFE.Errors.JWSInvalidNonce", 1, 1.0)
logEvent.AddError("JWS has an invalid anti-replay nonce") logEvent.AddError("JWS has an invalid anti-replay nonce")
err = core.BadNonceError(fmt.Sprintf("JWS has invalid anti-replay nonce")) return nil, nil, reg, probs.BadNonce("JWS has invalid anti-replay nonce")
return nil, nil, reg, err
} }
// Check that the "resource" field is present and has the correct value // Check that the "resource" field is present and has the correct value
@ -467,69 +424,41 @@ func (wfe *WebFrontEndImpl) verifyPOST(logEvent *requestEvent, request *http.Req
if err != nil { if err != nil {
wfe.stats.Inc("WFE.Errors.UnparsableJWSPayload", 1, 1.0) wfe.stats.Inc("WFE.Errors.UnparsableJWSPayload", 1, 1.0)
logEvent.AddError("unable to JSON parse resource from JWS payload: %s", err) logEvent.AddError("unable to JSON parse resource from JWS payload: %s", err)
puberr := core.SignatureValidationError("Request payload did not parse as JSON") return nil, nil, reg, probs.Malformed("Request payload did not parse as JSON")
return nil, nil, reg, puberr
} }
if parsedRequest.Resource == "" { if parsedRequest.Resource == "" {
wfe.stats.Inc("WFE.Errors.NoResourceInJWSPayload", 1, 1.0) wfe.stats.Inc("WFE.Errors.NoResourceInJWSPayload", 1, 1.0)
logEvent.AddError("JWS request payload does not specifiy a resource") logEvent.AddError("JWS request payload does not specify a resource")
err = core.MalformedRequestError("Request payload does not specify a resource") return nil, nil, reg, probs.Malformed("Request payload does not specify a resource")
return nil, nil, reg, err
} else if resource != core.AcmeResource(parsedRequest.Resource) { } else if resource != core.AcmeResource(parsedRequest.Resource) {
wfe.stats.Inc("WFE.Errors.MismatchedResourceInJWSPayload", 1, 1.0) wfe.stats.Inc("WFE.Errors.MismatchedResourceInJWSPayload", 1, 1.0)
logEvent.AddError("JWS request payload does not match resource") logEvent.AddError("JWS request payload does not match resource")
err = core.MalformedRequestError(fmt.Sprintf("JWS resource payload does not match the HTTP resource: %s != %s", parsedRequest.Resource, resource)) return nil, nil, reg, probs.Malformed("JWS resource payload does not match the HTTP resource: %s != %s", parsedRequest.Resource, resource)
return nil, nil, reg, err
} }
return []byte(payload), key, reg, nil return []byte(payload), key, reg, nil
} }
// Notify the client of an error condition and log it for audit purposes. // sendError sends an error response represented by the given ProblemDetails,
func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, logEvent *requestEvent, msg string, detail error, code int) { // and, if the ProblemDetails.Type is ServerInternalProblem, audit logs the
problem := probs.ProblemDetails{Detail: msg, HTTPStatus: code} // internal ierr.
switch code { func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, logEvent *requestEvent, prob *probs.ProblemDetails, ierr error) {
case http.StatusPreconditionFailed: code := probs.ProblemDetailsToStatusCode(prob)
fallthrough
case http.StatusForbidden:
problem.Type = probs.UnauthorizedProblem
case http.StatusConflict:
fallthrough
case http.StatusMethodNotAllowed:
fallthrough
case http.StatusNotFound:
fallthrough
case http.StatusBadRequest:
fallthrough
case http.StatusLengthRequired:
problem.Type = probs.MalformedProblem
case StatusRateLimited:
problem.Type = probs.RateLimitedProblem
case statusBadNonce:
problem.Type = probs.BadNonceProblem
problem.HTTPStatus = http.StatusBadRequest
code = http.StatusBadRequest
default: // Either http.StatusInternalServerError or an unexpected code
problem.Type = probs.ServerInternalProblem
}
// Record details to the log event // Record details to the log event
logEvent.AddError(msg) logEvent.AddError(fmt.Sprintf("%d :: %s :: %s", prob.HTTPStatus, prob.Type, prob.Detail))
// Only audit log internal errors so users cannot purposefully cause // Only audit log internal errors so users cannot purposefully cause
// auditable events. // auditable events.
if problem.Type == probs.ServerInternalProblem { if prob.Type == probs.ServerInternalProblem {
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3 // AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
wfe.log.Audit(fmt.Sprintf("Internal error - %s - %s", msg, detail)) wfe.log.Audit(fmt.Sprintf("Internal error - %s - %s", prob.Detail, ierr))
} else if statusCodeFromError(detail) != http.StatusInternalServerError {
// If not an internal error and problem is a custom error type
problem.Detail += fmt.Sprintf(" :: %s", detail)
} }
problemDoc, err := json.Marshal(problem) problemDoc, err := json.Marshal(prob)
if err != nil { if err != nil {
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3 // AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
wfe.log.Audit(fmt.Sprintf("Could not marshal error message: %s - %+v", err, problem)) wfe.log.Audit(fmt.Sprintf("Could not marshal error message: %s - %+v", err, prob))
problemDoc = []byte("{\"detail\": \"Problem marshalling error message.\"}") problemDoc = []byte("{\"detail\": \"Problem marshalling error message.\"}")
} }
@ -540,7 +469,7 @@ func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, logEvent *re
response.Write(problemDoc) response.Write(problemDoc)
wfe.stats.Inc(fmt.Sprintf("WFE.HTTP.ErrorCodes.%d", code), 1, 1.0) wfe.stats.Inc(fmt.Sprintf("WFE.HTTP.ErrorCodes.%d", code), 1, 1.0)
problemSegments := strings.Split(string(problem.Type), ":") problemSegments := strings.Split(string(prob.Type), ":")
if len(problemSegments) > 0 { if len(problemSegments) > 0 {
wfe.stats.Inc(fmt.Sprintf("WFE.HTTP.ProblemTypes.%s", problemSegments[len(problemSegments)-1]), 1, 1.0) wfe.stats.Inc(fmt.Sprintf("WFE.HTTP.ProblemTypes.%s", problemSegments[len(problemSegments)-1]), 1, 1.0)
} }
@ -553,28 +482,29 @@ func link(url, relation string) string {
// NewRegistration is used by clients to submit a new registration/account // NewRegistration is used by clients to submit a new registration/account
func (wfe *WebFrontEndImpl) NewRegistration(logEvent *requestEvent, response http.ResponseWriter, request *http.Request) { func (wfe *WebFrontEndImpl) NewRegistration(logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
body, key, _, err := wfe.verifyPOST(logEvent, request, false, core.ResourceNewReg) body, key, _, prob := wfe.verifyPOST(logEvent, request, false, core.ResourceNewReg)
if err != nil { if prob != nil {
// verifyPOST handles its own setting of logEvent.Errors // verifyPOST handles its own setting of logEvent.Errors
wfe.sendError(response, logEvent, malformedJWS, err, statusCodeFromError(err)) wfe.sendError(response, logEvent, prob, nil)
return return
} }
if existingReg, err := wfe.SA.GetRegistrationByKey(*key); err == nil { if existingReg, err := wfe.SA.GetRegistrationByKey(*key); err == nil {
response.Header().Set("Location", fmt.Sprintf("%s%d", wfe.RegBase, existingReg.ID)) response.Header().Set("Location", fmt.Sprintf("%s%d", wfe.RegBase, existingReg.ID))
wfe.sendError(response, logEvent, "Registration key is already in use", nil, http.StatusConflict) // TODO(#595): check for missing registration err
wfe.sendError(response, logEvent, probs.Conflict("Registration key is already in use"), err)
return return
} }
var init core.Registration var init core.Registration
err = json.Unmarshal(body, &init) err := json.Unmarshal(body, &init)
if err != nil { if err != nil {
wfe.sendError(response, logEvent, "Error unmarshaling JSON", err, http.StatusBadRequest) wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling JSON"), err)
return return
} }
if len(init.Agreement) > 0 && init.Agreement != wfe.SubscriberAgreementURL { 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) msg := fmt.Sprintf("Provided agreement URL [%s] does not match current agreement URL [%s]", init.Agreement, wfe.SubscriberAgreementURL)
wfe.sendError(response, logEvent, msg, nil, http.StatusBadRequest) wfe.sendError(response, logEvent, probs.Malformed(msg), nil)
return return
} }
init.Key = *key init.Key = *key
@ -585,7 +515,7 @@ func (wfe *WebFrontEndImpl) NewRegistration(logEvent *requestEvent, response htt
init.InitialIP = net.ParseIP(host) init.InitialIP = net.ParseIP(host)
} else { } else {
logEvent.AddError("Couldn't parse RemoteAddr: %s", request.RemoteAddr) logEvent.AddError("Couldn't parse RemoteAddr: %s", request.RemoteAddr)
wfe.sendError(response, logEvent, "couldn't parse the remote (that is, the client's) address", nil, http.StatusInternalServerError) wfe.sendError(response, logEvent, probs.ServerInternal("couldn't parse the remote (that is, the client's) address"), nil)
return return
} }
} }
@ -593,7 +523,7 @@ func (wfe *WebFrontEndImpl) NewRegistration(logEvent *requestEvent, response htt
reg, err := wfe.RA.NewRegistration(init) reg, err := wfe.RA.NewRegistration(init)
if err != nil { if err != nil {
logEvent.AddError("unable to create new registration: %s", err) logEvent.AddError("unable to create new registration: %s", err)
wfe.sendError(response, logEvent, "Error creating new registration", err, statusCodeFromError(err)) wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Error creating new registration"), err)
return return
} }
logEvent.Requester = reg.ID logEvent.Requester = reg.ID
@ -604,9 +534,10 @@ func (wfe *WebFrontEndImpl) NewRegistration(logEvent *requestEvent, response htt
regURL := fmt.Sprintf("%s%d", wfe.RegBase, reg.ID) regURL := fmt.Sprintf("%s%d", wfe.RegBase, reg.ID)
responseBody, err := json.Marshal(reg) responseBody, err := json.Marshal(reg)
if err != nil { if err != nil {
// StatusInternalServerError because we just created this registration, it should be OK. // ServerInternal because we just created this registration, and it
// should be OK.
logEvent.AddError("unable to marshal registration: %s", err) logEvent.AddError("unable to marshal registration: %s", err)
wfe.sendError(response, logEvent, "Error marshaling registration", err, http.StatusInternalServerError) wfe.sendError(response, logEvent, probs.ServerInternal("Error marshaling registration"), err)
return return
} }
@ -623,30 +554,24 @@ func (wfe *WebFrontEndImpl) NewRegistration(logEvent *requestEvent, response htt
// NewAuthorization is used by clients to submit a new ID Authorization // NewAuthorization is used by clients to submit a new ID Authorization
func (wfe *WebFrontEndImpl) NewAuthorization(logEvent *requestEvent, response http.ResponseWriter, request *http.Request) { func (wfe *WebFrontEndImpl) NewAuthorization(logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
body, _, currReg, err := wfe.verifyPOST(logEvent, request, true, core.ResourceNewAuthz) body, _, currReg, prob := wfe.verifyPOST(logEvent, request, true, core.ResourceNewAuthz)
if err != nil { if prob != nil {
// verifyPOST handles its own setting of logEvent.Errors // verifyPOST handles its own setting of logEvent.Errors
respMsg := malformedJWS wfe.sendError(response, logEvent, prob, nil)
respCode := statusCodeFromError(err)
if _, ok := err.(core.NoSuchRegistrationError); ok {
respMsg = unknownKey
respCode = http.StatusForbidden
}
wfe.sendError(response, logEvent, respMsg, err, respCode)
return return
} }
// Any version of the agreement is acceptable here. Version match is enforced in // Any version of the agreement is acceptable here. Version match is enforced in
// wfe.Registration when agreeing the first time. Agreement updates happen // wfe.Registration when agreeing the first time. Agreement updates happen
// by mailing subscribers and don't require a registration update. // by mailing subscribers and don't require a registration update.
if currReg.Agreement == "" { if currReg.Agreement == "" {
wfe.sendError(response, logEvent, "Must agree to subscriber agreement before any further actions", nil, http.StatusForbidden) wfe.sendError(response, logEvent, probs.Unauthorized("Must agree to subscriber agreement before any further actions"), nil)
return return
} }
var init core.Authorization var init core.Authorization
if err = json.Unmarshal(body, &init); err != nil { if err := json.Unmarshal(body, &init); err != nil {
logEvent.AddError("unable to JSON unmarshal Authorization: %s", err) logEvent.AddError("unable to JSON unmarshal Authorization: %s", err)
wfe.sendError(response, logEvent, "Error unmarshaling JSON", err, http.StatusBadRequest) wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling JSON"), err)
return return
} }
logEvent.Extra["Identifier"] = init.Identifier logEvent.Extra["Identifier"] = init.Identifier
@ -655,7 +580,7 @@ func (wfe *WebFrontEndImpl) NewAuthorization(logEvent *requestEvent, response ht
authz, err := wfe.RA.NewAuthorization(init, currReg.ID) authz, err := wfe.RA.NewAuthorization(init, currReg.ID)
if err != nil { if err != nil {
logEvent.AddError("unable to create new authz: %s", err) logEvent.AddError("unable to create new authz: %s", err)
wfe.sendError(response, logEvent, "Error creating new authz", err, statusCodeFromError(err)) wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Error creating new authz"), err)
return return
} }
logEvent.Extra["AuthzID"] = authz.ID logEvent.Extra["AuthzID"] = authz.ID
@ -665,8 +590,8 @@ func (wfe *WebFrontEndImpl) NewAuthorization(logEvent *requestEvent, response ht
wfe.prepAuthorizationForDisplay(&authz) wfe.prepAuthorizationForDisplay(&authz)
responseBody, err := json.Marshal(authz) responseBody, err := json.Marshal(authz)
if err != nil { if err != nil {
// StatusInternalServerError because we generated the authz, it should be OK // ServerInternal because we generated the authz, it should be OK
wfe.sendError(response, logEvent, "Error marshaling authz", err, http.StatusInternalServerError) wfe.sendError(response, logEvent, probs.ServerInternal("Error marshaling authz"), err)
return return
} }
@ -685,10 +610,10 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(logEvent *requestEvent, response h
// We don't ask verifyPOST to verify there is a correponding registration, // We don't ask verifyPOST to verify there is a correponding registration,
// because anyone with the right private key can revoke a certificate. // because anyone with the right private key can revoke a certificate.
body, requestKey, registration, err := wfe.verifyPOST(logEvent, request, false, core.ResourceRevokeCert) body, requestKey, registration, prob := wfe.verifyPOST(logEvent, request, false, core.ResourceRevokeCert)
if err != nil { if prob != nil {
// verifyPOST handles its own setting of logEvent.Errors // verifyPOST handles its own setting of logEvent.Errors
wfe.sendError(response, logEvent, malformedJWS, err, statusCodeFromError(err)) wfe.sendError(response, logEvent, prob, nil)
return return
} }
@ -696,29 +621,30 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(logEvent *requestEvent, response h
CertificateDER core.JSONBuffer `json:"certificate"` CertificateDER core.JSONBuffer `json:"certificate"`
} }
var revokeRequest RevokeRequest var revokeRequest RevokeRequest
if err = json.Unmarshal(body, &revokeRequest); err != nil { if err := json.Unmarshal(body, &revokeRequest); err != nil {
logEvent.AddError(fmt.Sprintf("Couldn't unmarshal in revoke request %s", string(body))) logEvent.AddError(fmt.Sprintf("Couldn't unmarshal in revoke request %s", string(body)))
wfe.sendError(response, logEvent, "Unable to read/verify body", err, http.StatusBadRequest) wfe.sendError(response, logEvent, probs.Malformed("Unable to JSON parse revoke request"), err)
return return
} }
providedCert, err := x509.ParseCertificate(revokeRequest.CertificateDER) providedCert, err := x509.ParseCertificate(revokeRequest.CertificateDER)
if err != nil { if err != nil {
logEvent.AddError("unable to parse revoke certificate DER: %s", err) logEvent.AddError("unable to parse revoke certificate DER: %s", err)
wfe.sendError(response, logEvent, "Unable to read/verify body", err, http.StatusBadRequest) wfe.sendError(response, logEvent, probs.Malformed("Unable to parse certificate DER"), err)
return return
} }
serial := core.SerialToString(providedCert.SerialNumber) serial := core.SerialToString(providedCert.SerialNumber)
logEvent.Extra["ProvidedCertificateSerial"] = serial logEvent.Extra["ProvidedCertificateSerial"] = serial
cert, err := wfe.SA.GetCertificate(serial) cert, err := wfe.SA.GetCertificate(serial)
// TODO(#991): handle db errors better
if err != nil || !bytes.Equal(cert.DER, revokeRequest.CertificateDER) { if err != nil || !bytes.Equal(cert.DER, revokeRequest.CertificateDER) {
wfe.sendError(response, logEvent, "No such certificate", err, http.StatusNotFound) wfe.sendError(response, logEvent, probs.NotFound("No such certificate"), err)
return return
} }
parsedCertificate, err := x509.ParseCertificate(cert.DER) parsedCertificate, err := x509.ParseCertificate(cert.DER)
if err != nil { if err != nil {
// InternalServerError because this is a failure to decode from our DB. // InternalServerError because this is a failure to decode from our DB.
wfe.sendError(response, logEvent, "Invalid certificate", err, http.StatusInternalServerError) wfe.sendError(response, logEvent, probs.ServerInternal("invalid parse of stored certificate"), err)
return return
} }
logEvent.Extra["RetrievedCertificateSerial"] = core.SerialToString(parsedCertificate.SerialNumber) logEvent.Extra["RetrievedCertificateSerial"] = core.SerialToString(parsedCertificate.SerialNumber)
@ -729,14 +655,15 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(logEvent *requestEvent, response h
certStatus, err := wfe.SA.GetCertificateStatus(serial) certStatus, err := wfe.SA.GetCertificateStatus(serial)
if err != nil { if err != nil {
logEvent.AddError("unable to get certificate status: %s", err) logEvent.AddError("unable to get certificate status: %s", err)
wfe.sendError(response, logEvent, "Certificate status not yet available", err, http.StatusNotFound) // TODO(#991): handle db errors
wfe.sendError(response, logEvent, probs.NotFound("Certificate status not yet available"), err)
return return
} }
logEvent.Extra["CertificateStatus"] = certStatus.Status logEvent.Extra["CertificateStatus"] = certStatus.Status
if certStatus.Status == core.OCSPStatusRevoked { if certStatus.Status == core.OCSPStatusRevoked {
logEvent.AddError("Certificate already revoked: %#v", serial) logEvent.AddError("Certificate already revoked: %#v", serial)
wfe.sendError(response, logEvent, "Certificate already revoked", nil, http.StatusConflict) wfe.sendError(response, logEvent, probs.Conflict("Certificate already revoked"), nil)
return return
} }
@ -744,9 +671,8 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(logEvent *requestEvent, response h
if !(core.KeyDigestEquals(requestKey, parsedCertificate.PublicKey) || if !(core.KeyDigestEquals(requestKey, parsedCertificate.PublicKey) ||
registration.ID == cert.RegistrationID) { registration.ID == cert.RegistrationID) {
wfe.sendError(response, logEvent, wfe.sendError(response, logEvent,
"Revocation request must be signed by private key of cert to be revoked, or by the account key of the account that issued it.", probs.Unauthorized("Revocation request must be signed by private key of cert to be revoked, or by the account key of the account that issued it."),
nil, nil)
http.StatusForbidden)
return return
} }
@ -754,7 +680,7 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(logEvent *requestEvent, response h
err = wfe.RA.RevokeCertificateWithReg(*parsedCertificate, 0, registration.ID) err = wfe.RA.RevokeCertificateWithReg(*parsedCertificate, 0, registration.ID)
if err != nil { if err != nil {
logEvent.AddError("failed to revoke certificate: %s", err) logEvent.AddError("failed to revoke certificate: %s", err)
wfe.sendError(response, logEvent, "Failed to revoke certificate", err, statusCodeFromError(err)) wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Failed to revoke certificate"), err)
} else { } else {
wfe.log.Debug(fmt.Sprintf("Revoked %v", serial)) wfe.log.Debug(fmt.Sprintf("Revoked %v", serial))
response.WriteHeader(http.StatusOK) response.WriteHeader(http.StatusOK)
@ -777,30 +703,24 @@ func (wfe *WebFrontEndImpl) logCsr(request *http.Request, cr core.CertificateReq
// NewCertificate is used by clients to request the issuance of a cert for an // NewCertificate is used by clients to request the issuance of a cert for an
// authorized identifier. // authorized identifier.
func (wfe *WebFrontEndImpl) NewCertificate(logEvent *requestEvent, response http.ResponseWriter, request *http.Request) { func (wfe *WebFrontEndImpl) NewCertificate(logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
body, _, reg, err := wfe.verifyPOST(logEvent, request, true, core.ResourceNewCert) body, _, reg, prob := wfe.verifyPOST(logEvent, request, true, core.ResourceNewCert)
if err != nil { if prob != nil {
// verifyPOST handles its own setting of logEvent.Errors // verifyPOST handles its own setting of logEvent.Errors
respMsg := malformedJWS wfe.sendError(response, logEvent, prob, nil)
respCode := statusCodeFromError(err)
if _, ok := err.(core.NoSuchRegistrationError); ok {
respMsg = unknownKey
respCode = http.StatusForbidden
}
wfe.sendError(response, logEvent, respMsg, err, respCode)
return return
} }
// Any version of the agreement is acceptable here. Version match is enforced in // Any version of the agreement is acceptable here. Version match is enforced in
// wfe.Registration when agreeing the first time. Agreement updates happen // wfe.Registration when agreeing the first time. Agreement updates happen
// by mailing subscribers and don't require a registration update. // by mailing subscribers and don't require a registration update.
if reg.Agreement == "" { if reg.Agreement == "" {
wfe.sendError(response, logEvent, "Must agree to subscriber agreement before any further actions", nil, http.StatusForbidden) wfe.sendError(response, logEvent, probs.Unauthorized("Must agree to subscriber agreement before any further actions"), nil)
return return
} }
var certificateRequest core.CertificateRequest var certificateRequest core.CertificateRequest
if err = json.Unmarshal(body, &certificateRequest); err != nil { if err := json.Unmarshal(body, &certificateRequest); err != nil {
logEvent.AddError("unable to JSON unmarshal CertificateRequest: %s", err) logEvent.AddError("unable to JSON unmarshal CertificateRequest: %s", err)
wfe.sendError(response, logEvent, "Error unmarshaling certificate request", err, http.StatusBadRequest) wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling certificate request"), err)
return return
} }
wfe.logCsr(request, certificateRequest, reg) wfe.logCsr(request, certificateRequest, reg)
@ -810,9 +730,9 @@ func (wfe *WebFrontEndImpl) NewCertificate(logEvent *requestEvent, response http
// bytes on the wire, and (b) the CA logs all rejections as audit events, but // 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 // a bad key from the client is just a malformed request and doesn't need to
// be audited. // be audited.
if err = core.GoodKey(certificateRequest.CSR.PublicKey); err != nil { if err := core.GoodKey(certificateRequest.CSR.PublicKey); err != nil {
logEvent.AddError("CSR public key failed GoodKey: %s", err) logEvent.AddError("CSR public key failed GoodKey: %s", err)
wfe.sendError(response, logEvent, "Invalid key in certificate request", err, http.StatusBadRequest) wfe.sendError(response, logEvent, probs.Malformed("Invalid key in certificate request :: %s", err), err)
return return
} }
logEvent.Extra["CSRDNSNames"] = certificateRequest.CSR.DNSNames logEvent.Extra["CSRDNSNames"] = certificateRequest.CSR.DNSNames
@ -828,7 +748,7 @@ func (wfe *WebFrontEndImpl) NewCertificate(logEvent *requestEvent, response http
cert, err := wfe.RA.NewCertificate(certificateRequest, reg.ID) cert, err := wfe.RA.NewCertificate(certificateRequest, reg.ID)
if err != nil { if err != nil {
logEvent.AddError("unable to create new cert: %s", err) logEvent.AddError("unable to create new cert: %s", err)
wfe.sendError(response, logEvent, "Error creating new cert", err, statusCodeFromError(err)) wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Error creating new cert"), err)
return return
} }
@ -839,7 +759,7 @@ func (wfe *WebFrontEndImpl) NewCertificate(logEvent *requestEvent, response http
parsedCertificate, err := x509.ParseCertificate([]byte(cert.DER)) parsedCertificate, err := x509.ParseCertificate([]byte(cert.DER))
if err != nil { if err != nil {
logEvent.AddError("unable to parse certificate: %s", err) logEvent.AddError("unable to parse certificate: %s", err)
wfe.sendError(response, logEvent, "Error creating new cert", err, http.StatusBadRequest) wfe.sendError(response, logEvent, probs.Malformed("Unable to parse certificate"), err)
return return
} }
serial := parsedCertificate.SerialNumber serial := parsedCertificate.SerialNumber
@ -864,7 +784,7 @@ func (wfe *WebFrontEndImpl) Challenge(
request *http.Request) { request *http.Request) {
notFound := func() { notFound := func() {
wfe.sendError(response, logEvent, "No such challenge", nil, http.StatusNotFound) wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil)
} }
// Challenge URIs are of the form /acme/challenge/<auth id>/<challenge id>. // Challenge URIs are of the form /acme/challenge/<auth id>/<challenge id>.
@ -886,6 +806,7 @@ func (wfe *WebFrontEndImpl) Challenge(
authz, err := wfe.SA.GetAuthorization(authorizationID) authz, err := wfe.SA.GetAuthorization(authorizationID)
if err != nil { if err != nil {
// TODO(#1198): handle db errors etc
notFound() notFound()
return return
} }
@ -893,7 +814,7 @@ func (wfe *WebFrontEndImpl) Challenge(
// After expiring, challenges are inaccessible // After expiring, challenges are inaccessible
if authz.Expires == nil || authz.Expires.Before(wfe.clk.Now()) { if authz.Expires == nil || authz.Expires.Before(wfe.clk.Now()) {
logEvent.AddError("Authorization %v expired in the past (%v)", authz.ID, *authz.Expires) logEvent.AddError("Authorization %v expired in the past (%v)", authz.ID, *authz.Expires)
wfe.sendError(response, logEvent, "Expired authorization", nil, http.StatusNotFound) wfe.sendError(response, logEvent, probs.NotFound("Expired authorization"), nil)
return return
} }
@ -957,7 +878,7 @@ func (wfe *WebFrontEndImpl) getChallenge(
// InternalServerError because this is a failure to decode data passed in // InternalServerError because this is a failure to decode data passed in
// by the caller, which got it from the DB. // by the caller, which got it from the DB.
logEvent.AddError("unable to marshal challenge: %s", err) logEvent.AddError("unable to marshal challenge: %s", err)
wfe.sendError(response, logEvent, "Failed to marshal challenge", err, http.StatusInternalServerError) wfe.sendError(response, logEvent, probs.ServerInternal("Failed to marshal challenge"), err)
return return
} }
@ -979,23 +900,17 @@ func (wfe *WebFrontEndImpl) postChallenge(
authz core.Authorization, authz core.Authorization,
challengeIndex int, challengeIndex int,
logEvent *requestEvent) { logEvent *requestEvent) {
body, _, currReg, err := wfe.verifyPOST(logEvent, request, true, core.ResourceChallenge) body, _, currReg, prob := wfe.verifyPOST(logEvent, request, true, core.ResourceChallenge)
if err != nil { if prob != nil {
// verifyPOST handles its own setting of logEvent.Errors // verifyPOST handles its own setting of logEvent.Errors
respMsg := malformedJWS wfe.sendError(response, logEvent, prob, nil)
respCode := http.StatusBadRequest
if _, ok := err.(core.NoSuchRegistrationError); ok {
respMsg = unknownKey
respCode = http.StatusForbidden
}
wfe.sendError(response, logEvent, respMsg, err, respCode)
return return
} }
// Any version of the agreement is acceptable here. Version match is enforced in // Any version of the agreement is acceptable here. Version match is enforced in
// wfe.Registration when agreeing the first time. Agreement updates happen // wfe.Registration when agreeing the first time. Agreement updates happen
// by mailing subscribers and don't require a registration update. // by mailing subscribers and don't require a registration update.
if currReg.Agreement == "" { if currReg.Agreement == "" {
wfe.sendError(response, logEvent, "Registration didn't agree to subscriber agreement before any further actions", nil, http.StatusForbidden) wfe.sendError(response, logEvent, probs.Unauthorized("Registration didn't agree to subscriber agreement before any further actions"), nil)
return return
} }
@ -1003,16 +918,18 @@ func (wfe *WebFrontEndImpl) postChallenge(
// the registration ID on the authz object // the registration ID on the authz object
if currReg.ID != authz.RegistrationID { if currReg.ID != authz.RegistrationID {
logEvent.AddError("User registration id: %d != Authorization registration id: %v", currReg.ID, authz.RegistrationID) logEvent.AddError("User registration id: %d != Authorization registration id: %v", currReg.ID, authz.RegistrationID)
wfe.sendError(response, logEvent, "User registration ID doesn't match registration ID in authorization", wfe.sendError(response,
logEvent,
probs.Unauthorized("User registration ID doesn't match registration ID in authorization"),
nil, nil,
http.StatusForbidden) )
return return
} }
var challengeUpdate core.Challenge var challengeUpdate core.Challenge
if err = json.Unmarshal(body, &challengeUpdate); err != nil { if err := json.Unmarshal(body, &challengeUpdate); err != nil {
logEvent.AddError("error JSON unmarshalling challenge response: %s", err) logEvent.AddError("error JSON unmarshalling challenge response: %s", err)
wfe.sendError(response, logEvent, "Error unmarshaling challenge response", err, http.StatusBadRequest) wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling challenge response"), err)
return return
} }
@ -1020,7 +937,7 @@ func (wfe *WebFrontEndImpl) postChallenge(
updatedAuthorization, err := wfe.RA.UpdateAuthorization(authz, challengeIndex, challengeUpdate) updatedAuthorization, err := wfe.RA.UpdateAuthorization(authz, challengeIndex, challengeUpdate)
if err != nil { if err != nil {
logEvent.AddError("unable to update challenge: %s", err) logEvent.AddError("unable to update challenge: %s", err)
wfe.sendError(response, logEvent, "Unable to update challenge", err, statusCodeFromError(err)) wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Unable to update challenge"), err)
return return
} }
@ -1029,9 +946,9 @@ func (wfe *WebFrontEndImpl) postChallenge(
wfe.prepChallengeForDisplay(authz, &challenge) wfe.prepChallengeForDisplay(authz, &challenge)
jsonReply, err := json.Marshal(challenge) jsonReply, err := json.Marshal(challenge)
if err != nil { if err != nil {
// StatusInternalServerError because we made the challenges, they should be OK // ServerInternal because we made the challenges, they should be OK
logEvent.AddError("failed to marshal challenge: %s", err) logEvent.AddError("failed to marshal challenge: %s", err)
wfe.sendError(response, logEvent, "Failed to marshal challenge", err, http.StatusInternalServerError) wfe.sendError(response, logEvent, probs.ServerInternal("Failed to marshal challenge"), err)
return return
} }
@ -1050,16 +967,10 @@ func (wfe *WebFrontEndImpl) postChallenge(
// Registration is used by a client to submit an update to their registration. // Registration is used by a client to submit an update to their registration.
func (wfe *WebFrontEndImpl) Registration(logEvent *requestEvent, response http.ResponseWriter, request *http.Request) { func (wfe *WebFrontEndImpl) Registration(logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
body, _, currReg, err := wfe.verifyPOST(logEvent, request, true, core.ResourceRegistration) body, _, currReg, prob := wfe.verifyPOST(logEvent, request, true, core.ResourceRegistration)
if err != nil { if prob != nil {
// verifyPOST handles its own setting of logEvent.Errors // verifyPOST handles its own setting of logEvent.Errors
respMsg := malformedJWS wfe.sendError(response, logEvent, prob, nil)
respCode := statusCodeFromError(err)
if _, ok := err.(core.NoSuchRegistrationError); ok {
respMsg = unknownKey
respCode = http.StatusForbidden
}
wfe.sendError(response, logEvent, respMsg, err, respCode)
return return
} }
@ -1069,16 +980,16 @@ func (wfe *WebFrontEndImpl) Registration(logEvent *requestEvent, response http.R
id, err := strconv.ParseInt(idStr, 10, 64) id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil { if err != nil {
logEvent.AddError("registration ID must be an integer, was %#v", idStr) logEvent.AddError("registration ID must be an integer, was %#v", idStr)
wfe.sendError(response, logEvent, "Registration ID must be an integer", err, http.StatusBadRequest) wfe.sendError(response, logEvent, probs.Malformed("Registration ID must be an integer"), err)
return return
} else if id <= 0 { } else if id <= 0 {
msg := fmt.Sprintf("Registration ID must be a positive non-zero integer, was %d", id) msg := fmt.Sprintf("Registration ID must be a positive non-zero integer, was %d", id)
logEvent.AddError(msg) logEvent.AddError(msg)
wfe.sendError(response, logEvent, msg, nil, http.StatusBadRequest) wfe.sendError(response, logEvent, probs.Malformed(msg), nil)
return return
} else if id != currReg.ID { } else if id != currReg.ID {
logEvent.AddError("Request signing key did not match registration key: %d != %d", id, currReg.ID) logEvent.AddError("Request signing key did not match registration key: %d != %d", id, currReg.ID)
wfe.sendError(response, logEvent, "Request signing key did not match registration key", nil, http.StatusForbidden) wfe.sendError(response, logEvent, probs.Unauthorized("Request signing key did not match registration key"), nil)
return return
} }
@ -1086,14 +997,14 @@ func (wfe *WebFrontEndImpl) Registration(logEvent *requestEvent, response http.R
err = json.Unmarshal(body, &update) err = json.Unmarshal(body, &update)
if err != nil { if err != nil {
logEvent.AddError("unable to JSON parse registration: %s", err) logEvent.AddError("unable to JSON parse registration: %s", err)
wfe.sendError(response, logEvent, "Error unmarshaling registration", err, http.StatusBadRequest) wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling registration"), err)
return return
} }
if len(update.Agreement) > 0 && update.Agreement != wfe.SubscriberAgreementURL { if len(update.Agreement) > 0 && update.Agreement != wfe.SubscriberAgreementURL {
msg := fmt.Sprintf("Provided agreement URL [%s] does not match current agreement URL [%s]", 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) logEvent.AddError(msg)
wfe.sendError(response, logEvent, msg, nil, http.StatusBadRequest) wfe.sendError(response, logEvent, probs.Malformed(msg), nil)
return return
} }
@ -1107,15 +1018,15 @@ func (wfe *WebFrontEndImpl) Registration(logEvent *requestEvent, response http.R
updatedReg, err := wfe.RA.UpdateRegistration(currReg, update) updatedReg, err := wfe.RA.UpdateRegistration(currReg, update)
if err != nil { if err != nil {
logEvent.AddError("unable to update registration: %s", err) logEvent.AddError("unable to update registration: %s", err)
wfe.sendError(response, logEvent, "Unable to update registration", err, statusCodeFromError(err)) wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Unable to update registration"), err)
return return
} }
jsonReply, err := json.Marshal(updatedReg) jsonReply, err := json.Marshal(updatedReg)
if err != nil { if err != nil {
// StatusInternalServerError because we just generated the reg, it should be OK // ServerInternal because we just generated the reg, it should be OK
logEvent.AddError("unable to marshal updated registration: %s", err) logEvent.AddError("unable to marshal updated registration: %s", err)
wfe.sendError(response, logEvent, "Failed to marshal registration", err, http.StatusInternalServerError) wfe.sendError(response, logEvent, probs.ServerInternal("Failed to marshal registration"), err)
return return
} }
response.Header().Set("Content-Type", "application/json") response.Header().Set("Content-Type", "application/json")
@ -1135,7 +1046,8 @@ func (wfe *WebFrontEndImpl) Authorization(logEvent *requestEvent, response http.
authz, err := wfe.SA.GetAuthorization(id) authz, err := wfe.SA.GetAuthorization(id)
if err != nil { if err != nil {
logEvent.AddError("No such authorization at id %s", id) logEvent.AddError("No such authorization at id %s", id)
wfe.sendError(response, logEvent, "Unable to find authorization", err, http.StatusNotFound) // TODO(#1199): handle db errors
wfe.sendError(response, logEvent, probs.NotFound("Unable to find authorization"), err)
return return
} }
logEvent.Extra["AuthorizationID"] = authz.ID logEvent.Extra["AuthorizationID"] = authz.ID
@ -1148,7 +1060,7 @@ func (wfe *WebFrontEndImpl) Authorization(logEvent *requestEvent, response http.
if authz.Expires == nil || authz.Expires.Before(wfe.clk.Now()) { if authz.Expires == nil || authz.Expires.Before(wfe.clk.Now()) {
msg := fmt.Sprintf("Authorization %v expired in the past (%v)", authz.ID, *authz.Expires) msg := fmt.Sprintf("Authorization %v expired in the past (%v)", authz.ID, *authz.Expires)
logEvent.AddError(msg) logEvent.AddError(msg)
wfe.sendError(response, logEvent, "Expired authorization", nil, http.StatusNotFound) wfe.sendError(response, logEvent, probs.NotFound("Expired authorization"), nil)
return return
} }
@ -1158,7 +1070,7 @@ func (wfe *WebFrontEndImpl) Authorization(logEvent *requestEvent, response http.
if err != nil { if err != nil {
// InternalServerError because this is a failure to decode from our DB. // InternalServerError because this is a failure to decode from our DB.
logEvent.AddError("Failed to JSON marshal authz: %s", err) logEvent.AddError("Failed to JSON marshal authz: %s", err)
wfe.sendError(response, logEvent, "Failed to JSON marshal authz", err, http.StatusInternalServerError) wfe.sendError(response, logEvent, probs.ServerInternal("Failed to JSON marshal authz"), err)
return return
} }
response.Header().Add("Link", link(wfe.NewCert, "next")) response.Header().Add("Link", link(wfe.NewCert, "next"))
@ -1181,27 +1093,28 @@ func (wfe *WebFrontEndImpl) Certificate(logEvent *requestEvent, response http.Re
// digits. // digits.
if !strings.HasPrefix(path, CertPath) { if !strings.HasPrefix(path, CertPath) {
logEvent.AddError("this request path should not have gotten to Certificate: %#v is not a prefix of %#v", path, CertPath) logEvent.AddError("this request path should not have gotten to Certificate: %#v is not a prefix of %#v", path, CertPath)
wfe.sendError(response, logEvent, "Certificate not found", nil, http.StatusNotFound) wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), nil)
addNoCacheHeader(response) addNoCacheHeader(response)
return return
} }
serial := path[len(CertPath):] serial := path[len(CertPath):]
if !core.ValidSerial(serial) { if !core.ValidSerial(serial) {
logEvent.AddError("certificate serial provided was not valid: %s", serial) logEvent.AddError("certificate serial provided was not valid: %s", serial)
wfe.sendError(response, logEvent, "Certificate not found", nil, http.StatusNotFound) wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), nil)
addNoCacheHeader(response) addNoCacheHeader(response)
return return
} }
logEvent.Extra["RequestedSerial"] = serial logEvent.Extra["RequestedSerial"] = serial
cert, err := wfe.SA.GetCertificate(serial) cert, err := wfe.SA.GetCertificate(serial)
// TODO(#991): handle db errors
if err != nil { if err != nil {
logEvent.AddError("unable to get certificate by serial id %#v: %s", serial, err) logEvent.AddError("unable to get certificate by serial id %#v: %s", serial, err)
if strings.HasPrefix(err.Error(), "gorp: multiple rows returned") { if strings.HasPrefix(err.Error(), "gorp: multiple rows returned") {
wfe.sendError(response, logEvent, "Multiple certificates with same short serial", err, http.StatusConflict) wfe.sendError(response, logEvent, probs.Conflict("Multiple certificates with same short serial"), err)
} else { } else {
addNoCacheHeader(response) addNoCacheHeader(response)
wfe.sendError(response, logEvent, "Certificate not found", err, http.StatusNotFound) wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), err)
} }
return return
} }

View File

@ -18,7 +18,6 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"reflect"
"sort" "sort"
"strings" "strings"
"testing" "testing"
@ -27,6 +26,7 @@ import (
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd" "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/jmhodges/clock" "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose" "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
@ -571,14 +571,14 @@ func TestIssueCertificate(t *testing.T) {
}) })
test.AssertEquals(t, test.AssertEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: No body on POST","status":400}`) `{"type":"urn:acme:error:malformed","detail":"No body on POST","status":400}`)
// POST, but body that isn't valid JWS // POST, but body that isn't valid JWS
responseWriter.Body.Reset() responseWriter.Body.Reset()
wfe.NewCertificate(newRequestEvent(), responseWriter, makePostRequest("hi")) wfe.NewCertificate(newRequestEvent(), responseWriter, makePostRequest("hi"))
test.AssertEquals(t, test.AssertEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Parse error reading JWS","status":400}`) `{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`)
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON. // POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
responseWriter.Body.Reset() responseWriter.Body.Reset()
@ -586,7 +586,7 @@ func TestIssueCertificate(t *testing.T) {
makePostRequest(signRequest(t, "foo", wfe.nonceService))) makePostRequest(signRequest(t, "foo", wfe.nonceService)))
test.AssertEquals(t, test.AssertEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Request payload did not parse as JSON","status":400}`) `{"type":"urn:acme:error:malformed","detail":"Request payload did not parse as JSON","status":400}`)
// Valid, signed JWS body, payload is '{}' // Valid, signed JWS body, payload is '{}'
responseWriter.Body.Reset() responseWriter.Body.Reset()
@ -595,7 +595,7 @@ func TestIssueCertificate(t *testing.T) {
signRequest(t, "{}", wfe.nonceService))) signRequest(t, "{}", wfe.nonceService)))
test.AssertEquals(t, test.AssertEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Request payload does not specify a resource","status":400}`) `{"type":"urn:acme:error:malformed","detail":"Request payload does not specify a resource","status":400}`)
// Valid, signed JWS body, payload is '{"resource":"new-cert"}' // Valid, signed JWS body, payload is '{"resource":"new-cert"}'
responseWriter.Body.Reset() responseWriter.Body.Reset()
@ -755,7 +755,7 @@ func TestBadNonce(t *testing.T) {
test.AssertNotError(t, err, "Failed to sign body") test.AssertNotError(t, err, "Failed to sign body")
wfe.NewRegistration(newRequestEvent(), responseWriter, wfe.NewRegistration(newRequestEvent(), responseWriter,
makePostRequest(result.FullSerialize())) makePostRequest(result.FullSerialize()))
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:badNonce","detail":"Unable to read/verify body :: JWS has no anti-replay nonce","status":400}`) test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:badNonce","detail":"JWS has no anti-replay nonce","status":400}`)
} }
func TestNewRegistration(t *testing.T) { func TestNewRegistration(t *testing.T) {
@ -799,19 +799,19 @@ func TestNewRegistration(t *testing.T) {
"Content-Length": []string{"0"}, "Content-Length": []string{"0"},
}, },
}, },
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: No body on POST","status":400}`, `{"type":"urn:acme:error:malformed","detail":"No body on POST","status":400}`,
}, },
// POST, but body that isn't valid JWS // POST, but body that isn't valid JWS
{ {
makePostRequestWithPath(NewRegPath, "hi"), makePostRequestWithPath(NewRegPath, "hi"),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Parse error reading JWS","status":400}`, `{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`,
}, },
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON. // POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
{ {
makePostRequestWithPath(NewRegPath, fooBody.FullSerialize()), makePostRequestWithPath(NewRegPath, fooBody.FullSerialize()),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Request payload did not parse as JSON","status":400}`, `{"type":"urn:acme:error:malformed","detail":"Request payload did not parse as JSON","status":400}`,
}, },
// Same signed body, but payload modified by one byte, breaking signature. // Same signed body, but payload modified by one byte, breaking signature.
@ -831,7 +831,7 @@ func TestNewRegistration(t *testing.T) {
"signature": "RjUQ679fxJgeAJlxqgvDP_sfGZnJ-1RgWF2qmcbnBWljs6h1qp63pLnJOl13u81bP_bCSjaWkelGG8Ymx_X-aQ" "signature": "RjUQ679fxJgeAJlxqgvDP_sfGZnJ-1RgWF2qmcbnBWljs6h1qp63pLnJOl13u81bP_bCSjaWkelGG8Ymx_X-aQ"
} }
`), `),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: JWS verification error","status":400}`, `{"type":"urn:acme:error:malformed","detail":"JWS verification error","status":400}`,
}, },
{ {
makePostRequestWithPath(NewRegPath, wrongAgreementBody.FullSerialize()), makePostRequestWithPath(NewRegPath, wrongAgreementBody.FullSerialize()),
@ -1062,12 +1062,12 @@ func TestAuthorization(t *testing.T) {
"Content-Length": []string{"0"}, "Content-Length": []string{"0"},
}, },
}) })
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: No body on POST","status":400}`) test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"No body on POST","status":400}`)
// POST, but body that isn't valid JWS // POST, but body that isn't valid JWS
responseWriter.Body.Reset() responseWriter.Body.Reset()
wfe.NewAuthorization(newRequestEvent(), responseWriter, makePostRequest("hi")) wfe.NewAuthorization(newRequestEvent(), responseWriter, makePostRequest("hi"))
test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Parse error reading JWS","status":400}`) test.AssertEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`)
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON. // POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
responseWriter.Body.Reset() responseWriter.Body.Reset()
@ -1075,7 +1075,7 @@ func TestAuthorization(t *testing.T) {
makePostRequest(signRequest(t, "foo", wfe.nonceService))) makePostRequest(signRequest(t, "foo", wfe.nonceService)))
test.AssertEquals(t, test.AssertEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Request payload did not parse as JSON","status":400}`) `{"type":"urn:acme:error:malformed","detail":"Request payload did not parse as JSON","status":400}`)
// Same signed body, but payload modified by one byte, breaking signature. // Same signed body, but payload modified by one byte, breaking signature.
// should fail JWS verification. // should fail JWS verification.
@ -1096,7 +1096,7 @@ func TestAuthorization(t *testing.T) {
`)) `))
test.AssertEquals(t, test.AssertEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: JWS verification error","status":400}`) `{"type":"urn:acme:error:malformed","detail":"JWS verification error","status":400}`)
responseWriter.Body.Reset() responseWriter.Body.Reset()
wfe.NewAuthorization(newRequestEvent(), responseWriter, wfe.NewAuthorization(newRequestEvent(), responseWriter,
@ -1167,7 +1167,7 @@ func TestRegistration(t *testing.T) {
wfe.Registration(newRequestEvent(), responseWriter, makePostRequestWithPath("/2", "invalid")) wfe.Registration(newRequestEvent(), responseWriter, makePostRequestWithPath("/2", "invalid"))
test.AssertEquals(t, test.AssertEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to read/verify body :: Parse error reading JWS","status":400}`) `{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`)
responseWriter.Body.Reset() responseWriter.Body.Reset()
key, err := jose.LoadPrivateKey([]byte(test2KeyPrivatePEM)) key, err := jose.LoadPrivateKey([]byte(test2KeyPrivatePEM))
@ -1350,13 +1350,13 @@ func TestLogCsrPem(t *testing.T) {
func TestLengthRequired(t *testing.T) { func TestLengthRequired(t *testing.T) {
wfe, _ := setupWFE(t) wfe, _ := setupWFE(t)
_, _, _, err := wfe.verifyPOST(newRequestEvent(), &http.Request{ _, _, _, prob := wfe.verifyPOST(newRequestEvent(), &http.Request{
Method: "POST", Method: "POST",
URL: mustParseURL("/"), URL: mustParseURL("/"),
}, false, "resource") }, false, "resource")
test.Assert(t, err != nil, "No error returned for request body missing Content-Length.") test.Assert(t, prob != nil, "No error returned for request body missing Content-Length.")
_, ok := err.(core.LengthRequiredError) test.AssertEquals(t, probs.MalformedProblem, prob.Type)
test.Assert(t, ok, "Error code for missing content-length wasn't 411.") test.AssertEquals(t, http.StatusLengthRequired, prob.HTTPStatus)
} }
type mockSADifferentStoredKey struct { type mockSADifferentStoredKey struct {
@ -1400,30 +1400,6 @@ func TestBadKeyCSR(t *testing.T) {
`{"type":"urn:acme:error:malformed","detail":"Invalid key in certificate request :: Key too small: 512","status":400}`) `{"type":"urn:acme:error:malformed","detail":"Invalid key in certificate request :: Key too small: 512","status":400}`)
} }
func TestStatusCodeFromError(t *testing.T) {
testCases := []struct {
err error
statusCode int
}{
{core.InternalServerError("foo"), 500},
{core.NotSupportedError("foo"), 501},
{core.MalformedRequestError("foo"), 400},
{core.UnauthorizedError("foo"), 403},
{core.NotFoundError("foo"), 404},
{core.SyntaxError("foo"), 400},
{core.SignatureValidationError("foo"), 400},
{core.RateLimitedError("foo"), 429},
{core.LengthRequiredError("foo"), 411},
{core.BadNonceError("foo"), statusBadNonce},
}
for _, c := range testCases {
got := statusCodeFromError(c.err)
if got != c.statusCode {
t.Errorf("Incorrect status code for %s. Expected %d, got %d", reflect.TypeOf(c.err).Name(), c.statusCode, got)
}
}
}
func newRequestEvent() *requestEvent { func newRequestEvent() *requestEvent {
return &requestEvent{Extra: make(map[string]interface{})} return &requestEvent{Extra: make(map[string]interface{})}
} }