Repository navigation
CoyoteCert 2.0 fixes a long list of protocol bugs found in a full review against RFC 8555 and RFC 9773, moves Hetzner to its new API, drops Buypass (shut down) and hardens storage, the CLI and the HTTP layer. Some fixes change behaviour, so this is a major release. Most applications that use the CoyoteCert builder only need the "Behaviour changes in the builder" section of the upgrade guide: UPGRADING.md.
Highlights
- ARI works. The renewal info cert ID now follows RFC 9773 (AKI key identifier plus serial). 1.x computed an ID that real CAs reject, so ARI windows and
replacesnever took effect. - Smarter renewal timing. Without ARI, renewal is due once a third of the lifetime remains (30 days for 90-day certificates, 2 days for 6-day ones) instead of a fixed 30 days. Certificates that expired, can't be parsed or no longer cover the configured identifiers are renewed.
- Hetzner works again. The handler uses the Hetzner Cloud API, since the old DNS Console API was retired. You need a Cloud API token.
- Profiles, CAA identities and EAB come from the CA directory. Profiles now work with Google Trust Services, an unknown profile throws instead of being dropped, and the ZeroSSL CAA list is complete.
DatabaseStorageworks on PostgreSQL and SQLite, not only MySQL, with per-driver DDL fromcreateTableSql().- Safer storage.
FilesystemStoragewrites atomically and keeps every private file at mode 0600, includingcert.json, which embeds the private key. - Self-healing accounts. A new account key is stored only once the CA accepts it, and a stored key the CA doesn't know is registered again automatically.
- PHPStan level 9 across the library, with typed reads of every CA response.
Fixes
- Challenges are cleaned up on every failure path, and an authorization that can't be fetched no longer reads as "passed"
- Signed POSTs are never resent after a transport failure; transport errors throw an
AcmeExceptionwith the curl error instead of a fake 504 - Order polling checks status codes, reports the order's error, understands HTTP-date
Retry-Afterand caps delays at 120 seconds preferredChain()matches the issuer of the topmost certificate exactly, like certbot, so cross-signed roots pick the right chain- Key rollover follows RFC 8555 §7.3.5 and no longer throws after the key has already changed
email()is sent to the CA as the account contactissue()withoutstorage()works, as documented- Route53 finds the right hosted zone (it only worked when the first zone matched), skips private zones and supports session tokens
- DigitalOcean purges stale records again; ClouDNS checks credentials first and reports failed deletes; DNS provider errors include the provider's message
- TLS-ALPN challenge certificates are signed with SHA-256 and use an IP SAN for IP identifiers (RFC 8738)
- The shell DNS hook has a timeout (120 seconds by default) and cleanup runs once per deployed value
- IPv6 identifiers work in the local HTTP self-test and in
isValidForDomains()
Security
- The local HTTP-01 self-test follows redirects only to http and https on ports 80 and 443, like Let's Encrypt, so a redirect can't point it at internal services
- The HTTP client only speaks HTTP and HTTPS, also after redirects
- Private keys, API tokens, passwords and EAB keys are marked
#[\SensitiveParameter]and stay out of exception traces - The ZeroSSL API key no longer appears in errors when EAB provisioning can't connect
- Identifiers with a trailing newline are rejected
CLI
- Output uses Symfony Console's SymfonyStyle;
nunomaduro/termwindis no longer a dependency - Errors go to stderr; invalid usage exits with
2, andcoyote statusexits with1for a missing or expired certificate - The ZeroSSL key and EAB credentials can come from
ZEROSSL_API_KEY,ACME_EAB_KIDandACME_EAB_HMAC, keeping them out of the process list coyote revokeno longer needs EAB credentials, andstatusandrevokefind a certificate of any key type
Breaking changes
The full list, with what to do for each, is in UPGRADING.md. The ones most likely to affect you:
BuypassGoandBuypassGoStagingare removed (Buypass ended its ACME service)- Hetzner needs a Cloud API token; DNS Console tokens no longer work
- Renewal timing changes as described above;
$daysBeforeExpiry/--daysstill force a fixed threshold - Identifiers are lower-cased, so certificates stored under a mixed-case domain by 1.x are reissued once
preferredChain()no longer matches partial issuer names such as'X1'DatabaseStorage::createTableSql()takes the PDO driver, and the MySQL table usesstore_keyas its primary key- Low-level API:
RenewalInfo::get()/certId()take only the certificate PEM,Certificate::revoke()takes aRevocationReason, and several unused classes and methods from the fork are removed ext-mbstringis no longer required
Full Changelog: v1.4.0...v2.0.0