Skip to content

v1.0.0

Choose a tag to compare

@bashgeek bashgeek released this 19 Apr 23:06
· 10 commits to main since this release
coyotecert-banner-2560x1706

CoyoteCert v1.0.0

The ACME v2 client PHP deserved. It ships. It doesn't fall off cliffs.

CoyoteCert 1.0.0 is a complete rewrite: a fully-featured ACME v2 client for PHP 8.3+ covering first-time issuance, ARI-guided renewal, and revocation. Fluent builder API, a CLI, six DNS providers, three storage backends, 94%+ test coverage across PHP 8.3, 8.4, and 8.5.


What's in the box

Every major CA, wired up and ready

Built-in providers for Let's Encrypt, ZeroSSL, Google Trust Services, SSL.com, and Buypass (production and staging). ZeroSSL auto-provisions EAB credentials from your API key so you never have to copy-paste a token. Google Trust Services and SSL.com accept pre-provisioned EAB directly. CustomProvider handles anything RFC 8555-compliant (internal PKI, private CAs, whatever you're running).

No default CA. Every call is explicit. Trade-offs around rate limits, data residency, certificate lifetime, and trust coverage belong to the caller.

A CLI that ships with the package

composer global require blendbyte/coyotecert and you get coyote issue and coyote status. Issue a wildcard cert in one command. Inspect expiry with another. Drop it in a cron job and forget it. Same providers, same key types, same storage layout as the library, no separate toolchain.

http-01, dns-01, and tls-alpn-01

All three standard ACME challenge types. Six built-in dns-01 handlers cover Cloudflare, Hetzner DNS, DigitalOcean, ClouDNS, AWS Route53 (SigV4 signing built in, no AWS SDK needed), and shell/exec for anything else. tls-alpn-01 ships as an abstract base: extend it, implement deploy() and cleanup(), and call generateAcmeCertificate() for the RFC 8737-encoded cert and key.

ARI: renewal on the CA's schedule, not yours

RFC 9773 (ACME Renewal Information) is supported out of the box. needsRenewal() and issueOrRenew() check the CA's renewalInfo endpoint automatically. If the CA says renew now, you renew now. If the ARI request fails, it falls back to the $daysBeforeExpiry threshold silently. No configuration, no opt-in.

ACME profiles

Let's Encrypt's shortlived profile gives you 6-day certificates with no OCSP or CRL overhead. Call ->profile('shortlived') unconditionally. CoyoteCert passes it through when the provider supports it and ignores it silently when it doesn't.

Pre-flight checks that actually catch problems early

CAA records are verified before the first CA call. If a record blocks your chosen CA, you get a CaaException immediately, before burning a rate-limit slot. http-01 and dns-01 tokens are verified locally before the CA comes to check. Unlike a certain cartoon coyote, we look before we order from the ACME Corporation.

Typed exceptions with the information you actually need

RateLimitException carries the CA's Retry-After seconds directly. AuthException means bad credentials, not a transient 5xx. AcmeException::getSubproblems() tells you exactly which domain in a multi-domain order failed and why.

Storage that fits your stack

Filesystem with file locking. PDO for MySQL, PostgreSQL, and SQLite. In-memory for tests. All three share the same StorageInterface; switching backends never touches issuance code. Account keys are namespaced per CA so multiple providers never collide. PEM files are written alongside JSON automatically so you can point nginx straight at them.

RFC 8738 IP address certificates

Pass an IPv4 or IPv6 address to ->identifiers() and CoyoteCert handles the rest: type: ip on the ACME order, IP: SAN entries in the CSR. Nothing extra required. Mixed hostname + IP certificates work the same way.

ECDSA-first, RSA when you need it

EC P-256 is the default for both the certificate key and the ACME account key. EC P-384, RSA-2048, and RSA-4096 are all available. Fast TLS handshakes by default, maximum compatibility on demand.

PSR-18 HTTP client integration

The built-in curl client needs zero extra dependencies. Swap it for Guzzle, Symfony HttpClient, or any PSR-18 client with one builder call. Request and stream factories are auto-detected when the client implements them.

Modern PHP, no magic

Strict types throughout. Backed enums for key types, revocation reasons, and challenge types. Readonly constructor promotion. No magic methods, no global state, no hidden opinions about your framework.

94%+ test coverage, Pebble-verified

Unit tests with mocked responses plus a live Pebble integration suite across PHP 8.3, 8.4, and 8.5. The integration suite runs the full order lifecycle against a real ACME server on every CI run.

First-party Laravel integration

blendbyte/coyotecert-laravel adds a service provider, Artisan commands (cert:issue, cert:renew, cert:status, cert:revoke), HTTP-01 challenge served through the cache store (no web server changes, works behind load balancers), Laravel Events, queue job support for dns-01, and a daily scheduled renewal task.


Quick start

composer require blendbyte/coyotecert
use CoyoteCert\CoyoteCert;
use CoyoteCert\Challenge\Dns\CloudflareDns01Handler;
use CoyoteCert\Provider\LetsEncrypt;
use CoyoteCert\Storage\FilesystemStorage;

$cert = CoyoteCert::with(new LetsEncrypt())
    ->storage(new FilesystemStorage('/var/certs'))
    ->identifiers(['example.com', '*.example.com'])
    ->email('admin@example.com')
    ->challenge(new CloudflareDns01Handler(apiToken: 'your-token'))
    ->issueOrRenew();

Account created, order placed, challenge deployed, certificate issued, files written to disk. Run it again tomorrow and it does nothing unless renewal is due.


Full documentation: README

Full Changelog: v0.1.4...v1.0.0