From 798531968736ed1211f553f32a49c7c7f0e3cef0 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 2 May 2026 11:22:48 +0200 Subject: [PATCH 1/6] add wiki --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b87eeedd..ba5d3591 100644 --- a/README.md +++ b/README.md @@ -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, long‑lived `_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 | From 7b19070d98ae6022fe1e202d96b5f7041c4fd8cb Mon Sep 17 00:00:00 2001 From: neil Date: Tue, 5 May 2026 20:26:37 +0200 Subject: [PATCH 2/6] fix ari https://github.com/acmesh-official/acme.sh/issues/6942#issuecomment-4381535765 --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 6c191c92..7e00c76a 100755 --- a/acme.sh +++ b/acme.sh @@ -6853,7 +6853,7 @@ deactivate() { #cert _getAKI() { _cert="$1" - openssl x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | tr -d ' :' + openssl x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | _egrep_o "[A-F0-9:]+" | tr -d ':' } #cert From 36667ab6568207b31c4828c0c0b23ea0e2efd469 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 6 May 2026 20:03:42 +0200 Subject: [PATCH 3/6] fix ari https://github.com/acmesh-official/acme.sh/issues/6942#issuecomment-4382355708 --- acme.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 7e00c76a..1ff1d9a9 100755 --- a/acme.sh +++ b/acme.sh @@ -6853,13 +6853,13 @@ deactivate() { #cert _getAKI() { _cert="$1" - openssl x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | _egrep_o "[A-F0-9:]+" | 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 From ac75c54ade31363a84bb5b226e34912eb287c833 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 6 May 2026 20:21:21 +0200 Subject: [PATCH 4/6] support NO_ARI=1 https://github.com/acmesh-official/acme.sh/discussions/6938 --- acme.sh | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/acme.sh b/acme.sh index 1ff1d9a9..4d12446a 100755 --- a/acme.sh +++ b/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" From 47378b563001a975b3bfd6380c1a482ad0b28729 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 6 May 2026 20:23:52 +0200 Subject: [PATCH 5/6] start 3.1.4 --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 4d12446a..bee84471 100755 --- a/acme.sh +++ b/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=3.1.3 +VER=3.1.4 PROJECT_NAME="acme.sh" From eaf4b62ba94bcb51edeabf84d2f055d2b9c59dde Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 6 May 2026 20:47:25 +0200 Subject: [PATCH 6/6] fix for ari --- acme.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/acme.sh b/acme.sh index bee84471..df11e6f3 100755 --- a/acme.sh +++ b/acme.sh @@ -6884,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)"