Skip to content

v2.0.0

Latest

Choose a tag to compare

@github-actions github-actions released this 12 Oct 03:11
· 1 commit to main since this release

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 replaces never 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.
  • DatabaseStorage works on PostgreSQL and SQLite, not only MySQL, with per-driver DDL from createTableSql().
  • Safer storage. FilesystemStorage writes atomically and keeps every private file at mode 0600, including cert.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 AcmeException with the curl error instead of a fake 504
  • Order polling checks status codes, reports the order's error, understands HTTP-date Retry-After and 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 contact
  • issue() without storage() 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/termwind is no longer a dependency
  • Errors go to stderr; invalid usage exits with 2, and coyote status exits with 1 for a missing or expired certificate
  • The ZeroSSL key and EAB credentials can come from ZEROSSL_API_KEY, ACME_EAB_KID and ACME_EAB_HMAC, keeping them out of the process list
  • coyote revoke no longer needs EAB credentials, and status and revoke find 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:

  • BuypassGo and BuypassGoStaging are 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 / --days still 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 uses store_key as its primary key
  • Low-level API: RenewalInfo::get() / certId() take only the certificate PEM, Certificate::revoke() takes a RevocationReason, and several unused classes and methods from the fork are removed
  • ext-mbstring is no longer required

Full Changelog: v1.4.0...v2.0.0