Allow account IDs in authz and challenge URLs (#7768)

This adds new handlers under `/acme/authz/` and `/acme/chall/` that
expect to be followed by `{regID}/{authzID}` and
`{regID}/{authzID}/{challengeID}`, respectively. For deployability, the
old handlers continue to work, and the URLs returned for newly created
objects will still point to the paths used by the old handlers
(`/acme/authz-v3/` and `/acme/chall-v3/`).

There are some self-referential URLs in authz and challenge responses,
like the Location header, and the URL of challenges embedded in an
authorization object. This PR updates `prepAuthorizationForDisplay` and
`prepChallengeForDisplay` so those URLs can be generated consistently
with the path that was requested.

For the WFE tests, in most cases I duplicated an entire test and then
updated it to test the `WithAccount` handler. The idea is that once
we're fully switched over to the new format we can delete the tests for
the non-`WithAccount` variants.

Part of #7683
This commit is contained in:
Jacob Hoffman-Andrews 2024-11-06 11:52:10 -08:00 committed by GitHub
parent 2603aa45a8
commit 2058d985cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 541 additions and 69 deletions

View File

@ -57,16 +57,18 @@ const (
acctPath = "/acme/acct/"
// When we moved to authzv2, we used a "-v3" suffix to avoid confusion
// regarding ACMEv2.
authzPath = "/acme/authz-v3/"
challengePath = "/acme/chall-v3/"
certPath = "/acme/cert/"
revokeCertPath = "/acme/revoke-cert"
buildIDPath = "/build"
rolloverPath = "/acme/key-change"
newNoncePath = "/acme/new-nonce"
newOrderPath = "/acme/new-order"
orderPath = "/acme/order/"
finalizeOrderPath = "/acme/finalize/"
authzPath = "/acme/authz-v3/"
authzPathWithAcct = "/acme/authz/"
challengePath = "/acme/chall-v3/"
challengePathWithAcct = "/acme/chall/"
certPath = "/acme/cert/"
revokeCertPath = "/acme/revoke-cert"
buildIDPath = "/build"
rolloverPath = "/acme/key-change"
newNoncePath = "/acme/new-nonce"
newOrderPath = "/acme/new-order"
orderPath = "/acme/order/"
finalizeOrderPath = "/acme/finalize/"
getAPIPrefix = "/get/"
getOrderPath = getAPIPrefix + "order/"
@ -432,13 +434,15 @@ func (wfe *WebFrontEndImpl) Handler(stats prometheus.Registerer, oTelHTTPOptions
// TODO(@cpu): After November 1st, 2020 support for "GET" to the following
// endpoints will be removed, leaving only POST-as-GET support.
wfe.HandleFunc(m, orderPath, wfe.GetOrder, "GET", "POST")
wfe.HandleFunc(m, authzPath, wfe.Authorization, "GET", "POST")
wfe.HandleFunc(m, challengePath, wfe.Challenge, "GET", "POST")
wfe.HandleFunc(m, authzPath, wfe.AuthorizationHandler, "GET", "POST")
wfe.HandleFunc(m, authzPathWithAcct, wfe.AuthorizationHandlerWithAccount, "GET", "POST")
wfe.HandleFunc(m, challengePath, wfe.ChallengeHandler, "GET", "POST")
wfe.HandleFunc(m, challengePathWithAcct, wfe.ChallengeHandlerWithAccount, "GET", "POST")
wfe.HandleFunc(m, certPath, wfe.Certificate, "GET", "POST")
// Boulder-specific GET-able resource endpoints
wfe.HandleFunc(m, getOrderPath, wfe.GetOrder, "GET")
wfe.HandleFunc(m, getAuthzPath, wfe.Authorization, "GET")
wfe.HandleFunc(m, getChallengePath, wfe.Challenge, "GET")
wfe.HandleFunc(m, getAuthzPath, wfe.AuthorizationHandler, "GET")
wfe.HandleFunc(m, getChallengePath, wfe.ChallengeHandler, "GET")
wfe.HandleFunc(m, getCertPath, wfe.Certificate, "GET")
// Endpoint for draft-ietf-acme-ari
@ -1088,31 +1092,55 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(
response.WriteHeader(http.StatusOK)
}
// Challenge handles POST requests to challenge URLs.
// ChallengeHandler handles POST requests to challenge URLs of the form /acme/chall-v3/<authorizationID>/<challengeID>.
// Such requests are clients' responses to the server's challenges.
func (wfe *WebFrontEndImpl) Challenge(
func (wfe *WebFrontEndImpl) ChallengeHandler(
ctx context.Context,
logEvent *web.RequestEvent,
response http.ResponseWriter,
request *http.Request) {
notFound := func() {
wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil)
}
slug := strings.Split(request.URL.Path, "/")
if len(slug) != 2 {
notFound()
wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil)
return
}
authorizationID, err := strconv.ParseInt(slug[0], 10, 64)
wfe.Challenge(ctx, logEvent, challengePath, response, request, slug[0], slug[1])
}
// ChallengeHandlerWithAccount handles POST requests to challenge URLs of the form /acme/chall/{regID}/{authzID}/{challID}.
func (wfe *WebFrontEndImpl) ChallengeHandlerWithAccount(
ctx context.Context,
logEvent *web.RequestEvent,
response http.ResponseWriter,
request *http.Request) {
slug := strings.Split(request.URL.Path, "/")
if len(slug) != 3 {
wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil)
return
}
// TODO(#7683): the regID is currently ignored.
wfe.Challenge(ctx, logEvent, challengePathWithAcct, response, request, slug[1], slug[2])
}
// Challenge handles POSTS to both formats of challenge URLs.
func (wfe *WebFrontEndImpl) Challenge(
ctx context.Context,
logEvent *web.RequestEvent,
handlerPath string,
response http.ResponseWriter,
request *http.Request,
authorizationIDStr string,
challengeID string) {
authorizationID, err := strconv.ParseInt(authorizationIDStr, 10, 64)
if err != nil {
wfe.sendError(response, logEvent, probs.Malformed("Invalid authorization ID"), nil)
return
}
challengeID := slug[1]
authzPB, err := wfe.ra.GetAuthorization(ctx, &rapb.GetAuthorizationRequest{Id: authorizationID})
if err != nil {
if errors.Is(err, berrors.NotFound) {
notFound()
wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil)
} else {
wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Problem getting authorization"), err)
}
@ -1133,7 +1161,7 @@ func (wfe *WebFrontEndImpl) Challenge(
}
challengeIndex := authz.FindChallengeByStringID(challengeID)
if challengeIndex == -1 {
notFound()
wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil)
return
}
@ -1157,11 +1185,11 @@ func (wfe *WebFrontEndImpl) Challenge(
challenge := authz.Challenges[challengeIndex]
switch request.Method {
case "GET", "HEAD":
wfe.getChallenge(response, request, authz, &challenge, logEvent)
wfe.getChallenge(handlerPath, response, request, authz, &challenge, logEvent)
case "POST":
logEvent.ChallengeType = string(challenge.Type)
wfe.postChallenge(ctx, response, request, authz, challengeIndex, logEvent)
wfe.postChallenge(ctx, handlerPath, response, request, authz, challengeIndex, logEvent)
}
}
@ -1186,9 +1214,17 @@ func prepAccountForDisplay(acct *core.Registration) {
// prepChallengeForDisplay takes a core.Challenge and prepares it for display to
// the client by filling in its URL field and clearing several unnecessary
// fields.
func (wfe *WebFrontEndImpl) prepChallengeForDisplay(request *http.Request, authz core.Authorization, challenge *core.Challenge) {
func (wfe *WebFrontEndImpl) prepChallengeForDisplay(
handlerPath string,
request *http.Request,
authz core.Authorization,
challenge *core.Challenge,
) {
// Update the challenge URL to be relative to the HTTP request Host
challenge.URL = web.RelativeEndpoint(request, fmt.Sprintf("%s%s/%s", challengePath, authz.ID, challenge.StringID()))
if handlerPath == challengePathWithAcct || handlerPath == authzPathWithAcct {
challenge.URL = web.RelativeEndpoint(request, fmt.Sprintf("%s%d/%s/%s", challengePathWithAcct, authz.RegistrationID, authz.ID, challenge.StringID()))
}
// Internally, we store challenge error problems with just the short form
// (e.g. "CAA") of the problem type. But for external display, we need to
@ -1211,9 +1247,9 @@ func (wfe *WebFrontEndImpl) prepChallengeForDisplay(request *http.Request, authz
// prepAuthorizationForDisplay takes a core.Authorization and prepares it for
// display to the client by preparing all its challenges.
func (wfe *WebFrontEndImpl) prepAuthorizationForDisplay(request *http.Request, authz *core.Authorization) {
func (wfe *WebFrontEndImpl) prepAuthorizationForDisplay(handlerPath string, request *http.Request, authz *core.Authorization) {
for i := range authz.Challenges {
wfe.prepChallengeForDisplay(request, *authz, &authz.Challenges[i])
wfe.prepChallengeForDisplay(handlerPath, request, *authz, &authz.Challenges[i])
}
// Shuffle the challenges so no one relies on their order.
@ -1235,15 +1271,15 @@ func (wfe *WebFrontEndImpl) prepAuthorizationForDisplay(request *http.Request, a
}
func (wfe *WebFrontEndImpl) getChallenge(
handlerPath string,
response http.ResponseWriter,
request *http.Request,
authz core.Authorization,
challenge *core.Challenge,
logEvent *web.RequestEvent) {
wfe.prepChallengeForDisplay(handlerPath, request, authz, challenge)
wfe.prepChallengeForDisplay(request, authz, challenge)
authzURL := urlForAuthz(authz, request)
authzURL := urlForAuthz(handlerPath, authz, request)
response.Header().Add("Location", challenge.URL)
response.Header().Add("Link", link(authzURL, "up"))
@ -1258,6 +1294,7 @@ func (wfe *WebFrontEndImpl) getChallenge(
func (wfe *WebFrontEndImpl) postChallenge(
ctx context.Context,
handlerPath string,
response http.ResponseWriter,
request *http.Request,
authz core.Authorization,
@ -1286,7 +1323,7 @@ func (wfe *WebFrontEndImpl) postChallenge(
// challenge details, not a POST to initiate a challenge
if string(body) == "" {
challenge := authz.Challenges[challengeIndex]
wfe.getChallenge(response, request, authz, &challenge, logEvent)
wfe.getChallenge(handlerPath, response, request, authz, &challenge, logEvent)
return
}
@ -1336,9 +1373,9 @@ func (wfe *WebFrontEndImpl) postChallenge(
// assumption: PerformValidation does not modify order of challenges
challenge := returnAuthz.Challenges[challengeIndex]
wfe.prepChallengeForDisplay(request, authz, &challenge)
wfe.prepChallengeForDisplay(handlerPath, request, authz, &challenge)
authzURL := urlForAuthz(authz, request)
authzURL := urlForAuthz(handlerPath, authz, request)
response.Header().Add("Location", challenge.URL)
response.Header().Add("Link", link(authzURL, "up"))
@ -1524,11 +1561,39 @@ func (wfe *WebFrontEndImpl) deactivateAuthorization(
return true
}
func (wfe *WebFrontEndImpl) Authorization(
// AuthorizationHandler handles requests to authorization URLs of the form /acme/authz/{authzID}.
func (wfe *WebFrontEndImpl) AuthorizationHandler(
ctx context.Context,
logEvent *web.RequestEvent,
response http.ResponseWriter,
request *http.Request) {
wfe.Authorization(ctx, authzPath, logEvent, response, request, request.URL.Path)
}
// AuthorizationHandlerWithAccount handles requests to authorization URLs of the form /acme/authz/{regID}/{authzID}.
func (wfe *WebFrontEndImpl) AuthorizationHandlerWithAccount(
ctx context.Context,
logEvent *web.RequestEvent,
response http.ResponseWriter,
request *http.Request) {
slug := strings.Split(request.URL.Path, "/")
if len(slug) != 2 {
wfe.sendError(response, logEvent, probs.NotFound("No such authorization"), nil)
return
}
// TODO(#7683): The regID is currently ignored.
wfe.Authorization(ctx, authzPathWithAcct, logEvent, response, request, slug[1])
}
// Authorization handles both `/acme/authz/{authzID}` and `/acme/authz/{regID}/{authzID}` requests,
// after the calling function has parsed out the authzID.
func (wfe *WebFrontEndImpl) Authorization(
ctx context.Context,
handlerPath string,
logEvent *web.RequestEvent,
response http.ResponseWriter,
request *http.Request,
authzIDStr string) {
var requestAccount *core.Registration
var requestBody []byte
// If the request is a POST it is either:
@ -1546,7 +1611,7 @@ func (wfe *WebFrontEndImpl) Authorization(
requestBody = body
}
authzID, err := strconv.ParseInt(request.URL.Path, 10, 64)
authzID, err := strconv.ParseInt(authzIDStr, 10, 64)
if err != nil {
wfe.sendError(response, logEvent, probs.Malformed("Invalid authorization ID"), nil)
return
@ -1615,7 +1680,7 @@ func (wfe *WebFrontEndImpl) Authorization(
return
}
wfe.prepAuthorizationForDisplay(request, &authz)
wfe.prepAuthorizationForDisplay(handlerPath, request, &authz)
err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, authz)
if err != nil {
@ -2731,6 +2796,10 @@ func extractRequesterIP(req *http.Request) (net.IP, error) {
return net.ParseIP(host), nil
}
func urlForAuthz(authz core.Authorization, request *http.Request) string {
func urlForAuthz(handlerPath string, authz core.Authorization, request *http.Request) string {
if handlerPath == challengePathWithAcct || handlerPath == authzPathWithAcct {
return web.RelativeEndpoint(request, fmt.Sprintf("%s%d/%s", authzPathWithAcct, authz.RegistrationID, authz.ID))
}
return web.RelativeEndpoint(request, authzPath+authz.ID)
}

View File

@ -1164,7 +1164,7 @@ func TestHTTPMethods(t *testing.T) {
}
}
func TestGetChallenge(t *testing.T) {
func TestGetChallengeHandler(t *testing.T) {
wfe, _, _ := setupWFE(t)
// The slug "7TyhFQ" is the StringID of a challenge with type "http-01" and
@ -1181,7 +1181,7 @@ func TestGetChallenge(t *testing.T) {
test.AssertNotError(t, err, "Could not make NewRequest")
req.URL.Path = fmt.Sprintf("1/%s", challSlug)
wfe.Challenge(ctx, newRequestEvent(), resp, req)
wfe.ChallengeHandler(ctx, newRequestEvent(), resp, req)
test.AssertEquals(t, resp.Code, http.StatusOK)
test.AssertEquals(t, resp.Header().Get("Location"), challengeURL)
test.AssertEquals(t, resp.Header().Get("Content-Type"), "application/json")
@ -1198,7 +1198,41 @@ func TestGetChallenge(t *testing.T) {
}
}
func TestChallenge(t *testing.T) {
func TestGetChallengeHandlerWithAccount(t *testing.T) {
wfe, _, _ := setupWFE(t)
// The slug "7TyhFQ" is the StringID of a challenge with type "http-01" and
// token "token".
challSlug := "7TyhFQ"
for _, method := range []string{"GET", "HEAD"} {
resp := httptest.NewRecorder()
// We set req.URL.Path separately to emulate the path-stripping that
// Boulder's request handler does.
challengeURL := fmt.Sprintf("http://localhost/acme/chall/1/1/%s", challSlug)
req, err := http.NewRequest(method, challengeURL, nil)
test.AssertNotError(t, err, "Could not make NewRequest")
req.URL.Path = fmt.Sprintf("1/1/%s", challSlug)
wfe.ChallengeHandlerWithAccount(ctx, newRequestEvent(), resp, req)
test.AssertEquals(t, resp.Code, http.StatusOK)
test.AssertEquals(t, resp.Header().Get("Location"), challengeURL)
test.AssertEquals(t, resp.Header().Get("Content-Type"), "application/json")
test.AssertEquals(t, resp.Header().Get("Link"), `<http://localhost/acme/authz/1/1>;rel="up"`)
// Body is only relevant for GET. For HEAD, body will
// be discarded by HandleFunc() anyway, so it doesn't
// matter what Challenge() writes to it.
if method == "GET" {
test.AssertUnmarshaledEquals(
t, resp.Body.String(),
`{"status": "valid", "type":"http-01","token":"token","url":"http://localhost/acme/chall/1/1/7TyhFQ"}`)
}
}
}
func TestChallengeHandler(t *testing.T) {
wfe, _, signer := setupWFE(t)
post := func(path string) *http.Request {
@ -1264,7 +1298,86 @@ func TestChallenge(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
responseWriter := httptest.NewRecorder()
wfe.Challenge(ctx, newRequestEvent(), responseWriter, tc.Request)
wfe.ChallengeHandler(ctx, newRequestEvent(), responseWriter, tc.Request)
// Check the response code, headers and body match expected
headers := responseWriter.Header()
body := responseWriter.Body.String()
test.AssertEquals(t, responseWriter.Code, tc.ExpectedStatus)
for h, v := range tc.ExpectedHeaders {
test.AssertEquals(t, headers.Get(h), v)
}
test.AssertUnmarshaledEquals(t, body, tc.ExpectedBody)
})
}
}
func TestChallengeHandlerWithAccount(t *testing.T) {
wfe, _, signer := setupWFE(t)
post := func(path string) *http.Request {
signedURL := fmt.Sprintf("http://localhost/%s", path)
_, _, jwsBody := signer.byKeyID(1, nil, signedURL, `{}`)
return makePostRequestWithPath(path, jwsBody)
}
postAsGet := func(keyID int64, path, body string) *http.Request {
_, _, jwsBody := signer.byKeyID(keyID, nil, fmt.Sprintf("http://localhost/%s", path), body)
return makePostRequestWithPath(path, jwsBody)
}
testCases := []struct {
Name string
Request *http.Request
ExpectedStatus int
ExpectedHeaders map[string]string
ExpectedBody string
}{
{
Name: "Valid challenge",
Request: post("1/1/7TyhFQ"),
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{
"Content-Type": "application/json",
"Location": "http://localhost/acme/chall/1/1/7TyhFQ",
"Link": `<http://localhost/acme/authz/1/1>;rel="up"`,
},
ExpectedBody: `{"status": "valid", "type":"http-01","token":"token","url":"http://localhost/acme/chall/1/1/7TyhFQ"}`,
},
{
Name: "Expired challenge",
Request: post("1/3/7TyhFQ"),
ExpectedStatus: http.StatusNotFound,
ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Expired authorization","status":404}`,
},
{
Name: "Missing challenge",
Request: post("1/1/"),
ExpectedStatus: http.StatusNotFound,
ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"No such challenge","status":404}`,
},
{
Name: "Unspecified database error",
Request: post("1/4/7TyhFQ"),
ExpectedStatus: http.StatusInternalServerError,
ExpectedBody: `{"type":"` + probs.ErrorNS + `serverInternal","detail":"Problem getting authorization","status":500}`,
},
{
Name: "POST-as-GET, wrong owner",
Request: postAsGet(1, "1/5/7TyhFQ", ""),
ExpectedStatus: http.StatusForbidden,
ExpectedBody: `{"type":"` + probs.ErrorNS + `unauthorized","detail":"User account ID doesn't match account ID in authorization","status":403}`,
},
{
Name: "Valid POST-as-GET",
Request: postAsGet(1, "1/1/7TyhFQ", ""),
ExpectedStatus: http.StatusOK,
ExpectedBody: `{"status": "valid", "type":"http-01", "token":"token", "url": "http://localhost/acme/chall/1/1/7TyhFQ"}`,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
responseWriter := httptest.NewRecorder()
wfe.ChallengeHandlerWithAccount(ctx, newRequestEvent(), responseWriter, tc.Request)
// Check the response code, headers and body match expected
headers := responseWriter.Header()
body := responseWriter.Body.String()
@ -1287,10 +1400,10 @@ func (ra *MockRAPerformValidationError) PerformValidation(context.Context, *rapb
return nil, errors.New("broken on purpose")
}
// TestUpdateChallengeFinalizedAuthz tests that POSTing a challenge associated
// TestUpdateChallengeHandlerFinalizedAuthz tests that POSTing a challenge associated
// with an already valid authorization just returns the challenge without calling
// the RA.
func TestUpdateChallengeFinalizedAuthz(t *testing.T) {
func TestUpdateChallengeHandlerFinalizedAuthz(t *testing.T) {
wfe, fc, signer := setupWFE(t)
wfe.ra = &MockRAPerformValidationError{MockRegistrationAuthority{clk: fc}}
responseWriter := httptest.NewRecorder()
@ -1298,7 +1411,7 @@ func TestUpdateChallengeFinalizedAuthz(t *testing.T) {
signedURL := "http://localhost/1/7TyhFQ"
_, _, jwsBody := signer.byKeyID(1, nil, signedURL, `{}`)
request := makePostRequestWithPath("1/7TyhFQ", jwsBody)
wfe.Challenge(ctx, newRequestEvent(), responseWriter, request)
wfe.ChallengeHandler(ctx, newRequestEvent(), responseWriter, request)
body := responseWriter.Body.String()
test.AssertUnmarshaledEquals(t, body, `{
@ -1309,10 +1422,32 @@ func TestUpdateChallengeFinalizedAuthz(t *testing.T) {
}`)
}
// TestUpdateChallengeRAError tests that when the RA returns an error from
// TestUpdateChallengeHandlerWithAccountFinalizedAuthz tests that POSTing a challenge associated
// with an already valid authorization just returns the challenge without calling
// the RA.
func TestUpdateChallengeHandlerWithAccountFinalizedAuthz(t *testing.T) {
wfe, fc, signer := setupWFE(t)
wfe.ra = &MockRAPerformValidationError{MockRegistrationAuthority{clk: fc}}
responseWriter := httptest.NewRecorder()
signedURL := "http://localhost/1/1/7TyhFQ"
_, _, jwsBody := signer.byKeyID(1, nil, signedURL, `{}`)
request := makePostRequestWithPath("1/1/7TyhFQ", jwsBody)
wfe.ChallengeHandlerWithAccount(ctx, newRequestEvent(), responseWriter, request)
body := responseWriter.Body.String()
test.AssertUnmarshaledEquals(t, body, `{
"status": "valid",
"type": "http-01",
"token": "token",
"url": "http://localhost/acme/chall/1/1/7TyhFQ"
}`)
}
// TestUpdateChallengeHandlerRAError tests that when the RA returns an error from
// PerformValidation that the WFE returns an internal server error as expected
// and does not panic or otherwise bug out.
func TestUpdateChallengeRAError(t *testing.T) {
func TestUpdateChallengeHandlerRAError(t *testing.T) {
wfe, fc, signer := setupWFE(t)
// Mock the RA to always fail PerformValidation
wfe.ra = &MockRAPerformValidationError{MockRegistrationAuthority{clk: fc}}
@ -1323,7 +1458,32 @@ func TestUpdateChallengeRAError(t *testing.T) {
responseWriter := httptest.NewRecorder()
request := makePostRequestWithPath("2/7TyhFQ", jwsBody)
wfe.Challenge(ctx, newRequestEvent(), responseWriter, request)
wfe.ChallengeHandler(ctx, newRequestEvent(), responseWriter, request)
// The result should be an internal server error problem.
body := responseWriter.Body.String()
test.AssertUnmarshaledEquals(t, body, `{
"type": "urn:ietf:params:acme:error:serverInternal",
"detail": "Unable to update challenge",
"status": 500
}`)
}
// TestUpdateChallengeHandlerWithAccountRAError tests that when the RA returns an error from
// PerformValidation that the WFE returns an internal server error as expected
// and does not panic or otherwise bug out.
func TestUpdateChallengeHandlerWithAccountRAError(t *testing.T) {
wfe, fc, signer := setupWFE(t)
// Mock the RA to always fail PerformValidation
wfe.ra = &MockRAPerformValidationError{MockRegistrationAuthority{clk: fc}}
// Update a pending challenge
signedURL := "http://localhost/1/2/7TyhFQ"
_, _, jwsBody := signer.byKeyID(1, nil, signedURL, `{}`)
responseWriter := httptest.NewRecorder()
request := makePostRequestWithPath("1/2/7TyhFQ", jwsBody)
wfe.ChallengeHandlerWithAccount(ctx, newRequestEvent(), responseWriter, request)
// The result should be an internal server error problem.
body := responseWriter.Body.String()
@ -1640,13 +1800,13 @@ func TestNewAccountNoID(t *testing.T) {
}`)
}
func TestGetAuthorization(t *testing.T) {
func TestGetAuthorizationHandler(t *testing.T) {
wfe, _, signer := setupWFE(t)
// Expired authorizations should be inaccessible
authzURL := "3"
responseWriter := httptest.NewRecorder()
wfe.Authorization(ctx, newRequestEvent(), responseWriter, &http.Request{
wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, &http.Request{
Method: "GET",
URL: mustParseURL(authzURL),
})
@ -1656,7 +1816,7 @@ func TestGetAuthorization(t *testing.T) {
responseWriter.Body.Reset()
// Ensure that a valid authorization can't be reached with an invalid URL
wfe.Authorization(ctx, newRequestEvent(), responseWriter, &http.Request{
wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, &http.Request{
URL: mustParseURL("1d"),
Method: "GET",
})
@ -1668,7 +1828,7 @@ func TestGetAuthorization(t *testing.T) {
responseWriter = httptest.NewRecorder()
// Ensure that a POST-as-GET to an authorization works
wfe.Authorization(ctx, newRequestEvent(), responseWriter, postAsGet)
wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, postAsGet)
test.AssertEquals(t, responseWriter.Code, http.StatusOK)
body := responseWriter.Body.String()
test.AssertUnmarshaledEquals(t, body, `
@ -1690,13 +1850,63 @@ func TestGetAuthorization(t *testing.T) {
}`)
}
// TestAuthorization500 tests that internal errors on GetAuthorization result in
func TestGetAuthorizationHandlerWithAccount(t *testing.T) {
wfe, _, signer := setupWFE(t)
// Expired authorizations should be inaccessible
authzURL := "1/3"
responseWriter := httptest.NewRecorder()
wfe.AuthorizationHandlerWithAccount(ctx, newRequestEvent(), responseWriter, &http.Request{
Method: "GET",
URL: mustParseURL(authzURL),
})
test.AssertEquals(t, responseWriter.Code, http.StatusNotFound)
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(),
`{"type":"`+probs.ErrorNS+`malformed","detail":"Expired authorization","status":404}`)
responseWriter.Body.Reset()
// Ensure that a valid authorization can't be reached with an invalid URL
wfe.AuthorizationHandlerWithAccount(ctx, newRequestEvent(), responseWriter, &http.Request{
URL: mustParseURL("1/1d"),
Method: "GET",
})
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(),
`{"type":"`+probs.ErrorNS+`malformed","detail":"Invalid authorization ID","status":400}`)
_, _, jwsBody := signer.byKeyID(1, nil, "http://localhost/1/1", "")
postAsGet := makePostRequestWithPath("1/1", jwsBody)
responseWriter = httptest.NewRecorder()
// Ensure that a POST-as-GET to an authorization works
wfe.AuthorizationHandlerWithAccount(ctx, newRequestEvent(), responseWriter, postAsGet)
test.AssertEquals(t, responseWriter.Code, http.StatusOK)
body := responseWriter.Body.String()
test.AssertUnmarshaledEquals(t, body, `
{
"identifier": {
"type": "dns",
"value": "not-an-example.com"
},
"status": "valid",
"expires": "2070-01-01T00:00:00Z",
"challenges": [
{
"status": "valid",
"type": "http-01",
"token":"token",
"url": "http://localhost/acme/chall/1/1/7TyhFQ"
}
]
}`)
}
// TestAuthorizationHandler500 tests that internal errors on GetAuthorization result in
// a 500.
func TestAuthorization500(t *testing.T) {
func TestAuthorizationHandler500(t *testing.T) {
wfe, _, _ := setupWFE(t)
responseWriter := httptest.NewRecorder()
wfe.Authorization(ctx, newRequestEvent(), responseWriter, &http.Request{
wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, &http.Request{
Method: "GET",
URL: mustParseURL("4"),
})
@ -1708,6 +1918,24 @@ func TestAuthorization500(t *testing.T) {
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), expected)
}
// TestAuthorizationHandlerWithAccount500 tests that internal errors on GetAuthorization result in
// a 500.
func TestAuthorizationHandlerWithAccount500(t *testing.T) {
wfe, _, _ := setupWFE(t)
responseWriter := httptest.NewRecorder()
wfe.AuthorizationHandlerWithAccount(ctx, newRequestEvent(), responseWriter, &http.Request{
Method: "GET",
URL: mustParseURL("1/4"),
})
expected := `{
"type": "urn:ietf:params:acme:error:serverInternal",
"detail": "Problem getting authorization",
"status": 500
}`
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), expected)
}
// RAWithFailedChallenges is a fake RA whose GetAuthorization method returns
// an authz with a failed challenge.
type RAWithFailedChallenge struct {
@ -1738,14 +1966,35 @@ func (ra *RAWithFailedChallenge) GetAuthorization(ctx context.Context, id *rapb.
}, nil
}
// TestAuthorizationChallengeNamespace tests that the runtime prefixing of
// TestAuthorizationChallengeHandlerNamespace tests that the runtime prefixing of
// Challenge Problem Types works as expected
func TestAuthorizationChallengeNamespace(t *testing.T) {
func TestAuthorizationChallengeHandlerNamespace(t *testing.T) {
wfe, clk, _ := setupWFE(t)
wfe.ra = &RAWithFailedChallenge{clk: clk}
responseWriter := httptest.NewRecorder()
wfe.Authorization(ctx, newRequestEvent(), responseWriter, &http.Request{
wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, &http.Request{
Method: "GET",
URL: mustParseURL("6"),
})
var authz core.Authorization
err := json.Unmarshal(responseWriter.Body.Bytes(), &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.ErrorNS prefix added
test.AssertEquals(t, string(authz.Challenges[0].Error.Type), probs.ErrorNS+"things:are:whack")
responseWriter.Body.Reset()
}
// TestAuthorizationChallengeHandlerWithAccountNamespace tests that the runtime prefixing of
// Challenge Problem Types works as expected
func TestAuthorizationChallengeHandlerWithAccountNamespace(t *testing.T) {
wfe, clk, _ := setupWFE(t)
wfe.ra = &RAWithFailedChallenge{clk: clk}
responseWriter := httptest.NewRecorder()
wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, &http.Request{
Method: "GET",
URL: mustParseURL("6"),
})
@ -2392,7 +2641,7 @@ func TestHeaderBoulderRequester(t *testing.T) {
test.AssertEquals(t, responseWriter.Header().Get("Boulder-Requester"), "1")
}
func TestDeactivateAuthorization(t *testing.T) {
func TestDeactivateAuthorizationHandler(t *testing.T) {
wfe, _, signer := setupWFE(t)
responseWriter := httptest.NewRecorder()
@ -2402,7 +2651,7 @@ func TestDeactivateAuthorization(t *testing.T) {
_, _, body := signer.byKeyID(1, nil, "http://localhost/1", payload)
request := makePostRequestWithPath("1", body)
wfe.Authorization(ctx, newRequestEvent(), responseWriter, request)
wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(),
`{"type": "`+probs.ErrorNS+`malformed","detail": "Invalid status value","status": 400}`)
@ -2412,7 +2661,48 @@ func TestDeactivateAuthorization(t *testing.T) {
_, _, body = signer.byKeyID(1, nil, "http://localhost/1", payload)
request = makePostRequestWithPath("1", body)
wfe.Authorization(ctx, newRequestEvent(), responseWriter, request)
wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(),
`{
"identifier": {
"type": "dns",
"value": "not-an-example.com"
},
"status": "deactivated",
"expires": "2070-01-01T00:00:00Z",
"challenges": [
{
"status": "valid",
"type": "http-01",
"token": "token",
"url": "http://localhost/acme/chall-v3/1/7TyhFQ"
}
]
}`)
}
func TestDeactivateAuthorizationHandlerWithAccount(t *testing.T) {
wfe, _, signer := setupWFE(t)
responseWriter := httptest.NewRecorder()
responseWriter.Body.Reset()
payload := `{"status":""}`
_, _, body := signer.byKeyID(1, nil, "http://localhost/1", payload)
request := makePostRequestWithPath("1", body)
wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(),
`{"type": "`+probs.ErrorNS+`malformed","detail": "Invalid status value","status": 400}`)
responseWriter.Body.Reset()
payload = `{"status":"deactivated"}`
_, _, body = signer.byKeyID(1, nil, "http://localhost/1", payload)
request = makePostRequestWithPath("1", body)
wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(),
`{
@ -3399,7 +3689,33 @@ func TestPrepAuthzForDisplay(t *testing.T) {
}
// This modifies the authz in-place.
wfe.prepAuthorizationForDisplay(&http.Request{Host: "localhost"}, authz)
wfe.prepAuthorizationForDisplay(authzPath, &http.Request{Host: "localhost"}, authz)
// Ensure ID and RegID are omitted.
authzJSON, err := json.Marshal(authz)
test.AssertNotError(t, err, "Failed to marshal authz")
test.AssertNotContains(t, string(authzJSON), "\"id\":\"12345\"")
test.AssertNotContains(t, string(authzJSON), "\"registrationID\":\"1\"")
}
func TestPrepAuthzWithAccountForDisplay(t *testing.T) {
t.Parallel()
wfe, _, _ := setupWFE(t)
authz := &core.Authorization{
ID: "12345",
Status: core.StatusPending,
RegistrationID: 1,
Identifier: identifier.NewDNS("example.com"),
Challenges: []core.Challenge{
{Type: core.ChallengeTypeDNS01, Status: core.StatusPending, Token: "token"},
{Type: core.ChallengeTypeHTTP01, Status: core.StatusPending, Token: "token"},
{Type: core.ChallengeTypeTLSALPN01, Status: core.StatusPending, Token: "token"},
},
}
// This modifies the authz in-place.
wfe.prepAuthorizationForDisplay(authzPathWithAcct, &http.Request{Host: "localhost"}, authz)
// Ensure ID and RegID are omitted.
authzJSON, err := json.Marshal(authz)
@ -3425,7 +3741,32 @@ func TestPrepRevokedAuthzForDisplay(t *testing.T) {
}
// This modifies the authz in-place.
wfe.prepAuthorizationForDisplay(&http.Request{Host: "localhost"}, authz)
wfe.prepAuthorizationForDisplay(authzPath, &http.Request{Host: "localhost"}, authz)
// All of the challenges should be revoked as well.
for _, chall := range authz.Challenges {
test.AssertEquals(t, chall.Status, core.StatusInvalid)
}
}
func TestPrepRevokedAuthzWithAccountForDisplay(t *testing.T) {
t.Parallel()
wfe, _, _ := setupWFE(t)
authz := &core.Authorization{
ID: "12345",
Status: core.StatusInvalid,
RegistrationID: 1,
Identifier: identifier.NewDNS("example.com"),
Challenges: []core.Challenge{
{Type: core.ChallengeTypeDNS01, Status: core.StatusPending, Token: "token"},
{Type: core.ChallengeTypeHTTP01, Status: core.StatusPending, Token: "token"},
{Type: core.ChallengeTypeTLSALPN01, Status: core.StatusPending, Token: "token"},
},
}
// This modifies the authz in-place.
wfe.prepAuthorizationForDisplay(authzPathWithAcct, &http.Request{Host: "localhost"}, authz)
// All of the challenges should be revoked as well.
for _, chall := range authz.Challenges {
@ -3448,7 +3789,30 @@ func TestPrepWildcardAuthzForDisplay(t *testing.T) {
}
// This modifies the authz in-place.
wfe.prepAuthorizationForDisplay(&http.Request{Host: "localhost"}, authz)
wfe.prepAuthorizationForDisplay(authzPath, &http.Request{Host: "localhost"}, authz)
// The identifier should not start with a star, but the authz should be marked
// as a wildcard.
test.AssertEquals(t, strings.HasPrefix(authz.Identifier.Value, "*."), false)
test.AssertEquals(t, authz.Wildcard, true)
}
func TestPrepWildcardAuthzWithAcountForDisplay(t *testing.T) {
t.Parallel()
wfe, _, _ := setupWFE(t)
authz := &core.Authorization{
ID: "12345",
Status: core.StatusPending,
RegistrationID: 1,
Identifier: identifier.NewDNS("*.example.com"),
Challenges: []core.Challenge{
{Type: core.ChallengeTypeDNS01, Status: core.StatusPending, Token: "token"},
},
}
// This modifies the authz in-place.
wfe.prepAuthorizationForDisplay(authzPathWithAcct, &http.Request{Host: "localhost"}, authz)
// The identifier should not start with a star, but the authz should be marked
// as a wildcard.
@ -3484,7 +3848,7 @@ func TestPrepAuthzForDisplayShuffle(t *testing.T) {
// Prep the authz 100 times, and count where each challenge ended up each time.
for range 100 {
// This modifies the authz in place
wfe.prepAuthorizationForDisplay(&http.Request{Host: "localhost"}, authz)
wfe.prepAuthorizationForDisplay(challengePath, &http.Request{Host: "localhost"}, authz)
for i, chall := range authz.Challenges {
counts[chall.Type][i] += 1
}
@ -3567,7 +3931,7 @@ func TestPrepAccountForDisplay(t *testing.T) {
test.AssertEquals(t, acct.ID, int64(0))
}
func TestGETAPIAuthz(t *testing.T) {
func TestGETAPIAuthorizationHandler(t *testing.T) {
wfe, _, _ := setupWFE(t)
makeGet := func(path, endpoint string) (*http.Request, *web.RequestEvent) {
return &http.Request{URL: &url.URL{Path: path}, Method: "GET"},
@ -3595,7 +3959,46 @@ func TestGETAPIAuthz(t *testing.T) {
for _, tc := range testCases {
responseWriter := httptest.NewRecorder()
req, logEvent := makeGet(tc.path, getAuthzPath)
wfe.Authorization(context.Background(), logEvent, responseWriter, req)
wfe.AuthorizationHandler(context.Background(), logEvent, responseWriter, req)
if responseWriter.Code == http.StatusOK && tc.expectTooFreshErr {
t.Errorf("expected too fresh error, got http.StatusOK")
} else {
test.AssertEquals(t, responseWriter.Code, http.StatusForbidden)
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tooFreshErr)
}
}
}
func TestGETAPIAuthorizationHandlerWitAccount(t *testing.T) {
wfe, _, _ := setupWFE(t)
makeGet := func(path, endpoint string) (*http.Request, *web.RequestEvent) {
return &http.Request{URL: &url.URL{Path: path}, Method: "GET"},
&web.RequestEvent{Endpoint: endpoint}
}
testCases := []struct {
name string
path string
expectTooFreshErr bool
}{
{
name: "fresh authz",
path: "1/1",
expectTooFreshErr: true,
},
{
name: "old authz",
path: "1/2",
expectTooFreshErr: false,
},
}
tooFreshErr := `{"type":"` + probs.ErrorNS + `unauthorized","detail":"Authorization is too new for GET API. You should only use this non-standard API to access resources created more than 10s ago","status":403}`
for _, tc := range testCases {
responseWriter := httptest.NewRecorder()
req, logEvent := makeGet(tc.path, getAuthzPath)
wfe.AuthorizationHandlerWithAccount(context.Background(), logEvent, responseWriter, req)
if responseWriter.Code == http.StatusOK && tc.expectTooFreshErr {
t.Errorf("expected too fresh error, got http.StatusOK")
@ -3634,7 +4037,7 @@ func TestGETAPIChallenge(t *testing.T) {
for _, tc := range testCases {
responseWriter := httptest.NewRecorder()
req, logEvent := makeGet(tc.path, getAuthzPath)
wfe.Challenge(context.Background(), logEvent, responseWriter, req)
wfe.ChallengeHandler(context.Background(), logEvent, responseWriter, req)
if responseWriter.Code == http.StatusOK && tc.expectTooFreshErr {
t.Errorf("expected too fresh error, got http.StatusOK")