mirror of
https://github.com/acmesh-official/acme.sh.git
synced 2026-10-21 21:31:51 +00:00
5 files changed
+959
-37
No files matched your search
@@ -146,6 +146,7 @@
|
|||||||
| 🌐 DNS mode | Use DNS TXT records |
|
| 🌐 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 |
|
| 🔗 [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 |
|
| 📡 [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, 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.
|
||||||
|
|
||||||
|
#### 🪄 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.
|
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 -
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1️⃣1️⃣ Issue Wildcard Certificates
|
### 1️⃣2️⃣ Issue Wildcard Certificates
|
||||||
|
|
||||||
It's simple! Just give a wildcard domain as the `-d` parameter:
|
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
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1️⃣2️⃣ How to Renew Certificates
|
### 1️⃣3️⃣ 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:
|
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.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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1️⃣3️⃣ How to Stop Certificate Renewal
|
### 1️⃣4️⃣ How to Stop Certificate Renewal
|
||||||
|
|
||||||
To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
|
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.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1️⃣4️⃣ How to Upgrade acme.sh
|
### 1️⃣5️⃣ How to Upgrade acme.sh
|
||||||
|
|
||||||
> 🚀 acme.sh is in constant development — it's strongly recommended to use the latest code.
|
> 🚀 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
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1️⃣5️⃣ Issue a Certificate from an Existing CSR
|
### 1️⃣6️⃣ Issue a Certificate from an Existing CSR
|
||||||
|
|
||||||
📚 https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR
|
📚 https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1️⃣6️⃣ Send Notifications in Cronjob
|
### 1️⃣7️⃣ Send Notifications in Cronjob
|
||||||
|
|
||||||
📚 https://github.com/acmesh-official/acme.sh/wiki/notify
|
📚 https://github.com/acmesh-official/acme.sh/wiki/notify
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1️⃣7️⃣ Under the Hood
|
### 1️⃣8️⃣ Under the Hood
|
||||||
|
|
||||||
> 🔧 Speak ACME language using shell, directly to "Let's Encrypt".
|
> 🔧 Speak ACME language using shell, directly to "Let's Encrypt".
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1️⃣8️⃣ Acknowledgments
|
### 1️⃣9️⃣ Acknowledgments
|
||||||
|
|
||||||
| Project | Link |
|
| 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
|
📄 **License:** GPLv3
|
||||||
|
|
||||||
@@ -565,7 +642,7 @@ Support this project with your organization. Your logo will show up here with a
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2️⃣0️⃣ Donate
|
### 2️⃣1️⃣ Donate
|
||||||
|
|
||||||
> 💝 Your donation makes **acme.sh** better!
|
> 💝 Your donation makes **acme.sh** better!
|
||||||
|
|
||||||
@@ -577,7 +654,7 @@ Support this project with your organization. Your logo will show up here with a
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2️⃣1️⃣ About This Repository
|
### 2️⃣2️⃣ About This Repository
|
||||||
|
|
||||||
> [!NOTE]
|
> [!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!
|
> 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!
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ DEFAULT_OPENSSL_BIN="openssl"
|
|||||||
VTYPE_HTTP="http-01"
|
VTYPE_HTTP="http-01"
|
||||||
VTYPE_DNS="dns-01"
|
VTYPE_DNS="dns-01"
|
||||||
VTYPE_ALPN="tls-alpn-01"
|
VTYPE_ALPN="tls-alpn-01"
|
||||||
|
VTYPE_DNS_PERSIST="dns-persist-01"
|
||||||
|
|
||||||
ID_TYPE_DNS="dns"
|
ID_TYPE_DNS="dns"
|
||||||
ID_TYPE_IP="ip"
|
ID_TYPE_IP="ip"
|
||||||
@@ -71,6 +72,7 @@ NO_VALUE="no"
|
|||||||
|
|
||||||
W_DNS="dns"
|
W_DNS="dns"
|
||||||
W_ALPN="alpn"
|
W_ALPN="alpn"
|
||||||
|
W_DNS_PERSIST="dns_persist"
|
||||||
DNS_ALIAS_PREFIX="="
|
DNS_ALIAS_PREFIX="="
|
||||||
|
|
||||||
MODE_STATELESS="stateless"
|
MODE_STATELESS="stateless"
|
||||||
@@ -1013,6 +1015,24 @@ _checkcert() {
|
|||||||
fi
|
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]
|
#Usage: hashalg [outputhex]
|
||||||
#Output Base64-encoded digest
|
#Output Base64-encoded digest
|
||||||
_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]
|
#Usage: hashalg secret_hex [outputhex]
|
||||||
#Output binary hmac
|
#Output binary hmac
|
||||||
_hmac() {
|
_hmac() {
|
||||||
@@ -1844,6 +1883,25 @@ _date2time() {
|
|||||||
return 1
|
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() {
|
_utc_date() {
|
||||||
date -u "+%Y-%m-%d %H:%M:%S"
|
date -u "+%Y-%m-%d %H:%M:%S"
|
||||||
}
|
}
|
||||||
@@ -4028,6 +4086,104 @@ deactivateaccount() {
|
|||||||
fi
|
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
|
# domain folder file
|
||||||
_findHook() {
|
_findHook() {
|
||||||
_hookdomain="$1"
|
_hookdomain="$1"
|
||||||
@@ -4709,13 +4865,41 @@ issue() {
|
|||||||
if [ "$_certificate_profile" ]; then
|
if [ "$_certificate_profile" ]; then
|
||||||
_newOrderObj="$_newOrderObj,\"profile\": \"$_certificate_profile\""
|
_newOrderObj="$_newOrderObj,\"profile\": \"$_certificate_profile\""
|
||||||
fi
|
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"
|
_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."
|
_err "Error creating new order."
|
||||||
_clearup
|
_clearup
|
||||||
_on_issue_err "$_post_hook"
|
_on_issue_err "$_post_hook"
|
||||||
return 1
|
return 1
|
||||||
fi
|
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 _contains "$response" "invalid"; then
|
||||||
if echo "$response" | _normalizeJson | grep '"status":"invalid"' >/dev/null 2>&1; then
|
if echo "$response" | _normalizeJson | grep '"status":"invalid"' >/dev/null 2>&1; then
|
||||||
_err "Create new order with invalid status."
|
_err "Create new order with invalid status."
|
||||||
@@ -4806,7 +4990,9 @@ $_authorizations_map"
|
|||||||
|
|
||||||
vtype="$VTYPE_HTTP"
|
vtype="$VTYPE_HTTP"
|
||||||
#todo, v2 wildcard force to use dns
|
#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"
|
vtype="$VTYPE_DNS"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -4864,18 +5050,7 @@ $_authorizations_map"
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$keyauthorization" ]; then
|
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)"
|
uri="$(echo "$entry" | _egrep_o '"url":"[^"]*' | cut -d '"' -f 4 | _head_n 1)"
|
||||||
|
|
||||||
_debug uri "$uri"
|
_debug uri "$uri"
|
||||||
|
|
||||||
if [ -z "$uri" ]; then
|
if [ -z "$uri" ]; then
|
||||||
@@ -4884,8 +5059,26 @@ $_authorizations_map"
|
|||||||
_on_issue_err "$_post_hook"
|
_on_issue_err "$_post_hook"
|
||||||
return 1
|
return 1
|
||||||
fi
|
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
|
fi
|
||||||
|
|
||||||
dvlist="$d$sep$keyauthorization$sep$uri$sep$vtype$sep$_currentRoot$sep$_authz_url"
|
dvlist="$d$sep$keyauthorization$sep$uri$sep$vtype$sep$_currentRoot$sep$_authz_url"
|
||||||
@@ -5427,7 +5620,7 @@ $_authorizations_map"
|
|||||||
Le_CertCreateTimeStr=$(_time2str "$Le_CertCreateTime")
|
Le_CertCreateTimeStr=$(_time2str "$Le_CertCreateTime")
|
||||||
_savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr"
|
_savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr"
|
||||||
|
|
||||||
if [ -z "$Le_RenewalDays" ] || [ "$Le_RenewalDays" -lt "0" ]; then
|
if [ -z "$Le_RenewalDays" ]; then
|
||||||
Le_RenewalDays="$DEFAULT_RENEW"
|
Le_RenewalDays="$DEFAULT_RENEW"
|
||||||
else
|
else
|
||||||
_savedomainconf "Le_RenewalDays" "$Le_RenewalDays"
|
_savedomainconf "Le_RenewalDays" "$Le_RenewalDays"
|
||||||
@@ -5486,11 +5679,49 @@ $_authorizations_map"
|
|||||||
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
|
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
|
||||||
fi
|
fi
|
||||||
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
|
else
|
||||||
Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60)
|
Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60)
|
||||||
Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400)
|
Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400)
|
||||||
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
|
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
|
||||||
fi
|
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_NextRenewTimeStr" "$Le_NextRenewTimeStr"
|
||||||
_savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime"
|
_savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime"
|
||||||
|
|
||||||
@@ -5586,7 +5817,31 @@ renew() {
|
|||||||
_debug2 "initpath again."
|
_debug2 "initpath again."
|
||||||
_initpath "$Le_Domain" "$_isEcc"
|
_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 "Skipping. Next renewal time is: $(__green "$Le_NextRenewTimeStr")"
|
||||||
_info "Add '$(__red '--force')' to force renewal."
|
_info "Add '$(__red '--force')' to force renewal."
|
||||||
if [ -z "$_ACME_IN_RENEWALL" ]; then
|
if [ -z "$_ACME_IN_RENEWALL" ]; then
|
||||||
@@ -6274,13 +6529,13 @@ installcronjob() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
_info "Installing cron job"
|
_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
|
if _exists uname && uname -a | grep SunOS >/dev/null; then
|
||||||
_CRONTAB_STDIN="$_CRONTAB --"
|
_CRONTAB_STDIN="$_CRONTAB --"
|
||||||
else
|
else
|
||||||
_CRONTAB_STDIN="$_CRONTAB -"
|
_CRONTAB_STDIN="$_CRONTAB -"
|
||||||
fi
|
fi
|
||||||
$_CRONTAB -l | {
|
$_CRONTAB -l 2>/dev/null | {
|
||||||
cat
|
cat
|
||||||
echo "$random_minute $random_hour * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null"
|
echo "$random_minute $random_hour * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null"
|
||||||
} | $_CRONTAB_STDIN
|
} | $_CRONTAB_STDIN
|
||||||
@@ -6608,21 +6863,29 @@ _getSerial() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#cert
|
#cert
|
||||||
_get_ARI() {
|
#Compute the ARI/replaces certID for a cert: base64url(AKI).base64url(Serial)
|
||||||
|
#per RFC 9773 Section 4.1.
|
||||||
|
_getARICertID() {
|
||||||
_cert="$1"
|
_cert="$1"
|
||||||
_aki=$(_getAKI "$_cert")
|
_aki=$(_getAKI "$_cert")
|
||||||
_ser=$(_getSerial "$_cert")
|
_ser=$(_getSerial "$_cert")
|
||||||
_debug2 "_aki" "$_aki"
|
_debug2 "_aki" "$_aki"
|
||||||
_debug2 "_ser" "$_ser"
|
_debug2 "_ser" "$_ser"
|
||||||
|
|
||||||
_akiurl="$(echo "$_aki" | _h2b | _base64 | tr -d = | _url_encode)"
|
_akiurl="$(echo "$_aki" | _h2b | _base64 | _url_replace)"
|
||||||
_debug2 "_akiurl" "$_akiurl"
|
_debug2 "_akiurl" "$_akiurl"
|
||||||
_serurl="$(echo "$_ser" | _h2b | _base64 | tr -d = | _url_encode)"
|
_serurl="$(echo "$_ser" | _h2b | _base64 | _url_replace)"
|
||||||
_debug2 "_serurl" "$_serurl"
|
_debug2 "_serurl" "$_serurl"
|
||||||
|
|
||||||
_ARI_URL="$ACME_RENEWAL_INFO/$_akiurl.$_serurl"
|
printf "%s.%s" "$_akiurl" "$_serurl"
|
||||||
_get "$_ARI_URL"
|
}
|
||||||
|
|
||||||
|
#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
|
# Detect profile file if not specified as environment variable
|
||||||
@@ -7158,6 +7421,8 @@ Commands:
|
|||||||
--update-account Update account info.
|
--update-account Update account info.
|
||||||
--register-account Register account key.
|
--register-account Register account key.
|
||||||
--deactivate-account Deactivate the account.
|
--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.
|
--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.
|
--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.
|
--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.
|
--dns [dns_hook] Use dns manual mode or dns api. Defaults to manual mode when argument is omitted.
|
||||||
See: $_DNS_API_WIKI
|
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.
|
--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.
|
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.
|
-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-kid <eab_key_id> Key Identifier for External Account Binding.
|
||||||
--eab-hmac-key <eab_hmac_key> HMAC key 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:
|
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.
|
-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.
|
--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.
|
--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.
|
--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.
|
--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.
|
--local-address <ip> Specifies the standalone/tls server listening address, in case you have multiple ip addresses.
|
||||||
@@ -7585,6 +7867,9 @@ _process() {
|
|||||||
_valid_to=""
|
_valid_to=""
|
||||||
_certificate_profile=""
|
_certificate_profile=""
|
||||||
_extended_key_usage=""
|
_extended_key_usage=""
|
||||||
|
_dns_persist_wildcard=""
|
||||||
|
_dns_persist_ca_name=""
|
||||||
|
_dns_persist_days=""
|
||||||
while [ ${#} -gt 0 ]; do
|
while [ ${#} -gt 0 ]; do
|
||||||
case "${1}" in
|
case "${1}" in
|
||||||
|
|
||||||
@@ -7679,6 +7964,20 @@ _process() {
|
|||||||
--deactivate-account)
|
--deactivate-account)
|
||||||
_CMD="deactivateaccount"
|
_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)
|
--set-notify)
|
||||||
_CMD="setnotify"
|
_CMD="setnotify"
|
||||||
;;
|
;;
|
||||||
@@ -7822,6 +8121,14 @@ _process() {
|
|||||||
_webroot="$_webroot,$wvalue"
|
_webroot="$_webroot,$wvalue"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
--dns-persist)
|
||||||
|
wvalue="$W_DNS_PERSIST"
|
||||||
|
if [ -z "$_webroot" ]; then
|
||||||
|
_webroot="$wvalue"
|
||||||
|
else
|
||||||
|
_webroot="$_webroot,$wvalue"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
--dnssleep)
|
--dnssleep)
|
||||||
_dnssleep="$2"
|
_dnssleep="$2"
|
||||||
Le_DNSSleep="$_dnssleep"
|
Le_DNSSleep="$_dnssleep"
|
||||||
@@ -8238,6 +8545,9 @@ _process() {
|
|||||||
deactivateaccount)
|
deactivateaccount)
|
||||||
deactivateaccount
|
deactivateaccount
|
||||||
;;
|
;;
|
||||||
|
makednspersistvalue)
|
||||||
|
makednspersistvalue "$_domain" "$_dns_persist_wildcard" "$_dns_persist_ca_name" "$_dns_persist_days"
|
||||||
|
;;
|
||||||
list)
|
list)
|
||||||
list "$_listraw" "$_domain"
|
list "$_listraw" "$_domain"
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||