From 7b5a902755313849907eff257fb74e25bb96442b Mon Sep 17 00:00:00 2001 From: neil Date: Tue, 1 Sep 2026 20:26:34 +0800 Subject: [PATCH] Let an explicit --days or --valid-to outrank the ARI window ARI has overridden Le_NextRenewTime unconditionally since 3.1.4, so a user who passed --days never got the schedule they asked for, and --valid-to was guarded at issue time but not on the renewal check: the guard survived one run before the next cron rewrote it and saved it back. An explicit --days or --valid-to now pins the schedule. The window is still taken when it is earlier than what the user asked for, so a CA can pull an urgent renewal forward but can never push a pinned renewal back. Le_RenewalDays is only written to the domain conf when --days was actually passed, so its presence there is what marks a schedule as pinned. A fixed-date --valid-to opts out of ARI entirely: that cert is not renewed automatically at all, so pulling it forward would change what it does, not just when it renews. Both call sites go through the new _calc_ari_renew_time. --- acme.sh | 96 +++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 77 insertions(+), 19 deletions(-) diff --git a/acme.sh b/acme.sh index 77e37fd1..d883f3e4 100755 --- a/acme.sh +++ b/acme.sh @@ -2033,6 +2033,32 @@ _calc_validto_renew_time() { fi } +#Usage: _calc_ari_renew_time aristarttime ariendtime now currenttime pinned +#Prints the renew time to take from the CA's ARI suggestedWindow, or nothing +#when the window must be ignored. The point inside the window is derived from +#the current time rather than its start, so renewals spread out across the +#network instead of all firing at the same instant. +#A schedule the user pinned with --days or --valid-to only yields to a window +#that is EARLIER than what the user asked for: the CA can still pull an urgent +#renewal forward, but it can never push a pinned renewal back. +_calc_ari_renew_time() { + _cart_start="$1" + _cart_end="$2" + _cart_now="$3" + _cart_current="$4" + _cart_pinned="$5" + if [ -z "$_cart_start" ] || [ -z "$_cart_end" ] || [ "$_cart_end" -le "$_cart_start" ]; then + return 0 + fi + _cart_window=$(_math "$_cart_end" - "$_cart_start") + _cart_offset=$(_math "$_cart_now" % "$_cart_window") + _cart_next=$(_math "$_cart_start" + "$_cart_offset") + if [ "$_cart_pinned" ] && [ "$_cart_current" ] && [ "$_cart_next" -ge "$_cart_current" ]; then + return 0 + fi + printf "%s" "$_cart_next" +} + _mktemp() { if _exists mktemp; then if mktemp 2>/dev/null; then @@ -6150,10 +6176,16 @@ $_authorizations_map" Le_CertCreateTimeStr=$(_time2str "$Le_CertCreateTime") _savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr" + # Le_RenewalDays is only written to the domain conf when the user actually + # passed --days; the default schedule is never saved. That is what makes the + # presence of the value a reliable "the user pinned this" flag, here and on + # every later renewal check. if [ -z "$Le_RenewalDays" ]; then Le_RenewalDays="$DEFAULT_RENEW" + _ari_pinned="" else _savedomainconf "Le_RenewalDays" "$Le_RenewalDays" + _ari_pinned="1" fi if [ "$CA_BUNDLE" ]; then @@ -6227,15 +6259,25 @@ $_authorizations_map" Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") fi - # RFC 9773 ARI: if the CA exposes renewalInfo, override Le_NextRenewTime - # with a time picked at random within the suggestedWindow. This both gives - # the CA full control over renewal scheduling and disperses renewals across - # the network so all clients don't hit the CA at the same instant. + # RFC 9773 ARI: if the CA exposes renewalInfo, take Le_NextRenewTime from + # the suggestedWindow. This gives the CA control over renewal scheduling and + # disperses renewals across the network so all clients don't hit the CA at + # the same instant. + # An explicit --days or --valid-to wins over the window, except when the CA + # wants the cert renewed EARLIER than the user asked for: an urgent renewal + # must still get through. A fixed-date --valid-to opts out entirely, because + # there the cert is pinned to an expiry and is not renewed automatically at + # all -- letting ARI pull it forward would change that, not just its timing. # Set NO_ARI=1 (env, account.conf, or ca.conf) to opt out and fall back to # the legacy time-based renewal calculation. + if [ "$_notAfter" ]; then + _ari_pinned="1" + fi if [ "$NO_ARI" = "1" ]; then _debug "NO_ARI=1, skipping ARI suggestedWindow override" - elif [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ] && [ -z "$_notAfter" ]; then + elif [ "$_valid_to" ] && ! _startswith "$_valid_to" "+"; then + _debug "Fixed --valid-to, skipping ARI suggestedWindow override" + elif [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ]; then _ari_resp_new="$(_get_ARI "$CERT_PATH")" _debug2 "_ari_resp_new" "$_ari_resp_new" _ari_start_new="$(echo "$_ari_resp_new" | _egrep_o '"start" *: *"[^"]*' | sed 's/.*"//')" @@ -6243,13 +6285,15 @@ $_authorizations_map" if [ "$_ari_start_new" ] && [ "$_ari_end_new" ]; then _ari_start_t_new="$(_date2time "$(echo "$_ari_start_new" | sed 's/\.[0-9]*//')")" _ari_end_t_new="$(_date2time "$(echo "$_ari_end_new" | sed 's/\.[0-9]*//')")" - if [ "$_ari_start_t_new" ] && [ "$_ari_end_t_new" ] && [ "$_ari_end_t_new" -gt "$_ari_start_t_new" ]; then - _ari_window=$(_math "$_ari_end_t_new" - "$_ari_start_t_new") - _ari_offset=$(_math "$(_time)" % "$_ari_window") - Le_NextRenewTime=$(_math "$_ari_start_t_new" + "$_ari_offset") + _ari_next_new="$(_calc_ari_renew_time "$_ari_start_t_new" "$_ari_end_t_new" "$(_time)" "$Le_NextRenewTime" "$_ari_pinned")" + if [ "$_ari_next_new" ]; then + Le_NextRenewTime="$_ari_next_new" Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") _info "ARI suggestedWindow: $(__green "$_ari_start_new") to $(__green "$_ari_end_new")" _info "Next renewal time picked from ARI window: $(__green "$Le_NextRenewTimeStr")" + elif [ "$_ari_pinned" ]; then + _info "ARI suggestedWindow: $(__green "$_ari_start_new") to $(__green "$_ari_end_new")" + _info "It is later than the renewal time you asked for, keeping: $(__green "$Le_NextRenewTimeStr")" fi fi fi @@ -6392,10 +6436,21 @@ renew() { # ARI (RFC 9773): fetch the CA's suggestedWindow on every renewal check. # If the window has started, renew now even if Le_NextRenewTime is in the future. + # Le_RenewalDays and Le_Valid_To are only in the domain conf when the user + # passed --days or --valid-to, so their presence is what pins the schedule. + # A pinned schedule still yields to a window that is EARLIER than it, so the + # CA can pull an urgent renewal forward. A fixed-date --valid-to opts out + # entirely: that cert is not renewed automatically at all. # Set NO_ARI=1 (env, account.conf, or ca.conf) to opt out and use only # Le_NextRenewTime for the renewal decision. + _ari_pinned="" + if [ "$Le_RenewalDays" ] || [ "$Le_Valid_To" ]; then + _ari_pinned="1" + fi if [ "$NO_ARI" = "1" ]; then _debug "NO_ARI=1, skipping ARI suggestedWindow check" + elif [ "$Le_Valid_To" ] && ! _startswith "$Le_Valid_To" "+"; then + _debug "Fixed --valid-to, skipping ARI suggestedWindow check" elif [ -z "$FORCE" ] && [ -f "$CERT_PATH" ]; then if _initAPI && [ "$ACME_RENEWAL_INFO" ]; then _ari_resp="$(_get_ARI "$CERT_PATH")" @@ -6414,16 +6469,19 @@ renew() { _debug "Le_NextRenewTime" "$Le_NextRenewTime" # Update ARI if needed if [ "$_ari_start_t" ] && [ "$_ari_end_t" ] && [ "$Le_NextRenewTime" ] && [ "$_ari_end_t" -gt "$_ari_start_t" ] && ([ "$Le_NextRenewTime" -lt "$_ari_start_t" ] || [ "$Le_NextRenewTime" -gt "$_ari_end_t" ]); then - _ari_old_time_str="$Le_NextRenewTimeStr" - _info "Current renewal time: $(__green "$_ari_old_time_str")" - _ari_window=$(_math "$_ari_end_t" - "$_ari_start_t") - _ari_offset=$(_math "$(_time)" % "$_ari_window") - Le_NextRenewTime=$(_math "$_ari_start_t" + "$_ari_offset") - Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") - _info "ARI suggestedWindow: $(__green "$_ari_start") to $(__green "$_ari_end")" - _info "Updating renewal time picked from ARI window: $(__green "$Le_NextRenewTimeStr")" - _savedomainconf Le_NextRenewTime "$Le_NextRenewTime" - _savedomainconf Le_NextRenewTimeStr "$Le_NextRenewTimeStr" + _ari_next="$(_calc_ari_renew_time "$_ari_start_t" "$_ari_end_t" "$(_time)" "$Le_NextRenewTime" "$_ari_pinned")" + if [ "$_ari_next" ]; then + _ari_old_time_str="$Le_NextRenewTimeStr" + _info "Current renewal time: $(__green "$_ari_old_time_str")" + Le_NextRenewTime="$_ari_next" + Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") + _info "ARI suggestedWindow: $(__green "$_ari_start") to $(__green "$_ari_end")" + _info "Updating renewal time picked from ARI window: $(__green "$Le_NextRenewTimeStr")" + _savedomainconf Le_NextRenewTime "$Le_NextRenewTime" + _savedomainconf Le_NextRenewTimeStr "$Le_NextRenewTimeStr" + else + _debug "ARI wants a later renewal than --days/--valid-to asked for, keeping $Le_NextRenewTimeStr" + fi fi if [ "$Le_NextRenewTime" ] && [ "$(_time)" -ge "$Le_NextRenewTime" ]; then _info "ARI suggested renewal has passed ($(__green "$Le_NextRenewTimeStr")), proceeding with renewal."