Skip to content

Commit f37531a

Browse files
committed
update readme
1 parent d3778d1 commit f37531a

1 file changed

Lines changed: 56 additions & 56 deletions

File tree

README.md

Lines changed: 56 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
**A PHP 8.3+ ACME v2 client for issuing, renewing, and revoking TLS certificates.** Works with Let's Encrypt, ZeroSSL, Google Trust Services, SSL.com, Buypass, and any RFC 8555-compliant CA. Fluent API, no framework dependencies, and solid test coverage.
1313

14-
ACME (Automatic Certificate Management Environment) is the protocol behind free, automated TLS certificates. CoyoteCert covers the full thing: account management, order lifecycle, HTTP-01, DNS-01, and TLS-ALPN-01 challenges, certificate issuance, smart renewal with ARI, and revocation. One `composer require` and you're set.
14+
ACME (Automatic Certificate Management Environment) is the protocol behind free, automated TLS certificates. Yes, same name as the cartoon supply company. CoyoteCert covers the full thing: account management, order lifecycle, HTTP-01, DNS-01, and TLS-ALPN-01 challenges, certificate issuance, smart renewal with ARI, and revocation. One `composer require` and you're set.
1515

1616
---
1717

@@ -51,23 +51,23 @@ Filesystem with file locking, PDO (MySQL, PostgreSQL, SQLite) with dialect-aware
5151

5252
### Typed exceptions for every failure mode
5353

54-
`RateLimitException` carries the CA's `Retry-After` seconds so your retry logic is precise. `AuthException` tells you credentials failed not a transient error. `AcmeException::getSubproblems()` surfaces RFC 8555 §6.7 per-identifier errors so a multi-domain order can report exactly which domains were rejected and why. All exceptions share a common base so a single `catch` still works when you don't need the detail.
54+
`RateLimitException` carries the CA's `Retry-After` seconds so your retry logic is precise. `AuthException` tells you credentials failed, not a transient error. `AcmeException::getSubproblems()` surfaces RFC 8555 §6.7 per-identifier errors so a multi-domain order can report exactly which domains were rejected and why. All exceptions share a common base so a single `catch` still works when you don't need the detail.
5555

5656
### CAA pre-check
5757

58-
Before submitting an order, CoyoteCert queries CAA DNS records for every requested domain. If a record exists and excludes the chosen CA, you get an immediate `CaaException` naming the blocking domain — no wasted rate-limit attempt, no waiting for an ACME order to fail minutes later. The check follows RFC 8659 tree-walking, handles `issuewild` tags for wildcard domains, and respects parameter extensions (e.g. `letsencrypt.org; validationmethods=http-01`). CAA identifiers are built into every provider; `->skipCaaCheck()` opts out when DNS is internal or split-horizon.
58+
Before submitting an order, CoyoteCert queries CAA DNS records for every requested domain. If a record exists and excludes the chosen CA, you get an immediate `CaaException` naming the blocking domain. No wasted rate-limit attempt, no waiting for an ACME order to fail minutes later. The check follows RFC 8659 tree-walking, handles `issuewild` tags for wildcard domains, and respects parameter extensions (e.g. `letsencrypt.org; validationmethods=http-01`). CAA identifiers are built into every provider; `->skipCaaCheck()` opts out when DNS is internal or split-horizon.
5959

6060
### Pre-flight self-test
6161

62-
Before asking the CA to validate, CoyoteCert does its own check first: it fetches the HTTP token or looks up the DNS TXT record itself. Misconfigured servers and propagation delays get caught before burning a rate-limit attempt.
62+
Before asking the CA to validate, CoyoteCert does its own check first: it fetches the HTTP token or looks up the DNS TXT record itself. Misconfigured servers and propagation delays get caught before they cost you a rate-limit attempt. Look before you leap.
6363

6464
### 94 %+ test coverage with real CA integration tests
6565

