Skip to content

Commit ec1abb4

Browse files
authored
ARI: Implement POST API (#6738)
Add ARI POST method stub implementation to the WFE. Fixes #6033
1 parent e1ed1a2 commit ec1abb4

File tree

2 files changed

+234
-12
lines changed

2 files changed

+234
-12
lines changed

wfe2/wfe.go

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ func (wfe *WebFrontEndImpl) Handler(stats prometheus.Registerer) http.Handler {
424424

425425
// Endpoint for draft-aaron-ari
426426
if features.Enabled(features.ServeRenewalInfo) {
427-
wfe.HandleFunc(m, renewalInfoPath, wfe.RenewalInfo, "GET")
427+
wfe.HandleFunc(m, renewalInfoPath, wfe.RenewalInfo, "GET", "POST")
428428
}
429429

430430
// Non-ACME endpoints
@@ -2257,6 +2257,11 @@ func (wfe *WebFrontEndImpl) RenewalInfo(ctx context.Context, logEvent *web.Reque
22572257
return
22582258
}
22592259

2260+
if request.Method == http.MethodPost {
2261+
wfe.UpdateRenewal(ctx, logEvent, response, request)
2262+
return
2263+
}
2264+
22602265
if len(request.URL.Path) == 0 {
22612266
wfe.sendError(response, logEvent, probs.NotFound("Must specify a request path"), nil)
22622267
return
@@ -2266,30 +2271,26 @@ func (wfe *WebFrontEndImpl) RenewalInfo(ctx context.Context, logEvent *web.Reque
22662271
// the base64url-encoded DER CertID sequence.
22672272
der, err := base64.RawURLEncoding.DecodeString(request.URL.Path)
22682273
if err != nil {
2269-
wfe.sendError(response, logEvent, probs.Malformed("Path was not base64url-encoded"), nil)
2274+
wfe.sendError(response, logEvent, probs.Malformed("Path was not base64url-encoded or had padding"), err)
22702275
return
22712276
}
22722277

22732278
var id certID
22742279
rest, err := asn1.Unmarshal(der, &id)
22752280
if err != nil || len(rest) != 0 {
2276-
wfe.sendError(response, logEvent, probs.Malformed("Path was not a DER-encoded CertID sequence"), nil)
2281+
wfe.sendError(response, logEvent, probs.Malformed("Path was not a DER-encoded CertID sequence"), err)
22772282
return
22782283
}
22792284

22802285
// Verify that the hash algorithm is SHA-256, so people don't use SHA-1 here.
22812286
if !id.HashAlgorithm.Algorithm.Equal(asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}) {
2282-
wfe.sendError(response, logEvent, probs.Malformed("Request used hash algorithm other than SHA-256"), nil)
2287+
wfe.sendError(response, logEvent, probs.Malformed("Request used hash algorithm other than SHA-256"), err)
22832288
return
22842289
}
22852290

22862291
// We can do all of our processing based just on the serial, because Boulder
22872292
// does not re-use the same serial across multiple issuers.
22882293
serial := core.SerialToString(id.SerialNumber)
2289-
if !core.ValidSerial(serial) {
2290-
wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), nil)
2291-
return
2292-
}
22932294
logEvent.Extra["RequestedSerial"] = serial
22942295

22952296
setDefaultRetryAfterHeader := func(response http.ResponseWriter) {
@@ -2356,6 +2357,72 @@ func (wfe *WebFrontEndImpl) RenewalInfo(ctx context.Context, logEvent *web.Reque
23562357
time.Unix(0, cert.Expires).UTC()))
23572358
}
23582359

