ACME Client
Una completa libreria client ACME v2 in PHP per automatizzare la gestione dei certificati SSL/TLS con Let's Encrypt, ZeroSSL e altre Autorità di Certificazione compatibili ACME.
Lingua / 语言: Inglese | 中文
Caratteristiche
- Supporto Protocollo ACME v2: Piena compatibilità con la specifica ACME v2
- Supporto Multi-CA: Funziona con Let's Encrypt, ZeroSSL e altri provider ACME
- Gestione Account: Crea, memorizza e gestisce account ACME
- Operazioni Certificato: Richiedi, rinnova e revoca certificati SSL
- Validazione Dominio: Supporto per le sfide HTTP-01 e DNS-01
- Supporto ARI: Informazioni di rinnovo automatico per tempistiche ottimali
- Tipi di Chiavi Flessibili: Supporto per chiavi RSA ed ECC
- Logging Completo: Logging integrato compatibile PSR-3
- Facile Integrazione: API semplice e intuitiva
Requisiti
- PHP 8.2 o superiore
- Estensione OpenSSL
- Estensione cURL
- Estensione JSON
- Estensione mbstring
Installazione
Installa tramite Composer:
composer require alapi/acme-clientAvvio rapido
1. Creare le chiavi dell'account locale
Hai due modi per creare e gestire le chiavi dell'account ACME:
Opzione A: Utilizzo di chiavi esistenti con la classe Account
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);
Opzione B: Utilizzo di AccountStorage per la gestione delle chiavi basata su file
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. Inizializza il client ACME
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. Registrare Account ACME
Per Let's Encrypt (non è richiesto EAB):
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";
}Per ZeroSSL (EAB richiesto):
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. Richiedi Certificato
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. Convalida Completa del Dominio
Dopo aver posizionato i file di verifica sul tuo server web:
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. Genera e Invia la 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";
}
Utilizzo Avanzato
Sfida DNS-01
Per certificati wildcard o quando la validazione HTTP non è possibile:
// 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
);
Rinnovo del Certificato con 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";
}
Revoca del certificato
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";
}Autorità di Certificazione Multiple
// 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'
);
Configurazione personalizzata del client HTTP
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'
]
]);
Registrazione
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);
Configurazione
Opzioni di Gestione Account
Utilizzo di AccountStorage per la gestione basata su file:
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'
);
Utilizzo della classe Account per chiavi esistenti:
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";
Gestione degli Errori
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";
}
Test
Esegui la suite di test:
composer testEsegui analisi statica:
composer analyseCorreggi lo stile del codice:
composer cs-fixConsiderazioni sulla Sicurezza
- Chiavi Private: Conserva le chiavi private in modo sicuro con permessi di file appropriati (600)
- Chiavi Account: Mantieni le chiavi dell’account separate dalle chiavi dei certificati
- Ambiente di Staging: Usa l’ambiente di staging per i test
- Limiti di Frequenza: Fai attenzione ai limiti di frequenza della CA
- Validazione: Valida sempre le challenge localmente prima di avviare la validazione ACME
Contribuire
- Fai un fork del repository
- Crea un branch per la funzionalità
- Apporta le tue modifiche
- Aggiungi test per la nuova funzionalità
- Esegui la suite di test
- Invia una pull request
Licenza
Questo progetto è rilasciato sotto la Licenza MIT - vedi il file LICENSE per i dettagli.
Link
Supporto
Se riscontri problemi o hai domande:
- Consulta la documentazione
- Cerca tra le issue esistenti
- Crea una nuova issue se necessario