6666
Every code path has unit tests with mocked responses. The integration suite runs against a live [Pebble](https://github.com/letsencrypt/pebble) server in CI across PHP 8.3, 8.4, and 8.5. No mock-only false confidence.
6767

68-
### CA-independent no hidden defaults
68+
### CA-independent: no hidden defaults
6969

70-
CoyoteCert has no default CA. Every issuance call requires you to pass a provider explicitly. Choosing a CA involves real trade-offs trust store coverage, rate limits, certificate lifetime, EAB requirements, data residency and that decision belongs to you, not the library.
70+
CoyoteCert has no default CA. Every issuance call requires you to pass a provider explicitly. Choosing a CA involves real trade-offs (trust store coverage, rate limits, certificate lifetime, EAB requirements, data residency) and that decision belongs to you, not the library.
7171

7272
### Modern, idiomatic PHP
7373

@@ -165,10 +165,10 @@ CoyoteCert ships with built-in providers for every major public ACME CA.
165165
use CoyoteCert\Provider\LetsEncrypt;
166166
use CoyoteCert\Provider\LetsEncryptStaging;
167167

168-
// Production issues browser-trusted certificates
168+
// Production: issues browser-trusted certificates
169169
CoyoteCert::with(new LetsEncrypt())
170170

171-
// Staging rate-limit-free, not browser-trusted; use during development
171+
// Staging: rate-limit-free, not browser-trusted; use during development
172172
CoyoteCert::with(new LetsEncryptStaging())
173173
```
174174

@@ -179,11 +179,11 @@ ZeroSSL requires EAB credentials. CoyoteCert can provision them automatically fr
179179
```php
180180
use CoyoteCert\Provider\ZeroSSL;
181181

182-
// Automatic provisioning CoyoteCert fetches EAB credentials from the ZeroSSL API
182+
// Automatic provisioning: CoyoteCert fetches EAB credentials from the ZeroSSL API
183183
CoyoteCert::with(new ZeroSSL(apiKey: 'your-zerossl-api-key'))
184184
->email('admin@example.com') // required for auto-provisioning
185185

186-
// Manual credentials skip the API call
186+
// Manual credentials: skip the API call
187187
CoyoteCert::with(new ZeroSSL(eabKid: 'kid', eabHmac: 'hmac'))
188188
```
189189

@@ -323,7 +323,7 @@ class Route53DnsPersist01Handler extends DnsPersist01Handler
323323

324324
Defined in [RFC 8737](https://datatracker.ietf.org/doc/html/rfc8737). The CA opens a TLS connection to port 443 of the domain and negotiates the `acme-tls/1` ALPN protocol. The server must present a self-signed certificate that contains a critical `id-pe-acmeIdentifier` extension (OID `1.3.6.1.5.5.7.1.31`) whose value is the SHA-256 digest of the key authorization. No port 80 access required.
325325

326-
Extend `TlsAlpn01Handler` and implement `deploy()` and `cleanup()`. Call `generateAcmeCertificate()` inside `deploy()` to obtain the certificate and key it handles all the RFC 8737 encoding automatically.
326+
Extend `TlsAlpn01Handler` and implement `deploy()` and `cleanup()`. Call `generateAcmeCertificate()` inside `deploy()` to obtain the certificate and key; it handles all the RFC 8737 encoding automatically.
327327

328328
```php
329329
use CoyoteCert\Challenge\TlsAlpn01Handler;
@@ -351,7 +351,7 @@ class MyTlsAlpn01Handler extends TlsAlpn01Handler
351351
->challenge(new MyTlsAlpn01Handler())
352352
```
353353

354-
> **Note:** TLS-ALPN-01 validates on port 443 only and does not require port 80. It is supported by Caddy, nginx (with the ACME plugin), and HAProxy. Wildcard certificates are not supported use DNS-01 for those.
354+
> **Note:** TLS-ALPN-01 validates on port 443 only and does not require port 80. It is supported by Caddy, nginx (with the ACME plugin), and HAProxy. Wildcard certificates are not supported; use DNS-01 for those.
355355
356356
---
357357

@@ -549,7 +549,7 @@ Register callbacks on the builder to react to certificate lifecycle events witho
549549

550550
### onIssued
551551

552-
Fires after every successful certificate issuance — whether first-time or a renewal.
552+
Fires after every successful certificate issuance, first-time or renewal.
553553

554554
```php
555555
CoyoteCert::with(new LetsEncrypt())
@@ -568,7 +568,7 @@ CoyoteCert::with(new LetsEncrypt())
568568

569569
### onRenewed
570570

571-
Fires only when an existing certificate is replaced i.e. storage already held a cert before the new one was issued. Fires _after_ `onIssued` callbacks.
571+
Fires only when an existing certificate is replaced (i.e. storage already held a cert before the new one was issued). Fires after `onIssued` callbacks.
572572

573573
```php
574574
CoyoteCert::with(new LetsEncrypt())
@@ -586,7 +586,7 @@ Both methods accept any `callable` and can be called multiple times. Callbacks r
586586

587587
## CAA pre-check
588588

589-
[CAA (Certification Authority Authorization)](https://en.wikipedia.org/wiki/DNS_Certification_Authority_Authorization) is a DNS record type that restricts which CAs are allowed to issue certificates for a domain. If `example.com` has `CAA 0 issue "digicert.com"`, Let's Encrypt will refuse the order but only after you have consumed a rate-limit attempt and waited for the ACME workflow to fail.
589+
[CAA (Certification Authority Authorization)](https://en.wikipedia.org/wiki/DNS_Certification_Authority_Authorization) is a DNS record type that restricts which CAs are allowed to issue certificates for a domain. If `example.com` has `CAA 0 issue "digicert.com"`, Let's Encrypt will refuse the order, but only after you have consumed a rate-limit attempt and waited for the ACME workflow to fail.
590590

591591
CoyoteCert runs the CAA check itself before submitting anything to the CA. If the records block the chosen CA, you get a `CaaException` immediately:
592592

@@ -615,7 +615,7 @@ try {
615615

616616
`CaaException` extends `AcmeException`, so existing catch blocks for the base type continue to work.
617617

618-
IP address identifiers are excluded from the CAA check CAA records apply to domain names only.
618+
IP address identifiers are excluded from the CAA check; CAA records apply to domain names only.
619619

620620
### Opting out
621621

@@ -637,7 +637,7 @@ CoyoteCert::with(new LetsEncrypt())
637637

638638
All exceptions extend `AcmeException`, so a single `catch (AcmeException $e)` covers everything. Catch the narrower types when you need to react differently to specific failure modes.
639639

640-
### Rate limits — with Retry-After
640+
### Rate limits: Retry-After included
641641

642642
```php
643643
use CoyoteCert\Exceptions\RateLimitException;
@@ -663,7 +663,7 @@ use CoyoteCert\Exceptions\AuthException;
663663
try {
664664
$api->account()->get();
665665
} catch (AuthException $e) {
666-
// 401 / 403 account key rejected or credentials revoked
666+
// 401 / 403: account key rejected or credentials revoked
667667
echo $e->getMessage();
668668
}
669669
```
@@ -695,15 +695,15 @@ try {
695695
### Exception hierarchy
696696

697697
```
698-
AcmeException base; always safe to catch
699-
├── AuthException 401/403 (bad credentials, revoked account)
700-
├── RateLimitException 429 (too many requests); carries getRetryAfter()
701-
├── CaaException CAA DNS record blocks issuance
702-
├── ChallengeException challenge validation failed
703-
├── CryptoException local key or certificate operation failed
704-
├── DomainValidationException pre-flight HTTP/DNS self-check failed
705-
├── OrderNotFoundException order ID not found on the CA
706-
└── StorageException storage backend error
698+
AcmeException - base; always safe to catch
699+
├── AuthException - 401/403 (bad credentials, revoked account)
700+
├── RateLimitException - 429 (too many requests); carries getRetryAfter()
701+
├── CaaException - CAA DNS record blocks issuance
702+
├── ChallengeException - challenge validation failed
703+
├── CryptoException - local key or certificate operation failed
704+
├── DomainValidationException - pre-flight HTTP/DNS self-check failed
705+
├── OrderNotFoundException - order ID not found on the CA
706+
└── StorageException - storage backend error
707707
```
708708

709709
---
@@ -734,7 +734,7 @@ CoyoteCert::with(new LetsEncrypt())
734734

735735
## IP address certificates (RFC 8738)
736736

737-
`->identifiers()` accepts IPv4 and IPv6 addresses alongside hostnames. CoyoteCert automatically sets `type: ip` on ACME identifiers and `IP:` SAN entries in the CSR — no extra API calls required.
737+
`->identifiers()` accepts IPv4 and IPv6 addresses alongside hostnames. CoyoteCert automatically sets `type: ip` on ACME identifiers and `IP:` SAN entries in the CSR. No extra API calls required.
738738

739739
```php
740740
// IPv4-only certificate (e.g. with Let's Encrypt shortlived profile)
@@ -753,7 +753,7 @@ CoyoteCert::with(new LetsEncrypt())
753753

754754
IP SANs are validated via HTTP-01 (the CA connects to the IP directly). Wildcards cannot be combined with IP identifiers.
755755

756-
Not all CAs support IP SANs check your CA's documentation. Let's Encrypt supports them on both the `classic` and `shortlived` profiles.
756+
Not all CAs support IP SANs; check your CA's documentation. Let's Encrypt supports them on both the `classic` and `shortlived` profiles.
757757

758758
---
759759

@@ -811,11 +811,11 @@ No configuration is required. CoyoteCert handles this transparently.
811811
Profiles let you request a specific certificate type from the CA. Let's Encrypt currently supports two:
812812

813813
```php
814-
->profile('shortlived') // 6-day certificate no OCSP/CRL infrastructure needed
814+
->profile('shortlived') // 6-day certificate, no OCSP/CRL infrastructure needed
815815
->profile('classic') // 90-day certificate (default if no profile specified)
816816
```
817817

818-
Short-lived certificates are renewed more frequently but eliminate the need for OCSP stapling, CRL checks, and revocation infrastructure. A significant operational simplification.
818+
Short-lived certificates are renewed more frequently but eliminate the need for OCSP stapling, CRL checks, and revocation infrastructure. Much simpler to operate.
819819

820820
Profiles are forwarded to the CA only if the provider reports `supportsProfiles() === true`. For CAs that don't support profiles (ZeroSSL, Buypass, etc.) the setting is silently ignored, so you can call `->profile()` unconditionally.
821821

@@ -836,7 +836,7 @@ CoyoteCert::with(new LetsEncrypt())
836836
->issueOrRenew();
837837
```
838838

839-
If no alternate chain matches, CoyoteCert falls back to the default chain returned by the CA so this call is always safe to include even when the CA offers only one chain.
839+
If no alternate chain matches, CoyoteCert falls back to the default chain returned by the CA, so this call is always safe to include even when the CA offers only one chain.
840840

841841
When using the low-level API directly, pass the preference as a second argument to `getBundle()`:
842842

@@ -852,10 +852,10 @@ $bundle = $api->certificate()->getBundle($order, 'ISRG Root X1');
852852
use CoyoteCert\Enums\KeyType;
853853

854854
// Certificate key type (default: EC_P256)
855-
->keyType(KeyType::EC_P256) // ECDSA P-256 fast, compact, widely supported
856-
->keyType(KeyType::EC_P384) // ECDSA P-384 higher security margin
855+
->keyType(KeyType::EC_P256) // ECDSA P-256: fast, compact, widely supported
856+
->keyType(KeyType::EC_P384) // ECDSA P-384: higher security margin
857857
->keyType(KeyType::RSA_2048) // RSA 2048-bit
858-
->keyType(KeyType::RSA_4096) // RSA 4096-bit maximum compatibility
858+
->keyType(KeyType::RSA_4096) // RSA 4096-bit: maximum compatibility
859859

860860
// ACME account key type (default: EC_P256)
861861
->accountKeyType(KeyType::RSA_2048)
@@ -907,17 +907,17 @@ $storage->deleteCertificate('example.com', KeyType::EC_P256);
907907
CoyoteCert ships with a built-in curl client that requires no extra dependencies. To use a custom HTTP client, pass any PSR-18 `ClientInterface`:
908908

909909
```php
910-
// Symfony HttpClient implements all three interfaces itself
910+
// Symfony HttpClient: implements all three interfaces itself
911911
->httpClient(new \Symfony\Component\HttpClient\Psr18Client())
912912

913-
// Guzzle pass request and stream factories separately
913+
// Guzzle: pass request and stream factories separately
914914
use GuzzleHttp\Client;
915915
use GuzzleHttp\Psr7\HttpFactory;
916916

917917
->httpClient(
918918
new Client(),
919919
new HttpFactory(), // RequestFactoryInterface
920-
new HttpFactory(), // StreamFactoryInterface same object works for both
920+
new HttpFactory(), // StreamFactoryInterface: same object works for both
921921
)
922922

923923
// Nyholm PSR-7 + any client
@@ -970,26 +970,26 @@ Log messages cover directory fetches, nonce acquisition, challenge deployment, v
970970
### Properties
971971

972972
```php
973-
$cert->certificate // string PEM leaf certificate
974-
$cert->privateKey // string PEM private key
975-
$cert->fullchain // string PEM leaf + intermediate chain
976-
$cert->caBundle // string PEM intermediate chain only
973+
$cert->certificate // string: PEM leaf certificate
974+
$cert->privateKey // string: PEM private key
975+
$cert->fullchain // string: PEM leaf + intermediate chain
976+
$cert->caBundle // string: PEM intermediate chain only
977977
$cert->issuedAt // DateTimeImmutable
978978
$cert->expiresAt // DateTimeImmutable
979-
$cert->domains // string[] domains as recorded at issuance time
979+
$cert->domains // string[]: domains as recorded at issuance time
980980
```
981981

982982
### Methods
983983

984984
```php
985985
// Quick expiry checks
986-
$cert->isExpired(); // bool true if the cert is past its expiry
987-
$cert->expiresWithin(30); // bool true if expiry is ≤ 30 days away
986+
$cert->isExpired(); // bool: true if the cert is past its expiry
987+
$cert->expiresWithin(30); // bool: true if expiry is ≤ 30 days away
988988

989-
// Days until expiry 0 if already expired
989+
// Days until expiry (0 if already expired)
990990
$cert->remainingDays();
991991

992-
// Ceiling of days until expiry negative if expired
992+
// Ceiling of days until expiry (negative if expired)
993993
$cert->daysUntilExpiry();
994994

995995
// Whether the certificate covers all the given domains (wildcard-aware)
@@ -1022,14 +1022,14 @@ $cert = StoredCertificate::fromArray($array);
10221022
## Builder reference
10231023

10241024
```php
1025-
CoyoteCert::with(AcmeProviderInterface $provider) // factory select the CA
1025+
CoyoteCert::with(AcmeProviderInterface $provider) // factory: select the CA
10261026
```
10271027

10281028
| Method | Type | Default | Description |
10291029
|---|---|---|---|
10301030
| `->email(string)` | fluent | `''` | Contact email; required for ZeroSSL auto-provisioning |
1031-
| `->identifiers(string\|array)` | fluent | | Domain(s) and/or IP(s) to certify; first entry is the primary |
1032-
| `->challenge(ChallengeHandlerInterface)` | fluent | | Challenge handler |
1031+
| `->identifiers(string\|array)` | fluent | - | Domain(s) and/or IP(s) to certify; first entry is the primary |
1032+
| `->challenge(ChallengeHandlerInterface)` | fluent | - | Challenge handler |
10331033
| `->storage(StorageInterface)` | fluent | none | Storage backend |
10341034
| `->keyType(KeyType)` | fluent | `EC_P256` | Certificate key algorithm |
10351035
| `->accountKeyType(KeyType)` | fluent | `EC_P256` | ACME account key algorithm |
@@ -1042,11 +1042,11 @@ CoyoteCert::with(AcmeProviderInterface $provider) // factory — select the CA
10421042
| `->skipCaaCheck()` | fluent | off | Disable CAA DNS pre-check (internal CAs, split-horizon DNS) |
10431043
| `->onIssued(callable)` | fluent | none | Callback fired after every successful issuance; receives `StoredCertificate` |
10441044
| `->onRenewed(callable)` | fluent | none | Callback fired when an existing cert is replaced; receives `StoredCertificate` |
1045-
| `->issue()` | terminal | | Issue unconditionally; returns `StoredCertificate` |
1046-
| `->renew()` | terminal | | Alias for `issue()` |
1047-
| `->issueOrRenew(int $days = 30)` | terminal | | Issue only when needed; returns `StoredCertificate` |
1048-
| `->needsRenewal(int $days = 30)` | query | | `true` if renewal is needed |
1049-
| `->revoke(StoredCertificate, RevocationReason)` | terminal | | Revoke a certificate |
1045+
| `->issue()` | terminal | - | Issue unconditionally; returns `StoredCertificate` |
1046+
| `->renew()` | terminal | - | Alias for `issue()` |
1047+
| `->issueOrRenew(int $days = 30)` | terminal | - | Issue only when needed; returns `StoredCertificate` |
1048+
| `->needsRenewal(int $days = 30)` | query | - | `true` if renewal is needed |
1049+
| `->revoke(StoredCertificate, RevocationReason)` | terminal | - | Revoke a certificate |
10501050

10511051
---
10521052

@@ -1107,7 +1107,7 @@ $ariUrl = $api->directory()->renewalInfo(); // null if not supported
11071107
```php
11081108
use CoyoteCert\Provider\Pebble;
11091109

1110-
// Default connects to localhost:14000, TLS verification enabled
1110+
// Default: connects to localhost:14000, TLS verification enabled
11111111
CoyoteCert::with(new Pebble())
11121112

11131113
// Pebble uses a self-signed CA, so disable TLS verification explicitly

0 commit comments

Comments
 (0)