🌐 Language
English | 简体中文 | 繁體中文 | 日本語 | 한국어 | हिन्दी | ไทย | Français | Deutsch | Español | Italiano | Русский | Português | Nederlands | Polski | العربية | فارسی | Türkçe | Tiếng Việt | Bahasa Indonesia | অসমীয়া
# ACME Client A comprehensive PHP ACME v2 client library for automating SSL/TLS certificate management with Let's Encrypt, ZeroSSL, and other ACME-compatible Certificate Authorities. [![github stats](https://img.shields.io/github/stars/anhao/acme-client?style=flat-square&label=github%20stats)](https://github.com/anhao/acme-client) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/anhao/acme-client) [![PHP Version](https://img.shields.io/badge/PHP-%3E%3D8.2-blue.svg)](https://www.php.net/) > **Language / 语言**: [English](README.md) | [中文](README_ZH.md) ## Features - **ACME v2 Protocol Support**: Full compatibility with ACME v2 specification - **Multiple CA Support**: Works with Let's Encrypt, ZeroSSL, and other ACME providers - **Account Management**: Create, store, and manage ACME accounts - **Certificate Operations**: Request, renew, and revoke SSL certificates - **Domain Validation**: Support for HTTP-01 and DNS-01 challenges - **ARI Support**: Automatic Renewal Information for optimal renewal timing - **Flexible Key Types**: Support for RSA and ECC keys - **Comprehensive Logging**: Built-in PSR-3 compatible logging - **Easy Integration**: Simple and intuitive API design ## Requirements - PHP 8.2 or higher - OpenSSL extension - cURL extension - JSON extension - mbstring extension ## Installation Install via Composer: ```bash composer require alapi/acme-client ``` ## Quick Start ### 1. Create Local Account Keys You have two ways to create and manage ACME account keys: **Option A: Using existing keys with Account class** ```php 'http://proxy.example.com:8080' ]); // Initialize client for Let's Encrypt production $acmeClient = new AcmeClient( staging: false, // Set to true for testing localAccount: $account, httpClient: $httpClient ); // Or use ZeroSSL $zeroSslClient = new AcmeClient( localAccount: $account, httpClient: $httpClient, baseUrl: 'https://acme.zerossl.com/v2/DV90/directory' ); ``` ### 3. Register ACME Account For Let's Encrypt (no EAB required): ```php try { // Register account with Let's Encrypt $accountData = $acmeClient->account()->create( contacts: ['mailto:admin@example.com'] ); echo "Account registered successfully!\n"; echo "Account URL: " . $accountData->url . "\n"; } catch (Exception $e) { echo "Registration failed: " . $e->getMessage() . "\n"; } ``` For ZeroSSL (EAB required): ```php try { // Get EAB credentials from ZeroSSL dashboard $eabKid = 'your-eab-kid'; $eabHmacKey = 'your-eab-hmac-key'; $accountData = $zeroSslClient->account()->create( eabKid: $eabKid, eabHmacKey: $eabHmacKey, contacts: ['mailto:admin@example.com'] ); echo "ZeroSSL account registered successfully!\n"; } catch (Exception $e) { echo "Registration failed: " . $e->getMessage() . "\n"; } ``` ### 4. Request Certificate ```php account()->get(); // Create new order for domains $domains = ['example.com', 'www.example.com']; $order = $acmeClient->order()->new($accountData, $domains); echo "Order created: " . $order->url . "\n"; echo "Status: " . $order->status . "\n"; // Check domain validations $validations = $acmeClient->domainValidation()->status($order); foreach ($validations as $validation) { $domain = $validation->identifier['value']; echo "Domain: $domain - Status: " . $validation->status . "\n"; if ($validation->isPending()) { // Get validation data for HTTP-01 challenge $challenges = $acmeClient->domainValidation()->getValidationData( [$validation], AuthorizationChallengeEnum::HTTP ); foreach ($challenges as $challenge) { echo "HTTP Challenge for $domain:\n"; echo " File: " . $challenge['filename'] . "\n"; echo " Content: " . $challenge['content'] . "\n"; echo " Place it at: http://$domain/.well-known/acme-challenge/" . $challenge['filename'] . "\n\n"; } } } } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ``` ### 5. Complete Domain Validation After placing the challenge files on your web server: ```php try { // Trigger validation for each domain foreach ($validations as $validation) { if ($validation->isPending()) { $response = $acmeClient->domainValidation()->validate( $accountData, $validation, AuthorizationChallengeEnum::HTTP, localTest: true // Performs local validation first ); echo "Validation triggered for: " . $validation->identifier['value'] . "\n"; } } // Wait for validation to complete $maxAttempts = 10; $attempt = 0; do { sleep(5); $attempt++; // Check order status $currentOrder = $acmeClient->order()->get($accountData, $order->url); echo "Order status: " . $currentOrder->status . "\n"; if ($currentOrder->status === 'ready') { echo "All validations completed successfully!\n"; break; } if ($currentOrder->status === 'invalid') { echo "Order validation failed!\n"; break; } } while ($attempt < $maxAttempts); } catch (Exception $e) { echo "Validation error: " . $e->getMessage() . "\n"; } ``` ### 6. Generate and Submit CSR ```php use ALAPI\Acme\Security\Cryptography\OpenSsl; try { if ($currentOrder->status === 'ready') { // Generate Certificate private key $certificatePrivateKey = OpenSsl::generatePrivateKey('RSA', 2048); // Generate Certificate Signing Request (CSR) using OpenSsl helper $csrString = OpenSsl::generateCsr($domains, $certificatePrivateKey); // Export private key for saving $privateKeyString = OpenSsl::openSslKeyToString($certificatePrivateKey); // Submit CSR to finalize order $finalizedOrder = $acmeClient->order()->finalize( $accountData, $currentOrder, $csrString ); echo "Order finalized successfully!\n"; echo "Certificate URL: " . $finalizedOrder->certificateUrl . "\n"; // Download certificate bundle $certificateBundle = $acmeClient->certificate()->get( $accountData, $finalizedOrder->certificateUrl ); // Save certificate and private key file_put_contents('certificate.pem', $certificateBundle->certificate); file_put_contents('fullchain.pem', $certificateBundle->fullchain); file_put_contents('private-key.pem', $privateKeyString); echo "Certificate saved to certificate.pem\n"; echo "Fullchain certificate saved to fullchain.pem\n"; echo "Private key saved to private-key.pem\n"; } } catch (Exception $e) { echo "Certificate generation error: " . $e->getMessage() . "\n"; } ``` ## Advanced Usage ### DNS-01 Challenge For wildcard certificates or when HTTP validation isn't possible: ```php // Get DNS challenge data $dnsChallenge = $acmeClient->domainValidation()->getValidationData( [$validation], AuthorizationChallengeEnum::DNS ); foreach ($dnsChallenge as $challenge) { echo "DNS Challenge for " . $challenge['domain'] . ":\n"; echo " Record Name: " . $challenge['domain'] . "\n"; echo " Record Type: TXT\n"; echo " Record Value: " . $challenge['digest'] . "\n\n"; } // After adding DNS records, trigger validation $response = $acmeClient->domainValidation()->validate( $accountData, $validation, AuthorizationChallengeEnum::DNS, localTest: true ); ``` ### Certificate Renewal with ARI ```php use ALAPI\Acme\Management\RenewalManager; // Load existing certificate $certificatePem = file_get_contents('certificate.pem'); // Create renewal manager $renewalManager = $acmeClient->renewalManager(defaultRenewalDays: 30); // Check if renewal is needed if ($renewalManager->shouldRenew($certificatePem)) { echo "Certificate needs renewal\n"; // Get ARI information if supported if ($acmeClient->directory()->supportsARI()) { $renewalInfo = $acmeClient->renewalInfo()->getFromCertificate($certificatePem); echo "Suggested renewal window:\n"; echo " Start: " . $renewalInfo->suggestedWindow['start'] . "\n"; echo " End: " . $renewalInfo->suggestedWindow['end'] . "\n"; if ($renewalInfo->shouldRenewNow()) { echo "ARI recommends renewing now\n"; // Proceed with renewal... } } } else { echo "Certificate renewal not needed yet\n"; } ``` ### Certificate Revocation ```php try { // Load certificate to revoke $certificatePem = file_get_contents('certificate.pem'); // Revoke certificate $success = $acmeClient->certificate()->revoke( $certificatePem, reason: 1 // 0=unspecified, 1=keyCompromise, 2=cACompromise, 3=affiliationChanged, 4=superseded, 5=cessationOfOperation ); if ($success) { echo "Certificate revoked successfully\n"; } else { echo "Certificate revocation failed\n"; } } catch (Exception $e) { echo "Revocation error: " . $e->getMessage() . "\n"; } ``` ### Multiple Certificate Authorities ```php // Let's Encrypt $letsEncrypt = new AcmeClient( staging: false, localAccount: $account, httpClient: $httpClient ); // ZeroSSL $zeroSSL = new AcmeClient( localAccount: $account, httpClient: $httpClient, baseUrl: 'https://acme.zerossl.com/v2/DV90/directory' ); // Google Trust Services $googleCA = new AcmeClient( localAccount: $account, httpClient: $httpClient, baseUrl: 'https://dv.acme-v02.api.pki.goog/directory' ); ``` ### Custom HTTP Client Configuration ```php use ALAPI\Acme\Http\Clients\ClientFactory; $httpClient = ClientFactory::create(30, [ 'proxy' => 'http://proxy.example.com:8080', 'verify' => true, // SSL verification 'timeout' => 30, 'connect_timeout' => 10, 'headers' => [ 'User-Agent' => 'MyApp ACME Client 1.0' ] ]); ``` ### Logging ```php use Monolog\Logger; use Monolog\Handler\StreamHandler; // Create logger $logger = new Logger('acme'); $logger->pushHandler(new StreamHandler('acme.log', Logger::INFO)); // Set logger on client $acmeClient->setLogger($logger); ``` ## Configuration ### Account Management Options **Using AccountStorage for file-based management:** ```php use ALAPI\Acme\Utils\AccountStorage; // Check if account files exist if (AccountStorage::exists('storage', 'my-account')) { $account = AccountStorage::loadFromFiles('storage', 'my-account'); } else { $account = AccountStorage::createAndSave('storage', 'my-account'); } // Load or create account automatically $account = AccountStorage::loadOrCreate( directory: 'storage', name: 'my-account', keyType: 'ECC', keySize: 'P-384' ); ``` **Using Account class for existing keys:** ```php use ALAPI\Acme\Accounts\Account; // From existing private key $privateKey = file_get_contents('/path/to/private.key'); $account = new Account($privateKey); // With both private and public keys $privateKey = file_get_contents('/path/to/private.key'); $publicKey = file_get_contents('/path/to/public.key'); $account = new Account($privateKey, $publicKey); // Create new account with specific key type $account = Account::createECC('P-384'); // or 'P-256', 'P-384' $account = Account::createRSA(4096); // or 2048, 3072 // Get account information echo "Key Type: " . $account->getKeyType() . "\n"; echo "Key Size: " . $account->getKeySize() . "\n"; ``` ## Error Handling ```php use ALAPI\Acme\Exceptions\AcmeException; use ALAPI\Acme\Exceptions\AcmeAccountException; use ALAPI\Acme\Exceptions\DomainValidationException; use ALAPI\Acme\Exceptions\AcmeCertificateException; try { // ACME operations here } catch (AcmeAccountException $e) { echo "Account error: " . $e->getMessage() . "\n"; echo "Detail: " . $e->getDetail() . "\n"; echo "Type: " . $e->getAcmeType() . "\n"; } catch (DomainValidationException $e) { echo "Validation error: " . $e->getMessage() . "\n"; } catch (AcmeCertificateException $e) { echo "Certificate error: " . $e->getMessage() . "\n"; } catch (AcmeException $e) { echo "ACME error: " . $e->getMessage() . "\n"; } catch (Exception $e) { echo "General error: " . $e->getMessage() . "\n"; } ``` ## Testing Run the test suite: ```bash composer test ``` Run static analysis: ```bash composer analyse ``` Fix code style: ```bash composer cs-fix ``` ## Security Considerations 1. **Private Keys**: Store private keys securely with appropriate file permissions (600) 2. **Account Keys**: Keep account keys separate from certificate keys 3. **Staging Environment**: Use staging environment for testing 4. **Rate Limits**: Be aware of CA rate limits 5. **Validation**: Always validate challenges locally before triggering ACME validation ## Contributing 1. Fork the repository 2. Create a feature branch 3. Make your changes 4. Add tests for new functionality 5. Run the test suite 6. Submit a pull request ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## Links - [Let's Encrypt Documentation](https://letsencrypt.org/docs/) - [ACME Specification RFC 8555](https://tools.ietf.org/html/rfc8555) - [ZeroSSL ACME Guide](https://zerossl.com/documentation/acme/) ## Support If you encounter any issues or have questions: 1. Check the [documentation](#quick-start) 2. Search existing [issues](https://github.com/anhao/acme-client/issues) 3. Create a [new issue](https://github.com/anhao/acme-client/issues/new) if needed