Fedora 40 and later ship wget2 as wget. Under -q it prints nothing for -S,
so $HTTP_HEADER stayed empty in wget mode (ACME_USE_WGET=1, or no curl):
no status code, no Replay-Nonce, no Location, and every ACME request
failed. Without -q its own "HTTP response 200 OK [url]" line would be read
as the status line, and a HEAD response gets no headers at all.
Detect wget2 by its --version line and run it without -q (and without -d,
whose demultiplexing expects wget 1.x output). _wget2_headers keeps only
the header blocks of its stderr; for a response without a body, where
wget2 prints no block, it rebuilds the status line from the "HTTP
response" line. HEAD goes through --method HEAD --save-headers, which
writes the headers into $HTTP_HEADER. wget 1.x and curl are unchanged.
_regAccount wrote CA_EMAIL before sending newAccount, so it recorded an
address the CA never stored: for an account key it already knows, the CA
answers 200 and ignores the contact of the request. A --register-account
-m on an existing account then left the local config claiming an email
the account does not have.
Save it in the 201 branch, and in the 200/409 branch say that the email
was not changed and point at --update-account -m. Nothing is printed
when the given address already matches the saved one.
_process() calls __initHome directly once the option loop is over, and
that sourcing of account.conf overwrote the address -m had just
exported -- before any command function reaches _initpath. Guarding
only _initpath was not enough: by then ACCOUNT_EMAIL already held the
saved address, so it was saved and restored unchanged.
Keep the live value across this sourcing as well.
_initpath sources account.conf (twice, counting __initHome), and the
assignment overwrote the ACCOUNT_EMAIL that -m had just exported, so
--update-account -m sent the saved address instead of the given one.
It also short-circuited _getAccountEmail, whose first branch is meant
for the live value: the saved global address then won over the per-CA
CA_EMAIL as well.
Save the live value before the sourcing and restore it after. The saved
address is still reached, by the _readaccountconf at the end of
_getAccountEmail, which is where it belongs in the order.
The retry was keyed on the message text and missed ZeroSSL, which answers
HTTP 401 with 'The "replaces" field does not identify a certificate that
belongs to this ACME account', so switching the ACME server failed every
renewal.
RFC 9773 Section 5 mandates a problem type only for the 409
"alreadyReplaced" case and leaves its other checks (same account, shared
identifier) to server policy, so the wording cannot be enumerated. Judge
the status code instead: _isARIReplacesRejected treats only a non-2xx
response as a rejection. That also removes a false positive on the
success path, where the server "MUST reflect that field in the response"
and the echoed base64url certID can itself contain "ARI".
fix#7280
The comment block told the reader to run "git verify-tag 3.1.5", which is
exactly the tag that fails: 3.1.5 was tagged by the GitHub release form as
a lightweight ref and carries no signature. Use 3.1.6 in the examples and
state the baseline, so the file that teaches verification does not hand out
a command that cannot work.
https://github.com/acmesh-official/acme.sh/issues/7273
The 3.1.5 tag was created server-side by publishing the GitHub release,
which can only produce a lightweight ref, so it carries no signature and
git verify-tag fails on it. Rather than force-move a published tag, state
that signing starts at 3.1.6 and cut that tag locally with git tag -s
before the release is published.
vtag.yml now fails the run when the pushed tag is not a tag object, so the
same mistake shows up as a red check on the release instead of arriving as
a user report. The mirror step runs first, so v<tag> is still created; its
early "already exists" return became an else branch so the check is never
skipped.
https://github.com/acmesh-official/acme.sh/issues/7273
Core acme.sh now defines a _post_file() function. Functions and variables
live in separate namespaces in POSIX sh, so this was not a real conflict,
but the identical name is confusing to read. Use _post_upfile, matching the
_post_action / _post_boundary / _post_data locals already in this function.
Follow-up to 9249c892 (#7249). The mkdir -p ran unconditionally before
the guarded cp calls, so a first deploy left an empty backup directory
behind. Move the mkdir into each guarded block.
HARICA answers a processing order with "Retry-After: <HTTP-date>" set
about two minutes after finalize (discussion 7190). Skipping the date
left the poll loop on its 2s fallback, and 30 rounds ran out 21 seconds
before the time the CA had named, so the issue failed although the
certificate was about to be signed.
Add _httpdate2time, which converts the IMF-fixdate form in shell
arithmetic: GNU, BSD and busybox date each want a different invocation
for it, and %a/%b are locale lookups. _retryafter_seconds prints
delay-seconds as before and turns a date into the seconds left until
then; a date in the past or an unparseable value still prints nothing.
Pebble reuses a valid authorization in a new order 50% of the time by
default, so the dns manual mode case was a coin flip: a reused
authorization left nothing for the TXT record to answer.
Pebble answers a processing order with "Retry-After: <HTTP-date>". The
poll loop cut the value at the first colon and fed "Fri,11Sep202604" to
"[ -gt 0 ]", which errored with "integer expression expected" on every
round. Add _retryafter_seconds, which prints the header only in its
delay-seconds form, and use it at all four Retry-After sites. The two
sites that already filtered on digits used "[0-9]\+", which Solaris grep
does not support.
1. The four backup cp calls (KEYFILE/CERTFILE/CAFILE/FULLCHAIN) ran
unguarded. With USE_SCP=yes MULTI_CALL is implicit, so each cp is its
own ssh call and a missing source aborted the deploy. In batched mode
it was masked because the exit code is that of the last command.
Each cp is now wrapped in a remote [ -f ] test.
2. deploy/ssh.sh tested DEPLOY_SSH_FULLCHAIN = "yes" instead of
DEPLOY_SSH_MULTI_CALL (since 2017). Effect was only that the
fullchain backup got deferred to the next batch. Fixed as well.
Please upgrade with acme.sh --upgrade -b dev and retest.
TEST_DNS_MANUAL=1 turns on le_test_dns_manual_renew, which answers the
dns-01 challenge through the pebble-challtestsrv that the compose setup
already runs.
ARI has overridden Le_NextRenewTime unconditionally since 3.1.4, so a user
who passed --days never got the schedule they asked for, and --valid-to was
guarded at issue time but not on the renewal check: the guard survived one
run before the next cron rewrote it and saved it back.
An explicit --days or --valid-to now pins the schedule. The window is still
taken when it is earlier than what the user asked for, so a CA can pull an
urgent renewal forward but can never push a pinned renewal back.
Le_RenewalDays is only written to the domain conf when --days was actually
passed, so its presence there is what marks a schedule as pinned.
A fixed-date --valid-to opts out of ARI entirely: that cert is not renewed
automatically at all, so pulling it forward would change what it does, not
just when it renews.
Both call sites go through the new _calc_ari_renew_time.
A value holding a backslash-digit sequence (a backreference to sed) or an
embedded line break made _setopt's replace command fail after the shell
had already truncated the conf file, wiping the whole domain conf; the
next renewal then fails with an empty Le_API and no validation method.
Same class as #2426, which escaped only '&' and '|'.
Escape the backslash too, write the sed output back only when sed
succeeds, reject values holding a line break, and rewrite the file with
printf instead of echo in the append path and in _clear_conf: dash's
builtin echo interprets backslash escapes and corrupted such values on
every rewrite.
https://github.com/acmesh-official/acme.sh/issues/7213
Versions up to 3.0.9 cached the internally-acquired access token as
SAVED_AZUREDNS_BEARERTOKEN. 3.1.0 repurposed that variable for
user-supplied bearer tokens, so after an upgrade the stale cached token
was read back as if user-supplied, skipped the refresh path, and failed
renewals with 401 forever once expired.
A bearer token is short-lived, so persisting it is never useful: take it
from the environment only, and clear any stale saved value on the next
run.
fix https://github.com/acmesh-official/acme.sh/issues/7218
The certificate REST API this hook drives is UniFi OS's own, not
specific to the self-hosted UniFi OS Server: user reports confirm it on
a UDM Pro (UniFi OS 5.1.26) and a UCG Fiber (5.0.16). Reframe the scope
around the endpoint rather than the product line, state that the choice
between unifi and unifios is local/SSH file access vs remote REST API,
and note that the management port is 11443 on UniFi OS Server but 443
on hardware, so DEPLOY_UNIFIOS_HOST must be set there.
The v-prefixed mirror was created from github.sha, so for an annotated or
signed tag it would point at the commit and drop the signature: "git
verify-tag v3.1.3" fails with "cannot verify a non-tag object of type
commit" while "git verify-tag 3.1.3" succeeds. Resolve refs/tags/<tag>
and mirror whatever object it points at instead, which keeps the current
behaviour for lightweight tags. Also move the workflow expressions into
env instead of interpolating them into the shell command.
The zone lookup walked the challenge name from the right and ended up
asking netcup for the full "_acme-challenge.<domain>" as a zone name.
That can never be a zone, so netcup answered 4013 "Validation Error",
which replaced the real 5028 "The zone <domain> could not be found" as
the error shown to the user.
Stop one label short of the full name, and fail explicitly when no zone
matched, reporting the last API response plus what to check. Before, a
run where every candidate returned 5028 fell through to logout and
returned success.
socat binds a single family unless told which one: up to 1.7.x the
default IP version for TCP-LISTEN is 4, and 1.8.0 made it "no
preference", which resolves to whatever getaddrinfo and bindv6only
happen to give. So an order carrying both an IPv4 and an IPv6
identifier could never pass both http-01 challenges.
Bind one socket per family instead, with ipv6only on the IPv6 one so
the two do not collide. IPv4-mapped IPv6 addresses are not a portable
alternative, OpenBSD does not support them at all. The IPv6 listener
is best effort, a host without IPv6 still gets the IPv4 one. The
python fallback does the same. --listen-v4 and --listen-v6 keep
forcing a single family, and passing both now means both.
Le_Listen_V4 and Le_Listen_V6 were mutually exclusive in the domain
conf, which silently dropped one of them on renewal, and
_starttlsserver let -4 win when both were set.
Fixes#7185
_get_root_by_getList() matched the candidate suffix as an unanchored
substring of the whole domains.getList response and never looked at the
IsOurDNS attribute. A domain parked on Namecheap's webhosting DNS is
listed with IsOurDNS="false", yet it was still accepted as the root zone,
so _get_root() returned success and the domains.dns.getHosts probe that
would have found the real zone never ran. Every following getHosts call
was then refused with error 2030288 "not using proper DNS servers" and
the challenge failed with "invalid tld".
Match the exact <Domain Name="..."> entry instead and require
IsOurDNS="true", so a subdomain delegated to Namecheap BasicDNS/FreeDNS
under a parent that is not on Namecheap DNS now resolves to its own zone.
Matching the entry exactly also drops the old substring/regex match, in
which the dots of a domain matched any character.
Fixes#7178
_getdeployconf assigns and exports the variable, it does not print the
value, so wrapping it in a command substitution ran it in a subshell and
always yielded an empty string. A MULTIDEPLOY_FILENAME saved by an
earlier run was therefore never restored on renewal and the hook
silently fell back to multideploy.yml. Call it the same way every other
deploy hook does.
Also treat a MULTIDEPLOY_FILENAME starting with '/' as an absolute path
instead of always resolving it under DOMAIN_PATH, so one deploy file can
live outside the certificate directory and be shared by all domains.
Names without a leading '/' keep resolving under DOMAIN_PATH as before.
_temp_admin_cleanup ran before _logout, so the logout request carried
the session id of an account synouser had already removed and DSM kept
the orphaned entry in Connected Users. Swap the order in both terminal
branches, and add the missing _logout to the two post-login error paths
(CRT list failure, certificate not found without SYNO_CREATE).
_logout overwrites the global $response, so the upload-failure branch
prints its error message before calling it.
Reported by @Bertl75 in #7174
The decision to resume a pending order is keyed on Le_Vlist, but the
decision to keep Le_OrderFinalize/Le_LinkOrder was keyed on the webroot
being exactly "dns". Any other webroot with a saved Le_Vlist skipped
newOrder and then finalized against an empty URL.
Key both on Le_Vlist, and always clear Le_LinkCert, which is per-run
state that is never read back from the saved domain conf.
Fixes#7177
_cyon_delete_txt relied on `printf "%b"` to convert a sed-injected literal
`\n` into a real newline, but `%b` also processes the `\"` escapes that the
JSON response is full of. glibc/bash/dash keep the backslash of such an
undefined escape, FreeBSD's printf (sh builtin and /usr/bin/printf alike)
drops it -- so `data-hash=\"..\"` became `data-hash=".."`, the extraction
regex matched nothing, _dns_entries stayed empty and no TXT record was ever
deleted.
Drop the newline injection and use _egrep_o, which already yields one match
per line, then parse each line with sed.
Also feed the read loop a newline-terminated list: `printf "%s"` left the
last line unterminated, so `read` returned non-zero at EOF and the loop
skipped the final entry on every platform.
Verified identical output on FreeBSD 14.3, Linux/bash and Linux/dash.
Fixes#7169
For -d '*.example.com' the printed record name kept the literal '*' label
(_validation-persist.*.example.com). The CA never queries that name, so
issuance fails with "No TXT record found for DNS-PERSIST-01 challenge".
Per draft-ietf-acme-dns-persist-01 sec 4 and 10.2 the record is published at
the base domain's Validation Domain Name; the wildcard scope comes from
'policy=wildcard' in the record value (sec 5.1), not from a '*' label in the
record name. Strip the leading "*." in a new _dns_persist_txt_name helper,
and imply --dns-persist-wildcard for a wildcard -d, since without
policy=wildcard the printed record can never authorize the wildcard.
Fixes#7168