alapi/acme-client
Composer 安装命令:
composer require alapi/acme-client
包简介
Acme client library for ACME v2 written in PHP.
README 文档
README
🌐 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.
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:
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 require_once 'vendor/autoload.php'; use ALAPI\Acme\Accounts\Account; // Create account from existing private key string $privateKeyPem = '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----'; $account = new Account($privateKeyPem); // Or create account with both private and public keys $publicKeyPem = '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----'; $account = new Account($privateKeyPem, $publicKeyPem); // Or create account from private key only (public key will be extracted) $account = Account::fromPrivateKey($privateKeyPem);
Option B: Using AccountStorage for file-based key management
<?php require_once 'vendor/autoload.php'; use ALAPI\Acme\Utils\AccountStorage; // Create new ECC account and save to files (recommended) $account = AccountStorage::createAndSave( directory: 'storage', name: 'my-account', keyType: 'ECC', keySize: 'P-384' ); // Or create RSA account and save to files $rsaAccount = AccountStorage::createAndSave( directory: 'storage', name: 'my-rsa-account', keyType: 'RSA', keySize: 4096 ); echo "Account keys created and saved successfully!\n";
2. Initialize ACME Client
<?php require_once 'vendor/autoload.php'; use ALAPI\Acme\AcmeClient; use ALAPI\Acme\Accounts\Account; use ALAPI\Acme\Utils\AccountStorage; use ALAPI\Acme\Http\Clients\ClientFactory; // Option A: Load account from files $account = AccountStorage::loadFromFiles('storage', 'my-account'); // Option B: Create account from existing keys $privateKey = '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----'; $account = new Account($privateKey); // Create HTTP client with optional proxy $httpClient = ClientFactory::create(timeout: 30, options: [ // 'proxy' => '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):
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):
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 use ALAPI\Acme\Enums\AuthorizationChallengeEnum; try { // Get account data $accountData = $acmeClient->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:
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
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:
// 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
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
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
// 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
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
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:
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:
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
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:
composer test
Run static analysis:
composer analyse
Fix code style:
composer cs-fix
Security Considerations
- Private Keys: Store private keys securely with appropriate file permissions (600)
- Account Keys: Keep account keys separate from certificate keys
- Staging Environment: Use staging environment for testing
- Rate Limits: Be aware of CA rate limits
- Validation: Always validate challenges locally before triggering ACME validation
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Run the test suite
- Submit a pull request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Links
Support
If you encounter any issues or have questions:
- Check the documentation
- Search existing issues
- Create a new issue if needed
alapi/acme-client 适用场景与选型建议
alapi/acme-client 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 62 次下载、GitHub Stars 达 2, 最近一次更新时间为 2025 年 08 月 06 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「letsencrypt」 「ACME」 「lets」 「acme-client」 「acme-v2」 「zerossl」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 alapi/acme-client 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 alapi/acme-client 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 alapi/acme-client 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
A package to easily make use of Lets Icons in your Laravel Blade views.
Let's Encrypt / ACME client written in PHP for the CLI.
PHP ACME client library
Yet another ACME client: Now with 20 percent less needed variables
Yet Another ACME client: a decoupled LetsEncrypt client
ACME v2 client for Let's Encrypt
统计信息
- 总下载量: 62
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 2
- 点击次数: 28
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2025-08-06