mirror of
https://github.com/acmesh-official/acme.sh.git
synced 2026-08-13 12:33:30 +02:00
Some checks failed
DNS / CheckToken (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
DNS / Hurd (push) Has been cancelled
DNS / OpenEuler (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
* dns_yc: restore YC_SA_Key_File in dns_yc_rm before signing the JWT dns_yc_rm() never rebuilt YC_SA_Key_File from YC_SA_Key_File_PEM_b64 / YC_SA_Key_File_Path like dns_yc_add() does. Per the DNS API dev guide, add()/rm() run in separate subshells, so rm() must repeat add()'s setup steps rather than rely on variables set during add(). Without it, when _yc_login() needs a fresh JWT during removal (the IAM token from the add phase isn't available), it signs with an empty/unset key path, and openssl fails with "Unknown key file format". The resulting auth failure then surfaces misleadingly as "invalid domain" in _get_root, and the TXT record is never deleted. Verified against a real Yandex Cloud account/zone with --staging: before the fix, removal failed with the same errors reported in the issue; after adding the missing key-restoration block, add + remove both succeed and the TXT record is actually deleted. * dns_yc: preserve other TXT values when removing one at the same name dns_yc_rm previously sent the full current data array (all existing TXT values at the name) to the deletions API, wiping out the whole rrset instead of only the value being removed. This breaks wildcard + base domain issuance, where both share the same _acme-challenge name with two different values: removing the first one deleted both, leaving nothing for the second removal to find. * dns_yc: read persisted config from domain conf before account conf YC_Zone_ID, YC_Folder_ID, YC_SA_ID, YC_SA_Key_ID (zone-ID mode) and YC_SA_Key_File_PEM_b64/Path were always saved via _savedomainconf (domain.conf), but only ever read back via _readaccountconf_mutable (account.conf). Once the env vars were unset, none of these could be recovered from the saved config, so dns_yc_add/dns_yc_rm failed with "You didn't specify a YC_SA_ID or YC_SA_Key_ID or YC_SA_Key_File." even though the values had been persisted correctly on the prior run. * dns_yc: replace grep -Fxv/sed with a portable loop in dns_yc_rm Solaris's /usr/bin/grep supports neither -F nor -x, so _remaining_txtvalue was always empty there and the preserve-other- values logic silently fell back to deleting the whole rrset (with a grep usage error on stderr on every rm). The sed trailing-comma strip had a matching issue on Solaris, whose sed drops an unterminated last line. CI didn't catch this because the fallback path also returns "done: true". Use a plain for-loop with word splitting instead. * dns_yc: use upsertRecordSets.deletions to remove a single TXT value updateRecordSets has no "merges" field (only deletions/additions), so the previous preserve-other-values logic silently did nothing -- the TXT record was never actually removed, a regression from before that change (which at least deleted the whole rrset). CI didn't catch it because _clearupdns runs dns_yc_rm in a subshell and ignores its exit code. upsertRecordSets.deletions removes only the specified value from the rrset directly, so the getRecordSet read and the remaining-value recomputation are no longer needed at all. Verified against a real zone (base + wildcard domain sharing one _acme-challenge name): adding both values then removing one leaves the other in place, and removing the second cleans up fully. * dns_yc: don't delete the user's own key file in YC_SA_Key_File_Path mode _yc_login unconditionally rm'd $YC_SA_Key_File after signing. That's fine for the PEM_b64 path, where it's a decoded temp file, but in YC_SA_Key_File_Path mode it's the user's own persistent key file -- the first successful login permanently deleted it, so every subsequent dns_yc_rm/renewal hit "Unknown key file format" (the exact symptom this PR is about, just from a different cause). Track whether the key file is our own temp copy and only delete it in that case. Verified with a stubbed _yc_login: a temp-mode key gets removed after login, a path-mode key survives. * dns_yc: clear both domain and account conf on invalid config The failure branch in dns_yc_add only ever called _clearaccountconf, but YC_Zone_ID/YC_Folder_ID/YC_SA_Key_File_PEM_b64/Path are persisted via _savedomainconf, and YC_SA_ID/YC_SA_Key_ID may have been saved via _saveaccountconf_mutable (Folder_ID mode, which stores under a SAVED_ prefix read back by _readaccountconf_mutable). Clearing only one store left stale values behind in whichever one wasn't touched. Verified by seeding both domain.conf and account.conf with leftover values, then triggering this branch and confirming both config files end up empty.
307 lines
11 KiB
Bash
307 lines
11 KiB
Bash
#!/usr/bin/env sh
|
|
# shellcheck disable=SC2034
|
|
dns_yc_info='Yandex Cloud DNS
|
|
Site: Cloud.Yandex.com
|
|
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_yc
|
|
Options:
|
|
YC_Zone_ID DNS Zone ID
|
|
YC_Folder_ID YC Folder ID
|
|
YC_SA_ID Service Account ID
|
|
YC_SA_Key_ID Service Account IAM Key ID
|
|
YC_SA_Key_File_Path Private key file path. Optional.
|
|
YC_SA_Key_File_PEM_b64 Base64 content of private key file. Use instead of Path to private key file. Optional.
|
|
Issues: github.com/acmesh-official/acme.sh/issues/4210
|
|
'
|
|
|
|
YC_Api="https://dns.api.cloud.yandex.net/dns/v1"
|
|
|
|
######## Public functions #####################
|
|
|
|
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
|
dns_yc_add() {
|
|
fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name
|
|
txtvalue=$2
|
|
|
|
# YC_SA_Key_File_PEM_b64/Path are always persisted to the domain conf below,
|
|
# so they must be recovered from there first (account conf is only a
|
|
# fallback for the YC_Folder_ID case, see the SA_ID/SA_Key_ID save below).
|
|
YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readdomainconf YC_SA_Key_File_PEM_b64)}"
|
|
YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readaccountconf_mutable YC_SA_Key_File_PEM_b64)}"
|
|
YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readdomainconf YC_SA_Key_File_Path)}"
|
|
YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readaccountconf_mutable YC_SA_Key_File_Path)}"
|
|
|
|
if [ "$YC_SA_Key_File_PEM_b64" ]; then
|
|
echo "$YC_SA_Key_File_PEM_b64" | _dbase64 >private.key
|
|
YC_SA_Key_File="private.key"
|
|
_yc_key_is_temp=1
|
|
_savedomainconf YC_SA_Key_File_PEM_b64 "$YC_SA_Key_File_PEM_b64"
|
|
else
|
|
YC_SA_Key_File="$YC_SA_Key_File_Path"
|
|
_yc_key_is_temp=""
|
|
_savedomainconf YC_SA_Key_File_Path "$YC_SA_Key_File_Path"
|
|
fi
|
|
|
|
YC_Zone_ID="${YC_Zone_ID:-$(_readdomainconf YC_Zone_ID)}"
|
|
YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable YC_Zone_ID)}"
|
|
YC_Folder_ID="${YC_Folder_ID:-$(_readdomainconf YC_Folder_ID)}"
|
|
YC_Folder_ID="${YC_Folder_ID:-$(_readaccountconf_mutable YC_Folder_ID)}"
|
|
YC_SA_ID="${YC_SA_ID:-$(_readdomainconf YC_SA_ID)}"
|
|
YC_SA_ID="${YC_SA_ID:-$(_readaccountconf_mutable YC_SA_ID)}"
|
|
YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readdomainconf YC_SA_Key_ID)}"
|
|
YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readaccountconf_mutable YC_SA_Key_ID)}"
|
|
|
|
if [ "$YC_SA_ID" ] && [ "$YC_SA_Key_ID" ] && [ "$YC_SA_Key_File" ]; then
|
|
if [ -f "$YC_SA_Key_File" ]; then
|
|
if _isRSA "$YC_SA_Key_File" >/dev/null 2>&1; then
|
|
if [ "$YC_Zone_ID" ]; then
|
|
_savedomainconf YC_Zone_ID "$YC_Zone_ID"
|
|
_savedomainconf YC_SA_ID "$YC_SA_ID"
|
|
_savedomainconf YC_SA_Key_ID "$YC_SA_Key_ID"
|
|
elif [ "$YC_Folder_ID" ]; then
|
|
_savedomainconf YC_Folder_ID "$YC_Folder_ID"
|
|
_saveaccountconf_mutable YC_SA_ID "$YC_SA_ID"
|
|
_saveaccountconf_mutable YC_SA_Key_ID "$YC_SA_Key_ID"
|
|
_clearaccountconf_mutable YC_Zone_ID
|
|
_clearaccountconf YC_Zone_ID
|
|
else
|
|
_err "You didn't specify a Yandex Cloud Zone ID or Folder ID yet."
|
|
return 1
|
|
fi
|
|
else
|
|
_err "YC_SA_Key_File not a RSA file(_isRSA function return false)."
|
|
return 1
|
|
fi
|
|
else
|
|
_err "YC_SA_Key_File not found in path $YC_SA_Key_File."
|
|
return 1
|
|
fi
|
|
else
|
|
# Clear both possible stores -- YC_Zone_ID/YC_Folder_ID/key material are
|
|
# persisted to the domain conf, while YC_SA_ID/YC_SA_Key_ID may have been
|
|
# saved account-wide (Folder_ID mode), so a plain _clearaccountconf alone
|
|
# would leave stale values behind in whichever store wasn't touched.
|
|
_cleardomainconf YC_Zone_ID
|
|
_clearaccountconf YC_Zone_ID
|
|
_cleardomainconf YC_Folder_ID
|
|
_clearaccountconf YC_Folder_ID
|
|
_cleardomainconf YC_SA_ID
|
|
_clearaccountconf_mutable YC_SA_ID
|
|
_cleardomainconf YC_SA_Key_ID
|
|
_clearaccountconf_mutable YC_SA_Key_ID
|
|
_cleardomainconf YC_SA_Key_File_PEM_b64
|
|
_clearaccountconf YC_SA_Key_File_PEM_b64
|
|
_cleardomainconf YC_SA_Key_File_Path
|
|
_clearaccountconf YC_SA_Key_File_Path
|
|
_err "You didn't specify a YC_SA_ID or YC_SA_Key_ID or YC_SA_Key_File."
|
|
return 1
|
|
fi
|
|
|
|
_debug "First detect the root zone"
|
|
if ! _get_root "$fulldomain"; then
|
|
_err "invalid domain"
|
|
return 1
|
|
fi
|
|
_debug _domain_id "$_domain_id"
|
|
_debug _sub_domain "$_sub_domain"
|
|
_debug _domain "$_domain"
|
|
|
|
_debug "Getting txt records"
|
|
if ! _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$_sub_domain"; then
|
|
_err "Error: $response"
|
|
return 1
|
|
fi
|
|
|
|
_info "Adding record"
|
|
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"merges\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
|
|
if _contains "$response" "\"done\": true"; then
|
|
_info "Added, OK"
|
|
return 0
|
|
else
|
|
_err "Add txt record error."
|
|
return 1
|
|
fi
|
|
fi
|
|
_err "Add txt record error."
|
|
return 1
|
|
|
|
}
|
|
|
|
#fulldomain txtvalue
|
|
dns_yc_rm() {
|
|
fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name
|
|
txtvalue=$2
|
|
|
|
YC_Zone_ID="${YC_Zone_ID:-$(_readdomainconf YC_Zone_ID)}"
|
|
YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable YC_Zone_ID)}"
|
|
YC_Folder_ID="${YC_Folder_ID:-$(_readdomainconf YC_Folder_ID)}"
|
|
YC_Folder_ID="${YC_Folder_ID:-$(_readaccountconf_mutable YC_Folder_ID)}"
|
|
YC_SA_ID="${YC_SA_ID:-$(_readdomainconf YC_SA_ID)}"
|
|
YC_SA_ID="${YC_SA_ID:-$(_readaccountconf_mutable YC_SA_ID)}"
|
|
YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readdomainconf YC_SA_Key_ID)}"
|
|
YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readaccountconf_mutable YC_SA_Key_ID)}"
|
|
|
|
# See dns_yc_add() for why domain conf is checked before account conf.
|
|
YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readdomainconf YC_SA_Key_File_PEM_b64)}"
|
|
YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readaccountconf_mutable YC_SA_Key_File_PEM_b64)}"
|
|
YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readdomainconf YC_SA_Key_File_Path)}"
|
|
YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readaccountconf_mutable YC_SA_Key_File_Path)}"
|
|
|
|
if [ "$YC_SA_Key_File_PEM_b64" ]; then
|
|
echo "$YC_SA_Key_File_PEM_b64" | _dbase64 >private.key
|
|
YC_SA_Key_File="private.key"
|
|
_yc_key_is_temp=1
|
|
else
|
|
YC_SA_Key_File="$YC_SA_Key_File_Path"
|
|
_yc_key_is_temp=""
|
|
fi
|
|
|
|
_debug "First detect the root zone"
|
|
if ! _get_root "$fulldomain"; then
|
|
_err "invalid domain"
|
|
return 1
|
|
fi
|
|
_debug _domain_id "$_domain_id"
|
|
_debug _sub_domain "$_sub_domain"
|
|
_debug _domain "$_domain"
|
|
|
|
# upsertRecordSets.deletions removes only the given value from the rrset,
|
|
# leaving any other values at the same name (e.g. base + wildcard domain)
|
|
# intact -- no need to read the current data set and recompute it.
|
|
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
|
|
if _contains "$response" "\"done\": true"; then
|
|
_info "Delete, OK"
|
|
return 0
|
|
else
|
|
_err "Delete record error."
|
|
return 1
|
|
fi
|
|
fi
|
|
_err "Delete record error."
|
|
return 1
|
|
}
|
|
|
|
#################### Private functions below ##################################
|
|
#_acme-challenge.www.domain.com
|
|
#returns
|
|
# _sub_domain=_acme-challenge.www
|
|
# _domain=domain.com
|
|
# _domain_id=sdjkglgdfewsdfg
|
|
_get_root() {
|
|
domain=$1
|
|
i=1
|
|
p=1
|
|
|
|
# Use Zone ID directly if provided
|
|
if [ "$YC_Zone_ID" ]; then
|
|
if ! _yc_rest GET "zones/$YC_Zone_ID"; then
|
|
return 1
|
|
else
|
|
if echo "$response" | tr -d " " | _egrep_o "\"id\":\"$YC_Zone_ID\"" >/dev/null; then
|
|
_domain=$(echo "$response" | _egrep_o "\"zone\": *\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
|
if [ "$_domain" ]; then
|
|
_cutlength=$((${#domain} - ${#_domain}))
|
|
_sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cutlength")
|
|
_domain_id=$YC_Zone_ID
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
else
|
|
return 1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
while true; do
|
|
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
|
_debug h "$h"
|
|
if [ -z "$h" ]; then
|
|
#not valid
|
|
return 1
|
|
fi
|
|
if [ "$YC_Folder_ID" ]; then
|
|
if ! _yc_rest GET "zones?folderId=$YC_Folder_ID"; then
|
|
return 1
|
|
fi
|
|
else
|
|
echo "You didn't specify a Yandex Cloud Folder ID."
|
|
return 1
|
|
fi
|
|
if _contains "$response" "\"zone\": \"$h\""; then
|
|
_domain_id=$(echo "$response" | _normalizeJson | _egrep_o "[^{]*\"zone\":\"$h\"[^}]*" | _egrep_o "\"id\"[^,]*" | _egrep_o "[^:][^:]*$" | tr -d '"')
|
|
_debug _domain_id "$_domain_id"
|
|
if [ "$_domain_id" ]; then
|
|
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
|
_domain=$h
|
|
return 0
|
|
fi
|
|
return 1
|
|
fi
|
|
p=$i
|
|
i=$(_math "$i" + 1)
|
|
done
|
|
return 1
|
|
}
|
|
|
|
_yc_rest() {
|
|
m=$1
|
|
ep="$2"
|
|
data="$3"
|
|
_debug "$ep"
|
|
|
|
if [ ! "$YC_Token" ]; then
|
|
_debug "Login"
|
|
_yc_login
|
|
else
|
|
_debug "Token already exists. Skip Login."
|
|
fi
|
|
|
|
token_trimmed=$(echo "$YC_Token" | tr -d '"')
|
|
|
|
export _H1="Content-Type: application/json"
|
|
export _H2="Authorization: Bearer $token_trimmed"
|
|
|
|
if [ "$m" != "GET" ]; then
|
|
_debug data "$data"
|
|
response="$(_post "$data" "$YC_Api/$ep" "" "$m")"
|
|
else
|
|
response="$(_get "$YC_Api/$ep")"
|
|
fi
|
|
|
|
if [ "$?" != "0" ]; then
|
|
_err "error $ep"
|
|
return 1
|
|
fi
|
|
_debug2 response "$response"
|
|
return 0
|
|
}
|
|
|
|
_yc_login() {
|
|
header=$(echo "{\"typ\":\"JWT\",\"alg\":\"PS256\",\"kid\":\"$YC_SA_Key_ID\"}" | _normalizeJson | _base64 | _url_replace)
|
|
_debug header "$header"
|
|
|
|
_current_timestamp=$(_time)
|
|
_expire_timestamp=$(_math "$_current_timestamp" + 1200) # 20 minutes
|
|
payload=$(echo "{\"iss\":\"$YC_SA_ID\",\"aud\":\"https://iam.api.cloud.yandex.net/iam/v1/tokens\",\"iat\":$_current_timestamp,\"exp\":$_expire_timestamp}" | _normalizeJson | _base64 | _url_replace)
|
|
_debug payload "$payload"
|
|
|
|
#signature=$(printf "%s.%s" "$header" "$payload" | ${ACME_OPENSSL_BIN:-openssl} dgst -sign "$YC_SA_Key_File -sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-1" | _base64 | _url_replace )
|
|
_signature=$(printf "%s.%s" "$header" "$payload" | _sign "$YC_SA_Key_File" "sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-1" | _url_replace)
|
|
_debug2 _signature "$_signature"
|
|
|
|
if [ "$_yc_key_is_temp" ]; then
|
|
rm -f "$YC_SA_Key_File"
|
|
fi
|
|
|
|
_jwt=$(printf "{\"jwt\": \"%s.%s.%s\"}" "$header" "$payload" "$_signature")
|
|
_debug2 _jwt "$_jwt"
|
|
|
|
export _H1="Content-Type: application/json"
|
|
_iam_response="$(_post "$_jwt" "https://iam.api.cloud.yandex.net/iam/v1/tokens" "" "POST")"
|
|
_debug3 _iam_response "$(echo "$_iam_response" | _normalizeJson)"
|
|
|
|
YC_Token="$(echo "$_iam_response" | _normalizeJson | _egrep_o "\"iamToken\"[^,]*" | _egrep_o "[^:][^:]*$" | tr -d '"')"
|
|
_debug3 YC_Token
|
|
|
|
return 0
|
|
}
|