2360+
// UpdateRenewal is used by the client to inform the server that they have
2361+
// replaced the certificate in question, so it can be safely revoked. All
2362+
// requests must be authenticated to the account which ordered the cert.
2363+
func (wfe *WebFrontEndImpl) UpdateRenewal(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) {
2364+
if !features.Enabled(features.ServeRenewalInfo) {
2365+
wfe.sendError(response, logEvent, probs.NotFound("Feature not enabled"), nil)
2366+
return
2367+
}
2368+
2369+
body, _, acct, prob := wfe.validPOSTForAccount(request, ctx, logEvent)
2370+
addRequesterHeader(response, logEvent.Requester)
2371+
if prob != nil {
2372+
// validPOSTForAccount handles its own setting of logEvent.Errors
2373+
wfe.sendError(response, logEvent, prob, nil)
2374+
return
2375+
}
2376+
2377+
var updateRenewalRequest struct {
2378+
CertID string `json:"certID"`
2379+
Replaced bool `json:"replaced"`
2380+
}
2381+
err := json.Unmarshal(body, &updateRenewalRequest)
2382+
if err != nil {
2383+
wfe.sendError(response, logEvent, probs.Malformed("Unable to unmarshal RenewalInfo POST request body"), err)
2384+
return
2385+
}
2386+
2387+
der, err := base64.RawURLEncoding.DecodeString(updateRenewalRequest.CertID)
2388+
if err != nil {
2389+
wfe.sendError(response, logEvent, probs.Malformed("certID was not base64url-encoded or contained padding"), err)
2390+
return
2391+
}
2392+
2393+
var id certID
2394+
rest, err := asn1.Unmarshal(der, &id)
2395+
if err != nil || len(rest) != 0 {
2396+
wfe.sendError(response, logEvent, probs.Malformed("certID was not a DER-encoded CertID ASN.1 sequence"), err)
2397+
return
2398+
}
2399+
2400+
if !id.HashAlgorithm.Algorithm.Equal(asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}) {
2401+
wfe.sendError(response, logEvent, probs.Malformed("Decoded CertID used a hashAlgorithm other than SHA-256"), err)
2402+
return
2403+
}
2404+
2405+
// We can do all of our processing based just on the serial, because Boulder
2406+
// does not re-use the same serial across multiple issuers.
2407+
serial := core.SerialToString(id.SerialNumber)
2408+
logEvent.Extra["RequestedSerial"] = serial
2409+
2410+
metadata, err := wfe.sa.GetSerialMetadata(ctx, &sapb.Serial{Serial: serial})
2411+
if err != nil {
2412+
wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), err)
2413+
return
2414+
}
2415+
2416+
if acct.ID != metadata.RegistrationID {
2417+
wfe.sendError(response, logEvent, probs.Unauthorized("Account ID doesn't match ID for certificate"), err)
2418+
return
2419+
}
2420+
2421+
// TODO(#6732): Write the replaced status to persistent storage.
2422+
2423+
response.WriteHeader(http.StatusOK)
2424+
}
2425+
23592426
func extractRequesterIP(req *http.Request) (net.IP, error) {
23602427
ip := net.ParseIP(req.Header.Get("X-Real-IP"))
23612428
if ip != nil {

wfe2/wfe_test.go

Lines changed: 159 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3459,12 +3459,14 @@ func TestARI(t *testing.T) {
34593459
msa := newMockSAWithCert(t, wfe.sa)
34603460
wfe.sa = msa
34613461

3462+
err := features.Set(map[string]bool{"ServeRenewalInfo": true})
3463+
test.AssertNotError(t, err, "setting feature flag")
3464+
defer features.Reset()
3465+
34623466
makeGet := func(path, endpoint string) (*http.Request, *web.RequestEvent) {
34633467
return &http.Request{URL: &url.URL{Path: path}, Method: "GET"},
34643468
&web.RequestEvent{Endpoint: endpoint, Extra: map[string]interface{}{}}
34653469
}
3466-
_ = features.Set(map[string]bool{"ServeRenewalInfo": true})
3467-
defer features.Reset()
34683470

34693471
// Load the certificate and its issuer.
34703472
cert, err := core.LoadCert("../test/hierarchy/ee-r3.cert.pem")
@@ -3586,12 +3588,14 @@ func TestIncidentARI(t *testing.T) {
35863588
expectSerialString := core.SerialToString(big.NewInt(12345))
35873589
wfe.sa = newMockSAWithIncident(wfe.sa, []string{expectSerialString})
35883590

3591+
err := features.Set(map[string]bool{"ServeRenewalInfo": true})
3592+
test.AssertNotError(t, err, "setting feature flag")
3593+
defer features.Reset()
3594+
35893595
makeGet := func(path, endpoint string) (*http.Request, *web.RequestEvent) {
35903596
return &http.Request{URL: &url.URL{Path: path}, Method: "GET"},
35913597
&web.RequestEvent{Endpoint: endpoint, Extra: map[string]interface{}{}}
35923598
}
3593-
_ = features.Set(map[string]bool{"ServeRenewalInfo": true})
3594-
defer features.Reset()
35953599

35963600
idBytes, err := asn1.Marshal(certID{
35973601
pkix.AlgorithmIdentifier{ // SHA256
@@ -3620,6 +3624,157 @@ func TestIncidentARI(t *testing.T) {
36203624
test.AssertEquals(t, ri.SuggestedWindow.End.Before(wfe.clk.Now()), true)
36213625
}
36223626

3627+
type mockSAWithSerialMetadata struct {
3628+
sapb.StorageAuthorityReadOnlyClient
3629+
serial string
3630+
regID int64
3631+
}
3632+
3633+
// GetSerialMetadata returns fake metadata if it recognizes the given serial.
3634+
func (sa *mockSAWithSerialMetadata) GetSerialMetadata(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*sapb.SerialMetadata, error) {
3635+
if req.Serial != sa.serial {
3636+
return nil, berrors.NotFoundError("metadata for certificate with serial %q not found", req.Serial)
3637+
}
3638+
3639+
return &sapb.SerialMetadata{
3640+
Serial: sa.serial,
3641+
RegistrationID: sa.regID,
3642+
}, nil
3643+
}
3644+
3645+
// TestUpdateARI tests that requests for real certs issued to the correct regID
3646+
// are accepted, while all others result in errors.
3647+
func TestUpdateARI(t *testing.T) {
3648+
wfe, _, signer := setupWFE(t)
3649+
3650+
err := features.Set(map[string]bool{"ServeRenewalInfo": true})
3651+
test.AssertNotError(t, err, "setting feature flag")
3652+
defer features.Reset()
3653+
3654+
makePost := func(regID int64, body string) *http.Request {
3655+
signedURL := fmt.Sprintf("http://localhost%s", renewalInfoPath)
3656+
_, _, jwsBody := signer.byKeyID(regID, nil, signedURL, body)
3657+
return makePostRequestWithPath(renewalInfoPath, jwsBody)
3658+
}
3659+
3660+
type jsonReq struct {
3661+
CertID string `json:"certID"`
3662+
Replaced bool `json:"replaced"`
3663+
}
3664+
3665+
// Load a cert, its issuer, and use OCSP to compute issuer name/key hashes.
3666+
cert, err := core.LoadCert("../test/hierarchy/ee-r3.cert.pem")
3667+
test.AssertNotError(t, err, "failed to load test certificate")
3668+
issuer, err := core.LoadCert("../test/hierarchy/int-r3.cert.pem")
3669+
test.AssertNotError(t, err, "failed to load test issuer")
3670+
ocspReqBytes, err := ocsp.CreateRequest(cert, issuer, &ocsp.RequestOptions{Hash: crypto.SHA256})
3671+
test.AssertNotError(t, err, "failed to create ocsp request")
3672+
ocspReq, err := ocsp.ParseRequest(ocspReqBytes)
3673+
test.AssertNotError(t, err, "failed to parse ocsp request")
3674+
3675+
// Set up the mock SA.
3676+
msa := mockSAWithSerialMetadata{wfe.sa, core.SerialToString(cert.SerialNumber), 1}
3677+
wfe.sa = &msa
3678+
3679+
// An empty POST should result in an error.
3680+
req := makePost(1, "")
3681+
responseWriter := httptest.NewRecorder()
3682+
wfe.UpdateRenewal(ctx, newRequestEvent(), responseWriter, req)
3683+
test.AssertEquals(t, responseWriter.Code, http.StatusBadRequest)
3684+
3685+
// Non-certID base64 should result in an error.
3686+
req = makePost(1, "aGVsbG8gd29ybGQK") // $ echo "hello world" | base64
3687+
responseWriter = httptest.NewRecorder()
3688+
wfe.UpdateRenewal(ctx, newRequestEvent(), responseWriter, req)
3689+
test.AssertEquals(t, responseWriter.Code, http.StatusBadRequest)
3690+
3691+
// Non-sha256 hash algorithm should result in an error.
3692+
idBytes, err := asn1.Marshal(certID{
3693+
pkix.AlgorithmIdentifier{ // definitely not SHA256
3694+
Algorithm: asn1.ObjectIdentifier{1, 2, 3, 4, 5},
3695+
Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
3696+
},
3697+
ocspReq.IssuerNameHash,
3698+
ocspReq.IssuerKeyHash,
3699+
cert.SerialNumber,
3700+
})
3701+
test.AssertNotError(t, err, "failed to marshal certID")
3702+
body, err := json.Marshal(jsonReq{
3703+
CertID: base64.RawURLEncoding.EncodeToString(idBytes),
3704+
Replaced: true,
3705+
})
3706+
test.AssertNotError(t, err, "failed to marshal request body")
3707+
req = makePost(1, string(body))
3708+
responseWriter = httptest.NewRecorder()
3709+
wfe.UpdateRenewal(ctx, newRequestEvent(), responseWriter, req)
3710+
test.AssertEquals(t, responseWriter.Code, http.StatusBadRequest)
3711+
3712+
// Unrecognized serial should result in an error.
3713+
idBytes, err = asn1.Marshal(certID{
3714+
pkix.AlgorithmIdentifier{ // SHA256
3715+
Algorithm: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1},
3716+
Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
3717+
},
3718+
ocspReq.IssuerNameHash,
3719+
ocspReq.IssuerKeyHash,
3720+
big.NewInt(12345),
3721+
})
3722+
test.AssertNotError(t, err, "failed to marshal certID")
3723+
body, err = json.Marshal(jsonReq{
3724+
CertID: base64.RawURLEncoding.EncodeToString(idBytes),
3725+
Replaced: true,
3726+
})
3727+
test.AssertNotError(t, err, "failed to marshal request body")
3728+
req = makePost(1, string(body))
3729+
responseWriter = httptest.NewRecorder()
3730+
wfe.UpdateRenewal(ctx, newRequestEvent(), responseWriter, req)
3731+
test.AssertEquals(t, responseWriter.Code, http.StatusNotFound)
3732+
3733+
// Recognized serial but owned by the wrong account should result in an error.
3734+
msa.regID = 2
3735+
idBytes, err = asn1.Marshal(certID{
3736+
pkix.AlgorithmIdentifier{ // SHA256
3737+
Algorithm: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1},
3738+
Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
3739+
},
3740+
ocspReq.IssuerNameHash,
3741+
ocspReq.IssuerKeyHash,
3742+
cert.SerialNumber,
3743+
})
3744+
test.AssertNotError(t, err, "failed to marshal certID")
3745+
body, err = json.Marshal(jsonReq{
3746+
CertID: base64.RawURLEncoding.EncodeToString(idBytes),
3747+
Replaced: true,
3748+
})
3749+
test.AssertNotError(t, err, "failed to marshal request body")
3750+
req = makePost(1, string(body))
3751+
responseWriter = httptest.NewRecorder()
3752+
wfe.UpdateRenewal(ctx, newRequestEvent(), responseWriter, req)
3753+
test.AssertEquals(t, responseWriter.Code, http.StatusForbidden)
3754+
3755+
// Recognized serial and owned by the right account should work.
3756+
msa.regID = 1
3757+
idBytes, err = asn1.Marshal(certID{
3758+
pkix.AlgorithmIdentifier{ // SHA256
3759+
Algorithm: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1},
3760+
Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
3761+
},
3762+
ocspReq.IssuerNameHash,
3763+
ocspReq.IssuerKeyHash,
3764+
cert.SerialNumber,
3765+
})
3766+
test.AssertNotError(t, err, "failed to marshal certID")
3767+
body, err = json.Marshal(jsonReq{
3768+
CertID: base64.RawURLEncoding.EncodeToString(idBytes),
3769+
Replaced: true,
3770+
})
3771+
test.AssertNotError(t, err, "failed to marshal request body")
3772+
req = makePost(1, string(body))
3773+
responseWriter = httptest.NewRecorder()
3774+
wfe.UpdateRenewal(ctx, newRequestEvent(), responseWriter, req)
3775+
test.AssertEquals(t, responseWriter.Code, http.StatusOK)
3776+
}
3777+
36233778
func TestOldTLSInbound(t *testing.T) {
36243779
wfe, _, _ := setupWFE(t)
36253780
req := &http.Request{

0 commit comments

Comments
 (0)