Compare commits

..
9 Commits
7 changed files with 53 additions and 605 deletions
+7 -30
View File
@@ -6,9 +6,6 @@ name: Mirror version tag
# pointing to the same object, so both forms exist. # pointing to the same object, so both forms exist.
# No retrigger loop: the tag filter never matches a "v"-prefixed tag, and # No retrigger loop: the tag filter never matches a "v"-prefixed tag, and
# refs created with GITHUB_TOKEN do not fire workflows anyway. # refs created with GITHUB_TOKEN do not fire workflows anyway.
# The job mirrors first and then fails when the pushed tag is lightweight,
# which is what the release form produces: that tag can never carry a
# signature, so the failure has to be loud.
on: on:
push: push:
@@ -29,39 +26,19 @@ jobs:
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }} TAG: ${{ github.ref_name }}
run: | run: |
if gh api "repos/$REPO/git/ref/tags/v$TAG" >/dev/null 2>&1; then
echo "Tag v$TAG already exists, nothing to do."
exit 0
fi
# Mirror the object the pushed tag actually points at: the commit # Mirror the object the pushed tag actually points at: the commit
# for a lightweight tag, the tag object itself for an annotated or # for a lightweight tag, the tag object itself for an annotated or
# signed one. Pointing the mirror at the commit would strip the # signed one. Pointing the mirror at the commit would strip the
# signature, so "git verify-tag v3.1.3" would fail while # signature, so "git verify-tag v3.1.3" would fail while
# "git verify-tag 3.1.3" succeeds. # "git verify-tag 3.1.3" succeeds.
_ref="$(gh api "repos/$REPO/git/ref/tags/$TAG" --jq '.object.sha + " " + .object.type')" sha="$(gh api "repos/$REPO/git/ref/tags/$TAG" --jq .object.sha)"
sha="${_ref%% *}"
objtype="${_ref##* }"
if [ -z "$sha" ] || [ "$sha" = "null" ]; then if [ -z "$sha" ] || [ "$sha" = "null" ]; then
echo "Could not resolve refs/tags/$TAG" echo "Could not resolve refs/tags/$TAG"
exit 1 exit 1
fi fi
if gh api "repos/$REPO/git/ref/tags/v$TAG" >/dev/null 2>&1; then gh api "repos/$REPO/git/refs" -f ref="refs/tags/v$TAG" -f sha="$sha"
echo "Tag v$TAG already exists, nothing to do." echo "Created tag v$TAG -> $sha"
else
gh api "repos/$REPO/git/refs" -f ref="refs/tags/v$TAG" -f sha="$sha"
echo "Created tag v$TAG -> $sha"
fi
- name: Check that the tag is signable
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
# A release published from the GitHub UI creates the tag server-side
# as a lightweight ref, which points straight at a commit and can
# never carry a signature (3.1.5 shipped that way, see issue 7273).
# The tag has to be created locally with "git tag -s" and pushed
# BEFORE the release is published, then selected on the release form.
objtype="$(gh api "repos/$REPO/git/ref/tags/$TAG" --jq .object.type)"
if [ "$objtype" != "tag" ]; then
echo "::error::refs/tags/$TAG points at a $objtype, not a tag object. It is a lightweight tag and carries no signature. Recreate it locally with 'git tag -s $TAG' and push it before publishing the release."
exit 1
fi
echo "refs/tags/$TAG is a tag object."
+4 -4
View File
@@ -233,7 +233,7 @@ acme.sh -h
#### 🔏 Verify a Release #### 🔏 Verify a Release
Release tags from `3.1.6` on are signed with the maintainer's SSH key. The Release tags from `3.1.5` on are signed with the maintainer's SSH key. The
signing happens on the maintainer's machine, so the private key is never signing happens on the maintainer's machine, so the private key is never
available to CI. The public half is [`allowed_signers`](allowed_signers) in available to CI. The public half is [`allowed_signers`](allowed_signers) in
this repository. From a clone: this repository. From a clone:
@@ -243,7 +243,7 @@ git config gpg.ssh.allowedSignersFile allowed_signers
``` ```
```bash ```bash
git verify-tag 3.1.6 git verify-tag 3.1.5
``` ```
The signature covers the tag object, which pins the commit and therefore the The signature covers the tag object, which pins the commit and therefore the
@@ -251,10 +251,10 @@ whole tree, so a good signature verifies every file at that release and no
separate tarball checksum is needed. Build a tarball from the verified tag: separate tarball checksum is needed. Build a tarball from the verified tag:
```bash ```bash
git archive --format=tar.gz --prefix=acme.sh-3.1.6/ 3.1.6 > acme.sh-3.1.6.tar.gz git archive --format=tar.gz --prefix=acme.sh-3.1.5/ 3.1.5 > acme.sh-3.1.5.tar.gz
``` ```
> ⚠️ Tags up to `3.1.5` are unsigned. > ⚠️ Tags up to `3.1.4` predate the signing key and are unsigned.
--- ---
+38 -164
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env sh #!/usr/bin/env sh
VER=3.1.7 VER=3.1.5
PROJECT_NAME="acme.sh" PROJECT_NAME="acme.sh"
@@ -2140,7 +2140,6 @@ _resethttp() {
__HTTP_INITIALIZED="" __HTTP_INITIALIZED=""
_ACME_CURL="" _ACME_CURL=""
_ACME_WGET="" _ACME_WGET=""
_ACME_WGET2=""
ACME_HTTP_NO_REDIRECTS="" ACME_HTTP_NO_REDIRECTS=""
} }
@@ -2191,15 +2190,7 @@ _inithttp() {
fi fi
if [ -z "$_ACME_WGET" ] && _exists "wget"; then if [ -z "$_ACME_WGET" ] && _exists "wget"; then
#wget2, the wget of Fedora 40 and later, prints nothing for -S under -q, _ACME_WGET="wget -q"
#so it runs without -q and _wget2_fix_header cleans up after it
_ACME_WGET2=""
if _contains "$(wget --version 2>&1 | _head_n 1)" "Wget2"; then
_ACME_WGET2=1
_ACME_WGET="wget"
else
_ACME_WGET="wget -q"
fi
if [ "$ACME_USE_IPV6_REQUESTS" ]; then if [ "$ACME_USE_IPV6_REQUESTS" ]; then
_ACME_WGET="$_ACME_WGET --inet6-only " _ACME_WGET="$_ACME_WGET --inet6-only "
elif [ "$ACME_USE_IPV4_REQUESTS" ]; then elif [ "$ACME_USE_IPV4_REQUESTS" ]; then
@@ -2209,8 +2200,7 @@ _inithttp() {
_ACME_WGET="$_ACME_WGET --max-redirect 0 " _ACME_WGET="$_ACME_WGET --max-redirect 0 "
fi fi
if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then
#the -d demultiplexing after each request expects wget 1.x output if [ "$_ACME_WGET" ] && _contains "$($_ACME_WGET --help 2>&1)" "--debug"; then
if [ -z "$_ACME_WGET2" ] && _contains "$($_ACME_WGET --help 2>&1)" "--debug"; then
_ACME_WGET="$_ACME_WGET -d " _ACME_WGET="$_ACME_WGET -d "
fi fi
fi fi
@@ -2230,30 +2220,6 @@ _inithttp() {
} }
#stdin: what wget2 -S wrote to stderr. Prints the response headers the way
#curl --dump-header writes them. wget2 frames every header block with lines of
#its own ("[0] Downloading ...", "# got header ...", and after the block
#"HTTP response 200 OK [url]", which callers would take for the status line),
#and prints no header block at all for a response without a body (a 204, the
#empty 200 of a revocation): then only the status line can be rebuilt, from
#the last "HTTP [ERROR ]response" line, with HTTP/1.1 assumed.
_wget2_headers() {
_w2h_in="$(cat)"
_w2h_cr="$(printf '\r')"
_w2h_blocks="$(printf "%s\n" "$_w2h_in" | sed -n "/^HTTP /d; /^HTTP\//,/^$_w2h_cr*\$/p")"
if [ "$_w2h_blocks" ]; then
printf "%s\n" "$_w2h_blocks"
else
printf "%s\n" "$_w2h_in" | sed -n 's/^HTTP ERROR response /HTTP response /; s/^HTTP response \([0-9][0-9]*\).*$/HTTP\/1.1 \1/p' | _tail_n 1
fi
}
#rewrite $HTTP_HEADER after a wget2 request, see _wget2_headers
_wget2_fix_header() {
_w2f_headers="$(_wget2_headers <"$HTTP_HEADER")"
printf "%s\n" "$_w2f_headers" >"$HTTP_HEADER"
}
# body url [needbase64] [POST|PUT|DELETE] [ContentType] # body url [needbase64] [POST|PUT|DELETE] [ContentType]
_post() { _post() {
body="$1" body="$1"
@@ -2349,15 +2315,7 @@ _post() {
response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")" response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")"
fi fi
elif [ "$httpmethod" = "HEAD" ]; then elif [ "$httpmethod" = "HEAD" ]; then
if [ "$_ACME_WGET2" ]; then if [ "$_postContentType" ]; then
#wget2 prints no headers for a HEAD response, not even with -S, but
#--save-headers writes them into the -O file
if [ "$_postContentType" ]; then
response="$($_WGET --method HEAD --save-headers -O "$HTTP_HEADER" --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --header "Content-Type: $_postContentType" "$_post_url" 2>/dev/null)"
else
response="$($_WGET --method HEAD --save-headers -O "$HTTP_HEADER" --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" "$_post_url" 2>/dev/null)"
fi
elif [ "$_postContentType" ]; then
response="$($_WGET --spider -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --header "Content-Type: $_postContentType" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")" response="$($_WGET --spider -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --header "Content-Type: $_postContentType" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")"
else else
response="$($_WGET --spider -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")" response="$($_WGET --spider -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")"
@@ -2383,9 +2341,6 @@ _post() {
cat "$HTTP_HEADER" >&2 cat "$HTTP_HEADER" >&2
_sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER" _sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
fi fi
if [ "$_ACME_WGET2" ]; then
_wget2_fix_header
fi
# remove leading whitespaces from header to match curl format # remove leading whitespaces from header to match curl format
_sed_i 's/^ //g' "$HTTP_HEADER" _sed_i 's/^ //g' "$HTTP_HEADER"
else else
@@ -2479,9 +2434,6 @@ _post_file() {
cat "$HTTP_HEADER" >&2 cat "$HTTP_HEADER" >&2
_sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER" _sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
fi fi
if [ "$_ACME_WGET2" ]; then
_wget2_fix_header
fi
# remove leading whitespaces from header to match curl format # remove leading whitespaces from header to match curl format
_sed_i 's/^ //g' "$HTTP_HEADER" _sed_i 's/^ //g' "$HTTP_HEADER"
else else
@@ -2549,9 +2501,6 @@ _get() {
cat "$HTTP_HEADER" >&2 cat "$HTTP_HEADER" >&2
_sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER" _sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
fi fi
if [ "$_ACME_WGET2" ]; then
_wget2_fix_header
fi
# remove leading whitespaces from header to match curl format # remove leading whitespaces from header to match curl format
_sed_i 's/^ //g' "$HTTP_HEADER" _sed_i 's/^ //g' "$HTTP_HEADER"
fi fi
@@ -2851,35 +2800,6 @@ _sed_escape_rhs() {
sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/|/\\|/g' sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/|/\\|/g'
} }
#_write_conf file content
#Replace the conf file with the content.
#Redirecting straight into the conf truncates it before anything is written, so
#a failed write (e.g. no space left on device) left an empty conf and the cert
#could not be renewed any more (#7247). Write a temp file next to the conf and
#rename it over the conf only after the content is verified.
_write_conf() {
__w_conf="$1"
__w_text="$2"
__w_tmp="$__w_conf.$$.tmp"
#cp -p, so the temp file carries the mode and owner of the conf
if ! cp -p "$__w_conf" "$__w_tmp" 2>/dev/null ||
! printf -- "%s\n" "$__w_text" 2>/dev/null >"$__w_tmp" ||
[ "$(cat "$__w_tmp")" != "$__w_text" ]; then
rm -f "$__w_tmp"
return 1
fi
if [ ! -L "$__w_conf" ] && mv -f "$__w_tmp" "$__w_conf" 2>/dev/null; then
return 0
fi
#a symlink or a bind mounted file cannot be renamed over, write in place
if ! cat "$__w_tmp" 2>/dev/null >"$__w_conf"; then
#the conf may be truncated now, the temp file is the only complete copy
_err "Cannot write $__w_conf, the new content is kept in $__w_tmp"
return 1
fi
rm -f "$__w_tmp"
}
#setopt "file" "opt" "=" "value" [";"] #setopt "file" "opt" "=" "value" [";"]
_setopt() { _setopt() {
__conf="$1" __conf="$1"
@@ -2906,29 +2826,39 @@ _setopt() {
return 1 return 1
;; ;;
esac esac
if ! __text="$(cat "$__conf")"; then if [ -n "$(_tail_c 1 <"$__conf")" ]; then
_err "Cannot read $__conf." echo >>"$__conf"
return 1
fi fi
#build the new content first and write it once through _write_conf:
#redirecting straight into the conf truncates it before anything is
#written, so a failing sed (#2426) or a failing write (#7247) left a
#truncated conf, and a short append left a half written line
__sed_err=""
if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then
_debug3 OK _debug3 OK
__val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)" __val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)"
__text="$(printf -- "%s\n" "$__text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|")" || __sed_err=1 text="$(cat "$__conf")"
#capture first, write only on success: redirecting sed straight into the
#conf file truncates it before sed runs, so a failing sed (e.g. on an
#unescaped special character in the value) wiped the whole conf (#2426)
if __text="$(printf -- "%s\n" "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|")"; then
printf -- "%s\n" "$__text" >"$__conf"
else
_err "Cannot save '$__opt' to $__conf."
return 1
fi
elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then
__val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)" __val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)"
__text="$(printf -- "%s\n" "$__text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|")" || __sed_err=1 text="$(cat "$__conf")"
if __text="$(printf -- "%s\n" "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|")"; then
printf -- "%s\n" "$__text" >"$__conf"
else
_err "Cannot save '$__opt' to $__conf."
return 1
fi
else else
_debug3 APP _debug3 APP
__text="$__text${__text:+$__nl}$__opt$__sep$__val$__end" #printf, not echo: dash's builtin echo interprets backslash escapes in
fi #the value and would corrupt it
if [ "$__sed_err" ] || ! _write_conf "$__conf" "$__text"; then printf -- "%s\n" "$__opt$__sep$__val$__end" >>"$__conf"
_err "Cannot save '$__opt' to $__conf."
return 1
fi fi
_debug3 "$(grep -n "^$__opt$__sep" "$__conf")" _debug3 "$(grep -n "^$__opt$__sep" "$__conf")"
} }
@@ -2958,11 +2888,7 @@ _clear_conf() {
_conf_data="$(cat "$_c_c_f")" _conf_data="$(cat "$_c_c_f")"
#printf, not echo: dash's builtin echo interprets backslash escapes and #printf, not echo: dash's builtin echo interprets backslash escapes and
#would corrupt saved values that contain them on every rewrite #would corrupt saved values that contain them on every rewrite
if ! _conf_data="$(printf -- "%s\n" "$_conf_data" | sed "/^$_sdkey *=.*$/d")" || printf -- "%s\n" "$_conf_data" | sed "/^$_sdkey *=.*$/d" >"$_c_c_f"
! _write_conf "$_c_c_f" "$_conf_data"; then
_err "Cannot clear '$_sdkey' in $_c_c_f."
return 1
fi
else else
_err "Config file is empty, cannot clear" _err "Config file is empty, cannot clear"
fi fi
@@ -3439,13 +3365,7 @@ __initHome() {
if [ -z "$ACCOUNT_CONF_PATH" ]; then if [ -z "$ACCOUNT_CONF_PATH" ]; then
if [ -f "$_DEFAULT_ACCOUNT_CONF_PATH" ]; then if [ -f "$_DEFAULT_ACCOUNT_CONF_PATH" ]; then
#Same as in _initpath: keep the live ACCOUNT_EMAIL across the sourcing,
#so that the -m address _process() exported is not replaced by the
#saved one. _process() calls __initHome directly, after the option
#loop, so this is the sourcing that -m used to lose to.
_ih_account_email="$ACCOUNT_EMAIL"
. "$_DEFAULT_ACCOUNT_CONF_PATH" . "$_DEFAULT_ACCOUNT_CONF_PATH"
ACCOUNT_EMAIL="$_ih_account_email"
fi fi
fi fi
@@ -3555,22 +3475,12 @@ _initpath() {
domain="$1" domain="$1"
_ilength="$2" _ilength="$2"
#Keep the live ACCOUNT_EMAIL, the one -m exported in _process() or the
#caller put in the environment. account.conf is sourced twice below (here
#and inside __initHome), and a sourced assignment would overwrite it with
#the saved address, so -m silently lost to whatever account.conf held.
#The saved address is not lost either way: _getAccountEmail() reads it
#with _readaccountconf as its last resort, after the per-CA CA_EMAIL.
_cli_account_email="$ACCOUNT_EMAIL"
__initHome __initHome
if [ -f "$ACCOUNT_CONF_PATH" ]; then if [ -f "$ACCOUNT_CONF_PATH" ]; then
. "$ACCOUNT_CONF_PATH" . "$ACCOUNT_CONF_PATH"
fi fi
ACCOUNT_EMAIL="$_cli_account_email"
if [ "$_ACME_IN_CRON" ]; then if [ "$_ACME_IN_CRON" ]; then
if [ ! "$_USER_PATH_EXPORTED" ]; then if [ ! "$_USER_PATH_EXPORTED" ]; then
_USER_PATH_EXPORTED=1 _USER_PATH_EXPORTED=1
@@ -4510,11 +4420,9 @@ _regAccount() {
_secure_debug3 _eab_kid "$_eab_kid" _secure_debug3 _eab_kid "$_eab_kid"
_secure_debug3 _eab_hmac_key "$_eab_hmac_key" _secure_debug3 _eab_hmac_key "$_eab_hmac_key"
_email="$(_getAccountEmail)" _email="$(_getAccountEmail)"
#CA_EMAIL is saved only once the CA has actually taken the contact, which if [ "$_email" ]; then
#is when it answers 201. For an account key it already knows it answers _savecaconf "CA_EMAIL" "$_email"
#200 and ignores the contact of the request, so saving here would record fi
#an address the CA never stored.
_saved_ca_email="$(_readcaconf CA_EMAIL)"
if [ "$ACME_DIRECTORY" = "$CA_ZEROSSL" ]; then if [ "$ACME_DIRECTORY" = "$CA_ZEROSSL" ]; then
if [ -z "$_eab_kid" ] || [ -z "$_eab_hmac_key" ]; then if [ -z "$_eab_kid" ] || [ -z "$_eab_hmac_key" ]; then
@@ -4593,15 +4501,8 @@ _regAccount() {
if [ "$code" = "" ] || [ "$code" = '201' ]; then if [ "$code" = "" ] || [ "$code" = '201' ]; then
echo "$response" >"$ACCOUNT_JSON_PATH" echo "$response" >"$ACCOUNT_JSON_PATH"
_info "Registered" _info "Registered"
if [ "$_email" ]; then
_savecaconf "CA_EMAIL" "$_email"
fi
elif [ "$code" = '409' ] || [ "$code" = '200' ]; then elif [ "$code" = '409' ] || [ "$code" = '200' ]; then
_info "Already registered" _info "Already registered"
if [ "$_email" ] && [ "$_email" != "$_saved_ca_email" ]; then
_info "The account email was not changed, the CA ignores the contact of an account it already has."
_info "Use '$PROJECT_ENTRY --update-account -m $_email' to change it."
fi
elif [ "$code" = '400' ] && _contains "$response" 'The account is not awaiting external account binding'; then elif [ "$code" = '400' ] && _contains "$response" 'The account is not awaiting external account binding'; then
_info "EAB already registered" _info "EAB already registered"
_eabAlreadyBound=1 _eabAlreadyBound=1
@@ -5689,9 +5590,11 @@ issue() {
_on_issue_err "$_post_hook" _on_issue_err "$_post_hook"
return 1 return 1
fi fi
# Retry without "replaces" whenever the CA rejected that field, e.g. after # RFC 9773 Section 5 only defines the "alreadyReplaced" error, but real CAs
# switching the ACME server: the prior cert belongs to the old CA. # (Let's Encrypt) may also reject with a malformed error if the prior cert
if [ "$_replaces_certID" ] && _isARIReplacesRejected "$code" "$response"; then # 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" "urn:ietf:params:acme:error:malformed" || _contains "$response" "'replaces'" || _contains "$response" "ARI"; }; then
_info "ARI 'replaces' rejected by CA, retrying newOrder without 'replaces'." _info "ARI 'replaces' rejected by CA, retrying newOrder without 'replaces'."
if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then
_err "Error creating new order." _err "Error creating new order."
@@ -8027,35 +7930,6 @@ _getARICertID() {
printf "%s.%s" "$_akiurl" "$_serurl" printf "%s.%s" "$_akiurl" "$_serurl"
} }
#httpcode response
#Returns 0 when a newOrder was rejected because of the ARI "replaces" field,
#so that the order can be retried without it.
#The status code decides first, and an empty code counts as "not rejected":
#an ACCEPTED order echoes the field back, since RFC 9773 Section 5 says that
#a server accepting a newOrder request with a "replaces" field "MUST reflect
#that field in the response", and the certID it carries is base64url, so the
#response of a SUCCESSFUL order can contain "replaces" and even "ARI".
#Matching on the message alone would then re-order without "replaces" and
#defeat ARI.
#Only the 409 "alreadyReplaced" type is mandated by RFC 9773 Section 5; the
#other checks it lists (same ACME account, shared identifier) are left to
#server policy, so the wording differs per CA: Let's Encrypt answers
#malformed when the prior cert was issued by a different issuer, ZeroSSL
#answers 401 with 'The "replaces" field does not identify a certificate that
#belongs to this ACME account'.
#https://github.com/acmesh-official/acme.sh/issues/7280
_isARIReplacesRejected() {
_ari_rej_code="$1"
_ari_rej_resp="$2"
if [ -z "$_ari_rej_code" ] || _startswith "$_ari_rej_code" "2"; then
return 1
fi
_contains "$_ari_rej_resp" "alreadyReplaced" ||
_contains "$_ari_rej_resp" "replaces" ||
_contains "$_ari_rej_resp" "ARI" ||
_contains "$_ari_rej_resp" "urn:ietf:params:acme:error:malformed"
}
#cert #cert
_get_ARI() { _get_ARI() {
_cert="$1" _cert="$1"
+2 -4
View File
@@ -4,19 +4,17 @@
# maintainer and is never available to CI, so a compromise of the build # maintainer and is never available to CI, so a compromise of the build
# pipeline cannot produce a tag that verifies against this file. # pipeline cannot produce a tag that verifies against this file.
# #
# Signing starts at 3.1.6; tags up to 3.1.5 are unsigned.
#
# Fingerprint: SHA256:M60qVafm/NUywQHXAkoQcj2v6KgkfrdSXv6mPejUUeE # Fingerprint: SHA256:M60qVafm/NUywQHXAkoQcj2v6KgkfrdSXv6mPejUUeE
# #
# To verify a release tag, from a clone of this repository: # To verify a release tag, from a clone of this repository:
# #
# git config gpg.ssh.allowedSignersFile allowed_signers # git config gpg.ssh.allowedSignersFile allowed_signers
# git verify-tag 3.1.6 # git verify-tag 3.1.5
# #
# A good signature covers the tag object, which pins the commit, which pins # A good signature covers the tag object, which pins the commit, which pins
# the whole tree -- so verifying the tag verifies every file at that # the whole tree -- so verifying the tag verifies every file at that
# release. Build a tarball from the verified tag with: # release. Build a tarball from the verified tag with:
# #
# git archive --format=tar.gz --prefix=acme.sh-3.1.6/ 3.1.6 > acme.sh-3.1.6.tar.gz # git archive --format=tar.gz --prefix=acme.sh-3.1.5/ 3.1.5 > acme.sh-3.1.5.tar.gz
# #
github@neilpang.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBTjI0HBJn3uhfT2DsNcFybfAZi3ADbIacMpz1BItKdB github@neilpang.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBTjI0HBJn3uhfT2DsNcFybfAZi3ADbIacMpz1BItKdB
+2 -2
View File
@@ -74,9 +74,9 @@ fritzbox_deploy() {
_info "Log in to the FRITZ!Box" _info "Log in to the FRITZ!Box"
_fritzbox_challenge="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua" | sed -e 's/^.*<Challenge>//' -e 's/<\/Challenge>.*$//')" _fritzbox_challenge="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua" | sed -e 's/^.*<Challenge>//' -e 's/<\/Challenge>.*$//')"
if _exists iconv; then if _exists iconv; then
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | iconv -f ASCII -t UTF-16LE | _digest md5 hex)" _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | iconv -f ASCII -t UTF16LE | _digest md5 hex)"
elif _exists uconv; then elif _exists uconv; then
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | uconv -f ASCII -t UTF-16LE | _digest md5 hex)" _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | uconv -f ASCII -t UTF16LE | _digest md5 hex)"
else else
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | perl -p -e 'use Encode qw/encode/; print encode("UTF-16LE","$_"); $_="";' | _digest md5 hex)" _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | perl -p -e 'use Encode qw/encode/; print encode("UTF-16LE","$_"); $_="";' | _digest md5 hex)"
fi fi
-348
View File
@@ -1,348 +0,0 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_bergdns_info='bergdns.at
Site: bergdns.at
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_bergdns
Options:
BERGDNS_API_KEY API key, as issued in the account UI. Needs read (to find the zone and the record) and write over the challenge names.
BERGDNS_API_URL API base URL. Optional. Default "https://bergdns.at/v1".
BERGDNS_TTL TTL of the challenge record, in seconds. Optional. Default "60".
BERGDNS_PROPAGATION_TIMEOUT Seconds to wait for the record to reach every secondary. Optional. Default "60". "0" does not wait.
Issues: github.com/acmesh-official/acme.sh/issues/7261
Author: Kenny Kropp <https://github.com/kekropp>
'
_BERGDNS_DEFAULT_URL='https://bergdns.at/v1'
_BERGDNS_DEFAULT_TTL='60'
_BERGDNS_DEFAULT_WAIT='60'
######## Public functions ####################################################
# Usage: dns_bergdns_add _acme-challenge.www.example.com "token"
# The value is added to the RRset, so a domain and its wildcard can be
# validated at the same time.
dns_bergdns_add() {
fulldomain=$(echo "$1" | _lower_case)
txtvalue=$2
_bergdns_init || return 1
_bergdns_find_zone "$fulldomain" || return 1
_bergdns_find_rrset "$fulldomain" || return 1
_info "Adding TXT $fulldomain in zone $_bergdns_zone_name"
if [ -z "$_bergdns_rrset_id" ]; then
if _bergdns_rest POST "zones/$_bergdns_zone_id/rrsets" \
"{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":$BERGDNS_TTL,\"records\":[\"\\\"$txtvalue\\\"\"]}"; then
_bergdns_rrset_id=$(echo "$response" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
_debug _bergdns_rrset_id "$_bergdns_rrset_id"
_bergdns_wait "$fulldomain"
return 0
fi
if [ "$_bergdns_code" != "rrset_exists" ]; then
_err "bergdns: could not add the challenge record: $_bergdns_error"
return 1
fi
# another run created it since the lookup above
_bergdns_find_rrset "$fulldomain" || return 1
if [ -z "$_bergdns_rrset_id" ]; then
_err "bergdns: could not find the challenge record at $fulldomain"
return 1
fi
fi
if ! _bergdns_rest POST \
"zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/records" \
"{\"records\":[\"\\\"$txtvalue\\\"\"]}"; then
_err "bergdns: could not add the challenge record: $_bergdns_error"
return 1
fi
_bergdns_wait "$fulldomain"
}
# Usage: dns_bergdns_rm _acme-challenge.www.example.com "token"
# Only this value is removed. Removing the last value deletes the RRset, and
# removing a value that does not exist is not an error.
dns_bergdns_rm() {
fulldomain=$(echo "$1" | _lower_case)
txtvalue=$2
_bergdns_init || return 1
_bergdns_find_zone "$fulldomain" || return 1
_bergdns_find_rrset "$fulldomain" || return 1
if [ -z "$_bergdns_rrset_id" ]; then
_info "bergdns: no TXT records at $fulldomain, nothing to remove"
return 0
fi
_info "Removing TXT $fulldomain from zone $_bergdns_zone_name"
if ! _bergdns_rest DELETE \
"zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/records" \
"{\"records\":[\"\\\"$txtvalue\\\"\"]}"; then
# Any flavour of not-found is a cleanup that has already happened: the
# RRset was removed by a previous run, or by the other half of a
# domain-and-wildcard pair taking the last value with it.
if [ "$_bergdns_status" = "404" ]; then
_info "bergdns: $fulldomain holds no such record any more, nothing to remove"
return 0
fi
_err "bergdns: could not remove the challenge record: $_bergdns_error"
return 1
fi
return 0
}
######## Private functions ###################################################
_bergdns_init() {
BERGDNS_API_KEY="${BERGDNS_API_KEY:-$(_readaccountconf_mutable BERGDNS_API_KEY)}"
BERGDNS_API_URL="${BERGDNS_API_URL:-$(_readaccountconf_mutable BERGDNS_API_URL)}"
BERGDNS_TTL="${BERGDNS_TTL:-$(_readaccountconf_mutable BERGDNS_TTL)}"
BERGDNS_PROPAGATION_TIMEOUT="${BERGDNS_PROPAGATION_TIMEOUT:-$(_readaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT)}"
if [ -z "$BERGDNS_API_KEY" ]; then
BERGDNS_API_KEY=""
_clearaccountconf_mutable BERGDNS_API_KEY
_err "You have not set BERGDNS_API_KEY. Create a key in the bergdns UI and export it:"
_err " export BERGDNS_API_KEY=\"bgd_...\""
return 1
fi
[ -n "$BERGDNS_API_URL" ] || BERGDNS_API_URL="$_BERGDNS_DEFAULT_URL"
[ -n "$BERGDNS_TTL" ] || BERGDNS_TTL="$_BERGDNS_DEFAULT_TTL"
[ -n "$BERGDNS_PROPAGATION_TIMEOUT" ] || BERGDNS_PROPAGATION_TIMEOUT="$_BERGDNS_DEFAULT_WAIT"
# strip trailing slashes
BERGDNS_API_URL=$(echo "$BERGDNS_API_URL" | sed 's#/*$##')
# The TTL is interpolated into the request body and the timeout is counted
# down in arithmetic, so a stray value from the environment or from an old
# account.conf has to be caught here rather than become malformed JSON and
# an opaque 400.
case "$BERGDNS_TTL" in
*[!0-9]* | '')
_err "bergdns: BERGDNS_TTL must be a number of seconds, not \"$BERGDNS_TTL\"."
return 1
;;
esac
case "$BERGDNS_PROPAGATION_TIMEOUT" in
*[!0-9]* | '')
_err "bergdns: BERGDNS_PROPAGATION_TIMEOUT must be a number of seconds, not \"$BERGDNS_PROPAGATION_TIMEOUT\"."
return 1
;;
esac
_saveaccountconf_mutable BERGDNS_API_KEY "$BERGDNS_API_KEY"
# Only what the user actually chose is written back, and a value equal to
# the default clears any older setting. Persisting a default would pin the
# install to today's value, and a later change to the shipped one -- a move
# of the API base above all -- would never reach it; leaving an old setting
# in place would mean the environment could never put one back to default.
if [ "$BERGDNS_API_URL" = "$_BERGDNS_DEFAULT_URL" ]; then
_clearaccountconf_mutable BERGDNS_API_URL
else
_saveaccountconf_mutable BERGDNS_API_URL "$BERGDNS_API_URL"
fi
if [ "$BERGDNS_TTL" = "$_BERGDNS_DEFAULT_TTL" ]; then
_clearaccountconf_mutable BERGDNS_TTL
else
_saveaccountconf_mutable BERGDNS_TTL "$BERGDNS_TTL"
fi
if [ "$BERGDNS_PROPAGATION_TIMEOUT" = "$_BERGDNS_DEFAULT_WAIT" ]; then
_clearaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT
else
_saveaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT "$BERGDNS_PROPAGATION_TIMEOUT"
fi
return 0
}
# Usage: _bergdns_find_zone _acme-challenge.www.example.com
# Sets _bergdns_zone_id and _bergdns_zone_name.
# Zones are addressed by an id, not by name, so the zone list is fetched once
# and the longest matching zone name wins.
_bergdns_find_zone() {
_bergdns_fqdn=$1
_bergdns_zone_id=""
_bergdns_zone_name=""
if ! _bergdns_rest GET "zones"; then
_err "bergdns: could not list zones: $_bergdns_error"
return 1
fi
# one zone object per line, so id and name stay together
_bergdns_zone_lines=$(echo "$response" | tr '{' '\n')
_bergdns_cand="$_bergdns_fqdn"
while [ -n "$_bergdns_cand" ]; do
_bergdns_line=$(echo "$_bergdns_zone_lines" | _bergdns_select "$_bergdns_cand" | _head_n 1)
if [ -n "$_bergdns_line" ]; then
_bergdns_zone_id=$(echo "$_bergdns_line" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
_bergdns_zone_name="$_bergdns_cand"
_debug _bergdns_zone_id "$_bergdns_zone_id"
_debug _bergdns_zone_name "$_bergdns_zone_name"
[ -n "$_bergdns_zone_id" ] && return 0
break
fi
case "$_bergdns_cand" in
*.*) _bergdns_cand=${_bergdns_cand#*.} ;;
*) _bergdns_cand="" ;;
esac
done
_err "bergdns: no zone in this account holds $_bergdns_fqdn."
_err "bergdns: the key must be able to read the zone as well as write the record."
return 1
}
# Usage: _bergdns_find_rrset _acme-challenge.www.example.com
# Sets _bergdns_rrset_id to the id of the TXT RRset at that name, or to an
# empty string if there is none. Records are addressed by id, so the zone's
# RRsets are listed to find it.
_bergdns_find_rrset() {
_bergdns_fqdn=$1
_bergdns_rrset_id=""
if ! _bergdns_rest GET "zones/$_bergdns_zone_id/rrsets"; then
_err "bergdns: could not list the records of $_bergdns_zone_name: $_bergdns_error"
return 1
fi
_bergdns_line=$(echo "$response" | tr '{' '\n' | _bergdns_select "$_bergdns_fqdn" TXT | _head_n 1)
if [ -n "$_bergdns_line" ]; then
_bergdns_rrset_id=$(echo "$_bergdns_line" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
fi
_debug _bergdns_rrset_id "$_bergdns_rrset_id"
return 0
}
# Usage: ... | _bergdns_select name [type]
# Reads one JSON object per line and prints those whose "name" is name and,
# when a type is given, whose "type" is that type. The two fields are matched
# one at a time, so neither the order the server writes its keys in nor
# anything sitting between them changes the answer.
#
# The comparison is a shell case, which is literal by construction: grep -F
# does not exist on Solaris, and _contains and _startswith would read the name
# as a regular expression. Each pattern anchors on the start of the object or
# on the comma before the key, so a key that merely ends in "name" cannot
# match.
_bergdns_select() {
_bergdns_sel_name=$1
_bergdns_sel_type=$2
while IFS= read -r _bergdns_sel_line || [ -n "$_bergdns_sel_line" ]; do
case "$_bergdns_sel_line" in
'"name":"'"$_bergdns_sel_name"'"'* | *',"name":"'"$_bergdns_sel_name"'"'*) ;;
*) continue ;;
esac
if [ -n "$_bergdns_sel_type" ]; then
case "$_bergdns_sel_line" in
'"type":"'"$_bergdns_sel_type"'"'* | *',"type":"'"$_bergdns_sel_type"'"'*) ;;
*) continue ;;
esac
fi
printf '%s\n' "$_bergdns_sel_line"
done
}
# Usage: _bergdns_wait _acme-challenge.www.example.com
# Polls the propagation endpoint until all bergdns nameservers serve the
# record. A timeout is logged but does not fail the issuance.
#
# This covers the zone transfer from the primary to the secondaries, which
# takes seconds; the resolver side is acme.sh's own _check_dns_entries, which
# runs after every record has been added and has a timeout of its own.
_bergdns_wait() {
_bergdns_fqdn=$1
if [ "$BERGDNS_PROPAGATION_TIMEOUT" = "0" ] || [ -z "$_bergdns_rrset_id" ]; then
return 0
fi
_bergdns_waited=0
while [ "$_bergdns_waited" -lt "$BERGDNS_PROPAGATION_TIMEOUT" ]; do
if _bergdns_rest GET "zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/propagation"; then
case "$response" in
*'"propagated":true'*)
_info "bergdns: $_bergdns_fqdn is served by every secondary after ${_bergdns_waited}s"
return 0
;;
esac
elif [ "$_bergdns_code" = "propagation_unavailable" ]; then
# propagation checks are not configured on this server
_info "bergdns: this deployment does not offer propagation checks; not waiting"
return 0
else
case "$_bergdns_status" in
429) ;; # rate limited, worth another go
4*)
# The check is refused rather than pending, and waiting will not
# change that. _check_dns_entries still has to pass, so this is not
# the place to fail the issuance.
_info "bergdns: the propagation check is unavailable ($_bergdns_error); not waiting"
return 0
;;
esac
fi
_sleep 5
_bergdns_waited=$((_bergdns_waited + 5))
done
_info "bergdns: $_bergdns_fqdn was not on every secondary after ${BERGDNS_PROPAGATION_TIMEOUT}s; continuing anyway"
return 0
}
# Usage: _bergdns_rest method endpoint [body]
# Sets response and _bergdns_status. On failure also sets _bergdns_error and,
# where the API itself answered, _bergdns_code.
_bergdns_rest() {
_bergdns_method=$1
_bergdns_endpoint=$2
_bergdns_body=$3
_bergdns_error=""
_bergdns_code=""
_bergdns_status=""
export _H1="Authorization: Bearer $BERGDNS_API_KEY"
export _H2="Accept: application/json"
_bergdns_url="$BERGDNS_API_URL/$_bergdns_endpoint"
_debug _bergdns_url "$_bergdns_url"
# drop the headers of the previous request, so that a request which never
# reaches the server cannot be read as carrying its status
if [ -f "$HTTP_HEADER" ]; then
: >"$HTTP_HEADER"
fi
if [ "$_bergdns_method" = "GET" ]; then
response="$(_get "$_bergdns_url")"
else
_debug2 _bergdns_body "$_bergdns_body"
response="$(_post "$_bergdns_body" "$_bergdns_url" "" "$_bergdns_method" "application/json")"
fi
_bergdns_ret="$?"
_debug2 response "$response"
if [ "$_bergdns_ret" != "0" ]; then
_bergdns_error="the request to $_bergdns_url could not be made"
return 1
fi
_bergdns_status="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug _bergdns_status "$_bergdns_status"
# The HTTP status decides. Errors from the API itself are RFC 9457
# problem+json and carry a "detail" to show and a stable "code" to branch on,
# but a request that never gets that far -- bergdns.at answers from behind a
# reverse proxy, whose 502 and 504 are HTML -- has neither, and reading the
# body alone would take those for success.
case "$_bergdns_status" in
2*) return 0 ;;
esac
_bergdns_error=$(echo "$response" | _egrep_o '"detail":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
_bergdns_code=$(echo "$response" | _egrep_o '"code":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
[ -n "$_bergdns_error" ] || _bergdns_error="$_bergdns_url answered HTTP ${_bergdns_status:-(none)}"
_debug _bergdns_code "$_bergdns_code"
return 1
}
-53
View File
@@ -1,53 +0,0 @@
#!/usr/bin/env sh
#Support Healthchecks.io (hosted or self-hosted)
#https://healthchecks.io/docs/http_api/
#Required:
#HEALTHCHECKS_URL="https://hc-ping.com/your-uuid"
#Use with --notify-level 3, so a ping is sent on every cron run, even when
#all certs are skipped. Otherwise Healthchecks reports the check as down.
healthchecks_send() {
_subject="$1"
_content="$2"
_statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
_debug "_subject" "$_subject"
_debug "_content" "$_content"
_debug "_statusCode" "$_statusCode"
HEALTHCHECKS_URL="${HEALTHCHECKS_URL:-$(_readaccountconf_mutable HEALTHCHECKS_URL)}"
if [ -z "$HEALTHCHECKS_URL" ]; then
HEALTHCHECKS_URL=""
_err "You didn't specify the Healthchecks.io ping url HEALTHCHECKS_URL yet."
_err "Example: export HEALTHCHECKS_URL=\"https://hc-ping.com/your-uuid\""
return 1
fi
_saveaccountconf_mutable HEALTHCHECKS_URL "$HEALTHCHECKS_URL"
_hc_url="${HEALTHCHECKS_URL%/}"
case "$_statusCode" in
0 | 2) ;;
1)
_hc_url="$_hc_url/fail"
;;
*)
_hc_url="$_hc_url/log"
;;
esac
_data="$_subject
$_content"
response="$(_post "$_data" "$_hc_url" "" "POST" "text/plain")"
if [ "$?" = "0" ] && [ "$response" = "OK" ]; then
_info "healthchecks ping success."
return 0
fi
_err "healthchecks ping error."
_err "$response"
return 1
}