@@ -67,6 +67,8 @@ const (
6767 getAuthzPath = getAPIPrefix + "authz-v3/"
6868 getChallengePath = getAPIPrefix + "chall-v3/"
6969 getCertPath = getAPIPrefix + "cert/"
70+
71+ renewalInfoPath = getAPIPrefix + "draft-aaron-ari/renewalInfo/"
7072)
7173
7274var errIncompleteGRPCResponse = errors .New ("incomplete gRPC response message" )
@@ -395,6 +397,11 @@ func (wfe *WebFrontEndImpl) Handler(stats prometheus.Registerer) http.Handler {
395397 wfe .HandleFunc (m , getChallengePath , wfe .Challenge , "GET" )
396398 wfe .HandleFunc (m , getCertPath , wfe .Certificate , "GET" )
397399
400+ // Endpoint for draft-aaron-ari
401+ if features .Enabled (features .ServeRenewalInfo ) {
402+ wfe .HandleFunc (m , renewalInfoPath , wfe .RenewalInfo , "GET" )
403+ }
404+
398405 // We don't use our special HandleFunc for "/" because it matches everything,
399406 // meaning we can wind up returning 405 when we mean to return 404. See
400407 // https://github.com/letsencrypt/boulder/issues/717
@@ -467,6 +474,10 @@ func (wfe *WebFrontEndImpl) Directory(
467474 "keyChange" : rolloverPath ,
468475 }
469476
477+ if features .Enabled (features .ServeRenewalInfo ) {
478+ directoryEndpoints ["renewalInfo" ] = renewalInfoPath
479+ }
480+
470481 if request .Method == http .MethodPost {
471482 acct , prob := wfe .validPOSTAsGETForAccount (request , ctx , logEvent )
472483 if prob != nil {
@@ -2350,6 +2361,62 @@ func (wfe *WebFrontEndImpl) FinalizeOrder(ctx context.Context, logEvent *web.Req
23502361 }
23512362}
23522363
2364+ // RenewalInfo is used to get information about the suggested renewal window
2365+ // for the given certificate. It only accepts unauthenticated GET requests.
2366+ func (wfe * WebFrontEndImpl ) RenewalInfo (ctx context.Context , logEvent * web.RequestEvent , response http.ResponseWriter , request * http.Request ) {
2367+ if ! features .Enabled (features .ServeRenewalInfo ) {
2368+ wfe .sendError (response , logEvent , probs .NotFound ("Feature not enabled" ), nil )
2369+ return
2370+ }
2371+
2372+ uid := strings .SplitN (request .URL .Path , "/" , 3 )
2373+ if len (uid ) != 3 {
2374+ wfe .sendError (response , logEvent , probs .Malformed ("Path did not include exactly issuerKeyHash, issuerNameHash, and serialNumber" ), nil )
2375+ return
2376+ }
2377+
2378+ // For now, discard issuerKeyHash and issuerNameHash, because *we* know
2379+ // (Boulder implementation-specific) that we do not re-use the same serial
2380+ // number across multiple different issuers.
2381+ serial := uid [2 ]
2382+ if ! core .ValidSerial (serial ) {
2383+ wfe .sendError (response , logEvent , probs .NotFound ("Certificate not found" ), nil )
2384+ return
2385+ }
2386+ logEvent .Extra ["RequestedSerial" ] = serial
2387+ beeline .AddFieldToTrace (ctx , "request.serial" , serial )
2388+
2389+ // We use GetCertificate, not GetPrecertificate, because we don't intend to
2390+ // serve ARI for certs that never made it past the precert stage.
2391+ cert , err := wfe .SA .GetCertificate (ctx , & sapb.Serial {Serial : serial })
2392+ if err != nil {
2393+ if errors .Is (err , berrors .NotFound ) {
2394+ wfe .sendError (response , logEvent , probs .NotFound ("Certificate not found" ), nil )
2395+ } else {
2396+ wfe .sendError (response , logEvent , probs .ServerInternal ("Unable to get certificate" ), err )
2397+ }
2398+ return
2399+ }
2400+
2401+ // This is a very simple renewal calculation: Calculate a point 2/3rds of the
2402+ // way through the validity period, then give a 2-day window around that.
2403+ validity := time .Unix (0 , cert .Expires ).Add (time .Second ).Sub (time .Unix (0 , cert .Issued ))
2404+ renewalOffset := time .Duration (int64 (0.33 * float64 (validity .Seconds ())))
2405+ idealRenewal := time .Unix (0 , cert .Expires ).UTC ().Add (- renewalOffset )
2406+ ri := core.RenewalInfo {
2407+ SuggestedWindow : core.SuggestedWindow {
2408+ Start : idealRenewal .Add (- 24 * time .Hour ),
2409+ End : idealRenewal .Add (24 * time .Hour ),
2410+ },
2411+ }
2412+
2413+ err = wfe .writeJsonResponse (response , logEvent , http .StatusOK , ri )
2414+ if err != nil {
2415+ wfe .sendError (response , logEvent , probs .ServerInternal ("Error marshalling renewalInfo" ), err )
2416+ return
2417+ }
2418+ }
2419+
23532420func extractRequesterIP (req * http.Request ) (net.IP , error ) {
23542421 ip := net .ParseIP (req .Header .Get ("X-Real-IP" ))
23552422 if ip != nil {
0 commit comments