Edit File: README.md
# ACMECert PHP client library for [Let's Encrypt](https://letsencrypt.org/) ([ACME v2 - RFC 8555](https://tools.ietf.org/html/rfc8555)) Version: 2.6 ## Description ACMECert is designed to help you to setup an automated SSL/TLS-certificate/renewal process with a few lines of PHP. It is self contained and contains a set of functions allowing you to: - generate [RSA](#acmecertgeneratersakey) / [EC (Elliptic Curve)](#acmecertgenerateeckey) keys - manage account: [register](#acmecertregister)/[update](#acmecertupdate)/[deactivate](#acmecertdeactivateaccount) and [account key roll-over](#acmecertkeychange) - [get](#acmecertgetcertificatechain)/[revoke](#acmecertrevoke) certificates (to renew a certificate just get a new one) - [parse certificates](#acmecertparsecertificate) / get the [remaining days](#acmecertgetremainingdays) a certificate is still valid > see [Function Reference](#function-reference) for a full list It abstacts away the complexity of the ACME protocol to get a certificate (create order, fetch authorizations, compute challenge tokens, polling for status, generate CSR, finalize order, request certificate) into a single function [getCertificateChain](#acmecertgetcertificatechain), where you specify a set of domains you want to get a certificate for and which challenge type to use (all [challenge types](https://letsencrypt.org/docs/challenge-types/) are supported). This function takes as third argument a user-defined callback function which gets invoked every time a challenge needs to be fulfilled. It is up to you to set/remove the challenge tokens: ```php $handler=function($opts){ // Write code to setup the challenge token here. // Return a function that gets called when the challenge token should be removed again: return function($opts){ // Write code to remove previously setup challenge token. }; }; $ac->getCertificateChain(..., ..., $handler); ``` > see description of [getCertificateChain](#acmecertgetcertificatechain) for details about the callback function. > > also see the [Get Certificate](#get-certificate-using-http-01-challenge) examples below. Instead of returning `FALSE` on error, every function in ACMECert throws an [Exception](http://php.net/manual/en/class.exception.php) if it fails or an [ACME_Exception](#acme_exception) if the ACME-Server reponded with an error message. ## Requirements - [x] PHP 5.3 or higher (for EC keys PHP 7.1 or higher is required) - [x] [OpenSSL extension](https://www.php.net/manual/de/book.openssl.php) - [x] enabled [fopen wrappers](https://www.php.net/manual/en/filesystem.configuration.php#ini.allow-url-fopen) (allow_url_fopen=1) **or** [cURL extension](https://www.php.net/manual/en/book.curl.php) ## Usage Examples #### Require ACMECert ```php require 'ACMECert.php'; ``` #### Choose Live or Staging Environment > Live ```php $ac=new ACMECert(); ``` > Staging ```php $ac=new ACMECert(false); ``` #### Generate RSA Private Key ```php $key=$ac->generateRSAKey(2048); file_put_contents('account_key.pem',$key); ``` > Equivalent to: `openssl genrsa 2048 -out account_key.pem` #### Generate EC Private Key ```php $key=$ac->generateECKey('P-384'); file_put_contents('account_key.pem',$key); ``` > Equivalent to: `openssl ecparam -name secp384r1 -genkey -noout -out account_key.pem` #### Register Account Key with Let's Encrypt ```php $ac->loadAccountKey('file://'.'account_key.pem'); $ret=$ac->register(true,'info@example.com'); print_r($ret); ``` > **WARNING: By passing **TRUE** as first parameter of the register function you agree to the terms of service of Let's Encrypt. See [Let’s Encrypt Subscriber Agreement](https://letsencrypt.org/repository/) for more information.** #### Get Account Information ```php $ac->loadAccountKey('file://'.'account_key.pem'); $ret=$ac->getAccount(); print_r($ret); ``` #### Account Key Roll-over ```php $ac->loadAccountKey('file://'.'account_key.pem'); $ret=$ac->keyChange('file://'.'new_account_key.pem'); print_r($ret); ``` #### Deactivate Account ```php $ac->loadAccountKey('file://'.'account_key.pem'); $ret=$ac->deactivateAccount(); print_r($ret); ``` #### Revoke Certificate ```php $ac->loadAccountKey('file://'.'account_key.pem'); $ac->revoke('file://'.'fullchain.pem'); ``` #### Get Remaining Days ```php $days=$ac->getRemainingDays('file://'.'fullchain.pem'); // certificate or certificate-chain if ($days>30) { // renew 30 days before expiry die('Certificate still good, exiting..'); } // get new certificate here.. ``` > This allows you to run your renewal script without the need to time it exactly, just run it often enough. (cronjob) #### Get Certificate using `http-01` challenge ```php $ac->loadAccountKey('file://'.'account_key.pem'); $domain_config=array( 'test1.example.com'=>array('challenge'=>'http-01','docroot'=>'/var/www/vhosts/test1.example.com'), 'test2.example.com'=>array('challenge'=>'http-01','docroot'=>'/var/www/vhosts/test2.example.com') ); $handler=function($opts){ $fn=$opts['config']['docroot'].$opts['key']; @mkdir(dirname($fn),0777,true); file_put_contents($fn,$opts['value']); return function($opts){ unlink($opts['config']['docroot'].$opts['key']); }; }; $fullchain=$ac->getCertificateChain('file://'.'cert_private_key.pem',$domain_config,$handler); file_put_contents('fullchain.pem',$fullchain); ``` #### Get Certificate using all (`http-01`,`dns-01` and `tls-alpn-01`) challenge types together ```php $ac->loadAccountKey('file://'.'account_key.pem'); $domain_config=array( 'example.com'=>array('challenge'=>'http-01','docroot'=>'/var/www/vhosts/example.com'), '*.example.com'=>array('challenge'=>'dns-01'), 'test.example.org'=>array('challenge'=>'tls-alpn-01') ); $handler=function($opts) use ($ac){ switch($opts['config']['challenge']){ case 'http-01': // automatic example: challenge directory/file is created.. $fn=$opts['config']['docroot'].$opts['key']; @mkdir(dirname($fn),0777,true); file_put_contents($fn,$opts['value']); return function($opts) use ($fn){ // ..and removed after validation completed unlink($fn); }; break; case 'dns-01': // manual example: echo 'Create DNS-TXT-Record '.$opts['key'].' with value '.$opts['value']."\n"; readline('Ready?'); return function($opts){ echo 'Remove DNS-TXT-Record '.$opts['key'].' with value '.$opts['value']."\n"; }; break; case 'tls-alpn-01': $cert=$ac->generateALPNCertificate('file://'.'some_private_key.pem',$opts['domain'],$opts['value']); // Use $cert and some_private_key.pem(<- does not have to be a specific key, // just make sure you generated one) to serve the certificate for $opts['domain'] // This example uses an included ALPN Responder - a standalone https-server // written in a few lines of node.js - which is able to complete this challenge. // store the generated verification certificate to be used by the ALPN Responder. file_put_contents('alpn_cert.pem',$cert); // To keep this example simple, the included Example ALPN Responder listens on port 443, // so - for the sake of this example - you have to stop the webserver here, like: shell_exec('/etc/init.d/apache2 stop'); // Start ALPN Responder (requires node.js) $resource=proc_open( 'node alpn_responder.js some_private_key.pem alpn_cert.pem', array( 0=>array('pipe','r'), 1=>array('pipe','w') ), $pipes ); // wait until alpn responder is listening fgets($pipes[1]); return function($opts) use ($resource,$pipes){ // Stop ALPN Responder fclose($pipes[0]); fclose($pipes[1]); proc_close($resource); shell_exec('/etc/init.d/apache2 start'); }; break; } }; // Example for using a pre-generated CSR as input to getCertificateChain instead of a private key: // $csr=$ac->generateCSR('file://'.'cert_private_key.pem',array_keys($domain_config)); // $fullchain=$ac->getCertificateChain($csr,$domain_config,$handler); $fullchain=$ac->getCertificateChain('file://'.'cert_private_key.pem',$domain_config,$handler); file_put_contents('fullchain.pem',$fullchain); ``` ## Logging ACMECert logs its actions using `error_log`, which logs messages to stderr per default in PHP CLI so it is easy to log to a file instead: ```php error_reporting(E_ALL); ini_set('log_errors',1); ini_set('error_log',dirname(__FILE__).'/ACMECert.log'); ``` ## ACME_Exception If the ACME-Server responded with an error message an `ACME_Exception` is thrown. (ACME_Exception extends Exception) `ACME_Exception` has two additional functions: * `getType()` to get the ACME error code: ```php require 'ACMECert.php'; $ac=new ACMECert(); $ac->loadAccountKey('file://'.'account_key.pem'); try { echo $ac->getAccountID().PHP_EOL; }catch(ACME_Exception $e){ if ($e->getType()=='urn:ietf:params:acme:error:accountDoesNotExist'){ echo 'Account does not exist'.PHP_EOL; }else{ throw $e; // another error occured } } ``` * `getSubproblems()` to get an array of `ACME_Exception`s if there is more than one error returned from the ACME-Server: ```php try { $cert=$ac->getCertificateChain('file://'.'cert_private_key.pem',$domain_config,$handler); } catch (ACME_Exception $e){ $ac->log($e->getMessage()); // log original error foreach($e->getSubproblems() as $subproblem){ $ac->log($subproblem->getMessage()); // log sub errors } } ``` ## Function Reference ### ACMECert::__construct Creates a new ACMECert instance. ```php public ACMECert::__construct ( bool $live = TRUE ) ``` ###### Parameters > **`live`** > > When **FALSE**, the ACME v2 [staging environment](https://acme-staging-v02.api.letsencrypt.org/) is used otherwise the [live environment](https://acme-v02.api.letsencrypt.org/). ###### Return Values > Returns a new ACMECert instance. --- ### ACMECert::generateRSAKey Generate RSA private key (used as account key or private key for a certificate). ```php public string ACMECert::generateRSAKey ( int $bits = 2048 ) ``` ###### Parameters > **`bits`** > > RSA key size in bits. ###### Return Values > Returns the generated RSA private key as PEM encoded string. ###### Errors/Exceptions > Throws an `Exception` if the RSA key could not be generated. --- ### ACMECert::generateECKey Generate Elliptic Curve (EC) private key (used as account key or private key for a certificate). ```php public string ACMECert::generateEcKey ( string $curve_name = 'P-384' ) ``` ###### Parameters > **`curve_name`** > > Supported Curves by Let’s Encrypt: > * `P-256` (prime256v1) > * `P-384` (secp384r1) > * ~~`P-521` (secp521r1)~~ ###### Return Values > Returns the generated EC private key as PEM encoded string. ###### Errors/Exceptions > Throws an `Exception` if the EC key could not be generated. --- ### ACMECert::loadAccountKey Load account key. ```php public void ACMECert::loadAccountKey ( mixed $account_key_pem ) ``` ###### Parameters > **`account_key_pem`** > > can be one of the following: > * a string containing a PEM formatted private key. > * a string beginning with `file://` containing the filename to read a PEM formatted private key from. ###### Return Values > No value is returned. ###### Errors/Exceptions > Throws an `Exception` if the account key could not be loaded. --- ### ACMECert::register Associate the loaded account key with a Let's Encrypt account and optionally specify contacts. ```php public array ACMECert::register ( bool $termsOfServiceAgreed = FALSE [, mixed $contacts = array() ] ) ``` ###### Parameters > **`termsOfServiceAgreed`** > > **WARNING: By passing `TRUE`, you agree to the terms of service of Let's Encrypt. See [Let’s Encrypt Subscriber Agreement](https://letsencrypt.org/repository/) for more information.** > > Must be set to **TRUE** in order to successully register an account. > **`contacts`** > > can be one of the following: > 1. A string containing an e-mail address > 2. Array of e-mail adresses ###### Return Values > Returns an array containing the account information. ###### Errors/Exceptions > Throws an `ACME_Exception` if the server responded with an error message or an `Exception` if an other registration error occured. --- ### ACMECert::update Update account contacts. ```php public array ACMECert::update ( mixed $contacts = array() ) ``` ###### Parameters > **`contacts`** > > can be one of the following: > * A string containing an e-mail address > * Array of e-mail adresses ###### Return Values > Returns an array containing the account information. ###### Errors/Exceptions > Throws an `ACME_Exception` if the server responded with an error message or an `Exception` if an other error occured updating the account. --- ### ACMECert::getAccount Get Account Information. ```php public array ACMECert::getAccount() ``` ###### Return Values > Returns an array containing the account information. ###### Errors/Exceptions > Throws an `ACME_Exception` if the server responded with an error message or an `Exception` if an other error occured getting the account information. --- ### ACMECert::getAccountID Get Account ID. ```php public string ACMECert::getAccountID() ``` ###### Return Values > Returns the Account ID ###### Errors/Exceptions > Throws an `ACME_Exception` if the server responded with an error message or an `Exception` if an other error occured getting the account id. --- ### ACMECert::keyChange Account Key Roll-over (exchange the current account key with another one). > If the Account Key Roll-over succeeded, the new account key is automatically loaded via [`loadAccountKey`](#acmecertloadaccountkey) ```php public array ACMECert::keyChange ( mixed $new_account_key_pem ) ``` ###### Parameters > **`new_account_key_pem`** > > can be one of the following: > * a string containing a PEM formatted private key. > * a string beginning with `file://` containing the filename to read a PEM formatted private key from. ###### Return Values > Returns an array containing the account information. ###### Errors/Exceptions > Throws an `ACME_Exception` if the server responded with an error message or an `Exception` if an other error occured during key change. --- ### ACMECert::deactivateAccount Deactivate account. ```php public array ACMECert::deactivateAccount() ``` ###### Return Values > Returns an array containing the account information. ###### Errors/Exceptions > Throws an `ACME_Exception` if the server responded with an error message or an `Exception` if an other error occured during account deactivation. --- ### ACMECert::getCertificateChain Get certificate-chain (certificate + the intermediate certificate). *This is what Apache >= 2.4.8 needs for [`SSLCertificateFile`](https://httpd.apache.org/docs/current/mod/mod_ssl.html#sslcertificatefile), and what Nginx needs for [`ssl_certificate`](http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate).* ```php public string ACMECert::getCertificateChain ( mixed $pem, array $domain_config, callable $callback ) ``` ###### Parameters > **`pem`** > > A Private Key used for the certificate (the needed CSR is generated automatically using the given key in this case) or an already existing CSR in one of the following formats: > > * a string containing a PEM formatted private key. > * a string beginning with `file://` containing the filename to read a PEM encoded private key from. > or > * a string beginning with `file://` containing the filename to read a PEM encoded CSR from. > * a string containing the content of a CSR, PEM encoded, may start with `-----BEGIN CERTIFICATE REQUEST-----` > **`domain_config`** > > An Array defining the domains and the corresponding challenge types to get a certificate for (up to 100 domains per certificate). > > The first one is used as `Common Name` for the certificate. > > Here is an example structure: > ```php > $domain_config=array( > '*.example.com'=>array('challenge'=>'dns-01'), > 'test.example.org'=>array('challenge'=>'tls-alpn-01') > 'test.example.net'=>array('challenge'=>'http-01','docroot'=>'/var/www/vhosts/test1.example.com'), > ); > ``` > > Hint: Wildcard certificates (`*.example.com`) are only supported with the `dns-01` challenge type. > > `challenge` is mandatory and has to be one of `http-01`, `dns-01` or `tls-alpn-01`. > All other keys are optional and up to you to be used and are later available in the callback function as `$opts['config']` > (see the [http-01 example](#get-certificate-using-http-01-challenge) where `docroot` is used this way) > **`callback`** > > Callback function which gets invoked every time a challenge needs to be fulfilled. > ```php > callable callback ( array $opts ) > ``` > > Inside a callback function you can return another callback function, which gets invoked after the verification completed and the challenge tokens can be removed again. > > > Hint: To get access to variables of the parent scope inside the callback function use the [`use`](http://php.net/manual/en/functions.anonymous.php) languange construct: > > ```php > > $handler=function($opts) use ($variable_from_parent_scope){}; > > ^^^ > > ``` > > ###### Parameters > **`opts`** > >> **`$opts['domain']`** >> >> Domain name to be validated. >> >> **`$opts['config']`** >> >> Corresponding element of the `domain_config` array. >> >> >> **`$opts['key']`** and **`$opts['value']`** >> >> Contain the following, depending on the chosen challenge type: >> >> Challenge Type | `$opts['key']` | `$opts['value']` >> --- | --- | --- >> http-01 | path + filename | file contents >> dns-01 | TXT Resource Record Name | TXT Resource Record Value >> tls-alpn-01 | unused | token used in the acmeIdentifier extension of the verification certificate; use [generateALPNCertificate](#acmecertgeneratealpncertificate) to generate the verification certificate from that token. (see the [tls-alpn-01 example](#get-certificate-using-all-http-01dns-01-and-tls-alpn-01-challenge-types-together)) ###### Return Values > Returns a PEM encoded certificate chain. ###### Errors/Exceptions > Throws an `ACME_Exception` if the server responded with an error message or an `Exception` if an other error occured obtaining the certificate. --- ### ACMECert::revoke Revoke certificate. ```php public void ACMECert::revoke ( mixed $pem ) ``` ###### Parameters > **`pem`** > > can be one of the following: > * a string beginning with `file://` containing the filename to read a PEM encoded certificate or certificate-chain from. > * a string containing the content of a certificate or certificate-chain, PEM encoded, may start with `-----BEGIN CERTIFICATE-----` ###### Return Values > No value is returned. > > If the function completes without Exception, the certificate was successully revoked. ###### Errors/Exceptions > Throws an `ACME_Exception` if the server responded with an error message or an `Exception` if an other error occured revoking the certificate. --- ### ACMECert::generateCSR Generate CSR for a set of domains. ```php public string ACMECert::generateCSR ( mixed $private_key, array $domains ) ``` ###### Parameters > **`private_key`** > > can be one of the following: > * a string containing a PEM formatted private key. > * a string beginning with `file://` containing the filename to read a PEM formatted private key from. > **`domains`** > > Array of domains ###### Return Values > Returns the generated CSR as string. ###### Errors/Exceptions > Throws an `Exception` if the CSR could not be generated. --- ### ACMECert::generateALPNCertificate Generate a self signed verification certificate containing the acmeIdentifier extension used in **`tls-alpn-01`** challenge. ```php public string ACMECert::generateALPNCertificate ( mixed $private_key, string $domain, string $token ) ``` ###### Parameters > **`private_key`** > > private key used for the certificate. > > can be one of the following: > * a string containing a PEM formatted private key. > * a string beginning with `file://` containing the filename to read a PEM formatted private key from. > **`domain`** > > domain name to be validated. > **`token`** > > verification token. ###### Return Values > Returns a PEM encoded verification certificate. ###### Errors/Exceptions > Throws an `Exception` if the certificate could not be generated. --- ### ACMECert::parseCertificate Get information about a certificate. ```php public array ACMECert::parseCertificate ( mixed $pem ) ``` ###### Parameters > **`pem`** > > can be one of the following: > * a string beginning with `file://` containing the filename to read a PEM encoded certificate or certificate-chain from. > * a string containing the content of a certificate or certificate-chain, PEM encoded, may start with `-----BEGIN CERTIFICATE-----` ###### Return Values > Returns an array containing information about the certificate. ###### Errors/Exceptions > Throws an `Exception` if the certificate could not be parsed. --- ### ACMECert::getRemainingDays Get the number of days the certificate is still valid. ```php public float ACMECert::getRemainingDays ( mixed $pem ) ``` ###### Parameters > **`pem`** > > can be one of the following: > * a string beginning with `file://` containing the filename to read a PEM encoded certificate or certificate-chain from. > * a string containing the content of a certificate or certificate-chain, PEM encoded, may start with `-----BEGIN CERTIFICATE-----` ###### Return Values > Returns how many days the certificate is still valid. ###### Errors/Exceptions > Throws an `Exception` if the certificate could not be parsed. --- > MIT License > > Copyright (c) 2018 Stefan Körfgen > > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal > in the Software without restriction, including without limitation the rights > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell > copies of the Software, and to permit persons to whom the Software is > furnished to do so, subject to the following conditions: > > The above copyright notice and this permission notice shall be included in all > copies or substantial portions of the Software. > > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE > SOFTWARE.