Skip to content

Commit 2058d98

Browse files
authored
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
1 parent 2603aa4 commit 2058d98

File tree

2 files changed

+540
-68
lines changed

2 files changed

+540
-68
lines changed

wfe2/wfe.go

Lines changed: 107 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,18 @@ const (
5757
acctPath = "/acme/acct/"
5858
// When we moved to authzv2, we used a "-v3" suffix to avoid confusion
5959
// regarding ACMEv2.
60-
authzPath = "/acme/authz-v3/"
61-
challengePath = "/acme/chall-v3/"
62-
certPath = "/acme/cert/"
63-
revokeCertPath = "/acme/revoke-cert"
64-
buildIDPath = "/build"
65-
rolloverPath = "/acme/key-change"
66-
newNoncePath = "/acme/new-nonce"
67-
newOrderPath = "/acme/new-order"
68-
orderPath = "/acme/order/"
69-
finalizeOrderPath = "/acme/finalize/"
60+
authzPath = "/acme/authz-v3/"
61+
authzPathWithAcct = "/acme/authz/"
62+
challengePath = "/acme/chall-v3/"
63+
challengePathWithAcct = "/acme/chall/"
64+
certPath = "/acme/cert/"
65+
revokeCertPath = "/acme/revoke-cert"
66+
buildIDPath = "/build"
67+
rolloverPath = "/acme/key-change"
68+
newNoncePath = "/acme/new-nonce"
69+
newOrderPath = "/acme/new-order"
70+
orderPath = "/acme/order/"
71+
finalizeOrderPath = "/acme/finalize/"
7072

7173
getAPIPrefix = "/get/"
7274
getOrderPath = getAPIPrefix + "order/"
@@ -432,13 +434,15 @@ func (wfe *WebFrontEndImpl) Handler(stats prometheus.Registerer, oTelHTTPOptions
432434
// TODO(@cpu): After November 1st, 2020 support for "GET" to the following
433435
// endpoints will be removed, leaving only POST-as-GET support.
434436
wfe.HandleFunc(m, orderPath, wfe.GetOrder, "GET", "POST")
435-
wfe.HandleFunc(m, authzPath, wfe.Authorization, "GET", "POST")
436-
wfe.HandleFunc(m, challengePath, wfe.Challenge, "GET", "POST")
437+
wfe.HandleFunc(m, authzPath, wfe.AuthorizationHandler, "GET", "POST")
438+
wfe.HandleFunc(m, authzPathWithAcct, wfe.AuthorizationHandlerWithAccount, "GET", "POST")
439+
wfe.HandleFunc(m, challengePath, wfe.ChallengeHandler, "GET", "POST")
440+
wfe.HandleFunc(m, challengePathWithAcct, wfe.ChallengeHandlerWithAccount, "GET", "POST")
437441
wfe.HandleFunc(m, certPath, wfe.Certificate, "GET", "POST")
438442
// Boulder-specific GET-able resource endpoints
439443
wfe.HandleFunc(m, getOrderPath, wfe.GetOrder, "GET")
440-
wfe.HandleFunc(m, getAuthzPath, wfe.Authorization, "GET")
441-
wfe.HandleFunc(m, getChallengePath, wfe.Challenge, "GET")
444+
wfe.HandleFunc(m, getAuthzPath, wfe.AuthorizationHandler, "GET")
445+
wfe.HandleFunc(m, getChallengePath, wfe.ChallengeHandler, "GET")
442446
wfe.HandleFunc(m, getCertPath, wfe.Certificate, "GET")
443447

444448
// Endpoint for draft-ietf-acme-ari
@@ -1088,31 +1092,55 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(
10881092
response.WriteHeader(http.StatusOK)
10891093
}
10901094

1091-
// Challenge handles POST requests to challenge URLs.
1095+
// ChallengeHandler handles POST requests to challenge URLs of the form /acme/chall-v3/<authorizationID>/<challengeID>.
10921096
// Such requests are clients' responses to the server's challenges.
1093-
func (wfe *WebFrontEndImpl) Challenge(
1097+
func (wfe *WebFrontEndImpl) ChallengeHandler(
10941098
ctx context.Context,
10951099
logEvent *web.RequestEvent,
10961100
response http.ResponseWriter,
10971101
request *http.Request) {
1098-
notFound := func() {
1102+
slug := strings.Split(request.URL.Path, "/")
1103+
if len(slug) != 2 {
10991104
wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil)
1105+
return
11001106
}
1107+
1108+
wfe.Challenge(ctx, logEvent, challengePath, response, request, slug[0], slug[1])
1109+
}
1110+
1111+
// ChallengeHandlerWithAccount handles POST requests to challenge URLs of the form /acme/chall/{regID}/{authzID}/{challID}.
1112+
func (wfe *WebFrontEndImpl) ChallengeHandlerWithAccount(
1113+
ctx context.Context,
1114+
logEvent *web.RequestEvent,
1115+
response http.ResponseWriter,
1116+
request *http.Request) {
11011117
slug := strings.Split(request.URL.Path, "/")
1102-
if len(slug) != 2 {
1103-
notFound()
1118+
if len(slug) != 3 {
1119+
wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil)
11041120
return
11051121
}
1106-
authorizationID, err := strconv.ParseInt(slug[0], 10, 64)
1122+
// TODO(#7683): the regID is currently ignored.
1123+
wfe.Challenge(ctx, logEvent, challengePathWithAcct, response, request, slug[1], slug[2])
1124+
}
1125+
1126+
// Challenge handles POSTS to both formats of challenge URLs.
1127+
func (wfe *WebFrontEndImpl) Challenge(
1128+
ctx context.Context,
1129+
logEvent *web.RequestEvent,
1130+
handlerPath string,
1131+
response http.ResponseWriter,
1132+
request *http.Request,
1133+
authorizationIDStr string,
1134+
challengeID string) {
1135+
authorizationID, err := strconv.ParseInt(authorizationIDStr, 10, 64)
11071136
if err != nil {
11081137
wfe.sendError(response, logEvent, probs.Malformed("Invalid authorization ID"), nil)
11091138
return
11101139
}
1111-
challengeID := slug[1]
11121140
authzPB, err := wfe.ra.GetAuthorization(ctx, &rapb.GetAuthorizationRequest{Id: authorizationID})
11131141
if err != nil {
11141142
if errors.Is(err, berrors.NotFound) {
1115-
notFound()
1143+
wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil)
11161144
} else {
11171145
wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Problem getting authorization"), err)
11181146
}
@@ -1133,7 +1161,7 @@ func (wfe *WebFrontEndImpl) Challenge(
11331161
}
11341162
challengeIndex := authz.FindChallengeByStringID(challengeID)
11351163
if challengeIndex == -1 {
1136-
notFound()
1164+
wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil)
11371165
return
11381166
}
11391167

@@ -1157,11 +1185,11 @@ func (wfe *WebFrontEndImpl) Challenge(
11571185
challenge := authz.Challenges[challengeIndex]
11581186
switch request.Method {
11591187
case "GET", "HEAD":
1160-
wfe.getChallenge(response, request, authz, &challenge, logEvent)
1188+
wfe.getChallenge(handlerPath, response, request, authz, &challenge, logEvent)
11611189

11621190
case "POST":
11631191
logEvent.ChallengeType = string(challenge.Type)
1164-
wfe.postChallenge(ctx, response, request, authz, challengeIndex, logEvent)
1192+
wfe.postChallenge(ctx, handlerPath, response, request, authz, challengeIndex, logEvent)
11651193
}
11661194
}
11671195

@@ -1186,9 +1214,17 @@ func prepAccountForDisplay(acct *core.Registration) {
11861214
// prepChallengeForDisplay takes a core.Challenge and prepares it for display to
11871215
// the client by filling in its URL field and clearing several unnecessary
11881216
// fields.
1189-
func (wfe *WebFrontEndImpl) prepChallengeForDisplay(request *http.Request, authz core.Authorization, challenge *core.Challenge) {
1217+
func (wfe *WebFrontEndImpl) prepChallengeForDisplay(
1218+
handlerPath string,
1219+
request *http.Request,
1220+
authz core.Authorization,
1221+
challenge *core.Challenge,
1222+
) {
11901223
// Update the challenge URL to be relative to the HTTP request Host
11911224
challenge.URL = web.RelativeEndpoint(request, fmt.Sprintf("%s%s/%s", challengePath, authz.ID, challenge.StringID()))
1225+
if handlerPath == challengePathWithAcct || handlerPath == authzPathWithAcct {
1226+
challenge.URL = web.RelativeEndpoint(request, fmt.Sprintf("%s%d/%s/%s", challengePathWithAcct, authz.RegistrationID, authz.ID, challenge.StringID()))
1227+
}
11921228

11931229
// Internally, we store challenge error problems with just the short form
11941230
// (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
12111247

12121248
// prepAuthorizationForDisplay takes a core.Authorization and prepares it for
12131249
// display to the client by preparing all its challenges.
1214-
func (wfe *WebFrontEndImpl) prepAuthorizationForDisplay(request *http.Request, authz *core.Authorization) {
1250+
func (wfe *WebFrontEndImpl) prepAuthorizationForDisplay(handlerPath string, request *http.Request, authz *core.Authorization) {
12151251
for i := range authz.Challenges {
1216-
wfe.prepChallengeForDisplay(request, *authz, &authz.Challenges[i])
1252+
wfe.prepChallengeForDisplay(handlerPath, request, *authz, &authz.Challenges[i])
12171253
}
12181254

12191255
// Shuffle the challenges so no one relies on their order.
@@ -1235,15 +1271,15 @@ func (wfe *WebFrontEndImpl) prepAuthorizationForDisplay(request *http.Request, a
12351271
}
12361272

12371273
func (wfe *WebFrontEndImpl) getChallenge(
1274+
handlerPath string,
12381275
response http.ResponseWriter,
12391276
request *http.Request,
12401277
authz core.Authorization,
12411278
challenge *core.Challenge,
12421279
logEvent *web.RequestEvent) {
1280+
wfe.prepChallengeForDisplay(handlerPath, request, authz, challenge)
12431281

1244-
wfe.prepChallengeForDisplay(request, authz, challenge)
1245-
1246-
authzURL := urlForAuthz(authz, request)
1282+
authzURL := urlForAuthz(handlerPath, authz, request)
12471283
response.Header().Add("Location", challenge.URL)
12481284
response.Header().Add("Link", link(authzURL, "up"))
12491285

@@ -1258,6 +1294,7 @@ func (wfe *WebFrontEndImpl) getChallenge(
12581294

12591295
func (wfe *WebFrontEndImpl) postChallenge(
12601296
ctx context.Context,
1297+
handlerPath string,
12611298
response http.ResponseWriter,
12621299
request *http.Request,
12631300
authz core.Authorization,
@@ -1286,7 +1323,7 @@ func (wfe *WebFrontEndImpl) postChallenge(
12861323
// challenge details, not a POST to initiate a challenge
12871324
if string(body) == "" {
12881325
challenge := authz.Challenges[challengeIndex]
1289-
wfe.getChallenge(response, request, authz, &challenge, logEvent)
1326+
wfe.getChallenge(handlerPath, response, request, authz, &challenge, logEvent)
12901327
return
12911328
}
12921329

@@ -1336,9 +1373,9 @@ func (wfe *WebFrontEndImpl) postChallenge(
13361373

13371374
// assumption: PerformValidation does not modify order of challenges
13381375
challenge := returnAuthz.Challenges[challengeIndex]
1339-
wfe.prepChallengeForDisplay(request, authz, &challenge)
1376+
wfe.prepChallengeForDisplay(handlerPath, request, authz, &challenge)
13401377

1341-
authzURL := urlForAuthz(authz, request)
1378+
authzURL := urlForAuthz(handlerPath, authz, request)
13421379
response.Header().Add("Location", challenge.URL)
13431380
response.Header().Add("Link", link(authzURL, "up"))
13441381

@@ -1524,11 +1561,39 @@ func (wfe *WebFrontEndImpl) deactivateAuthorization(
15241561
return true
15251562
}
15261563

1527-
func (wfe *WebFrontEndImpl) Authorization(
1564+
// AuthorizationHandler handles requests to authorization URLs of the form /acme/authz/{authzID}.
1565+
func (wfe *WebFrontEndImpl) AuthorizationHandler(
1566+
ctx context.Context,
1567+
logEvent *web.RequestEvent,
1568+
response http.ResponseWriter,
1569+
request *http.Request) {
1570+
wfe.Authorization(ctx, authzPath, logEvent, response, request, request.URL.Path)
1571+
}
1572+
1573+
// AuthorizationHandlerWithAccount handles requests to authorization URLs of the form /acme/authz/{regID}/{authzID}.
1574+
func (wfe *WebFrontEndImpl) AuthorizationHandlerWithAccount(
15281575
ctx context.Context,
15291576
logEvent *web.RequestEvent,
15301577
response http.ResponseWriter,
15311578
request *http.Request) {
1579+
slug := strings.Split(request.URL.Path, "/")
1580+
if len(slug) != 2 {
1581+
wfe.sendError(response, logEvent, probs.NotFound("No such authorization"), nil)
1582+
return
1583+
}
1584+
// TODO(#7683): The regID is currently ignored.
1585+
wfe.Authorization(ctx, authzPathWithAcct, logEvent, response, request, slug[1])
1586+
}
1587+
1588+
// Authorization handles both `/acme/authz/{authzID}` and `/acme/authz/{regID}/{authzID}` requests,
1589+
// after the calling function has parsed out the authzID.
1590+
func (wfe *WebFrontEndImpl) Authorization(
1591+
ctx context.Context,
1592+
handlerPath string,
1593+
logEvent *web.RequestEvent,
1594+
response http.ResponseWriter,
1595+
request *http.Request,
1596+
authzIDStr string) {
15321597
var requestAccount *core.Registration
15331598
var requestBody []byte
15341599
// If the request is a POST it is either:
@@ -1546,7 +1611,7 @@ func (wfe *WebFrontEndImpl) Authorization(
15461611
requestBody = body
15471612
}
15481613

1549-
authzID, err := strconv.ParseInt(request.URL.Path, 10, 64)
1614+
authzID, err := strconv.ParseInt(authzIDStr, 10, 64)
15501615
if err != nil {
15511616
wfe.sendError(response, logEvent, probs.Malformed("Invalid authorization ID"), nil)
15521617
return
@@ -1615,7 +1680,7 @@ func (wfe *WebFrontEndImpl) Authorization(
16151680
return
16161681
}
16171682

1618-
wfe.prepAuthorizationForDisplay(request, &authz)
1683+
wfe.prepAuthorizationForDisplay(handlerPath, request, &authz)
16191684

16201685
err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, authz)
16211686
if err != nil {
@@ -2731,6 +2796,10 @@ func extractRequesterIP(req *http.Request) (net.IP, error) {
27312796
return net.ParseIP(host), nil
27322797
}
27332798

2734-
func urlForAuthz(authz core.Authorization, request *http.Request) string {
2799+
func urlForAuthz(handlerPath string, authz core.Authorization, request *http.Request) string {
2800+
if handlerPath == challengePathWithAcct || handlerPath == authzPathWithAcct {
2801+
return web.RelativeEndpoint(request, fmt.Sprintf("%s%d/%s", authzPathWithAcct, authz.RegistrationID, authz.ID))
2802+
}
2803+
27352804
return web.RelativeEndpoint(request, authzPath+authz.ID)
27362805
}

0 commit comments

Comments
 (0)