Merge pull request #6937 from acmesh-official/dev

sync
This commit is contained in:
neil
2026-05-02 10:56:54 +02:00
committed by GitHub
5 changed files with 959 additions and 37 deletions

103
README.md
View File

@@ -146,6 +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/)) |
---
@@ -396,7 +397,51 @@ acme.sh --renew -d example.com
---
### 🔟 Issue Certificates of Different Key Types (ECC or RSA)
### 🔟 Use 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.
#### 🪄 Step 1: Print the TXT record value
```bash
acme.sh --make-dns-persist-value -d example.com [--server letsencrypt] [--dns-persist-wildcard] [--dns-persist-ca-name "sectigo.com"] [--dns-persist-days 365]
```
Options:
| Flag | Description |
|------|-------------|
| `--server <ca>` | Pick the CA (default is your configured default). The account is registered automatically if you have not used this CA before. |
| `--dns-persist-wildcard` | Adds `policy=wildcard` to the record so it also authorizes wildcard / subdomain certs. |
| `--dns-persist-ca-name <name>` | Use a specific CA identity domain (e.g. `sectigo.com`). If omitted, identities are read from the ACME directory's `caaIdentities` field and one record per identity is printed — you only need to add **any one** of them. |
| `--dns-persist-days <N>` | Adds `persistUntil=<unix-timestamp>` to the record, set to N days from now. The CA will refuse new validations against the record after that time. Omit for a record with no expiry. |
You should get an output like:
```sh
TXT persist domain:_validation-persist.example.com
TXT persist value :"letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/123456789"
```
#### ✍️ Step 2: Add the TXT record to your DNS
Add the printed `TXT persist domain` / `TXT persist value` pair as a TXT record at your DNS provider, then wait for it to propagate.
#### 📜 Step 3: Issue the certificate
```bash
acme.sh --issue -d example.com --dns-persist
```
**Done!** No challenge token is provisioned during issuance — the CA reads the persistent TXT record directly.
> 🔄 Renewals just work: `acme.sh --renew -d example.com` (or the cron job) reuses the same TXT record automatically — no further DNS edits needed.
---
### 1⃣1⃣ Issue Certificates of Different Key Types (ECC or RSA)
Just set the `keylength` to a valid, supported value.
@@ -427,7 +472,7 @@ acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com -
---
### 11️⃣ Issue Wildcard Certificates
### 12️⃣ Issue Wildcard Certificates
It's simple! Just give a wildcard domain as the `-d` parameter:
@@ -439,9 +484,9 @@ acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf
---
### 12️⃣ How to Renew Certificates
### 13️⃣ How to Renew Certificates
> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days.
> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days, **or earlier when the CA's ARI says so** (see below).
However, you can force a renewal:
@@ -455,9 +500,41 @@ acme.sh --renew -d example.com --force
acme.sh --renew -d example.com --force --ecc
```
#### 📡 ACME Renewal Information (ARI) — RFC 9773
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 |
|------|------|-----|
| 🔍 **Polls `suggestedWindow`** | Every cron run, before deciding to skip | Lets the CA shift the renewal time forward in case of an incident (key compromise, mass revocation, etc.) |
| 🎯 **Picks a random renewal time** inside the window | Right after a successful issuance/renewal | Disperses renewals across the network so all clients don't hit the CA at the same instant |
| 🔗 **Sends `replaces=<certID>`** in `newOrder` | On renewal | Lets the CA correlate the new order with the certificate it supersedes (RFC 9773 §5) |
| ↩️ **Retries without `replaces`** | If the CA rejects with `alreadyReplaced` or an ARI validation error | Robust against edge cases (e.g. switching CAs, retired issuers) |
**Renewal trigger logic:** the cert is renewed if **any one** of the following becomes true:
1. `--force` is given
2. The CA's **ARI `suggestedWindow` has started**
3. The cached `Le_NextRenewTime` has passed (default fallback for CAs without ARI)
You can see the resulting next renewal time (already ARI-picked when applicable) in:
```sh
acme.sh --info -d example.com
# Look for: Le_NextRenewTimeStr=...
```
For the live ARI window the CA is currently advertising, run with `--debug 2`:
```sh
acme.sh --renew -d example.com --debug 2 2>&1 | grep -i 'ARI suggestedWindow'
```
> 💡 If your CA does not advertise `renewalInfo`, `acme.sh` falls back to the classic 30-day rule — no behavior change.
---
### 13️⃣ How to Stop Certificate Renewal
### 14️⃣ How to Stop Certificate Renewal
To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
@@ -471,7 +548,7 @@ The cert/key file is not removed from the disk.
---
### 14️⃣ How to Upgrade acme.sh
### 15️⃣ How to Upgrade acme.sh
> 🚀 acme.sh is in constant development — it's strongly recommended to use the latest code.
@@ -495,25 +572,25 @@ acme.sh --upgrade --auto-upgrade 0
---
### 15️⃣ Issue a Certificate from an Existing CSR
### 16️⃣ Issue a Certificate from an Existing CSR
📚 https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR
---
### 16️⃣ Send Notifications in Cronjob
### 17️⃣ Send Notifications in Cronjob
📚 https://github.com/acmesh-official/acme.sh/wiki/notify
---
### 17️⃣ Under the Hood
### 18️⃣ Under the Hood
> 🔧 Speak ACME language using shell, directly to "Let's Encrypt".
---
### 18️⃣ Acknowledgments
### 19️⃣ Acknowledgments
| Project | Link |
|---------|------|
@@ -555,7 +632,7 @@ Support this project with your organization. Your logo will show up here with a
---
### 1️⃣9️⃣ License & Others
### 2️⃣0️⃣ License & Others
📄 **License:** GPLv3
@@ -565,7 +642,7 @@ Support this project with your organization. Your logo will show up here with a
---
### 20️⃣ Donate
### 21️⃣ Donate
> 💝 Your donation makes **acme.sh** better!
@@ -577,7 +654,7 @@ Support this project with your organization. Your logo will show up here with a
---
### 21️⃣ About This Repository
### 22️⃣ About This Repository
> [!NOTE]
> This repository is officially maintained by <strong>ZeroSSL</strong> as part of our commitment to providing secure and reliable SSL/TLS solutions. We welcome contributions and feedback from the community!

358
acme.sh
View File

@@ -59,6 +59,7 @@ DEFAULT_OPENSSL_BIN="openssl"
VTYPE_HTTP="http-01"
VTYPE_DNS="dns-01"
VTYPE_ALPN="tls-alpn-01"
VTYPE_DNS_PERSIST="dns-persist-01"
ID_TYPE_DNS="dns"
ID_TYPE_IP="ip"
@@ -71,6 +72,7 @@ NO_VALUE="no"
W_DNS="dns"
W_ALPN="alpn"
W_DNS_PERSIST="dns_persist"
DNS_ALIAS_PREFIX="="
MODE_STATELESS="stateless"
@@ -1013,6 +1015,24 @@ _checkcert() {
fi
}
#file
_enddate() {
_cf="$1"
_res="$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -enddate -in "$_cf")"
if [ "$?" != "0" ] || [ -z "$_res" ]; then
return 1
fi
case "$_res" in
notAfter=*)
echo "${_res#notAfter=}"
;;
*)
return 1
;;
esac
}
#Usage: hashalg [outputhex]
#Output Base64-encoded digest
_digest() {
@@ -1037,6 +1057,25 @@ _digest() {
}
#Usage: certpath hashalg
#Output certificate fingerprint without colons
_fingerprint() {
cert="$1"
alg="$2"
if [ -z "$alg" ]; then
_usage "Usage: _fingerprint certpath hashalg"
return 1
fi
if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
# openssl prints "SHA1 Fingerprint=AA:BB:CC:..."; strip prefix and colons.
${ACME_OPENSSL_BIN:-openssl} x509 -in "$cert" -noout -fingerprint -"$alg" | sed 's/.*=//; s/://g'
else
_err "$alg is not supported yet"
return 1
fi
}
#Usage: hashalg secret_hex [outputhex]
#Output binary hmac
_hmac() {
@@ -1844,6 +1883,25 @@ _date2time() {
return 1
}
#support the output format of openssl -enddate:
# Apr 01 08:10:33 2022 GMT to 1641283833
_ssldate2time() {
#Linux
if date -u -d "$1" +"%s" 2>/dev/null; then
return
fi
#Solaris
if gdate -u -d "$1" +"%s" 2>/dev/null; then
return
fi
#Mac/BSD
if date -j -f "%b %d %T %Y %Z" "$1" +"%s" 2>/dev/null; then
return
fi
_err "Cannot parse _ssldate2time $1"
return 1
}
_utc_date() {
date -u "+%Y-%m-%d %H:%M:%S"
}
@@ -4028,6 +4086,104 @@ deactivateaccount() {
fi
}
#domain wildcard ca_name days
#Print the TXT record(s) the user must add to enable persistent DNS validation
#per draft-ietf-acme-dns-persist-01.
makednspersistvalue() {
_mdpv_domain="$1"
_mdpv_wildcard="$2"
_mdpv_ca_name="$3"
_mdpv_days="$4"
if [ -z "$_mdpv_domain" ]; then
_err "Please specify a domain with -d."
return 1
fi
if [ -n "$_mdpv_days" ]; then
case "$_mdpv_days" in
'' | *[!0-9]*)
_err "--dns-persist-days must be a positive integer, got: $_mdpv_days"
return 1
;;
esac
if [ "$_mdpv_days" -lt 1 ]; then
_err "--dns-persist-days must be at least 1."
return 1
fi
fi
_initpath
_accUri="$(_readcaconf ACCOUNT_URL)"
if [ -z "$_accUri" ]; then
_info "No account is registered for $ACME_DIRECTORY yet, registering one now..."
if ! _regAccount "$DEFAULT_ACCOUNT_KEY_LENGTH"; then
_err "Cannot register account."
return 1
fi
_accUri="$(_readcaconf ACCOUNT_URL)"
fi
if [ -z "$_accUri" ]; then
_err "Cannot determine the ACME account URL."
return 1
fi
_debug "Account URL" "$_accUri"
_txt_name="_validation-persist.$_mdpv_domain"
_txt_suffix="; accounturi=$_accUri"
if [ "$_mdpv_wildcard" = "1" ]; then
_txt_suffix="$_txt_suffix; policy=wildcard"
fi
if [ -n "$_mdpv_days" ]; then
_persist_until=$(_math "$(_time)" + "$_mdpv_days" \* 86400)
_txt_suffix="$_txt_suffix; persistUntil=$_persist_until"
_info "persistUntil set to $(__green "$(_time2str "$_persist_until")") ($_mdpv_days days from now)"
fi
if [ -n "$_mdpv_ca_name" ]; then
_info ""
_info "Add the following DNS TXT record to enable persistent DNS validation:"
_info ""
_info "$(printf 'TXT persist domain:%s' "$(__green "$_txt_name")")"
_info "$(printf 'TXT persist value :%s' "$(__green "\"$_mdpv_ca_name$_txt_suffix\"")")"
_info ""
return 0
fi
_info "Fetching ACME directory: $ACME_DIRECTORY"
_dir_resp="$(_get "$ACME_DIRECTORY" "" 30)"
if [ "$?" != "0" ] || [ -z "$_dir_resp" ]; then
_err "Cannot fetch ACME directory: $ACME_DIRECTORY"
return 1
fi
_dir_resp="$(echo "$_dir_resp" | _json_decode)"
_debug2 _dir_resp "$_dir_resp"
_caa_array="$(echo "$_dir_resp" | tr -d ' \r\n\t' | _egrep_o '"caaIdentities":\[[^]]*\]')"
_debug2 _caa_array "$_caa_array"
_caaids="$(echo "$_caa_array" | sed 's/.*\[//' | sed 's/\].*//' | tr ',' '\n' | tr -d '"')"
_debug2 _caaids "$_caaids"
if [ -z "$_caaids" ]; then
_err "The directory does not include 'caaIdentities'. Please specify --dns-persist-ca-name explicitly."
return 1
fi
_info ""
_info "Add ANY ONE of the following DNS TXT records to enable persistent DNS validation."
_info "(You only need to add one; pick whichever issuer identity you prefer.)"
for _id in $_caaids; do
[ -z "$_id" ] && continue
_info ""
_info "$(printf 'TXT persist domain:%s' "$(__green "$_txt_name")")"
_info "$(printf 'TXT persist value :%s' "$(__green "\"$_id$_txt_suffix\"")")"
done
_info ""
}
# domain folder file
_findHook() {
_hookdomain="$1"
@@ -4709,13 +4865,41 @@ issue() {
if [ "$_certificate_profile" ]; then
_newOrderObj="$_newOrderObj,\"profile\": \"$_certificate_profile\""
fi
# RFC 9773 Section 5: include "replaces" only when this is an actual
# 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.
_replaces_certID=""
if [ "$_ACME_IS_RENEW" = "1" ] && [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ]; then
_replaces_certID="$(_getARICertID "$CERT_PATH")"
_debug "Adding ARI replaces" "$_replaces_certID"
fi
_debug "STEP 1, Ordering a Certificate"
if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then
_newOrderReplacesObj="$_newOrderObj"
if [ "$_replaces_certID" ]; then
_newOrderReplacesObj="$_newOrderObj,\"replaces\": \"$_replaces_certID\""
fi
if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderReplacesObj}"; then
_err "Error creating new order."
_clearup
_on_issue_err "$_post_hook"
return 1
fi
# RFC 9773 Section 5 only defines the "alreadyReplaced" error, but real CAs
# (Let's Encrypt) may also reject with a malformed error if the prior cert
# was issued by a different issuer / different CA. Retry without "replaces"
# whenever the failure mentions ARI or the replaces field.
if [ "$_replaces_certID" ] && { _contains "$response" "alreadyReplaced" || _contains "$response" "'replaces'" || _contains "$response" "ARI"; }; then
_info "ARI 'replaces' rejected by CA, retrying newOrder without 'replaces'."
if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then
_err "Error creating new order."
_clearup
_on_issue_err "$_post_hook"
return 1
fi
fi
if _contains "$response" "invalid"; then
if echo "$response" | _normalizeJson | grep '"status":"invalid"' >/dev/null 2>&1; then
_err "Create new order with invalid status."
@@ -4806,7 +4990,9 @@ $_authorizations_map"
vtype="$VTYPE_HTTP"
#todo, v2 wildcard force to use dns
if _startswith "$_currentRoot" "$W_DNS"; then
if [ "$_currentRoot" = "$W_DNS_PERSIST" ]; then
vtype="$VTYPE_DNS_PERSIST"
elif _startswith "$_currentRoot" "$W_DNS"; then
vtype="$VTYPE_DNS"
fi
@@ -4864,18 +5050,7 @@ $_authorizations_map"
fi
if [ -z "$keyauthorization" ]; then
token="$(echo "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')"
_debug token "$token"
if [ -z "$token" ]; then
_err "Cannot get domain token $entry"
_clearup
_on_issue_err "$_post_hook"
return 1
fi
uri="$(echo "$entry" | _egrep_o '"url":"[^"]*' | cut -d '"' -f 4 | _head_n 1)"
_debug uri "$uri"
if [ -z "$uri" ]; then
@@ -4884,8 +5059,26 @@ $_authorizations_map"
_on_issue_err "$_post_hook"
return 1
fi
keyauthorization="$token.$thumbprint"
_debug keyauthorization "$keyauthorization"
if [ "$vtype" = "$VTYPE_DNS_PERSIST" ]; then
# dns-persist-01 challenges have no token; the TXT record is
# provisioned out-of-band. Use a non-empty placeholder so the
# downstream code does not treat this entry as already verified.
keyauthorization="$VTYPE_DNS_PERSIST"
_debug keyauthorization "$keyauthorization"
else
token="$(echo "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')"
_debug token "$token"
if [ -z "$token" ]; then
_err "Cannot get domain token $entry"
_clearup
_on_issue_err "$_post_hook"
return 1
fi
keyauthorization="$token.$thumbprint"
_debug keyauthorization "$keyauthorization"
fi
fi
dvlist="$d$sep$keyauthorization$sep$uri$sep$vtype$sep$_currentRoot$sep$_authz_url"
@@ -5427,7 +5620,7 @@ $_authorizations_map"
Le_CertCreateTimeStr=$(_time2str "$Le_CertCreateTime")
_savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr"
if [ -z "$Le_RenewalDays" ] || [ "$Le_RenewalDays" -lt "0" ]; then
if [ -z "$Le_RenewalDays" ]; then
Le_RenewalDays="$DEFAULT_RENEW"
else
_savedomainconf "Le_RenewalDays" "$Le_RenewalDays"
@@ -5486,11 +5679,49 @@ $_authorizations_map"
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
fi
fi
elif [ "$Le_RenewalDays" -lt "0" ]; then
_enddate_value=$(_enddate "$CERT_PATH")
if [ "$?" != "0" ] || [ -z "$_enddate_value" ]; then
_err "Failed to get certificate end date for $CERT_PATH"
return 1
fi
_endtime=$(_ssldate2time "$_enddate_value")
if [ "$?" != "0" ] || [ -z "$_endtime" ]; then
_err "Cannot parse _enddate_value: $_enddate_value"
return 1
fi
Le_NextRenewTime=$(_math "$_endtime" + "$Le_RenewalDays" \* 24 \* 60 \* 60)
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
else
Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60)
Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400)
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.
if [ "$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/.*"//')"
_ari_end_new="$(echo "$_ari_resp_new" | _egrep_o '"end" *: *"[^"]*' | sed 's/.*"//')"
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")
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")"
fi
fi
fi
_savedomainconf "Le_NextRenewTimeStr" "$Le_NextRenewTimeStr"
_savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime"
@@ -5586,7 +5817,31 @@ renew() {
_debug2 "initpath again."
_initpath "$Le_Domain" "$_isEcc"
if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then
# 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.
_ari_should_renew=""
if [ -z "$FORCE" ] && [ -f "$CERT_PATH" ]; then
if _initAPI && [ "$ACME_RENEWAL_INFO" ]; then
_ari_resp="$(_get_ARI "$CERT_PATH")"
_debug2 "_ari_resp" "$_ari_resp"
_ari_start="$(echo "$_ari_resp" | _egrep_o '"start" *: *"[^"]*' | sed 's/.*"//')"
_ari_end="$(echo "$_ari_resp" | _egrep_o '"end" *: *"[^"]*' | sed 's/.*"//')"
_debug "ARI suggestedWindow.start" "$_ari_start"
_debug "ARI suggestedWindow.end" "$_ari_end"
if [ "$_ari_start" ]; then
_ari_start_t="$(_date2time "$(echo "$_ari_start" | sed 's/\.[0-9]*//')")"
_debug "_ari_start_t" "$_ari_start_t"
if [ "$_ari_start_t" ] && [ "$(_time)" -ge "$_ari_start_t" ]; then
_info "ARI suggestedWindow has started ($(__green "$_ari_start")), proceeding with renewal."
_ari_should_renew="1"
else
_info "ARI suggestedWindow starts at: $(__green "$_ari_start")"
fi
fi
fi
fi
if [ -z "$FORCE" ] && [ -z "$_ari_should_renew" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then
_info "Skipping. Next renewal time is: $(__green "$Le_NextRenewTimeStr")"
_info "Add '$(__red '--force')' to force renewal."
if [ -z "$_ACME_IN_RENEWALL" ]; then
@@ -6274,13 +6529,13 @@ installcronjob() {
return 1
fi
_info "Installing cron job"
if ! $_CRONTAB -l | grep "$PROJECT_ENTRY --cron"; then
if ! $_CRONTAB -l 2>/dev/null | grep "$PROJECT_ENTRY --cron"; then
if _exists uname && uname -a | grep SunOS >/dev/null; then
_CRONTAB_STDIN="$_CRONTAB --"
else
_CRONTAB_STDIN="$_CRONTAB -"
fi
$_CRONTAB -l | {
$_CRONTAB -l 2>/dev/null | {
cat
echo "$random_minute $random_hour * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null"
} | $_CRONTAB_STDIN
@@ -6608,21 +6863,29 @@ _getSerial() {
}
#cert
_get_ARI() {
#Compute the ARI/replaces certID for a cert: base64url(AKI).base64url(Serial)
#per RFC 9773 Section 4.1.
_getARICertID() {
_cert="$1"
_aki=$(_getAKI "$_cert")
_ser=$(_getSerial "$_cert")
_debug2 "_aki" "$_aki"
_debug2 "_ser" "$_ser"
_akiurl="$(echo "$_aki" | _h2b | _base64 | tr -d = | _url_encode)"
_akiurl="$(echo "$_aki" | _h2b | _base64 | _url_replace)"
_debug2 "_akiurl" "$_akiurl"
_serurl="$(echo "$_ser" | _h2b | _base64 | tr -d = | _url_encode)"
_serurl="$(echo "$_ser" | _h2b | _base64 | _url_replace)"
_debug2 "_serurl" "$_serurl"
_ARI_URL="$ACME_RENEWAL_INFO/$_akiurl.$_serurl"
_get "$_ARI_URL"
printf "%s.%s" "$_akiurl" "$_serurl"
}
#cert
_get_ARI() {
_cert="$1"
_ari_certID="$(_getARICertID "$_cert")"
_ARI_URL="$ACME_RENEWAL_INFO/$_ari_certID"
_get "$_ARI_URL"
}
# Detect profile file if not specified as environment variable
@@ -7158,6 +7421,8 @@ Commands:
--update-account Update account info.
--register-account Register account key.
--deactivate-account Deactivate the account.
--make-dns-persist-value Print the DNS TXT record(s) to enable persistent DNS validation
(draft-ietf-acme-dns-persist-01). Use with -d <domain>.
--create-account-key Create an account private key, professional use.
--install-cronjob Install the cron job to renew certs, you don't need to call this. The 'install' command can automatically install the cron job.
--uninstall-cronjob Uninstall the cron job. The 'uninstall' command can do this automatically.
@@ -7205,6 +7470,10 @@ Parameters:
--dns [dns_hook] Use dns manual mode or dns api. Defaults to manual mode when argument is omitted.
See: $_DNS_API_WIKI
--dns-persist Use dns-persist-01 validation (draft-ietf-acme-dns-persist-01).
Requires the persistent _validation-persist TXT record to already
exist. Use '--make-dns-persist-value' to print the value to add.
--dnssleep <seconds> The time in seconds to wait for all the txt records to propagate in dns api mode.
It's not necessary to use this by default, $PROJECT_NAME polls dns status by DOH automatically.
-k, --keylength <bits> Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256, ec-384, ec-521.
@@ -7215,6 +7484,18 @@ Parameters:
--eab-kid <eab_key_id> Key Identifier for External Account Binding.
--eab-hmac-key <eab_hmac_key> HMAC key for External Account Binding.
--dns-persist-wildcard Used with '--make-dns-persist-value'. Adds 'policy=wildcard' to the
generated TXT record so the issuer is also authorized for wildcards
and subdomains (draft-ietf-acme-dns-persist-01).
--dns-persist-ca-name <name> Used with '--make-dns-persist-value'. Use the given CA identity domain
(e.g. 'ssl.com') as the issuer-domain-name in the TXT record. If
omitted, the identities are read from the ACME directory's
'caaIdentities' field and one record is printed per identity.
--dns-persist-days <N> Used with '--make-dns-persist-value'. Add a 'persistUntil' field to
the TXT record so the record self-expires N days from now (the CA
will refuse new validations against the record after that time).
If omitted, the record has no expiry.
These parameters are to install the cert to nginx/Apache or any other server after issue/renew a cert:
@@ -7235,6 +7516,7 @@ Parameters:
-m, --email <email> Specifies the account email, only valid for the '--install' and '--update-account' command.
--accountkey <file> Specifies the account key path, only valid for the '--install' command.
--days <ndays> Specifies the days to renew the cert when using '--issue' command. The default value is $DEFAULT_RENEW days.
Negative values could be used to specify a number of days relative to the expiration date of the certificate.
--httpport <port> Specifies the standalone listening port. Only valid if the server is behind a reverse proxy or load balancer.
--tlsport <port> Specifies the standalone tls listening port. Only valid if the server is behind a reverse proxy or load balancer.
--local-address <ip> Specifies the standalone/tls server listening address, in case you have multiple ip addresses.
@@ -7585,6 +7867,9 @@ _process() {
_valid_to=""
_certificate_profile=""
_extended_key_usage=""
_dns_persist_wildcard=""
_dns_persist_ca_name=""
_dns_persist_days=""
while [ ${#} -gt 0 ]; do
case "${1}" in
@@ -7679,6 +7964,20 @@ _process() {
--deactivate-account)
_CMD="deactivateaccount"
;;
--make-dns-persist-value | --makednspersistvalue)
_CMD="makednspersistvalue"
;;
--dns-persist-wildcard | --dnspersistwildcard)
_dns_persist_wildcard="1"
;;
--dns-persist-ca-name | --dnspersistcaname)
_dns_persist_ca_name="$2"
shift
;;
--dns-persist-days | --dnspersistdays)
_dns_persist_days="$2"
shift
;;
--set-notify)
_CMD="setnotify"
;;
@@ -7822,6 +8121,14 @@ _process() {
_webroot="$_webroot,$wvalue"
fi
;;
--dns-persist)
wvalue="$W_DNS_PERSIST"
if [ -z "$_webroot" ]; then
_webroot="$wvalue"
else
_webroot="$_webroot,$wvalue"
fi
;;
--dnssleep)
_dnssleep="$2"
Le_DNSSleep="$_dnssleep"
@@ -8238,6 +8545,9 @@ _process() {
deactivateaccount)
deactivateaccount
;;
makednspersistvalue)
makednspersistvalue "$_domain" "$_dns_persist_wildcard" "$_dns_persist_ca_name" "$_dns_persist_days"
;;
list)
list "$_listraw" "$_domain"
;;

158
deploy/windows_rdp.sh Normal file
View File

@@ -0,0 +1,158 @@
#!/usr/bin/env sh
# install a certificate on a Windows host over OpenSSH and bind it to the Remote
# Desktop listener (RDP-Tcp).
#
# One ssh invocation does the whole job:
# * the PFX is built locally, base64'd, and embedded as a string literal
# inside a generated PowerShell script;
# * the script is piped to `powershell.exe -Command -` over ssh. No scp,
# no temp files on the Windows host.
#
# First run:
# export DEPLOY_WIN_RDP_HOST=winserver.example.com
# acme.sh --deploy -d winserver.example.com --deploy-hook windows_rdp
#
# Available variables:
# DEPLOY_WIN_RDP_HOST required SSH host
# DEPLOY_WIN_RDP_USER optional SSH user, must be a local administrator (can also by set via ssh_config)
# DEPLOY_WIN_RDP_PORT optional SSH port, default 22
# DEPLOY_WIN_RDP_SSH_OPTS optional extra ssh options, e.g.
# "-i /root/.ssh/win_id_ed25519 -o StrictHostKeyChecking=yes"
# DEPLOY_WIN_RDP_LISTENER optional RDP listener name, default RDP-Tcp
# DEPLOY_WIN_RDP_RESTART optional "1" to restart TermService after install.
# Active RDP sessions will drop!
windows_rdp_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if ! _exists "ssh"; then
_err "ssh is required but was not found in PATH."
return 1
fi
# ---- configuration ------------------------------------------------------
_getdeployconf DEPLOY_WIN_RDP_HOST
_getdeployconf DEPLOY_WIN_RDP_USER
_getdeployconf DEPLOY_WIN_RDP_PORT
_getdeployconf DEPLOY_WIN_RDP_SSH_OPTS
_getdeployconf DEPLOY_WIN_RDP_LISTENER
_getdeployconf DEPLOY_WIN_RDP_RESTART
if [ -z "$DEPLOY_WIN_RDP_HOST" ]; then
_err "DEPLOY_WIN_RDP_HOST must be set."
return 1
fi
_savedeployconf DEPLOY_WIN_RDP_HOST "$DEPLOY_WIN_RDP_HOST"
[ -n "$DEPLOY_WIN_RDP_USER" ] && _savedeployconf DEPLOY_WIN_RDP_USER "$DEPLOY_WIN_RDP_USER"
[ -n "$DEPLOY_WIN_RDP_PORT" ] && _savedeployconf DEPLOY_WIN_RDP_PORT "$DEPLOY_WIN_RDP_PORT"
[ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ] && _savedeployconf DEPLOY_WIN_RDP_SSH_OPTS "$DEPLOY_WIN_RDP_SSH_OPTS"
[ -n "$DEPLOY_WIN_RDP_LISTENER" ] && _savedeployconf DEPLOY_WIN_RDP_LISTENER "$DEPLOY_WIN_RDP_LISTENER"
[ -n "$DEPLOY_WIN_RDP_RESTART" ] && _savedeployconf DEPLOY_WIN_RDP_RESTART "$DEPLOY_WIN_RDP_RESTART"
_port="${DEPLOY_WIN_RDP_PORT:-22}"
_listener="${DEPLOY_WIN_RDP_LISTENER:-RDP-Tcp}"
if [ -n "$DEPLOY_WIN_RDP_USER" ]; then
_target="$DEPLOY_WIN_RDP_USER@$DEPLOY_WIN_RDP_HOST"
else
_target="$DEPLOY_WIN_RDP_HOST"
fi
_pfx_pass="acme"
# ---- build thumbprint + PFX locally ------------------------------------
_thumb="$(_fingerprint "$_ccert" 'sha1')"
if [ -z "$_thumb" ]; then
_err "Failed to compute certificate thumbprint."
return 1
fi
_debug "Thumbprint: $_thumb"
_debug "Building PFX at $_pfx_file"
_pfx_file="$(_mktemp)"
if ! _toPkcs "$_pfx_file" "$_ckey" "$_ccert" "$_cca" "$_pfx_pass"; then
_err "Failed to build PFX archive."
rm -f "$_pfx_file"
return 1
fi
_pfx_b64=$(_base64 "multiline" <"$_pfx_file")
rm -f "$_pfx_file"
# ---- build installer script --------------------------------------------
if [ "$DEPLOY_WIN_RDP_RESTART" = "1" ]; then
_restart_ps='Restart-Service -Name TermService -Force'
else
_restart_ps='# New RdP connections will pick up the new cert automatically.'
fi
# Escape every literal `$` with `\$` so the shell does not expand it.
# Values substituted from shell: $_pfx_b64, $_pfx_pass, $_thumb, $_listener.
_ps1=$(
cat <<PSEOF
\$ErrorActionPreference = 'Stop'
\$pfxBytes = [Convert]::FromBase64String('${_pfx_b64}')
# Note: It is quite important to use a X509Certificate2Collection here in any case, since we otherwise
# could run into quite a lot of trouble when importing the certificate including its entire chain
# and its private key. Windows might behave arbitrarily and not consistently import the certificate
# at all - unless "Exportable" is included in the storage flags. However, then the certificate seems
# unaccessible to TermService for some weird reasons despite all permissions being set (at least on my
# Win 11 lab machine). This might be some security setting that prevents TermService from working with
# exportable keys? I don't know - importing the entire collection including chain or not always fixes
# the issues.
#
# Note2: If you should have kicked yourself out for some reason, then deleting the certificate will make
# TermService restore the original, self-signed certificate after at least after the second login attempt.
# Deleting the certificate can be easily accomplished via the Powershell, since SSH access will still be
# present in any case - the following command should get you out of trouble:
# \$cert = Get-ChildItem -Path 'Cert:\LocalMachine\My\\${_thumb}' | Select-Object -First 1 | Remove-Item
\$flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]'MachineKeySet,PersistKeySet'
\$certs = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
\$certs.Import(\$pfxBytes, '${_pfx_pass}', \$flags)
\$store = [System.Security.Cryptography.X509Certificates.X509Store]::new('My', 'LocalMachine')
\$store.Open('ReadWrite')
\$store.AddRange(\$certs)
\$store.Close()
Write-Host "Installed certs into LocalMachine\\My"
\$ts = Get-CimInstance -Namespace root/cimv2/terminalservices -ClassName Win32_TSGeneralSetting -Filter "TerminalName='${_listener}'"
if (-not \$ts) { throw "Listener '${_listener}' not found." }
Set-CimInstance -InputObject \$ts -Property @{SSLCertificateSHA1Hash="${_thumb}"}
Write-Host "Listener ${_listener} now uses ${_thumb}"
${_restart_ps}
PSEOF
)
_debug "Powershell script:${_ps1}"
# ---- run over a single ssh connection ----------------------------------
_ssh_opts="-o BatchMode=yes -p $_port"
if [ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ]; then
_ssh_opts="$_ssh_opts $DEPLOY_WIN_RDP_SSH_OPTS"
fi
_info "Deploying to $DEPLOY_WIN_RDP_HOST ..."
# shellcheck disable=SC2086
if ! printf '%s\n' "$_ps1" | ssh $_ssh_opts "$_target" \
'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command -'; then
_err "Remote install failed. Re-run acme.sh with --debug to see the PowerShell output."
return 1
fi
_info "Certificate for $_cdomain deployed and bound to $_listener on $DEPLOY_WIN_RDP_HOST."
return 0
}

267
dnsapi/dns_eurodns.sh Normal file
View File

@@ -0,0 +1,267 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_eurodns_info='EuroDNS
Site: eurodns.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_eurodns
Options:
EURODNS_APP_ID Application ID
EURODNS_API_KEY API Key
EURODNS_TTL TTL. Default: "600".
Issues: github.com/acmesh-official/acme.sh/issues
Author: Nicolas Santorelli
'
#
# EuroDNS DNS API
#
# EuroDNS API documentation:
# https://docapi.eurodns.com
#
# Usage:
# export EURODNS_APP_ID="your-app-id"
# export EURODNS_API_KEY="your-api-key"
# acme.sh --issue --dns dns_eurodns -d example.com -d *.example.com
#
# The credentials will be saved in ~/.acme.sh/account.conf
#
# Optional:
# export EURODNS_API_URL="https://rest-api.eurodns.com" # Default API URL
# export EURODNS_TTL=600 # Default TTL (minimum 600 for EuroDNS)
#
EURODNS_API_DEFAULT="https://rest-api.eurodns.com"
EURODNS_TTL_DEFAULT=600
######## Public functions #####################
#Usage: dns_eurodns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_eurodns_add() {
fulldomain="$(echo "$1" | _lower_case)"
txtvalue=$2
_info "Using EuroDNS DNS API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}"
EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}"
EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}"
EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}"
EURODNS_TTL="${EURODNS_TTL:-$(_readaccountconf_mutable EURODNS_TTL)}"
EURODNS_TTL="${EURODNS_TTL:-$EURODNS_TTL_DEFAULT}"
if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then
EURODNS_APP_ID=""
EURODNS_API_KEY=""
_err "You didn't specify EuroDNS App ID and API Key."
_err "Please export EURODNS_APP_ID and EURODNS_API_KEY and try again."
return 1
fi
_saveaccountconf_mutable EURODNS_APP_ID "$EURODNS_APP_ID"
_saveaccountconf_mutable EURODNS_API_KEY "$EURODNS_API_KEY"
if [ "$EURODNS_API_URL" != "$EURODNS_API_DEFAULT" ]; then
_saveaccountconf_mutable EURODNS_API_URL "$EURODNS_API_URL"
fi
if [ "$EURODNS_TTL" != "$EURODNS_TTL_DEFAULT" ]; then
_saveaccountconf_mutable EURODNS_TTL "$EURODNS_TTL"
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "Invalid domain"
return 1
fi
_debug _domain "$_domain"
_debug _sub_domain "$_sub_domain"
_info "Adding TXT record"
if _eurodns_add_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then
_info "Added TXT record successfully."
return 0
else
_err "Failed to add TXT record."
return 1
fi
}
#Usage: fulldomain txtvalue
dns_eurodns_rm() {
fulldomain="$(echo "$1" | _lower_case)"
txtvalue=$2
_info "Using EuroDNS DNS API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}"
EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}"
EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}"
EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}"
if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then
EURODNS_APP_ID=""
EURODNS_API_KEY=""
_err "You didn't specify EuroDNS App ID and API Key."
return 1
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "Invalid domain"
return 1
fi
_debug _domain "$_domain"
_debug _sub_domain "$_sub_domain"
_info "Removing TXT record"
if _eurodns_rm_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then
_info "Removed TXT record successfully."
return 0
else
_err "Failed to remove TXT record."
return 1
fi
}
#################### Private functions below ##################################
# _sub_domain=_acme-challenge.www
# _domain=domain.com
_get_root() {
domain=$1
i=1
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
return 1
fi
_eurodns_rest GET "dns-zones/$h"
if [ "$?" != "0" ]; then
if [ "$_code" = "404" ]; then
_debug "Zone $h not found, continuing..."
else
_err "API error looking up zone $h"
return 1
fi
p=$i
i=$(_math "$i" + 1)
continue
fi
if _contains "$response" '"name"'; then
if [ "$i" = "1" ]; then
_sub_domain="@"
else
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
fi
_domain=$h
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
return 1
}
_eurodns_add_txt_record() {
domain=$1
subdomain=$2
txtvalue=$3
data='[{"type":"TXT","host":"'"$subdomain"'","rdata":"'"$txtvalue"'","ttl":'"$EURODNS_TTL"'}]'
_debug "Adding TXT record via API"
if _eurodns_rest POST "dns-zones/$domain/dns-records" "$data"; then
if _contains "$response" "$txtvalue"; then
return 0
fi
fi
_err "Failed to add TXT record"
return 1
}
_eurodns_rm_txt_record() {
domain=$1
subdomain=$2
txtvalue=$3
_debug "Getting current zone data for $domain"
if ! _eurodns_rest GET "dns-zones/$domain"; then
_err "Failed to get zone data"
return 1
fi
zone_data=$(echo "$response" | _normalizeJson)
_debug2 zone_data "$zone_data"
# Find the record ID matching our TXT record
record_id=$(echo "$zone_data" | tr '{' '\n' | grep -F '"TXT"' | grep -F "\"$subdomain\"" | grep -F "\"$txtvalue\"" | _egrep_o '"id" *: *[0-9]+' | cut -d : -f 2 | _head_n 1)
_debug record_id "$record_id"
if [ -z "$record_id" ]; then
_info "TXT record not found or already removed"
return 0
fi
_debug "Deleting TXT record $record_id"
if ! _eurodns_rest DELETE "dns-zones/$domain/dns-records/$record_id"; then
_err "Failed to delete TXT record"
return 1
fi
return 0
}
# Usage: _eurodns_rest METHOD ENDPOINT [DATA]
_eurodns_rest() {
method=$1
endpoint=$2
data="$3"
export _H1="X-APP-ID: $EURODNS_APP_ID"
export _H2="X-API-KEY: $EURODNS_API_KEY"
export _H3="Content-Type: application/json"
url="$EURODNS_API_URL/$endpoint"
_debug2 url "$url"
_debug2 method "$method"
_debug2 data "$data"
: >"$HTTP_HEADER"
if [ "$method" = "GET" ]; then
response="$(_get "$url")"
else
response="$(_post "$data" "$url" "" "$method")"
fi
_ret="$?"
unset _H1 _H2 _H3
_debug2 response "$response"
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
_debug2 _code "$_code"
if [ "$_ret" != "0" ]; then
_err "Error calling API: $endpoint"
return 1
fi
if [ "$_code" != "200" ] && [ "$_code" != "201" ] && [ "$_code" != "204" ]; then
if [ "$_code" != "404" ]; then
_err "API error (HTTP $_code): $response"
fi
return 1
fi
return 0
}

110
dnsapi/dns_firestorm.sh Normal file
View File

@@ -0,0 +1,110 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_firestorm_info='Firestorm.ch
Site: firestorm.ch
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_firestorm
Options:
FST_Key Customer ID
FST_Secret API Secret
FST_Url API URL. Optional. Default "https://api.firestorm.ch/acme-dns".
Issues: github.com/acmesh-official/acme.sh/issues/6839
Author: FireStorm GmbH
'
FST_Url_DEFAULT="https://api.firestorm.ch/acme-dns"
######## Public functions #####################
# Usage: dns_firestorm_add _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_firestorm_add() {
fulldomain=$1
txtvalue=$2
FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}"
FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}"
FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}"
if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then
_err "FST_Key and FST_Secret must be set"
_err "Get your API credentials at https://admin.firestorm.ch"
return 1
fi
FST_Url="${FST_Url:-$FST_Url_DEFAULT}"
_saveaccountconf_mutable FST_Key "$FST_Key"
_saveaccountconf_mutable FST_Secret "$FST_Secret"
if [ "$FST_Url" != "$FST_Url_DEFAULT" ]; then
_saveaccountconf_mutable FST_Url "$FST_Url"
else
_clearaccountconf_mutable FST_Url
fi
subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//')
_info "Adding TXT record for $fulldomain"
_debug "Subdomain" "$subdomain"
_debug "TXT value" "$txtvalue"
body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}"
response="$(_firestorm_api "update" "$body")"
if _contains "$response" "$txtvalue"; then
_info "TXT record added successfully"
return 0
fi
_err "Failed to add TXT record: $response"
return 1
}
# Usage: dns_firestorm_rm _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_firestorm_rm() {
fulldomain=$1
txtvalue=$2
FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}"
FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}"
FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}"
FST_Url="${FST_Url:-$FST_Url_DEFAULT}"
if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then
_err "FST_Key and FST_Secret must be set"
return 1
fi
subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//')
_info "Removing TXT record for $fulldomain"
body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}"
response="$(_firestorm_api "remove" "$body")"
if _contains "$response" "removed"; then
_info "TXT record removed"
return 0
fi
_err "Failed to remove TXT record: $response"
return 1
}
#################### Private functions below ##################################
# Escape special characters for safe JSON string interpolation
_json_safe() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
_firestorm_api() {
action=$1
data=$2
export _H1="X-Api-User: $FST_Key"
export _H2="X-Api-Key: $FST_Secret"
export _H3="Content-Type: application/json"
_post "$data" "$FST_Url/$action" "" "POST"
}