Merge pull request #6948 from acmesh-official/dev

sync
This commit is contained in:
neil
2026-05-06 20:52:11 +02:00
committed by GitHub
2 changed files with 33 additions and 7 deletions

View File

@@ -146,7 +146,7 @@
| 🌐 DNS mode | Use DNS TXT records |
| 🔗 [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) | Use DNS alias for verification |
| 📡 [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) | Stateless verification |
| 📌 DNS persist mode | Persistent DNS TXT record ([draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)) |
| 📌 [DNS persist mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode) | Persistent DNS TXT record ([draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)) |
---
@@ -399,6 +399,8 @@ acme.sh --renew -d example.com
### 🔟 Use DNS Persist Mode
📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode
📚 Spec: [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)
DNS persist mode lets you place a **single, longlived `_validation-persist` TXT record** in your zone and reuse it for every subsequent issuance and renewal. There is no per-issuance challenge token, so renewals require **no DNS edits** — useful when DNS API access is not available but you still want unattended renewals.
@@ -502,6 +504,8 @@ acme.sh --renew -d example.com --force --ecc
#### 📡 ACME Renewal Information (ARI) — RFC 9773
📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/ARI
If the CA exposes a `renewalInfo` endpoint in its ACME directory (Let's Encrypt, ZeroSSL, etc.), `acme.sh` follows [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html) automatically — **no flag needed, no opt-in**:
| What | When | Why |

34
acme.sh
View File

@@ -1,6 +1,6 @@
#!/usr/bin/env sh
VER=3.1.3
VER=3.1.4
PROJECT_NAME="acme.sh"
@@ -4870,8 +4870,12 @@ issue() {
# renewal (--renew path), the CA advertises renewalInfo, and a prior
# cert exists. --issue (even with --force) is not a renewal per RFC 9773
# which speaks of "a clear predecessor certificate" issued by this CA.
# NO_ARI=1 (env, account.conf, or ca.conf) disables ARI entirely, so the
# "replaces" field is also omitted.
_replaces_certID=""
if [ "$_ACME_IS_RENEW" = "1" ] && [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ]; then
if [ "$NO_ARI" = "1" ]; then
_debug "NO_ARI=1, omitting ARI 'replaces' field from newOrder"
elif [ "$_ACME_IS_RENEW" = "1" ] && [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ]; then
_replaces_certID="$(_getARICertID "$CERT_PATH")"
_debug "Adding ARI replaces" "$_replaces_certID"
fi
@@ -5703,7 +5707,11 @@ $_authorizations_map"
# 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.
if [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ] && [ -z "$_notAfter" ]; then
# Set NO_ARI=1 (env, account.conf, or ca.conf) to opt out and fall back to
# the legacy time-based renewal calculation.
if [ "$NO_ARI" = "1" ]; then
_debug "NO_ARI=1, skipping ARI suggestedWindow override"
elif [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ] && [ -z "$_notAfter" ]; 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/.*"//')"
@@ -5819,8 +5827,12 @@ 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.
# Set NO_ARI=1 (env, account.conf, or ca.conf) to opt out and use only
# Le_NextRenewTime for the renewal decision.
_ari_should_renew=""
if [ -z "$FORCE" ] && [ -f "$CERT_PATH" ]; then
if [ "$NO_ARI" = "1" ]; then
_debug "NO_ARI=1, skipping ARI suggestedWindow check"
elif [ -z "$FORCE" ] && [ -f "$CERT_PATH" ]; then
if _initAPI && [ "$ACME_RENEWAL_INFO" ]; then
_ari_resp="$(_get_ARI "$CERT_PATH")"
_debug2 "_ari_resp" "$_ari_resp"
@@ -6853,13 +6865,13 @@ deactivate() {
#cert
_getAKI() {
_cert="$1"
openssl x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | tr -d ' :'
${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | tr -d ': ' | sed "s/keyid//"
}
#cert
_getSerial() {
_cert="$1"
openssl x509 -in "$_cert" -serial -noout | cut -d = -f 2
${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -serial -noout | cut -d = -f 2
}
#cert
@@ -6872,6 +6884,16 @@ _getARICertID() {
_debug2 "_aki" "$_aki"
_debug2 "_ser" "$_ser"
# RFC 9773 Section 4.1 requires the DER-encoded INTEGER value bytes of
# serialNumber. When the high bit of the first byte is set (>= 0x80) DER
# prepends a 0x00 sign byte to keep the integer positive; openssl's hex
# output strips that, so add it back. Boulder (LE) accepts either form,
# but Sectigo (ZeroSSL) is strict and rejects newOrder with HTTP 401
# "replaces field does not identify a certificate" if the byte is missing.
case "$_ser" in
[89aAbBcCdDeEfF]*) _ser="00$_ser" ;;
esac
_akiurl="$(echo "$_aki" | _h2b | _base64 | _url_replace)"
_debug2 "_akiurl" "$_akiurl"
_serurl="$(echo "$_ser" | _h2b | _base64 | _url_replace)"