X Tutup
Skip to content

Commit 18f5562

Browse files
authored
First draft of ACME Renewal Info (letsencrypt#5691)
Add a new feature flag to control whether or not the experimental ARI information is exposed. Add a new entry to the Directory object which provides the base URL for ARI requests. Add a new handler to the WFE which parses incoming requests and returns reasonable renewalInfo. Part of letsencrypt#5674
1 parent acca5f9 commit 18f5562

File tree

4 files changed

+86
-2
lines changed

4 files changed

+86
-2
lines changed

core/objects.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,3 +511,15 @@ type SCTDERs [][]byte
511511
// CertDER is a convenience type that helps differentiate what the
512512
// underlying byte slice contains
513513
type CertDER []byte
514+
515+
// SuggestedWindow is a type exposed inside the RenewalInfo resource.
516+
type SuggestedWindow struct {
517+
Start time.Time `json:"start"`
518+
End time.Time `json:"end"`
519+
}
520+
521+
// RenewalInfo is a type which is exposed to clients which query the renewalInfo
522+
// endpoint specified in draft-aaron-ari.
523+
type RenewalInfo struct {
524+
SuggestedWindow SuggestedWindow `json:"suggestedWindow"`
525+
}

features/featureflag_string.go

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

features/features.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ const (
4949
// ECDSAForAll enables all accounts, regardless of their presence in the CA's
5050
// ecdsaAllowedAccounts config value, to get issuance from ECDSA issuers.
5151
ECDSAForAll
52+
// ServeRenewalInfo exposes the renewalInfo endpoint in the directory and for
53+
// GET requests. WARNING: This feature is a draft and highly unstable.
54+
ServeRenewalInfo
5255
)
5356

5457
// List of features and their default value, protected by fMu
@@ -70,6 +73,7 @@ var features = map[FeatureFlag]bool{
7073
NonCFSSLSigner: false,
7174
ECDSAForAll: false,
7275
StreamlineOrderAndAuthzs: false,
76+
ServeRenewalInfo: false,
7377
}
7478

7579
var fMu = new(sync.RWMutex)

wfe2/wfe.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

7274
var 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+
23532420
func extractRequesterIP(req *http.Request) (net.IP, error) {
23542421
ip := net.ParseIP(req.Header.Get("X-Real-IP"))
23552422
if ip != nil {

0 commit comments

Comments
 (0)
X Tutup