Web Analytics

acme-client

⭐ 2 stars Korean by anhao

🌐 언어

ACME 클라이언트

Let's Encrypt, ZeroSSL 및 기타 ACME 호환 인증 기관과 함께 SSL/TLS 인증서 관리를 자동화하는 포괄적인 PHP ACME v2 클라이언트 라이브러리입니다.

github stats License: MIT PHP Version

Language / 语言: English | 中文

기능

요구 사항

설치

Composer를 통해 설치:

composer require alapi/acme-client

빠른 시작

1. 로컬 계정 키 생성

ACME 계정 키를 생성하고 관리하는 두 가지 방법이 있습니다:

옵션 A: 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);

옵션 B: 파일 기반 키 관리를 위한 AccountStorage 사용

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. 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. ACME 계정 등록

Let's Encrypt용 (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";
}

ZeroSSL의 경우 (EAB 필요):

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. 인증서 요청

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. 도메인 검증 완료

도전 과제 파일을 웹 서버에 배치한 후:

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. 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"; }

고급 사용법

DNS-01 챌린지

와일드카드 인증서 또는 HTTP 검증이 불가능할 때:

// 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 );

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"; }

인증서 폐기

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";
}

다중 인증 기관

// 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' );

사용자 정의 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' ] ]);

로깅

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);

구성

계정 관리 옵션

파일 기반 관리를 위한 AccountStorage 사용:

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' );

기존 키에 대해 Account 클래스 사용:

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";

오류 처리

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"; }

테스트

테스트 스위트를 실행하세요:

composer test

정적 분석 실행:

composer analyse
코드 스타일 수정:

composer cs-fix

보안 고려사항

기여 방법

라이선스

이 프로젝트는 MIT 라이선스 하에 배포됩니다 - 자세한 내용은 LICENSE 파일을 참조하세요.

링크

지원

문제가 발생하거나 질문이 있을 경우:

--- Tranlated By Open Ai Tx | Last indexed: 2025-08-15 ---