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:
		
							parent
							
								
									2603aa45a8
								
							
						
					
					
						commit
						2058d985cc
					
				
							
								
								
									
										147
									
								
								wfe2/wfe.go
								
								
								
								
							
							
						
						
									
										147
									
								
								wfe2/wfe.go
								
								
								
								
							|  | @ -57,16 +57,18 @@ const ( | ||||||
| 	acctPath      = "/acme/acct/" | 	acctPath      = "/acme/acct/" | ||||||
| 	// When we moved to authzv2, we used a "-v3" suffix to avoid confusion
 | 	// When we moved to authzv2, we used a "-v3" suffix to avoid confusion
 | ||||||
| 	// regarding ACMEv2.
 | 	// regarding ACMEv2.
 | ||||||
| 	authzPath         = "/acme/authz-v3/" | 	authzPath             = "/acme/authz-v3/" | ||||||
| 	challengePath     = "/acme/chall-v3/" | 	authzPathWithAcct     = "/acme/authz/" | ||||||
| 	certPath          = "/acme/cert/" | 	challengePath         = "/acme/chall-v3/" | ||||||
| 	revokeCertPath    = "/acme/revoke-cert" | 	challengePathWithAcct = "/acme/chall/" | ||||||
| 	buildIDPath       = "/build" | 	certPath              = "/acme/cert/" | ||||||
| 	rolloverPath      = "/acme/key-change" | 	revokeCertPath        = "/acme/revoke-cert" | ||||||
| 	newNoncePath      = "/acme/new-nonce" | 	buildIDPath           = "/build" | ||||||
| 	newOrderPath      = "/acme/new-order" | 	rolloverPath          = "/acme/key-change" | ||||||
| 	orderPath         = "/acme/order/" | 	newNoncePath          = "/acme/new-nonce" | ||||||
| 	finalizeOrderPath = "/acme/finalize/" | 	newOrderPath          = "/acme/new-order" | ||||||
|  | 	orderPath             = "/acme/order/" | ||||||
|  | 	finalizeOrderPath     = "/acme/finalize/" | ||||||
| 
 | 
 | ||||||
| 	getAPIPrefix     = "/get/" | 	getAPIPrefix     = "/get/" | ||||||
| 	getOrderPath     = getAPIPrefix + "order/" | 	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
 | 	// TODO(@cpu): After November 1st, 2020 support for "GET" to the following
 | ||||||
| 	// endpoints will be removed, leaving only POST-as-GET support.
 | 	// endpoints will be removed, leaving only POST-as-GET support.
 | ||||||
| 	wfe.HandleFunc(m, orderPath, wfe.GetOrder, "GET", "POST") | 	wfe.HandleFunc(m, orderPath, wfe.GetOrder, "GET", "POST") | ||||||
| 	wfe.HandleFunc(m, authzPath, wfe.Authorization, "GET", "POST") | 	wfe.HandleFunc(m, authzPath, wfe.AuthorizationHandler, "GET", "POST") | ||||||
| 	wfe.HandleFunc(m, challengePath, wfe.Challenge, "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") | 	wfe.HandleFunc(m, certPath, wfe.Certificate, "GET", "POST") | ||||||
| 	// Boulder-specific GET-able resource endpoints
 | 	// Boulder-specific GET-able resource endpoints
 | ||||||
| 	wfe.HandleFunc(m, getOrderPath, wfe.GetOrder, "GET") | 	wfe.HandleFunc(m, getOrderPath, wfe.GetOrder, "GET") | ||||||
| 	wfe.HandleFunc(m, getAuthzPath, wfe.Authorization, "GET") | 	wfe.HandleFunc(m, getAuthzPath, wfe.AuthorizationHandler, "GET") | ||||||
| 	wfe.HandleFunc(m, getChallengePath, wfe.Challenge, "GET") | 	wfe.HandleFunc(m, getChallengePath, wfe.ChallengeHandler, "GET") | ||||||
| 	wfe.HandleFunc(m, getCertPath, wfe.Certificate, "GET") | 	wfe.HandleFunc(m, getCertPath, wfe.Certificate, "GET") | ||||||
| 
 | 
 | ||||||
| 	// Endpoint for draft-ietf-acme-ari
 | 	// Endpoint for draft-ietf-acme-ari
 | ||||||
|  | @ -1088,31 +1092,55 @@ func (wfe *WebFrontEndImpl) RevokeCertificate( | ||||||
| 	response.WriteHeader(http.StatusOK) | 	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.
 | // Such requests are clients' responses to the server's challenges.
 | ||||||
| func (wfe *WebFrontEndImpl) Challenge( | func (wfe *WebFrontEndImpl) ChallengeHandler( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	logEvent *web.RequestEvent, | 	logEvent *web.RequestEvent, | ||||||
| 	response http.ResponseWriter, | 	response http.ResponseWriter, | ||||||
| 	request *http.Request) { | 	request *http.Request) { | ||||||
| 	notFound := func() { |  | ||||||
| 		wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil) |  | ||||||
| 	} |  | ||||||
| 	slug := strings.Split(request.URL.Path, "/") | 	slug := strings.Split(request.URL.Path, "/") | ||||||
| 	if len(slug) != 2 { | 	if len(slug) != 2 { | ||||||
| 		notFound() | 		wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil) | ||||||
| 		return | 		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 { | 	if err != nil { | ||||||
| 		wfe.sendError(response, logEvent, probs.Malformed("Invalid authorization ID"), nil) | 		wfe.sendError(response, logEvent, probs.Malformed("Invalid authorization ID"), nil) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	challengeID := slug[1] |  | ||||||
| 	authzPB, err := wfe.ra.GetAuthorization(ctx, &rapb.GetAuthorizationRequest{Id: authorizationID}) | 	authzPB, err := wfe.ra.GetAuthorization(ctx, &rapb.GetAuthorizationRequest{Id: authorizationID}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if errors.Is(err, berrors.NotFound) { | 		if errors.Is(err, berrors.NotFound) { | ||||||
| 			notFound() | 			wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil) | ||||||
| 		} else { | 		} else { | ||||||
| 			wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Problem getting authorization"), err) | 			wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Problem getting authorization"), err) | ||||||
| 		} | 		} | ||||||
|  | @ -1133,7 +1161,7 @@ func (wfe *WebFrontEndImpl) Challenge( | ||||||
| 	} | 	} | ||||||
| 	challengeIndex := authz.FindChallengeByStringID(challengeID) | 	challengeIndex := authz.FindChallengeByStringID(challengeID) | ||||||
| 	if challengeIndex == -1 { | 	if challengeIndex == -1 { | ||||||
| 		notFound() | 		wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -1157,11 +1185,11 @@ func (wfe *WebFrontEndImpl) Challenge( | ||||||
| 	challenge := authz.Challenges[challengeIndex] | 	challenge := authz.Challenges[challengeIndex] | ||||||
| 	switch request.Method { | 	switch request.Method { | ||||||
| 	case "GET", "HEAD": | 	case "GET", "HEAD": | ||||||
| 		wfe.getChallenge(response, request, authz, &challenge, logEvent) | 		wfe.getChallenge(handlerPath, response, request, authz, &challenge, logEvent) | ||||||
| 
 | 
 | ||||||
| 	case "POST": | 	case "POST": | ||||||
| 		logEvent.ChallengeType = string(challenge.Type) | 		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
 | // prepChallengeForDisplay takes a core.Challenge and prepares it for display to
 | ||||||
| // the client by filling in its URL field and clearing several unnecessary
 | // the client by filling in its URL field and clearing several unnecessary
 | ||||||
| // fields.
 | // 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
 | 	// 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())) | 	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
 | 	// 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
 | 	// (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
 | // prepAuthorizationForDisplay takes a core.Authorization and prepares it for
 | ||||||
| // display to the client by preparing all its challenges.
 | // 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 { | 	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.
 | 	// 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( | func (wfe *WebFrontEndImpl) getChallenge( | ||||||
|  | 	handlerPath string, | ||||||
| 	response http.ResponseWriter, | 	response http.ResponseWriter, | ||||||
| 	request *http.Request, | 	request *http.Request, | ||||||
| 	authz core.Authorization, | 	authz core.Authorization, | ||||||
| 	challenge *core.Challenge, | 	challenge *core.Challenge, | ||||||
| 	logEvent *web.RequestEvent) { | 	logEvent *web.RequestEvent) { | ||||||
|  | 	wfe.prepChallengeForDisplay(handlerPath, request, authz, challenge) | ||||||
| 
 | 
 | ||||||
| 	wfe.prepChallengeForDisplay(request, authz, challenge) | 	authzURL := urlForAuthz(handlerPath, authz, request) | ||||||
| 
 |  | ||||||
| 	authzURL := urlForAuthz(authz, request) |  | ||||||
| 	response.Header().Add("Location", challenge.URL) | 	response.Header().Add("Location", challenge.URL) | ||||||
| 	response.Header().Add("Link", link(authzURL, "up")) | 	response.Header().Add("Link", link(authzURL, "up")) | ||||||
| 
 | 
 | ||||||
|  | @ -1258,6 +1294,7 @@ func (wfe *WebFrontEndImpl) getChallenge( | ||||||
| 
 | 
 | ||||||
| func (wfe *WebFrontEndImpl) postChallenge( | func (wfe *WebFrontEndImpl) postChallenge( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
|  | 	handlerPath string, | ||||||
| 	response http.ResponseWriter, | 	response http.ResponseWriter, | ||||||
| 	request *http.Request, | 	request *http.Request, | ||||||
| 	authz core.Authorization, | 	authz core.Authorization, | ||||||
|  | @ -1286,7 +1323,7 @@ func (wfe *WebFrontEndImpl) postChallenge( | ||||||
| 	// challenge details, not a POST to initiate a challenge
 | 	// challenge details, not a POST to initiate a challenge
 | ||||||
| 	if string(body) == "" { | 	if string(body) == "" { | ||||||
| 		challenge := authz.Challenges[challengeIndex] | 		challenge := authz.Challenges[challengeIndex] | ||||||
| 		wfe.getChallenge(response, request, authz, &challenge, logEvent) | 		wfe.getChallenge(handlerPath, response, request, authz, &challenge, logEvent) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -1336,9 +1373,9 @@ func (wfe *WebFrontEndImpl) postChallenge( | ||||||
| 
 | 
 | ||||||
| 	// assumption: PerformValidation does not modify order of challenges
 | 	// assumption: PerformValidation does not modify order of challenges
 | ||||||
| 	challenge := returnAuthz.Challenges[challengeIndex] | 	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("Location", challenge.URL) | ||||||
| 	response.Header().Add("Link", link(authzURL, "up")) | 	response.Header().Add("Link", link(authzURL, "up")) | ||||||
| 
 | 
 | ||||||
|  | @ -1524,11 +1561,39 @@ func (wfe *WebFrontEndImpl) deactivateAuthorization( | ||||||
| 	return true | 	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, | 	ctx context.Context, | ||||||
| 	logEvent *web.RequestEvent, | 	logEvent *web.RequestEvent, | ||||||
| 	response http.ResponseWriter, | 	response http.ResponseWriter, | ||||||
| 	request *http.Request) { | 	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 requestAccount *core.Registration | ||||||
| 	var requestBody []byte | 	var requestBody []byte | ||||||
| 	// If the request is a POST it is either:
 | 	// If the request is a POST it is either:
 | ||||||
|  | @ -1546,7 +1611,7 @@ func (wfe *WebFrontEndImpl) Authorization( | ||||||
| 		requestBody = body | 		requestBody = body | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	authzID, err := strconv.ParseInt(request.URL.Path, 10, 64) | 	authzID, err := strconv.ParseInt(authzIDStr, 10, 64) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		wfe.sendError(response, logEvent, probs.Malformed("Invalid authorization ID"), nil) | 		wfe.sendError(response, logEvent, probs.Malformed("Invalid authorization ID"), nil) | ||||||
| 		return | 		return | ||||||
|  | @ -1615,7 +1680,7 @@ func (wfe *WebFrontEndImpl) Authorization( | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	wfe.prepAuthorizationForDisplay(request, &authz) | 	wfe.prepAuthorizationForDisplay(handlerPath, request, &authz) | ||||||
| 
 | 
 | ||||||
| 	err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, authz) | 	err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, authz) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -2731,6 +2796,10 @@ func extractRequesterIP(req *http.Request) (net.IP, error) { | ||||||
| 	return net.ParseIP(host), nil | 	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) | 	return web.RelativeEndpoint(request, authzPath+authz.ID) | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										463
									
								
								wfe2/wfe_test.go
								
								
								
								
							
							
						
						
									
										463
									
								
								wfe2/wfe_test.go
								
								
								
								
							|  | @ -1164,7 +1164,7 @@ func TestHTTPMethods(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestGetChallenge(t *testing.T) { | func TestGetChallengeHandler(t *testing.T) { | ||||||
| 	wfe, _, _ := setupWFE(t) | 	wfe, _, _ := setupWFE(t) | ||||||
| 
 | 
 | ||||||
| 	// The slug "7TyhFQ" is the StringID of a challenge with type "http-01" and
 | 	// 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") | 		test.AssertNotError(t, err, "Could not make NewRequest") | ||||||
| 		req.URL.Path = fmt.Sprintf("1/%s", challSlug) | 		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.Code, http.StatusOK) | ||||||
| 		test.AssertEquals(t, resp.Header().Get("Location"), challengeURL) | 		test.AssertEquals(t, resp.Header().Get("Location"), challengeURL) | ||||||
| 		test.AssertEquals(t, resp.Header().Get("Content-Type"), "application/json") | 		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) | 	wfe, _, signer := setupWFE(t) | ||||||
| 
 | 
 | ||||||
| 	post := func(path string) *http.Request { | 	post := func(path string) *http.Request { | ||||||
|  | @ -1264,7 +1298,86 @@ func TestChallenge(t *testing.T) { | ||||||
| 	for _, tc := range testCases { | 	for _, tc := range testCases { | ||||||
| 		t.Run(tc.Name, func(t *testing.T) { | 		t.Run(tc.Name, func(t *testing.T) { | ||||||
| 			responseWriter := httptest.NewRecorder() | 			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
 | 			// Check the response code, headers and body match expected
 | ||||||
| 			headers := responseWriter.Header() | 			headers := responseWriter.Header() | ||||||
| 			body := responseWriter.Body.String() | 			body := responseWriter.Body.String() | ||||||
|  | @ -1287,10 +1400,10 @@ func (ra *MockRAPerformValidationError) PerformValidation(context.Context, *rapb | ||||||
| 	return nil, errors.New("broken on purpose") | 	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
 | // with an already valid authorization just returns the challenge without calling
 | ||||||
| // the RA.
 | // the RA.
 | ||||||
| func TestUpdateChallengeFinalizedAuthz(t *testing.T) { | func TestUpdateChallengeHandlerFinalizedAuthz(t *testing.T) { | ||||||
| 	wfe, fc, signer := setupWFE(t) | 	wfe, fc, signer := setupWFE(t) | ||||||
| 	wfe.ra = &MockRAPerformValidationError{MockRegistrationAuthority{clk: fc}} | 	wfe.ra = &MockRAPerformValidationError{MockRegistrationAuthority{clk: fc}} | ||||||
| 	responseWriter := httptest.NewRecorder() | 	responseWriter := httptest.NewRecorder() | ||||||
|  | @ -1298,7 +1411,7 @@ func TestUpdateChallengeFinalizedAuthz(t *testing.T) { | ||||||
| 	signedURL := "http://localhost/1/7TyhFQ" | 	signedURL := "http://localhost/1/7TyhFQ" | ||||||
| 	_, _, jwsBody := signer.byKeyID(1, nil, signedURL, `{}`) | 	_, _, jwsBody := signer.byKeyID(1, nil, signedURL, `{}`) | ||||||
| 	request := makePostRequestWithPath("1/7TyhFQ", jwsBody) | 	request := makePostRequestWithPath("1/7TyhFQ", jwsBody) | ||||||
| 	wfe.Challenge(ctx, newRequestEvent(), responseWriter, request) | 	wfe.ChallengeHandler(ctx, newRequestEvent(), responseWriter, request) | ||||||
| 
 | 
 | ||||||
| 	body := responseWriter.Body.String() | 	body := responseWriter.Body.String() | ||||||
| 	test.AssertUnmarshaledEquals(t, body, `{ | 	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
 | // PerformValidation that the WFE returns an internal server error as expected
 | ||||||
| // and does not panic or otherwise bug out.
 | // and does not panic or otherwise bug out.
 | ||||||
| func TestUpdateChallengeRAError(t *testing.T) { | func TestUpdateChallengeHandlerRAError(t *testing.T) { | ||||||
| 	wfe, fc, signer := setupWFE(t) | 	wfe, fc, signer := setupWFE(t) | ||||||
| 	// Mock the RA to always fail PerformValidation
 | 	// Mock the RA to always fail PerformValidation
 | ||||||
| 	wfe.ra = &MockRAPerformValidationError{MockRegistrationAuthority{clk: fc}} | 	wfe.ra = &MockRAPerformValidationError{MockRegistrationAuthority{clk: fc}} | ||||||
|  | @ -1323,7 +1458,32 @@ func TestUpdateChallengeRAError(t *testing.T) { | ||||||
| 	responseWriter := httptest.NewRecorder() | 	responseWriter := httptest.NewRecorder() | ||||||
| 	request := makePostRequestWithPath("2/7TyhFQ", jwsBody) | 	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.
 | 	// The result should be an internal server error problem.
 | ||||||
| 	body := responseWriter.Body.String() | 	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) | 	wfe, _, signer := setupWFE(t) | ||||||
| 
 | 
 | ||||||
| 	// Expired authorizations should be inaccessible
 | 	// Expired authorizations should be inaccessible
 | ||||||
| 	authzURL := "3" | 	authzURL := "3" | ||||||
| 	responseWriter := httptest.NewRecorder() | 	responseWriter := httptest.NewRecorder() | ||||||
| 	wfe.Authorization(ctx, newRequestEvent(), responseWriter, &http.Request{ | 	wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, &http.Request{ | ||||||
| 		Method: "GET", | 		Method: "GET", | ||||||
| 		URL:    mustParseURL(authzURL), | 		URL:    mustParseURL(authzURL), | ||||||
| 	}) | 	}) | ||||||
|  | @ -1656,7 +1816,7 @@ func TestGetAuthorization(t *testing.T) { | ||||||
| 	responseWriter.Body.Reset() | 	responseWriter.Body.Reset() | ||||||
| 
 | 
 | ||||||
| 	// Ensure that a valid authorization can't be reached with an invalid URL
 | 	// 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"), | 		URL:    mustParseURL("1d"), | ||||||
| 		Method: "GET", | 		Method: "GET", | ||||||
| 	}) | 	}) | ||||||
|  | @ -1668,7 +1828,7 @@ func TestGetAuthorization(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	responseWriter = httptest.NewRecorder() | 	responseWriter = httptest.NewRecorder() | ||||||
| 	// Ensure that a POST-as-GET to an authorization works
 | 	// 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) | 	test.AssertEquals(t, responseWriter.Code, http.StatusOK) | ||||||
| 	body := responseWriter.Body.String() | 	body := responseWriter.Body.String() | ||||||
| 	test.AssertUnmarshaledEquals(t, body, ` | 	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.
 | // a 500.
 | ||||||
| func TestAuthorization500(t *testing.T) { | func TestAuthorizationHandler500(t *testing.T) { | ||||||
| 	wfe, _, _ := setupWFE(t) | 	wfe, _, _ := setupWFE(t) | ||||||
| 
 | 
 | ||||||
| 	responseWriter := httptest.NewRecorder() | 	responseWriter := httptest.NewRecorder() | ||||||
| 	wfe.Authorization(ctx, newRequestEvent(), responseWriter, &http.Request{ | 	wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, &http.Request{ | ||||||
| 		Method: "GET", | 		Method: "GET", | ||||||
| 		URL:    mustParseURL("4"), | 		URL:    mustParseURL("4"), | ||||||
| 	}) | 	}) | ||||||
|  | @ -1708,6 +1918,24 @@ func TestAuthorization500(t *testing.T) { | ||||||
| 	test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), expected) | 	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
 | // RAWithFailedChallenges is a fake RA whose GetAuthorization method returns
 | ||||||
| // an authz with a failed challenge.
 | // an authz with a failed challenge.
 | ||||||
| type RAWithFailedChallenge struct { | type RAWithFailedChallenge struct { | ||||||
|  | @ -1738,14 +1966,35 @@ func (ra *RAWithFailedChallenge) GetAuthorization(ctx context.Context, id *rapb. | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TestAuthorizationChallengeNamespace tests that the runtime prefixing of
 | // TestAuthorizationChallengeHandlerNamespace tests that the runtime prefixing of
 | ||||||
| // Challenge Problem Types works as expected
 | // Challenge Problem Types works as expected
 | ||||||
| func TestAuthorizationChallengeNamespace(t *testing.T) { | func TestAuthorizationChallengeHandlerNamespace(t *testing.T) { | ||||||
| 	wfe, clk, _ := setupWFE(t) | 	wfe, clk, _ := setupWFE(t) | ||||||
| 	wfe.ra = &RAWithFailedChallenge{clk: clk} | 	wfe.ra = &RAWithFailedChallenge{clk: clk} | ||||||
| 
 | 
 | ||||||
| 	responseWriter := httptest.NewRecorder() | 	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", | 		Method: "GET", | ||||||
| 		URL:    mustParseURL("6"), | 		URL:    mustParseURL("6"), | ||||||
| 	}) | 	}) | ||||||
|  | @ -2392,7 +2641,7 @@ func TestHeaderBoulderRequester(t *testing.T) { | ||||||
| 	test.AssertEquals(t, responseWriter.Header().Get("Boulder-Requester"), "1") | 	test.AssertEquals(t, responseWriter.Header().Get("Boulder-Requester"), "1") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestDeactivateAuthorization(t *testing.T) { | func TestDeactivateAuthorizationHandler(t *testing.T) { | ||||||
| 	wfe, _, signer := setupWFE(t) | 	wfe, _, signer := setupWFE(t) | ||||||
| 	responseWriter := httptest.NewRecorder() | 	responseWriter := httptest.NewRecorder() | ||||||
| 
 | 
 | ||||||
|  | @ -2402,7 +2651,7 @@ func TestDeactivateAuthorization(t *testing.T) { | ||||||
| 	_, _, body := signer.byKeyID(1, nil, "http://localhost/1", payload) | 	_, _, body := signer.byKeyID(1, nil, "http://localhost/1", payload) | ||||||
| 	request := makePostRequestWithPath("1", body) | 	request := makePostRequestWithPath("1", body) | ||||||
| 
 | 
 | ||||||
| 	wfe.Authorization(ctx, newRequestEvent(), responseWriter, request) | 	wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, request) | ||||||
| 	test.AssertUnmarshaledEquals(t, | 	test.AssertUnmarshaledEquals(t, | ||||||
| 		responseWriter.Body.String(), | 		responseWriter.Body.String(), | ||||||
| 		`{"type": "`+probs.ErrorNS+`malformed","detail": "Invalid status value","status": 400}`) | 		`{"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) | 	_, _, body = signer.byKeyID(1, nil, "http://localhost/1", payload) | ||||||
| 	request = makePostRequestWithPath("1", body) | 	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, | 	test.AssertUnmarshaledEquals(t, | ||||||
| 		responseWriter.Body.String(), | 		responseWriter.Body.String(), | ||||||
| 		`{ | 		`{ | ||||||
|  | @ -3399,7 +3689,33 @@ func TestPrepAuthzForDisplay(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// This modifies the authz in-place.
 | 	// 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.
 | 	// Ensure ID and RegID are omitted.
 | ||||||
| 	authzJSON, err := json.Marshal(authz) | 	authzJSON, err := json.Marshal(authz) | ||||||
|  | @ -3425,7 +3741,32 @@ func TestPrepRevokedAuthzForDisplay(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// This modifies the authz in-place.
 | 	// 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.
 | 	// All of the challenges should be revoked as well.
 | ||||||
| 	for _, chall := range authz.Challenges { | 	for _, chall := range authz.Challenges { | ||||||
|  | @ -3448,7 +3789,30 @@ func TestPrepWildcardAuthzForDisplay(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// This modifies the authz in-place.
 | 	// 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
 | 	// The identifier should not start with a star, but the authz should be marked
 | ||||||
| 	// as a wildcard.
 | 	// 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.
 | 	// Prep the authz 100 times, and count where each challenge ended up each time.
 | ||||||
| 	for range 100 { | 	for range 100 { | ||||||
| 		// This modifies the authz in place
 | 		// 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 { | 		for i, chall := range authz.Challenges { | ||||||
| 			counts[chall.Type][i] += 1 | 			counts[chall.Type][i] += 1 | ||||||
| 		} | 		} | ||||||
|  | @ -3567,7 +3931,7 @@ func TestPrepAccountForDisplay(t *testing.T) { | ||||||
| 	test.AssertEquals(t, acct.ID, int64(0)) | 	test.AssertEquals(t, acct.ID, int64(0)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestGETAPIAuthz(t *testing.T) { | func TestGETAPIAuthorizationHandler(t *testing.T) { | ||||||
| 	wfe, _, _ := setupWFE(t) | 	wfe, _, _ := setupWFE(t) | ||||||
| 	makeGet := func(path, endpoint string) (*http.Request, *web.RequestEvent) { | 	makeGet := func(path, endpoint string) (*http.Request, *web.RequestEvent) { | ||||||
| 		return &http.Request{URL: &url.URL{Path: path}, Method: "GET"}, | 		return &http.Request{URL: &url.URL{Path: path}, Method: "GET"}, | ||||||
|  | @ -3595,7 +3959,46 @@ func TestGETAPIAuthz(t *testing.T) { | ||||||
| 	for _, tc := range testCases { | 	for _, tc := range testCases { | ||||||
| 		responseWriter := httptest.NewRecorder() | 		responseWriter := httptest.NewRecorder() | ||||||
| 		req, logEvent := makeGet(tc.path, getAuthzPath) | 		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 { | 		if responseWriter.Code == http.StatusOK && tc.expectTooFreshErr { | ||||||
| 			t.Errorf("expected too fresh error, got http.StatusOK") | 			t.Errorf("expected too fresh error, got http.StatusOK") | ||||||
|  | @ -3634,7 +4037,7 @@ func TestGETAPIChallenge(t *testing.T) { | ||||||
| 	for _, tc := range testCases { | 	for _, tc := range testCases { | ||||||
| 		responseWriter := httptest.NewRecorder() | 		responseWriter := httptest.NewRecorder() | ||||||
| 		req, logEvent := makeGet(tc.path, getAuthzPath) | 		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 { | 		if responseWriter.Code == http.StatusOK && tc.expectTooFreshErr { | ||||||
| 			t.Errorf("expected too fresh error, got http.StatusOK") | 			t.Errorf("expected too fresh error, got http.StatusOK") | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue