Prefix problem type with namespace at runtime. (#3039)

To support having problem types that use either the classic
"urn:acme:error" namespace or the new "urn:ietf:params:acme:error"
namespace as appropriate we need to prefix the problem type at runtime
right before returning it through the WFE to the user as JSON. This
commit updates the WFE/WFE2 to do this for both problems sent through
sendError as well as problems embedded in challenges. For the latter
we do not modify problems with a type that is already prefixed to
support backwards compatibility.

Resolves #2938

Note: We should cut a follow-up issue to devise a way to share some
common code between the WFE and WFE2. For example, the
prepChallengeForDisplay should probably be hoisted to a common
"web" package
This commit is contained in:
Daniel McCarney 2017-09-06 15:55:10 -04:00 committed by Roland Bracewell Shoemaker
parent f193137405
commit baf32878c0
8 changed files with 285 additions and 140 deletions

View File

@ -15,6 +15,7 @@ import (
"github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/revocation"
sapb "github.com/letsencrypt/boulder/sa/proto"
)
@ -494,3 +495,47 @@ func (m *Mailer) Close() error {
func (m *Mailer) Connect() error {
return nil
}
// mockSAWithFailedChallenges is a mocks.StorageAuthority that has
// a `GetAuthorization` implementation that can return authorizations with
// failed challenges.
type SAWithFailedChallenges struct {
StorageAuthority
Clk clock.FakeClock
}
func (sa *SAWithFailedChallenges) GetAuthorization(_ context.Context, id string) (core.Authorization, error) {
authz := core.Authorization{
ID: "valid",
Status: core.StatusValid,
RegistrationID: 1,
Identifier: core.AcmeIdentifier{Type: "dns", Value: "not-an-example.com"},
Challenges: []core.Challenge{
{
ID: 23,
Type: "dns",
},
},
}
prob := &probs.ProblemDetails{
Type: "things:are:whack",
Detail: "whack attack",
HTTPStatus: 555,
}
exp := sa.Clk.Now().AddDate(100, 0, 0)
authz.Expires = &exp
// "oldNS" returns an authz with a failed challenge that has the problem type
// statically prefixed by the V1ErrorNS
if id == "oldNS" {
prob.Type = probs.V1ErrorNS + prob.Type
authz.Challenges[0].Error = prob
return authz, nil
}
// "failed" returns an authz with a failed challenge that has no error
// namespace on the problem type.
if id == "failed" {
authz.Challenges[0].Error = prob
return authz, nil
}
return core.Authorization{}, berrors.NotFoundError("no authorization found with id %q", id)
}

View File

@ -7,19 +7,20 @@ import (
// Error types that can be used in ACME payloads
const (
ConnectionProblem = ProblemType("urn:acme:error:connection")
MalformedProblem = ProblemType("urn:acme:error:malformed")
ServerInternalProblem = ProblemType("urn:acme:error:serverInternal")
TLSProblem = ProblemType("urn:acme:error:tls")
UnauthorizedProblem = ProblemType("urn:acme:error:unauthorized")
UnknownHostProblem = ProblemType("urn:acme:error:unknownHost")
RateLimitedProblem = ProblemType("urn:acme:error:rateLimited")
BadNonceProblem = ProblemType("urn:acme:error:badNonce")
InvalidEmailProblem = ProblemType("urn:acme:error:invalidEmail")
RejectedIdentifierProblem = ProblemType("urn:acme:error:rejectedIdentifier")
ConnectionProblem = ProblemType("connection")
MalformedProblem = ProblemType("malformed")
ServerInternalProblem = ProblemType("serverInternal")
TLSProblem = ProblemType("tls")
UnauthorizedProblem = ProblemType("unauthorized")
UnknownHostProblem = ProblemType("unknownHost")
RateLimitedProblem = ProblemType("rateLimited")
BadNonceProblem = ProblemType("badNonce")
InvalidEmailProblem = ProblemType("invalidEmail")
RejectedIdentifierProblem = ProblemType("rejectedIdentifier")
AccountDoesNotExistProblem = ProblemType("accountDoesNotExist")
v2ErrorNS = "urn:ietf:params:acme:error:"
AccountDoesNotExistProblem = ProblemType(v2ErrorNS + "accountDoesNotExist")
V1ErrorNS = "urn:acme:error:"
V2ErrorNS = "urn:ietf:params:acme:error:"
)
// ProblemType defines the error types in the ACME protocol

View File

@ -14,7 +14,7 @@ func TestProblemDetails(t *testing.T) {
Detail: "Wat? o.O",
HTTPStatus: 403,
}
test.AssertEquals(t, pd.Error(), "urn:acme:error:malformed :: Wat? o.O")
test.AssertEquals(t, pd.Error(), "malformed :: Wat? o.O")
}
func TestProblemDetailsToStatusCode(t *testing.T) {

View File

@ -813,7 +813,7 @@ func TestDNSValidationEmpty(t *testing.T) {
"empty-txts.com",
chalDNS,
core.Authorization{})
test.AssertEquals(t, prob.Error(), "urn:acme:error:unauthorized :: No TXT records found for DNS challenge")
test.AssertEquals(t, prob.Error(), "unauthorized :: No TXT records found for DNS challenge")
}
func TestPerformValidationValid(t *testing.T) {

View File

@ -603,8 +603,10 @@ func (wfe *WebFrontEndImpl) verifyPOST(ctx context.Context, logEvent *requestEve
// sendError sends an error response represented by the given ProblemDetails,
// and, if the ProblemDetails.Type is ServerInternalProblem, audit logs the
// internal ierr.
// internal ierr. The rendered Problem will have its Type prefixed with the ACME
// v1 namespace.
func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, logEvent *requestEvent, prob *probs.ProblemDetails, ierr error) {
// Determine the HTTP status code to use for this problem
code := probs.ProblemDetailsToStatusCode(prob)
// Record details to the log event
@ -620,22 +622,21 @@ func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, logEvent *re
}
}
// Increment a stat for this problem type
wfe.stats.Inc(fmt.Sprintf("HTTP.ProblemTypes.%s", prob.Type), 1)
// Prefix the problem type with the ACME V1 error namespace and marshal to JSON
prob.Type = probs.V1ErrorNS + prob.Type
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
// Write the JSON problem response
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 {
@ -1078,12 +1079,20 @@ func (wfe *WebFrontEndImpl) Challenge(
// 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) {
// Update the challenge URI to be relative to the HTTP request Host
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.
// Ensure the challenge ID isn't written. 0 is considered "empty" for the purpose of the JSON omitempty tag.
challenge.ID = 0
// Historically the Type field of a problem was always prefixed with a static
// error namespace. To support the V2 API and migrating to the correct IETF
// namespace we now prefix the Type with the correct namespace at runtime when
// we write the problem JSON to the user. We skip this process if the
// challenge error type has already been prefixed with the V1ErrorNS.
if challenge.Error != nil && !strings.HasPrefix(string(challenge.Error.Type), probs.V1ErrorNS) {
challenge.Error.Type = probs.V1ErrorNS + challenge.Error.Type
}
}
// prepAuthorizationForDisplay takes a core.Authorization and prepares it for

View File

@ -468,7 +468,7 @@ func TestHandleFunc(t *testing.T) {
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), sortHeader(strings.Join(addHeadIfGet(c.allowed), ", ")))
assertJSONEquals(t,
rw.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Method not allowed","status":405}`)
}
nonce := rw.Header().Get("Replay-Nonce")
test.AssertNotEquals(t, nonce, lastNonce)
@ -479,7 +479,7 @@ func TestHandleFunc(t *testing.T) {
// Disallowed method returns error JSON in body
runWrappedHandler(&http.Request{Method: "PUT"}, "GET", "POST")
test.AssertEquals(t, rw.Header().Get("Content-Type"), "application/problem+json")
assertJSONEquals(t, rw.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
assertJSONEquals(t, rw.Body.String(), `{"type":"`+probs.V1ErrorNS+`malformed","detail":"Method not allowed","status":405}`)
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), "GET, HEAD, POST")
// Disallowed method special case: response to HEAD has got no body
@ -493,7 +493,7 @@ func TestHandleFunc(t *testing.T) {
test.AssertEquals(t, rw.Code, http.StatusMethodNotAllowed)
test.AssertEquals(t, rw.Header().Get("Content-Type"), "application/problem+json")
test.AssertEquals(t, rw.Header().Get("Allow"), "POST")
assertJSONEquals(t, rw.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
assertJSONEquals(t, rw.Body.String(), `{"type":"`+probs.V1ErrorNS+`malformed","detail":"Method not allowed","status":405}`)
wfe.AllowOrigins = []string{"*"}
testOrigin := "https://example.com"
@ -893,7 +893,7 @@ func TestIssueCertificate(t *testing.T) {
})
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Method not allowed","status":405}`)
// POST, but no body.
responseWriter.Body.Reset()
@ -905,14 +905,14 @@ func TestIssueCertificate(t *testing.T) {
})
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"No body on POST","status":400}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"No body on POST","status":400}`)
// POST, but body that isn't valid JWS
responseWriter.Body.Reset()
wfe.NewCertificate(ctx, newRequestEvent(), responseWriter, makePostRequest("hi"))
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Parse error reading JWS","status":400}`)
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
responseWriter.Body.Reset()
@ -920,7 +920,7 @@ func TestIssueCertificate(t *testing.T) {
makePostRequest(signRequest(t, "foo", wfe.nonceService)))
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Request payload did not parse as JSON","status":400}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Request payload did not parse as JSON","status":400}`)
// Valid, signed JWS body, payload is '{}'
responseWriter.Body.Reset()
@ -929,7 +929,7 @@ func TestIssueCertificate(t *testing.T) {
signRequest(t, "{}", wfe.nonceService)))
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Request payload does not specify a resource","status":400}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Request payload does not specify a resource","status":400}`)
// Valid, signed JWS body, payload is '{"resource":"new-cert"}'
responseWriter.Body.Reset()
@ -937,7 +937,7 @@ func TestIssueCertificate(t *testing.T) {
makePostRequest(signRequest(t, `{"resource":"new-cert"}`, wfe.nonceService)))
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Error parsing certificate request: asn1: syntax error: sequence truncated","status":400}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Error parsing certificate request: asn1: syntax error: sequence truncated","status":400}`)
// Valid, signed JWS body, payload has an invalid signature on CSR and no authorizations:
// alias b64url="base64 -w0 | sed -e 's,+,-,g' -e 's,/,_,g'"
@ -952,7 +952,7 @@ func TestIssueCertificate(t *testing.T) {
}`, wfe.nonceService)))
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Error creating new cert :: invalid signature on CSR","status":400}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Error creating new cert :: invalid signature on CSR","status":400}`)
// Valid, signed JWS body, payload has a valid CSR but no authorizations:
// openssl req -outform der -new -nodes -key wfe/test/178.key -subj /CN=meep.com | b64url
@ -965,7 +965,7 @@ func TestIssueCertificate(t *testing.T) {
}`, wfe.nonceService)))
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:unauthorized","detail":"Error creating new cert :: authorizations for these names not found or expired: meep.com","status":403}`)
`{"type":"`+probs.V1ErrorNS+`unauthorized","detail":"Error creating new cert :: authorizations for these names not found or expired: meep.com","status":403}`)
assertCsrLogged(t, mockLog)
mockLog.Clear()
@ -1018,7 +1018,7 @@ func TestIssueCertificate(t *testing.T) {
}`, wfe.nonceService)))
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"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","status":400}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"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","status":400}`)
// Test the CSR signature type counter works
test.AssertEquals(t, test.CountCounter("type", "SHA256-RSA", wfe.csrSignatureAlgs), 4)
@ -1099,7 +1099,7 @@ func TestChallenge(t *testing.T) {
signRequest(t, `{"resource":"challenge"}`, wfe.nonceService)))
test.AssertEquals(t, responseWriter.Code, http.StatusNotFound)
assertJSONEquals(t, responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Expired authorization","status":404}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Expired authorization","status":404}`)
// Challenge Not found
challengeURL = ""
@ -1109,7 +1109,7 @@ func TestChallenge(t *testing.T) {
signRequest(t, `{"resource":"challenge"}`, wfe.nonceService)))
test.AssertEquals(t, responseWriter.Code, http.StatusNotFound)
assertJSONEquals(t, responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"No such challenge","status":404}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"No such challenge","status":404}`)
// Unspecified database error
errorURL := "error_result/24"
@ -1119,7 +1119,7 @@ func TestChallenge(t *testing.T) {
signRequest(t, `{"resource":"challenge"}`, wfe.nonceService)))
test.AssertEquals(t, responseWriter.Code, http.StatusInternalServerError)
assertJSONEquals(t, responseWriter.Body.String(),
`{"type":"urn:acme:error:serverInternal","detail":"Problem getting authorization","status":500}`)
`{"type":"`+probs.V1ErrorNS+`serverInternal","detail":"Problem getting authorization","status":500}`)
}
@ -1137,7 +1137,7 @@ func TestBadNonce(t *testing.T) {
test.AssertNotError(t, err, "Failed to sign body")
wfe.NewRegistration(ctx, newRequestEvent(), responseWriter,
makePostRequest(result.FullSerialize()))
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:badNonce","detail":"JWS has no anti-replay nonce","status":400}`)
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"`+probs.V1ErrorNS+`badNonce","detail":"JWS has no anti-replay nonce","status":400}`)
}
func TestNewECDSARegistration(t *testing.T) {
@ -1176,7 +1176,7 @@ func TestNewECDSARegistration(t *testing.T) {
test.AssertNotError(t, err, "Failed to signer.Sign")
wfe.NewRegistration(ctx, newRequestEvent(), responseWriter, makePostRequest(result.FullSerialize()))
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Registration key is already in use","status":409}`)
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"`+probs.V1ErrorNS+`malformed","detail":"Registration key is already in use","status":409}`)
test.AssertEquals(t, responseWriter.Header().Get("Location"), "http://localhost/acme/reg/3")
test.AssertEquals(t, responseWriter.Code, 409)
}
@ -1209,7 +1209,7 @@ func TestEmptyRegistration(t *testing.T) {
makePostRequestWithPath("1", emptyBody.FullSerialize()))
// There should be no error
test.AssertNotContains(t, responseWriter.Body.String(), "urn:acme:error")
test.AssertNotContains(t, responseWriter.Body.String(), probs.V1ErrorNS)
// We should get back a populated Registration
var reg core.Registration
@ -1245,7 +1245,7 @@ func TestNewRegistration(t *testing.T) {
Method: "GET",
URL: mustParseURL(newRegPath),
},
`{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`,
`{"type":"` + probs.V1ErrorNS + `malformed","detail":"Method not allowed","status":405}`,
},
// POST, but no body.
@ -1257,19 +1257,19 @@ func TestNewRegistration(t *testing.T) {
"Content-Length": {"0"},
},
},
`{"type":"urn:acme:error:malformed","detail":"No body on POST","status":400}`,
`{"type":"` + probs.V1ErrorNS + `malformed","detail":"No body on POST","status":400}`,
},
// POST, but body that isn't valid JWS
{
makePostRequestWithPath(newRegPath, "hi"),
`{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`,
`{"type":"` + probs.V1ErrorNS + `malformed","detail":"Parse error reading JWS","status":400}`,
},
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
{
makePostRequestWithPath(newRegPath, fooBody.FullSerialize()),
`{"type":"urn:acme:error:malformed","detail":"Request payload did not parse as JSON","status":400}`,
`{"type":"` + probs.V1ErrorNS + `malformed","detail":"Request payload did not parse as JSON","status":400}`,
},
// Same signed body, but payload modified by one byte, breaking signature.
@ -1289,11 +1289,11 @@ func TestNewRegistration(t *testing.T) {
"signature": "RjUQ679fxJgeAJlxqgvDP_sfGZnJ-1RgWF2qmcbnBWljs6h1qp63pLnJOl13u81bP_bCSjaWkelGG8Ymx_X-aQ"
}
`),
`{"type":"urn:acme:error:malformed","detail":"JWS verification error","status":400}`,
`{"type":"` + probs.V1ErrorNS + `malformed","detail":"JWS verification error","status":400}`,
},
{
makePostRequestWithPath(newRegPath, wrongAgreementBody.FullSerialize()),
`{"type":"urn:acme:error:malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [` + agreementURL + `]","status":400}`,
`{"type":"` + probs.V1ErrorNS + `malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [` + agreementURL + `]","status":400}`,
},
}
for _, rt := range regErrTests {
@ -1340,7 +1340,7 @@ func TestNewRegistration(t *testing.T) {
makePostRequest(result.FullSerialize()))
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Registration key is already in use","status":409}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Registration key is already in use","status":409}`)
test.AssertEquals(
t, responseWriter.Header().Get("Location"),
"http://localhost/acme/reg/1")
@ -1460,7 +1460,7 @@ func TestRevokeCertificateReasons(t *testing.T) {
wfe.RevokeCertificate(ctx, newRequestEvent(), responseWriter,
makePostRequest(result.FullSerialize()))
test.AssertEquals(t, responseWriter.Code, 400)
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"unsupported revocation reason code provided","status":400}`)
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"`+probs.V1ErrorNS+`malformed","detail":"unsupported revocation reason code provided","status":400}`)
responseWriter = httptest.NewRecorder()
unsupported = revocation.Reason(100)
@ -1471,7 +1471,7 @@ func TestRevokeCertificateReasons(t *testing.T) {
wfe.RevokeCertificate(ctx, newRequestEvent(), responseWriter,
makePostRequest(result.FullSerialize()))
test.AssertEquals(t, responseWriter.Code, 400)
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"unsupported revocation reason code provided","status":400}`)
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"`+probs.V1ErrorNS+`malformed","detail":"unsupported revocation reason code provided","status":400}`)
}
// Valid revocation request for existing, non-revoked cert, signed with account
@ -1511,7 +1511,7 @@ func TestRevokeCertificateWrongKey(t *testing.T) {
makePostRequest(result.FullSerialize()))
test.AssertEquals(t, responseWriter.Code, 403)
assertJSONEquals(t, responseWriter.Body.String(),
`{"type":"urn:acme:error:unauthorized","detail":"Revocation request must be signed by private key of cert to be revoked, by the account key of the account that issued it, or by the account key of an account that holds valid authorizations for all names in the certificate.","status":403}`)
`{"type":"`+probs.V1ErrorNS+`unauthorized","detail":"Revocation request must be signed by private key of cert to be revoked, by the account key of the account that issued it, or by the account key of an account that holds valid authorizations for all names in the certificate.","status":403}`)
}
// Valid revocation request for already-revoked cert
@ -1548,7 +1548,7 @@ func TestRevokeCertificateAlreadyRevoked(t *testing.T) {
makePostRequest(result.FullSerialize()))
test.AssertEquals(t, responseWriter.Code, 409)
assertJSONEquals(t, responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Certificate already revoked","status":409}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Certificate already revoked","status":409}`)
}
func TestRevokeCertificateWithAuthz(t *testing.T) {
@ -1579,7 +1579,7 @@ func TestAuthorization(t *testing.T) {
Method: "GET",
URL: mustParseURL(newAuthzPath),
})
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"`+probs.V1ErrorNS+`malformed","detail":"Method not allowed","status":405}`)
// POST, but no body.
responseWriter.Body.Reset()
@ -1589,12 +1589,12 @@ func TestAuthorization(t *testing.T) {
"Content-Length": {"0"},
},
})
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"No body on POST","status":400}`)
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"`+probs.V1ErrorNS+`malformed","detail":"No body on POST","status":400}`)
// POST, but body that isn't valid JWS
responseWriter.Body.Reset()
wfe.NewAuthorization(ctx, newRequestEvent(), responseWriter, makePostRequest("hi"))
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`)
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"`+probs.V1ErrorNS+`malformed","detail":"Parse error reading JWS","status":400}`)
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
responseWriter.Body.Reset()
@ -1602,7 +1602,7 @@ func TestAuthorization(t *testing.T) {
makePostRequest(signRequest(t, "foo", wfe.nonceService)))
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Request payload did not parse as JSON","status":400}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Request payload did not parse as JSON","status":400}`)
// Same signed body, but payload modified by one byte, breaking signature.
// should fail JWS verification.
@ -1623,7 +1623,7 @@ func TestAuthorization(t *testing.T) {
`))
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"JWS verification error","status":400}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"JWS verification error","status":400}`)
responseWriter.Body.Reset()
wfe.NewAuthorization(ctx, newRequestEvent(), responseWriter,
@ -1651,7 +1651,7 @@ func TestAuthorization(t *testing.T) {
})
test.AssertEquals(t, responseWriter.Code, http.StatusNotFound)
assertJSONEquals(t, responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Expired authorization","status":404}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Expired authorization","status":404}`)
responseWriter.Body.Reset()
// Ensure that a valid authorization can't be reached with an invalid URL
@ -1660,7 +1660,48 @@ func TestAuthorization(t *testing.T) {
Method: "GET",
})
assertJSONEquals(t, responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to find authorization","status":404}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Unable to find authorization","status":404}`)
}
// TestAuthorizationChallengeNamespace tests that the runtime prefixing of
// Challenge Problem Types works as expected
func TestAuthorizationChallengeNamespace(t *testing.T) {
wfe, clk := setupWFE(t)
mockSA := &mocks.SAWithFailedChallenges{Clk: clk}
wfe.SA = mockSA
// For "oldNS" the SA mock returns an authorization with a failed challenge
// that has an error with the type already prefixed by the v1 error NS
authzURL := "oldNS"
responseWriter := httptest.NewRecorder()
wfe.Authorization(ctx, newRequestEvent(), responseWriter, &http.Request{
Method: "GET",
URL: mustParseURL(authzURL),
})
var authz core.Authorization
err := json.Unmarshal([]byte(responseWriter.Body.String()), &authz)
test.AssertNotError(t, err, "Couldn't unmarshal returned authorization object")
test.AssertEquals(t, len(authz.Challenges), 1)
// The Challenge Error Type should have its prefix unmodified
test.AssertEquals(t, string(authz.Challenges[0].Error.Type), probs.V1ErrorNS+"things:are:whack")
// For "failed" the SA mock returns an authorization with a failed challenge
// that has an error with the type not prefixed by an error namespace.
authzURL = "failed"
responseWriter = httptest.NewRecorder()
wfe.Authorization(ctx, newRequestEvent(), responseWriter, &http.Request{
Method: "GET",
URL: mustParseURL(authzURL),
})
err = json.Unmarshal([]byte(responseWriter.Body.String()), &authz)
test.AssertNotError(t, err, "Couldn't unmarshal returned authorization object")
test.AssertEquals(t, len(authz.Challenges), 1)
// The Challenge Error Type should have had the probs.V1ErrorNS prefix added
test.AssertEquals(t, string(authz.Challenges[0].Error.Type), probs.V1ErrorNS+"things:are:whack")
responseWriter.Body.Reset()
}
func contains(s []string, e string) bool {
@ -1685,7 +1726,7 @@ func TestRegistration(t *testing.T) {
})
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Method not allowed","status":405}`)
responseWriter.Body.Reset()
// Test GET proper entry returns 405
@ -1695,14 +1736,14 @@ func TestRegistration(t *testing.T) {
})
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Method not allowed","status":405}`)
responseWriter.Body.Reset()
// Test POST invalid JSON
wfe.Registration(ctx, newRequestEvent(), responseWriter, makePostRequestWithPath("2", "invalid"))
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Parse error reading JWS","status":400}`)
responseWriter.Body.Reset()
key := loadPrivateKey(t, []byte(test2KeyPrivatePEM))
@ -1717,7 +1758,7 @@ func TestRegistration(t *testing.T) {
makePostRequestWithPath("2", result.FullSerialize()))
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:unauthorized","detail":"No registration exists matching provided key","status":403}`)
`{"type":"`+probs.V1ErrorNS+`unauthorized","detail":"No registration exists matching provided key","status":403}`)
responseWriter.Body.Reset()
key = loadPrivateKey(t, []byte(test1KeyPrivatePEM))
@ -1733,7 +1774,7 @@ func TestRegistration(t *testing.T) {
makePostRequestWithPath("1", result.FullSerialize()))
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [`+agreementURL+`]","status":400}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [`+agreementURL+`]","status":400}`)
responseWriter.Body.Reset()
// Test POST valid JSON with registration up in the mock (with correct agreement URL)
@ -1741,7 +1782,7 @@ func TestRegistration(t *testing.T) {
test.AssertNotError(t, err, "Couldn't sign")
wfe.Registration(ctx, newRequestEvent(), responseWriter,
makePostRequestWithPath("1", result.FullSerialize()))
test.AssertNotContains(t, responseWriter.Body.String(), "urn:acme:error")
test.AssertNotContains(t, responseWriter.Body.String(), probs.V1ErrorNS)
links := responseWriter.Header()["Link"]
test.AssertEquals(t, contains(links, "<http://localhost/acme/new-authz>;rel=\"next\""), true)
test.AssertEquals(t, contains(links, "<"+agreementURL+">;rel=\"terms-of-service\""), true)
@ -1753,7 +1794,7 @@ func TestRegistration(t *testing.T) {
wfe.Registration(ctx, newRequestEvent(), responseWriter,
makePostRequestWithPath("/a/bunch/of/garbage/1", result.FullSerialize()))
test.AssertContains(t, responseWriter.Body.String(), "400")
test.AssertContains(t, responseWriter.Body.String(), "urn:acme:error:malformed")
test.AssertContains(t, responseWriter.Body.String(), probs.V1ErrorNS+"malformed")
responseWriter.Body.Reset()
// Test POST valid JSON with registration up in the mock (with old agreement URL)
@ -1763,7 +1804,7 @@ func TestRegistration(t *testing.T) {
test.AssertNotError(t, err, "Couldn't sign")
wfe.Registration(ctx, newRequestEvent(), responseWriter,
makePostRequestWithPath("1", result.FullSerialize()))
test.AssertNotContains(t, responseWriter.Body.String(), "urn:acme:error")
test.AssertNotContains(t, responseWriter.Body.String(), probs.V1ErrorNS)
links = responseWriter.Header()["Link"]
test.AssertEquals(t, contains(links, "<http://localhost/acme/new-authz>;rel=\"next\""), true)
test.AssertEquals(t, contains(links, "<http://example.invalid/new-terms>;rel=\"terms-of-service\""), true)
@ -1852,7 +1893,7 @@ func TestGetCertificate(t *testing.T) {
mux.ServeHTTP(responseWriter, req)
test.AssertEquals(t, responseWriter.Code, 404)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache")
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found","status":404}`)
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"`+probs.V1ErrorNS+`malformed","detail":"Certificate not found","status":404}`)
reqlogs = mockLog.GetAllMatching(`Terminated request`)
test.AssertEquals(t, len(reqlogs), 1)
@ -1864,7 +1905,7 @@ func TestGetCertificate(t *testing.T) {
mux.ServeHTTP(responseWriter, req)
test.AssertEquals(t, responseWriter.Code, 404)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache")
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found","status":404}`)
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"`+probs.V1ErrorNS+`malformed","detail":"Certificate not found","status":404}`)
// Invalid serial, no cache
responseWriter = httptest.NewRecorder()
@ -1872,7 +1913,7 @@ func TestGetCertificate(t *testing.T) {
mux.ServeHTTP(responseWriter, req)
test.AssertEquals(t, responseWriter.Code, 404)
test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache")
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Certificate not found","status":404}`)
assertJSONEquals(t, responseWriter.Body.String(), `{"type":"`+probs.V1ErrorNS+`malformed","detail":"Certificate not found","status":404}`)
}
func assertCsrLogged(t *testing.T, mockLog *blog.Mock) {
@ -1975,7 +2016,7 @@ func TestBadKeyCSR(t *testing.T) {
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Invalid key in certificate request :: key too small: 512","status":400}`)
`{"type":"`+probs.V1ErrorNS+`malformed","detail":"Invalid key in certificate request :: key too small: 512","status":400}`)
}
// This uses httptest.NewServer because ServeMux.ServeHTTP won't prevent the
@ -2072,7 +2113,7 @@ func TestDeactivateAuthorization(t *testing.T) {
makePostRequestWithPath("valid", signRequest(t, `{"resource":"authz","status":""}`, wfe.nonceService)))
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type": "urn:acme:error:malformed","detail": "Invalid status value","status": 400}`)
`{"type": "`+probs.V1ErrorNS+`malformed","detail": "Invalid status value","status": 400}`)
responseWriter.Body.Reset()
wfe.Authorization(ctx, newRequestEvent(), responseWriter,
@ -2104,7 +2145,7 @@ func TestDeactivateRegistration(t *testing.T) {
makePostRequestWithPath("1", signRequest(t, `{"resource":"reg","status":"asd"}`, wfe.nonceService)))
assertJSONEquals(t,
responseWriter.Body.String(),
`{"type": "urn:acme:error:malformed","detail": "Invalid value provided for status field","status": 400}`)
`{"type": "`+probs.V1ErrorNS+`malformed","detail": "Invalid value provided for status field","status": 400}`)
responseWriter.Body.Reset()
wfe.Registration(ctx, newRequestEvent(), responseWriter,
@ -2164,7 +2205,7 @@ func TestDeactivateRegistration(t *testing.T) {
assertJSONEquals(t,
responseWriter.Body.String(),
`{
"type": "urn:acme:error:unauthorized",
"type": "`+probs.V1ErrorNS+`unauthorized",
"detail": "Registration is not valid, has status 'deactivated'",
"status": 403
}`)
@ -2183,7 +2224,7 @@ func TestKeyRollover(t *testing.T) {
assertJSONEquals(t,
responseWriter.Body.String(),
`{
"type": "urn:acme:error:malformed",
"type": "`+probs.V1ErrorNS+`malformed",
"detail": "Parse error reading JWS",
"status": 400
}`)
@ -2196,7 +2237,7 @@ func TestKeyRollover(t *testing.T) {
// Missing account URL
"{}",
`{
"type": "urn:acme:error:malformed",
"type": "` + probs.V1ErrorNS + `malformed",
"detail": "Incorrect account URL provided in payload",
"status": 400
}`,
@ -2205,7 +2246,7 @@ func TestKeyRollover(t *testing.T) {
{
`{"account":"http://localhost/acme/reg/1"}`,
`{
"type": "urn:acme:error:malformed",
"type": "` + probs.V1ErrorNS + `malformed",
"detail": "Unable to marshal new JWK",
"status": 400
}`,
@ -2214,7 +2255,7 @@ func TestKeyRollover(t *testing.T) {
{
`{"newKey":{"kty":"RSA","n":"yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ","e":"AQAB"},"account":"http://localhost/acme/reg/1"}`,
`{
"type": "urn:acme:error:malformed",
"type": "` + probs.V1ErrorNS + `malformed",
"detail": "New JWK in inner payload doesn't match key used to sign inner JWS",
"status": 400
}`,

View File

@ -406,8 +406,10 @@ func (wfe *WebFrontEndImpl) Directory(ctx context.Context, logEvent *requestEven
// sendError sends an error response represented by the given ProblemDetails,
// and, if the ProblemDetails.Type is ServerInternalProblem, audit logs the
// internal ierr.
// internal ierr. The rendered Problem will have its Type prefixed with the ACME
// v2 error namespace.
func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, logEvent *requestEvent, prob *probs.ProblemDetails, ierr error) {
// Determine the HTTP status code to use for this problem
code := probs.ProblemDetailsToStatusCode(prob)
// Record details to the log event
@ -423,23 +425,21 @@ func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, logEvent *re
}
}
// Increment a stat for this problem type
wfe.stats.httpErrorCount.With(prometheus.Labels{"type": string(prob.Type)}).Inc()
// Prefix the problem type with the ACME V2 error namespace and marshal to JSON
prob.Type = probs.V2ErrorNS + prob.Type
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
// Write the JSON problem response
response.Header().Set("Content-Type", "application/problem+json")
response.WriteHeader(code)
response.Write(problemDoc)
problemSegments := strings.Split(string(prob.Type), ":")
if len(problemSegments) > 0 {
probType := problemSegments[len(problemSegments)-1]
wfe.stats.httpErrorCount.With(prometheus.Labels{"type": probType}).Inc()
}
}
func link(url, relation string) string {
@ -829,12 +829,20 @@ func (wfe *WebFrontEndImpl) Challenge(
// 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) {
// Update the challenge URI to be relative to the HTTP request Host
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.
// Ensure the challenge ID isn't written. 0 is considered "empty" for the purpose of the JSON omitempty tag.
challenge.ID = 0
// Historically the Type field of a problem was always prefixed with a static
// error namespace. To support the V2 API and migrating to the correct IETF
// namespace we now prefix the Type with the correct namespace at runtime when
// we write the problem JSON to the user. We skip this process if the
// challenge error type has already been prefixed with the V1ErrorNS.
if challenge.Error != nil && !strings.HasPrefix(string(challenge.Error.Type), probs.V1ErrorNS) {
challenge.Error.Type = probs.V2ErrorNS + challenge.Error.Type
}
}
// prepAuthorizationForDisplay takes a core.Authorization and prepares it for

View File

@ -435,7 +435,7 @@ func TestHandleFunc(t *testing.T) {
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), sortHeader(strings.Join(addHeadIfGet(c.allowed), ", ")))
test.AssertUnmarshaledEquals(t,
rw.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
`{"type":"`+probs.V2ErrorNS+`malformed","detail":"Method not allowed","status":405}`)
}
nonce := rw.Header().Get("Replay-Nonce")
test.AssertNotEquals(t, nonce, lastNonce)
@ -446,7 +446,7 @@ func TestHandleFunc(t *testing.T) {
// Disallowed method returns error JSON in body
runWrappedHandler(&http.Request{Method: "PUT"}, "GET", "POST")
test.AssertEquals(t, rw.Header().Get("Content-Type"), "application/problem+json")
test.AssertUnmarshaledEquals(t, rw.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
test.AssertUnmarshaledEquals(t, rw.Body.String(), `{"type":"`+probs.V2ErrorNS+`malformed","detail":"Method not allowed","status":405}`)
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), "GET, HEAD, POST")
// Disallowed method special case: response to HEAD has got no body
@ -460,7 +460,7 @@ func TestHandleFunc(t *testing.T) {
test.AssertEquals(t, rw.Code, http.StatusMethodNotAllowed)
test.AssertEquals(t, rw.Header().Get("Content-Type"), "application/problem+json")
test.AssertEquals(t, rw.Header().Get("Allow"), "POST")
test.AssertUnmarshaledEquals(t, rw.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
test.AssertUnmarshaledEquals(t, rw.Body.String(), `{"type":"`+probs.V2ErrorNS+`malformed","detail":"Method not allowed","status":405}`)
wfe.AllowOrigins = []string{"*"}
testOrigin := "https://example.com"
@ -941,19 +941,19 @@ func TestChallenge(t *testing.T) {
Name: "Expired challenge",
Path: "expired/23",
ExpectedStatus: http.StatusNotFound,
ExpectedBody: `{"type":"urn:acme:error:malformed","detail":"Expired authorization","status":404}`,
ExpectedBody: `{"type":"` + probs.V2ErrorNS + `malformed","detail":"Expired authorization","status":404}`,
},
{
Name: "Missing challenge",
Path: "",
ExpectedStatus: http.StatusNotFound,
ExpectedBody: `{"type":"urn:acme:error:malformed","detail":"No such challenge","status":404}`,
ExpectedBody: `{"type":"` + probs.V2ErrorNS + `malformed","detail":"No such challenge","status":404}`,
},
{
Name: "Unspecified database error",
Path: "error_result/24",
ExpectedStatus: http.StatusInternalServerError,
ExpectedBody: `{"type":"urn:acme:error:serverInternal","detail":"Problem getting authorization","status":500}`,
ExpectedBody: `{"type":"` + probs.V2ErrorNS + `serverInternal","detail":"Problem getting authorization","status":500}`,
},
}
@ -1000,7 +1000,7 @@ func TestBadNonce(t *testing.T) {
test.AssertNotError(t, err, "Failed to sign body")
wfe.NewAccount(ctx, newRequestEvent(), responseWriter,
makePostRequestWithPath("nonce", result.FullSerialize()))
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:badNonce","detail":"JWS has no anti-replay nonce","status":400}`)
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), `{"type":"`+probs.V2ErrorNS+`badNonce","detail":"JWS has no anti-replay nonce","status":400}`)
}
func TestNewECDSAAccount(t *testing.T) {
@ -1043,7 +1043,7 @@ func TestNewECDSAAccount(t *testing.T) {
// POST, Valid JSON, Key already in use
wfe.NewAccount(ctx, newRequestEvent(), responseWriter, request)
responseBody = responseWriter.Body.String()
test.AssertUnmarshaledEquals(t, responseBody, `{"type":"urn:acme:error:malformed","detail":"Account key is already in use","status":409}`)
test.AssertUnmarshaledEquals(t, responseBody, `{"type":"`+probs.V2ErrorNS+`malformed","detail":"Account key is already in use","status":409}`)
test.AssertEquals(t, responseWriter.Header().Get("Location"), "http://localhost/acme/acct/3")
test.AssertEquals(t, responseWriter.Code, 409)
}
@ -1078,7 +1078,7 @@ func TestEmptyAccount(t *testing.T) {
responseBody := responseWriter.Body.String()
// There should be no error
test.AssertNotContains(t, responseBody, "urn:acme:error")
test.AssertNotContains(t, responseBody, probs.V2ErrorNS)
// We should get back a populated Account
var acct core.Registration
@ -1122,19 +1122,19 @@ func TestNewAccount(t *testing.T) {
"Content-Length": {"0"},
},
},
`{"type":"urn:acme:error:malformed","detail":"No body on POST","status":400}`,
`{"type":"` + probs.V2ErrorNS + `malformed","detail":"No body on POST","status":400}`,
},
// POST, but body that isn't valid JWS
{
makePostRequestWithPath(newAcctPath, "hi"),
`{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`,
`{"type":"` + probs.V2ErrorNS + `malformed","detail":"Parse error reading JWS","status":400}`,
},
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
{
makePostRequestWithPath(newAcctPath, fooBody),
`{"type":"urn:acme:error:malformed","detail":"Request payload did not parse as JSON","status":400}`,
`{"type":"` + probs.V2ErrorNS + `malformed","detail":"Request payload did not parse as JSON","status":400}`,
},
// Same signed body, but payload modified by one byte, breaking signature.
@ -1142,11 +1142,11 @@ func TestNewAccount(t *testing.T) {
{
makePostRequestWithPath(newAcctPath,
`{"payload":"Zm9x","protected":"eyJhbGciOiJSUzI1NiIsImp3ayI6eyJrdHkiOiJSU0EiLCJuIjoicW5BUkxyVDdYejRnUmNLeUxkeWRtQ3ItZXk5T3VQSW1YNFg0MHRoazNvbjI2RmtNem5SM2ZSanM2NmVMSzdtbVBjQlo2dU9Kc2VVUlU2d0FhWk5tZW1vWXgxZE12cXZXV0l5aVFsZUhTRDdROHZCcmhSNnVJb080akF6SlpSLUNoelp1U0R0N2lITi0zeFVWc3B1NVhHd1hVX01WSlpzaFR3cDRUYUZ4NWVsSElUX09iblR2VE9VM1hoaXNoMDdBYmdaS21Xc1ZiWGg1cy1DcklpY1U0T2V4SlBndW5XWl9ZSkp1ZU9LbVR2bkxsVFY0TXpLUjJvWmxCS1oyN1MwLVNmZFZfUUR4X3lkbGU1b01BeUtWdGxBVjM1Y3lQTUlzWU53Z1VHQkNkWV8yVXppNWVYMGxUYzdNUFJ3ejZxUjFraXAtaTU5VmNHY1VRZ3FIVjZGeXF3IiwiZSI6IkFRQUIifSwia2lkIjoiIiwibm9uY2UiOiJyNHpuenZQQUVwMDlDN1JwZUtYVHhvNkx3SGwxZVBVdmpGeXhOSE1hQnVvIiwidXJsIjoiaHR0cDovL2xvY2FsaG9zdC9hY21lL25ldy1yZWcifQ","signature":"jcTdxSygm_cvD7KbXqsxgnoPApCTSkV4jolToSOd2ciRkg5W7Yl0ZKEEKwOc-dYIbQiwGiDzisyPCicwWsOUA1WSqHylKvZ3nxSMc6KtwJCW2DaOqcf0EEjy5VjiZJUrOt2c-r6b07tbn8sfOJKwlF2lsOeGi4s-rtvvkeQpAU-AWauzl9G4bv2nDUeCviAZjHx_PoUC-f9GmZhYrbDzAvXZ859ktM6RmMeD0OqPN7bhAeju2j9Gl0lnryZMtq2m0J2m1ucenQBL1g4ZkP1JiJvzd2cAz5G7Ftl2YeJJyWhqNd3qq0GVOt1P11s8PTGNaSoM0iR9QfUxT9A6jxARtg"}`),
`{"type":"urn:acme:error:malformed","detail":"JWS verification error","status":400}`,
`{"type":"` + probs.V2ErrorNS + `malformed","detail":"JWS verification error","status":400}`,
},
{
makePostRequestWithPath(newAcctPath, wrongAgreementBody),
`{"type":"urn:acme:error:malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [` + agreementURL + `]","status":400}`,
`{"type":"` + probs.V2ErrorNS + `malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [` + agreementURL + `]","status":400}`,
},
}
for _, rt := range acctErrTests {
@ -1192,7 +1192,7 @@ func TestNewAccount(t *testing.T) {
wfe.NewAccount(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Account key is already in use","status":409}`)
`{"type":"`+probs.V2ErrorNS+`malformed","detail":"Account key is already in use","status":409}`)
test.AssertEquals(
t, responseWriter.Header().Get("Location"),
"http://localhost/acme/acct/1")
@ -1211,7 +1211,7 @@ func TestGetAuthorization(t *testing.T) {
})
test.AssertEquals(t, responseWriter.Code, http.StatusNotFound)
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Expired authorization","status":404}`)
`{"type":"`+probs.V2ErrorNS+`malformed","detail":"Expired authorization","status":404}`)
responseWriter.Body.Reset()
// Ensure that a valid authorization can't be reached with an invalid URL
@ -1220,7 +1220,48 @@ func TestGetAuthorization(t *testing.T) {
Method: "GET",
})
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Unable to find authorization","status":404}`)
`{"type":"`+probs.V2ErrorNS+`malformed","detail":"Unable to find authorization","status":404}`)
}
// TestAuthorizationChallengeNamespace tests that the runtime prefixing of
// Challenge Problem Types works as expected
func TestAuthorizationChallengeNamespace(t *testing.T) {
wfe, clk := setupWFE(t)
mockSA := &mocks.SAWithFailedChallenges{Clk: clk}
wfe.SA = mockSA
// For "oldNS" the SA mock returns an authorization with a failed challenge
// that has an error with the type already prefixed by the v1 error NS
authzURL := "oldNS"
responseWriter := httptest.NewRecorder()
wfe.Authorization(ctx, newRequestEvent(), responseWriter, &http.Request{
Method: "GET",
URL: mustParseURL(authzURL),
})
var authz core.Authorization
err := json.Unmarshal([]byte(responseWriter.Body.String()), &authz)
test.AssertNotError(t, err, "Couldn't unmarshal returned authorization object")
test.AssertEquals(t, len(authz.Challenges), 1)
// The Challenge Error Type should have its prefix unmodified
test.AssertEquals(t, string(authz.Challenges[0].Error.Type), probs.V1ErrorNS+"things:are:whack")
// For "failed" the SA mock returns an authorization with a failed challenge
// that has an error with the type not prefixed by an error namespace.
authzURL = "failed"
responseWriter = httptest.NewRecorder()
wfe.Authorization(ctx, newRequestEvent(), responseWriter, &http.Request{
Method: "GET",
URL: mustParseURL(authzURL),
})
err = json.Unmarshal([]byte(responseWriter.Body.String()), &authz)
test.AssertNotError(t, err, "Couldn't unmarshal returned authorization object")
test.AssertEquals(t, len(authz.Challenges), 1)
// The Challenge Error Type should have had the probs.V2ErrorNS prefix added
test.AssertEquals(t, string(authz.Challenges[0].Error.Type), probs.V2ErrorNS+"things:are:whack")
responseWriter.Body.Reset()
}
func contains(s []string, e string) bool {
@ -1244,14 +1285,14 @@ func TestAccount(t *testing.T) {
})
test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Method not allowed","status":405}`)
`{"type":"`+probs.V2ErrorNS+`malformed","detail":"Method not allowed","status":405}`)
responseWriter.Body.Reset()
// Test POST invalid JSON
wfe.Account(ctx, newRequestEvent(), responseWriter, makePostRequestWithPath("2", "invalid"))
test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`)
`{"type":"`+probs.V2ErrorNS+`malformed","detail":"Parse error reading JWS","status":400}`)
responseWriter.Body.Reset()
key := loadKey(t, []byte(test2KeyPrivatePEM))
@ -1269,7 +1310,7 @@ func TestAccount(t *testing.T) {
wfe.Account(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(),
`{"type":"urn:ietf:params:acme:error:accountDoesNotExist","detail":"Account \"http://localhost/acme/acct/102\" not found","status":400}`)
`{"type":"`+probs.V2ErrorNS+`accountDoesNotExist","detail":"Account \"http://localhost/acme/acct/102\" not found","status":400}`)
responseWriter.Body.Reset()
key = loadKey(t, []byte(test1KeyPrivatePEM))
@ -1286,7 +1327,7 @@ func TestAccount(t *testing.T) {
wfe.Account(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [`+agreementURL+`]","status":400}`)
`{"type":"`+probs.V2ErrorNS+`malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [`+agreementURL+`]","status":400}`)
responseWriter.Body.Reset()
// Test POST valid JSON with account up in the mock (with correct agreement URL)
@ -1295,7 +1336,7 @@ func TestAccount(t *testing.T) {
request = makePostRequestWithPath(path, body)
wfe.Account(ctx, newRequestEvent(), responseWriter, request)
test.AssertNotContains(t, responseWriter.Body.String(), "urn:acme:error")
test.AssertNotContains(t, responseWriter.Body.String(), probs.V2ErrorNS)
links := responseWriter.Header()["Link"]
test.AssertEquals(t, contains(links, "<"+agreementURL+">;rel=\"terms-of-service\""), true)
responseWriter.Body.Reset()
@ -1308,7 +1349,7 @@ func TestAccount(t *testing.T) {
wfe.Account(ctx, newRequestEvent(), responseWriter, request)
test.AssertContains(t, responseWriter.Body.String(), "400")
test.AssertContains(t, responseWriter.Body.String(), "urn:acme:error:malformed")
test.AssertContains(t, responseWriter.Body.String(), probs.V2ErrorNS+"malformed")
responseWriter.Body.Reset()
// Test POST valid JSON with account up in the mock (with old agreement URL)
@ -1320,7 +1361,7 @@ func TestAccount(t *testing.T) {
request = makePostRequestWithPath(path, body)
wfe.Account(ctx, newRequestEvent(), responseWriter, request)
test.AssertNotContains(t, responseWriter.Body.String(), "urn:acme:error")
test.AssertNotContains(t, responseWriter.Body.String(), probs.V2ErrorNS)
links = responseWriter.Header()["Link"]
test.AssertEquals(t, contains(links, "<http://example.invalid/new-terms>;rel=\"terms-of-service\""), true)
responseWriter.Body.Reset()
@ -1368,7 +1409,7 @@ func TestGetCertificate(t *testing.T) {
noCache := "public, max-age=0, no-cache"
goodSerial := "/acme/cert/0000000000000000000000000000000000b2"
notFound := `{"type":"urn:acme:error:malformed","detail":"Certificate not found","status":404}`
notFound := `{"type":"` + probs.V2ErrorNS + `malformed","detail":"Certificate not found","status":404}`
testCases := []struct {
Name string
@ -1547,7 +1588,7 @@ func TestDeactivateAuthorization(t *testing.T) {
wfe.Authorization(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(),
`{"type": "urn:acme:error:malformed","detail": "Invalid status value","status": 400}`)
`{"type": "`+probs.V2ErrorNS+`malformed","detail": "Invalid status value","status": 400}`)
responseWriter.Body.Reset()
payload = `{"status":"deactivated"}`
@ -1587,7 +1628,7 @@ func TestDeactivateAccount(t *testing.T) {
wfe.Account(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(),
`{"type": "urn:acme:error:malformed","detail": "Invalid value provided for status field","status": 400}`)
`{"type": "`+probs.V2ErrorNS+`malformed","detail": "Invalid value provided for status field","status": 400}`)
responseWriter.Body.Reset()
payload = `{"status":"deactivated"}`
@ -1652,7 +1693,7 @@ func TestDeactivateAccount(t *testing.T) {
test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(),
`{
"type": "urn:acme:error:unauthorized",
"type": "`+probs.V2ErrorNS+`unauthorized",
"detail": "Account is not valid, has status \"deactivated\"",
"status": 403
}`)
@ -1691,27 +1732,27 @@ func TestNewOrder(t *testing.T) {
"Content-Length": {"0"},
},
},
ExpectedBody: `{"type":"urn:acme:error:malformed","detail":"No body on POST","status":400}`,
ExpectedBody: `{"type":"` + probs.V2ErrorNS + `malformed","detail":"No body on POST","status":400}`,
},
{
Name: "POST, with an invalid JWS body",
Request: makePostRequestWithPath("hi", "hi"),
ExpectedBody: `{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`,
ExpectedBody: `{"type":"` + probs.V2ErrorNS + `malformed","detail":"Parse error reading JWS","status":400}`,
},
{
Name: "POST, properly signed JWS, payload isn't valid",
Request: signAndPost(t, targetPath, signedURL, "foo", 1, wfe.nonceService),
ExpectedBody: `{"type":"urn:acme:error:malformed","detail":"Request payload did not parse as JSON","status":400}`,
ExpectedBody: `{"type":"` + probs.V2ErrorNS + `malformed","detail":"Request payload did not parse as JSON","status":400}`,
},
{
Name: "POST, properly signed JWS, trivial JSON payload",
Request: signAndPost(t, targetPath, signedURL, "{}", 1, wfe.nonceService),
ExpectedBody: `{"type":"urn:acme:error:malformed","detail":"Error parsing certificate request: asn1: syntax error: sequence truncated","status":400}`,
ExpectedBody: `{"type":"` + probs.V2ErrorNS + `malformed","detail":"Error parsing certificate request: asn1: syntax error: sequence truncated","status":400}`,
},
{
Name: "POST, properly signed JWS, CSR from an old OpenSSL",
Request: signAndPost(t, targetPath, signedURL, oldOpenSSLCSRPayload, 1, wfe.nonceService),
ExpectedBody: `{"type":"urn:acme:error:malformed","detail":"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","status":400}`,
ExpectedBody: `{"type":"` + probs.V2ErrorNS + `malformed","detail":"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","status":400}`,
},
{
Name: "POST, properly signed JWS, authorizations for all names in CSR",
@ -1755,7 +1796,7 @@ func TestKeyRollover(t *testing.T) {
test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(),
`{
"type": "urn:acme:error:malformed",
"type": "`+probs.V2ErrorNS+`malformed",
"detail": "Parse error reading JWS",
"status": 400
}`)
@ -1771,7 +1812,7 @@ func TestKeyRollover(t *testing.T) {
Name: "Missing account URL",
Payload: `{"newKey":` + string(newJWKJSON) + `}`,
ExpectedResponse: `{
"type": "urn:acme:error:malformed",
"type": "` + probs.V2ErrorNS + `malformed",
"detail": "Inner key rollover request specified Account \"\", but outer JWS has Key ID \"http://localhost/acme/acct/1\"",
"status": 400
}`,
@ -1782,7 +1823,7 @@ func TestKeyRollover(t *testing.T) {
Name: "Missing new key from inner payload",
Payload: `{"account":"http://localhost/acme/acct/1"}`,
ExpectedResponse: `{
"type": "urn:acme:error:malformed",
"type": "` + probs.V2ErrorNS + `malformed",
"detail": "Inner JWS does not verify with specified new key",
"status": 400
}`,
@ -1792,7 +1833,7 @@ func TestKeyRollover(t *testing.T) {
Name: "New key is the same as the old key",
Payload: `{"newKey":{"kty":"RSA","n":"yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ","e":"AQAB"},"account":"http://localhost/acme/acct/1"}`,
ExpectedResponse: `{
"type": "urn:acme:error:malformed",
"type": "` + probs.V2ErrorNS + `malformed",
"detail": "New key specified by rollover request is the same as the old key",
"status": 400
}`,
@ -1802,7 +1843,7 @@ func TestKeyRollover(t *testing.T) {
Name: "Inner JWS signed by the wrong key",
Payload: `{"newKey":` + string(newJWKJSON) + `,"account":"http://localhost/acme/acct/1"}`,
ExpectedResponse: `{
"type": "urn:acme:error:malformed",
"type": "` + probs.V2ErrorNS + `malformed",
"detail": "Inner JWS does not verify with specified new key",
"status": 400
}`,
@ -1858,32 +1899,32 @@ func TestOrder(t *testing.T) {
{
Name: "404 request",
Path: "1/2",
Response: `{"type":"urn:acme:error:malformed","detail":"No order for ID 2", "status":404}`,
Response: `{"type":"` + probs.V2ErrorNS + `malformed","detail":"No order for ID 2", "status":404}`,
},
{
Name: "Invalid request path",
Path: "asd",
Response: `{"type":"urn:acme:error:malformed","detail":"Invalid request path","status":400}`,
Response: `{"type":"` + probs.V2ErrorNS + `malformed","detail":"Invalid request path","status":400}`,
},
{
Name: "Invalid account ID",
Path: "asd/asd",
Response: `{"type":"urn:acme:error:malformed","detail":"Invalid account ID","status":400}`,
Response: `{"type":"` + probs.V2ErrorNS + `malformed","detail":"Invalid account ID","status":400}`,
},
{
Name: "Invalid order ID",
Path: "1/asd",
Response: `{"type":"urn:acme:error:malformed","detail":"Invalid order ID","status":400}`,
Response: `{"type":"` + probs.V2ErrorNS + `malformed","detail":"Invalid order ID","status":400}`,
},
{
Name: "Real request, wrong account",
Path: "2/1",
Response: `{"type":"urn:acme:error:malformed","detail":"No order found for account ID 2", "status":404}`,
Response: `{"type":"` + probs.V2ErrorNS + `malformed","detail":"No order found for account ID 2", "status":404}`,
},
{
Name: "Internal error request",
Path: "1/3",
Response: `{"type":"urn:acme:error:serverInternal","detail":"Failed to retrieve order for ID 3","status":500}`,
Response: `{"type":"` + probs.V2ErrorNS + `serverInternal","detail":"Failed to retrieve order for ID 3","status":500}`,
},
}
@ -1993,13 +2034,13 @@ func TestRevokeCertificateReasons(t *testing.T) {
Name: "Unsupported reason",
Reason: &reason2,
ExpectedHTTPCode: http.StatusBadRequest,
ExpectedBody: `{"type":"urn:acme:error:malformed","detail":"unsupported revocation reason code provided","status":400}`,
ExpectedBody: `{"type":"` + probs.V2ErrorNS + `malformed","detail":"unsupported revocation reason code provided","status":400}`,
},
{
Name: "Non-existent reason",
Reason: &reason100,
ExpectedHTTPCode: http.StatusBadRequest,
ExpectedBody: `{"type":"urn:acme:error:malformed","detail":"unsupported revocation reason code provided","status":400}`,
ExpectedBody: `{"type":"` + probs.V2ErrorNS + `malformed","detail":"unsupported revocation reason code provided","status":400}`,
},
}
@ -2058,7 +2099,7 @@ func TestRevokeCertificateWrongKey(t *testing.T) {
test.AssertEquals(t, responseWriter.Code, 403)
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(),
`{"type":"urn:acme:error:unauthorized","detail":"The key ID specified in the revocation request does not hold valid authorizations for all names in the certificate to be revoked","status":403}`)
`{"type":"`+probs.V2ErrorNS+`unauthorized","detail":"The key ID specified in the revocation request does not hold valid authorizations for all names in the certificate to be revoked","status":403}`)
}
// Valid revocation request for already-revoked cert
@ -2092,7 +2133,7 @@ func TestRevokeCertificateAlreadyRevoked(t *testing.T) {
test.AssertEquals(t, responseWriter.Code, 409)
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Certificate already revoked","status":409}`)
`{"type":"`+probs.V2ErrorNS+`malformed","detail":"Certificate already revoked","status":409}`)
}
func TestRevokeCertificateWithAuthz(t *testing.T) {