acme-proxy 0.2.0

An ACME (RFC 8555) server that issues from a local CA, relays to an upstream CA, or delegates to a script
Documentation
# Renewal Information (ARI)

`acme-proxy` implements the ACME Renewal Information extension (RFC 9773). ARI
allows a CA to signal to ACME clients when they should ideally renew their
certificates. This prevents the "stampede" effect where thousands of
certificates expire simultaneously, and allows the CA to orchestrate graceful
mass-revocations by shortening the renewal window.

## `GET /renewalInfo/{certID}`

This is an unauthenticated endpoint clients use to ask "when should I renew this
certificate?". The response provides a time window (Start and End).

The window is resolved in this order:

1. **Revoked certificates win.** If the certificate is known to be revoked
   locally (looked up by its serial), the answer is a window **entirely in the
   past**, prompting a compliant client to renew immediately. This is checked
   **before** the signer backend is consulted, so a locally revoked certificate
   is never talked out of renewing by an upstream CA that has not noticed yet.
2. **The backend's own opinion**, if it has one. The `relay` backend relays
   the upstream CA's answer verbatim, including RFC 9773 §4.2's optional
   `explanationURL` — so if Let's Encrypt signals early renewal, that reaches
   your internal clients unchanged. The `custom` backend can do the same through
   its `renewal_info` hook, but **only when `signer.custom.supports_renewal_info
   = true`**; otherwise the hook is never invoked.
3. **A local estimate**, when the backend has no opinion — always the case for
   `local_ca`. The certificate's `notBefore` and `notAfter` are read by
   **parsing the certificate itself**, and the suggested window runs from **⅔ to
   ¾ of the validity period**. For a 90-day certificate that is roughly day 60
   to day 67, leaving a comfortable margin before expiry rather than running up
   to it.

Spreading clients across that window is what prevents the "stampede" effect
where a whole fleet renews at the same moment.

## The `replaces` field

During a `newOrder` request, an ARI-aware client can include a `replaces` field
containing the `certID` of the certificate it intends to replace.

`acme-proxy` validates it against all three of RFC 9773 §5's correspondence
rules. The `certID` is decoded into an Authority Key Identifier and a serial
number, the predecessor order is looked up, and then:

1. **The AKI must match** the predecessor's issuer — the serial alone is not
   enough, since serials are only unique per issuer. (A certificate stored
   before this check existed may have no recorded AKI; that case falls back to
   matching on serial alone, for backwards compatibility.)
2. **The predecessor must belong to the requesting account.** You cannot claim
   to replace someone else's certificate.
3. **The new order must share at least one identifier** with the predecessor. A
   renewal that covers none of the same names is not a replacement.

Failing any of them is `malformed`; an unknown `certID` likewise.

**Concurrency guard**: a client cannot have two orders replacing the same
predecessor. A second `newOrder` naming a `replaces` value already claimed gets
`409 alreadyReplaced`. An order that later becomes `invalid` releases its claim,
so a failed attempt does not permanently block a retry.

The accepted `replaces` value is stored and reflected back on the `201` response
*and* on every subsequent poll of the order.