Skip to content

Commit c284efd

Browse files
authored
Merge branch 'dev' into dev
2 parents afdd4a2 + aba2583 commit c284efd

3 files changed

Lines changed: 235 additions & 2 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@
146146
| 🌐 DNS mode | Use DNS TXT records |
147147
| 🔗 [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) | Use DNS alias for verification |
148148
| 📡 [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) | Stateless verification |
149-
| 📌 DNS persist mode | Persistent DNS TXT record ([draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)) |
149+
| 📌 [DNS persist mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode) | Persistent DNS TXT record ([draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)) |
150150

151151
---
152152

@@ -399,6 +399,8 @@ acme.sh --renew -d example.com
399399

400400
### 🔟 Use DNS Persist Mode
401401

402+
📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode
403+
402404
📚 Spec: [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)
403405

404406
DNS persist mode lets you place a **single, long‑lived `_validation-persist` TXT record** in your zone and reuse it for every subsequent issuance and renewal. There is no per-issuance challenge token, so renewals require **no DNS edits** — useful when DNS API access is not available but you still want unattended renewals.
@@ -502,6 +504,8 @@ acme.sh --renew -d example.com --force --ecc
502504

503505
#### 📡 ACME Renewal Information (ARI) — RFC 9773
504506

507+
📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/ARI
508+
505509
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**:
506510

507511
| What | When | Why |

acme.sh

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1015,6 +1015,24 @@ _checkcert() {
10151015
fi
10161016
}
10171017

1018+
#file
1019+
_enddate() {
1020+
_cf="$1"
1021+
_res="$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -enddate -in "$_cf")"
1022+
if [ "$?" != "0" ] || [ -z "$_res" ]; then
1023+
return 1
1024+
fi
1025+
1026+
case "$_res" in
1027+
notAfter=*)
1028+
echo "${_res#notAfter=}"
1029+
;;
1030+
*)
1031+
return 1
1032+
;;
1033+
esac
1034+
}
1035+
10181036
#Usage: hashalg [outputhex]
10191037
#Output Base64-encoded digest
10201038
_digest() {
@@ -1039,6 +1057,25 @@ _digest() {
10391057

10401058
}
10411059

1060+
#Usage: certpath hashalg
1061+
#Output certificate fingerprint without colons
1062+
_fingerprint() {
1063+
cert="$1"
1064+
alg="$2"
1065+
if [ -z "$alg" ]; then
1066+
_usage "Usage: _fingerprint certpath hashalg"
1067+
return 1
1068+
fi
1069+
1070+
if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
1071+
# openssl prints "SHA1 Fingerprint=AA:BB:CC:..."; strip prefix and colons.
1072+
${ACME_OPENSSL_BIN:-openssl} x509 -in "$cert" -noout -fingerprint -"$alg" | sed 's/.*=//; s/://g'
1073+
else
1074+
_err "$alg is not supported yet"
1075+
return 1
1076+
fi
1077+
}
1078+
10421079
#Usage: hashalg secret_hex [outputhex]
10431080
#Output binary hmac
10441081
_hmac() {
@@ -1846,6 +1883,25 @@ _date2time() {
18461883
return 1
18471884
}
18481885

1886+
#support the output format of openssl -enddate:
1887+
# Apr 01 08:10:33 2022 GMT to 1641283833
1888+
_ssldate2time() {
1889+
#Linux
1890+
if date -u -d "$1" +"%s" 2>/dev/null; then
1891+
return
1892+
fi
1893+
#Solaris
1894+
if gdate -u -d "$1" +"%s" 2>/dev/null; then
1895+
return
1896+
fi
1897+
#Mac/BSD
1898+
if date -j -f "%b %d %T %Y %Z" "$1" +"%s" 2>/dev/null; then
1899+
return
1900+
fi
1901+
_err "Cannot parse _ssldate2time $1"
1902+
return 1
1903+
}
1904+
18491905
_utc_date() {
18501906
date -u "+%Y-%m-%d %H:%M:%S"
18511907
}
@@ -5564,7 +5620,7 @@ $_authorizations_map"
55645620
Le_CertCreateTimeStr=$(_time2str "$Le_CertCreateTime")
55655621
_savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr"
55665622

5567-
if [ -z "$Le_RenewalDays" ] || [ "$Le_RenewalDays" -lt "0" ]; then
5623+
if [ -z "$Le_RenewalDays" ]; then
55685624
Le_RenewalDays="$DEFAULT_RENEW"
55695625
else
55705626
_savedomainconf "Le_RenewalDays" "$Le_RenewalDays"
@@ -5623,6 +5679,20 @@ $_authorizations_map"
56235679
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
56245680
fi
56255681
fi
5682+
elif [ "$Le_RenewalDays" -lt "0" ]; then
5683+
_enddate_value=$(_enddate "$CERT_PATH")
5684+
if [ "$?" != "0" ] || [ -z "$_enddate_value" ]; then
5685+
_err "Failed to get certificate end date for $CERT_PATH"
5686+
return 1
5687+
fi
5688+
5689+
_endtime=$(_ssldate2time "$_enddate_value")
5690+
if [ "$?" != "0" ] || [ -z "$_endtime" ]; then
5691+
_err "Cannot parse _enddate_value: $_enddate_value"
5692+
return 1
5693+
fi
5694+
Le_NextRenewTime=$(_math "$_endtime" + "$Le_RenewalDays" \* 24 \* 60 \* 60)
5695+
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
56265696
else
56275697
Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60)
56285698
Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400)
@@ -7446,6 +7516,7 @@ Parameters:
74467516
-m, --email <email> Specifies the account email, only valid for the '--install' and '--update-account' command.
74477517
--accountkey <file> Specifies the account key path, only valid for the '--install' command.
74487518
--days <ndays> Specifies the days to renew the cert when using '--issue' command. The default value is $DEFAULT_RENEW days.
7519+
Negative values could be used to specify a number of days relative to the expiration date of the certificate.
74497520
--httpport <port> Specifies the standalone listening port. Only valid if the server is behind a reverse proxy or load balancer.
74507521
--tlsport <port> Specifies the standalone tls listening port. Only valid if the server is behind a reverse proxy or load balancer.
74517522
--local-address <ip> Specifies the standalone/tls server listening address, in case you have multiple ip addresses.

