Skip to content

Commit fb7906e

Browse files
authored
wfe: add /get/certinfo (#8323)
This provides NotAfter for any serial number allocated, which includes precertificates that did not have a corresponding final certificate issued. Fixes #8287
1 parent 473b405 commit fb7906e

File tree

2 files changed

+63
-2
lines changed

2 files changed

+63
-2
lines changed

wfe2/wfe.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,9 @@ const (
6969
renewalInfoPath = "/acme/renewal-info/"
7070

7171
// Non-ACME paths.
72-
getCertPath = "/get/cert/"
73-
buildIDPath = "/build"
72+
getCertPath = "/get/cert/"
73+
getCertInfoPath = "/get/certinfo/"
74+
buildIDPath = "/build"
7475
)
7576

7677
const (
@@ -423,6 +424,7 @@ func (wfe *WebFrontEndImpl) Handler(stats prometheus.Registerer, oTelHTTPOptions
423424

424425
// Boulder specific endpoints
425426
wfe.HandleFunc(m, getCertPath, wfe.Certificate, "GET")
427+
wfe.HandleFunc(m, getCertInfoPath, wfe.CertificateInfo, "GET")
426428
wfe.HandleFunc(m, buildIDPath, wfe.BuildID, "GET")
427429

428430
// Endpoint for draft-ietf-acme-ari
@@ -1627,6 +1629,34 @@ func (wfe *WebFrontEndImpl) Authorization(
16271629
}
16281630
}
16291631

1632+
// CertificateInfo is a Boulder-specific endpoint to return notAfter, even for serials
1633+
// which only appear in a precertificate and don't have a corresponding final cert.
1634+
//
1635+
// This is used by our CRL monitoring infrastructure to determine when it is acceptable
1636+
// for a serial to be removed from a CRL.
1637+
func (wfe *WebFrontEndImpl) CertificateInfo(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) {
1638+
serial := request.URL.Path
1639+
if !core.ValidSerial(serial) {
1640+
wfe.sendError(response, logEvent, probs.NotFound("Certificate not found"), nil)
1641+
return
1642+
}
1643+
metadata, err := wfe.sa.GetSerialMetadata(ctx, &sapb.Serial{Serial: serial})
1644+
if err != nil {
1645+
wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Error getting certificate metadata"), err)
1646+
return
1647+
}
1648+
certInfoStruct := struct {
1649+
NotAfter time.Time `json:"notAfter"`
1650+
}{
1651+
NotAfter: metadata.Expires.AsTime(),
1652+
}
1653+
err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, certInfoStruct)
1654+
if err != nil {
1655+
wfe.sendError(response, logEvent, probs.ServerInternal("Error marshalling certInfoStruct"), err)
1656+
return
1657+
}
1658+
}
1659+
16301660
// Certificate is used by clients to request a copy of their current certificate, or to
16311661
// request a reissuance of the certificate.
16321662
func (wfe *WebFrontEndImpl) Certificate(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) {

wfe2/wfe_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2129,6 +2129,37 @@ func (sa *mockSAWithIncident) IncidentsForSerial(_ context.Context, req *sapb.Se
21292129
return &sapb.Incidents{}, nil
21302130
}
21312131

2132+
type mockSAWithSerialMetadata struct {
2133+
sapb.StorageAuthorityReadOnlyClient
2134+
}
2135+
2136+
func (sa *mockSAWithSerialMetadata) GetSerialMetadata(ctx context.Context, serial *sapb.Serial, _ ...grpc.CallOption) (*sapb.SerialMetadata, error) {
2137+
return &sapb.SerialMetadata{
2138+
Expires: timestamppb.New(time.Date(2025, 7, 29, 0, 0, 0, 0, time.UTC)),
2139+
}, nil
2140+
}
2141+
2142+
func TestGetCertInfo(t *testing.T) {
2143+
wfe, _, _ := setupWFE(t)
2144+
wfe.sa = &mockSAWithSerialMetadata{}
2145+
responseWriter := httptest.NewRecorder()
2146+
2147+
wfe.CertificateInfo(context.Background(), newRequestEvent(), responseWriter, &http.Request{
2148+
Method: "GET",
2149+
URL: &url.URL{Path: "aabbccddeeffaabbccddeeff000102030405"},
2150+
})
2151+
2152+
if responseWriter.Code != http.StatusOK {
2153+
t.Errorf("got HTTP status code %d, want %d", responseWriter.Code, http.StatusOK)
2154+
}
2155+
expected := `{
2156+
"notAfter": "2025-07-29T00:00:00Z"
2157+
}`
2158+
if responseWriter.Body.String() != expected {
2159+
t.Errorf("got response body %q, want %q", responseWriter.Body.String(), expected)
2160+
}
2161+
}
2162+
21322163
func TestGetCertificate(t *testing.T) {
21332164
wfe, _, signer := setupWFE(t)
21342165
wfe.sa = newMockSAWithCert(t, wfe.sa)

0 commit comments

Comments
 (0)