deploy/windows_rdp.sh

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env sh
2+
3+
# install a certificate on a Windows host over OpenSSH and bind it to the Remote
4+
# Desktop listener (RDP-Tcp).
5+
#
6+
# One ssh invocation does the whole job:
7+
# * the PFX is built locally, base64'd, and embedded as a string literal
8+
# inside a generated PowerShell script;
9+
# * the script is piped to `powershell.exe -Command -` over ssh. No scp,
10+
# no temp files on the Windows host.
11+
#
12+
# First run:
13+
# export DEPLOY_WIN_RDP_HOST=winserver.example.com
14+
# acme.sh --deploy -d winserver.example.com --deploy-hook windows_rdp
15+
#
16+
# Available variables:
17+
# DEPLOY_WIN_RDP_HOST required SSH host
18+
# DEPLOY_WIN_RDP_USER optional SSH user, must be a local administrator (can also by set via ssh_config)
19+
# DEPLOY_WIN_RDP_PORT optional SSH port, default 22
20+
# DEPLOY_WIN_RDP_SSH_OPTS optional extra ssh options, e.g.
21+
# "-i /root/.ssh/win_id_ed25519 -o StrictHostKeyChecking=yes"
22+
# DEPLOY_WIN_RDP_LISTENER optional RDP listener name, default RDP-Tcp
23+
# DEPLOY_WIN_RDP_RESTART optional "1" to restart TermService after install.
24+
# Active RDP sessions will drop!
25+
26+
windows_rdp_deploy() {
27+
_cdomain="$1"
28+
_ckey="$2"
29+
_ccert="$3"
30+
_cca="$4"
31+
_cfullchain="$5"
32+
33+
_debug _cdomain "$_cdomain"
34+
_debug _ckey "$_ckey"
35+
_debug _ccert "$_ccert"
36+
_debug _cca "$_cca"
37+
_debug _cfullchain "$_cfullchain"
38+
39+
if ! _exists "ssh"; then
40+
_err "ssh is required but was not found in PATH."
41+
return 1
42+
fi
43+
44+
# ---- configuration ------------------------------------------------------
45+
_getdeployconf DEPLOY_WIN_RDP_HOST
46+
_getdeployconf DEPLOY_WIN_RDP_USER
47+
_getdeployconf DEPLOY_WIN_RDP_PORT
48+
_getdeployconf DEPLOY_WIN_RDP_SSH_OPTS
49+
_getdeployconf DEPLOY_WIN_RDP_LISTENER
50+
_getdeployconf DEPLOY_WIN_RDP_RESTART
51+
52+
if [ -z "$DEPLOY_WIN_RDP_HOST" ]; then
53+
_err "DEPLOY_WIN_RDP_HOST must be set."
54+
return 1
55+
fi
56+
57+
_savedeployconf DEPLOY_WIN_RDP_HOST "$DEPLOY_WIN_RDP_HOST"
58+
[ -n "$DEPLOY_WIN_RDP_USER" ] && _savedeployconf DEPLOY_WIN_RDP_USER "$DEPLOY_WIN_RDP_USER"
59+
[ -n "$DEPLOY_WIN_RDP_PORT" ] && _savedeployconf DEPLOY_WIN_RDP_PORT "$DEPLOY_WIN_RDP_PORT"
60+
[ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ] && _savedeployconf DEPLOY_WIN_RDP_SSH_OPTS "$DEPLOY_WIN_RDP_SSH_OPTS"
61+
[ -n "$DEPLOY_WIN_RDP_LISTENER" ] && _savedeployconf DEPLOY_WIN_RDP_LISTENER "$DEPLOY_WIN_RDP_LISTENER"
62+
[ -n "$DEPLOY_WIN_RDP_RESTART" ] && _savedeployconf DEPLOY_WIN_RDP_RESTART "$DEPLOY_WIN_RDP_RESTART"
63+
64+
_port="${DEPLOY_WIN_RDP_PORT:-22}"
65+
_listener="${DEPLOY_WIN_RDP_LISTENER:-RDP-Tcp}"
66+
if [ -n "$DEPLOY_WIN_RDP_USER" ]; then
67+
_target="$DEPLOY_WIN_RDP_USER@$DEPLOY_WIN_RDP_HOST"
68+
else
69+
_target="$DEPLOY_WIN_RDP_HOST"
70+
fi
71+
_pfx_pass="acme"
72+
73+
# ---- build thumbprint + PFX locally ------------------------------------
74+
_thumb="$(_fingerprint "$_ccert" 'sha1')"
75+
if [ -z "$_thumb" ]; then
76+
_err "Failed to compute certificate thumbprint."
77+
return 1
78+
fi
79+
_debug "Thumbprint: $_thumb"
80+
81+
_debug "Building PFX at $_pfx_file"
82+
_pfx_file="$(_mktemp)"
83+
if ! _toPkcs "$_pfx_file" "$_ckey" "$_ccert" "$_cca" "$_pfx_pass"; then
84+
_err "Failed to build PFX archive."
85+
rm -f "$_pfx_file"
86+
return 1
87+
fi
88+
_pfx_b64=$(_base64 "multiline" <"$_pfx_file")
89+
rm -f "$_pfx_file"
90+
91+
# ---- build installer script --------------------------------------------
92+
if [ "$DEPLOY_WIN_RDP_RESTART" = "1" ]; then
93+
_restart_ps='Restart-Service -Name TermService -Force'
94+
else
95+
_restart_ps='# New RdP connections will pick up the new cert automatically.'
96+
fi
97+
98+
# Escape every literal `$` with `\$` so the shell does not expand it.
99+
# Values substituted from shell: $_pfx_b64, $_pfx_pass, $_thumb, $_listener.
100+
_ps1=$(
101+
cat <<PSEOF
102+
103+
\$ErrorActionPreference = 'Stop'
104+
105+
\$pfxBytes = [Convert]::FromBase64String('${_pfx_b64}')
106+
107+
# Note: It is quite important to use a X509Certificate2Collection here in any case, since we otherwise
108+
# could run into quite a lot of trouble when importing the certificate including its entire chain
109+
# and its private key. Windows might behave arbitrarily and not consistently import the certificate
110+
# at all - unless "Exportable" is included in the storage flags. However, then the certificate seems
111+
# unaccessible to TermService for some weird reasons despite all permissions being set (at least on my
112+
# Win 11 lab machine). This might be some security setting that prevents TermService from working with
113+
# exportable keys? I don't know - importing the entire collection including chain or not always fixes
114+
# the issues.
115+
#
116+
# Note2: If you should have kicked yourself out for some reason, then deleting the certificate will make
117+
# TermService restore the original, self-signed certificate after at least after the second login attempt.
118+
# Deleting the certificate can be easily accomplished via the Powershell, since SSH access will still be
119+
# present in any case - the following command should get you out of trouble:
120+
# \$cert = Get-ChildItem -Path 'Cert:\LocalMachine\My\\${_thumb}' | Select-Object -First 1 | Remove-Item
121+
122+
\$flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]'MachineKeySet,PersistKeySet'
123+
\$certs = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
124+
\$certs.Import(\$pfxBytes, '${_pfx_pass}', \$flags)
125+
126+
\$store = [System.Security.Cryptography.X509Certificates.X509Store]::new('My', 'LocalMachine')
127+
\$store.Open('ReadWrite')
128+
\$store.AddRange(\$certs)
129+
\$store.Close()
130+
Write-Host "Installed certs into LocalMachine\\My"
131+
132+
\$ts = Get-CimInstance -Namespace root/cimv2/terminalservices -ClassName Win32_TSGeneralSetting -Filter "TerminalName='${_listener}'"
133+
if (-not \$ts) { throw "Listener '${_listener}' not found." }
134+
Set-CimInstance -InputObject \$ts -Property @{SSLCertificateSHA1Hash="${_thumb}"}
135+
Write-Host "Listener ${_listener} now uses ${_thumb}"
136+
137+
${_restart_ps}
138+
PSEOF
139+
)
140+
_debug "Powershell script:${_ps1}"
141+
142+
# ---- run over a single ssh connection ----------------------------------
143+
_ssh_opts="-o BatchMode=yes -p $_port"
144+
if [ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ]; then
145+
_ssh_opts="$_ssh_opts $DEPLOY_WIN_RDP_SSH_OPTS"
146+
fi
147+
148+
_info "Deploying to $DEPLOY_WIN_RDP_HOST ..."
149+
# shellcheck disable=SC2086
150+
if ! printf '%s\n' "$_ps1" | ssh $_ssh_opts "$_target" \
151+
'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command -'; then
152+
_err "Remote install failed. Re-run acme.sh with --debug to see the PowerShell output."
153+
return 1
154+
fi
155+
156+
_info "Certificate for $_cdomain deployed and bound to $_listener on $DEPLOY_WIN_RDP_HOST."
157+
return 0
158+
}

0 commit comments

Comments
 (0)