diff --git a/ACMECert.php b/ACMECert.php index f2e92d3..75a6e3d 100644 --- a/ACMECert.php +++ b/ACMECert.php @@ -27,6 +27,7 @@ // https://github.com/skoerfgen/ACMECert class ACMECert extends ACMEv2 { // ACMECert - PHP client library for Let's Encrypt (ACME v2) + private $alternate_chains=array(); public function register($termsOfServiceAgreed=false,$contacts=array()){ $this->log('Registering account'); @@ -261,6 +262,19 @@ function($domain){ throw new Exception('Order failed'); } + public function getCertificateChains($pem,$domain_config,$callback,$authz_reuse=true){ + $default_chain=$this->getCertificateChain($pem,$domain_config,$callback,$authz_reuse); + + $out=array(); + $out[$this->getIssuerCN($default_chain)]=$default_chain; + + foreach($this->alternate_chains as $link){ + $chain=$this->request_certificate(array('certificate'=>$link),false); + $out[$this->getIssuerCN($chain)]=$chain; + } + return $out; + } + public function generateCSR($domain_key_pem,$domains){ if (false===($domain_key=openssl_pkey_get_private($domain_key_pem))){ throw new Exception('Could not load domain key: '.$domain_key_pem.' ('.$this->get_openssl_error().')'); @@ -395,9 +409,18 @@ private function poll($initial,$type,&$ret){ throw new Exception('Aborted after '.$max_tries.' tries'); } - private function request_certificate($ret){ + private function request_certificate($ret,$set_alternate_chains=true){ $this->log('Requesting certificate-chain'); $ret=$this->request($ret['certificate'],''); + + if ($set_alternate_chains) { + if (isset($ret['headers']['link']['alternate'])){ + $this->alternate_chains=$ret['headers']['link']['alternate']; + }else{ + $this->alternate_chains=array(); + } + } + if ($ret['headers']['content-type']!=='application/pem-certificate-chain'){ throw new Exception('Unexpected content-type: '.$ret['headers']['content-type']); } @@ -447,6 +470,22 @@ private function make_contacts_array($contacts){ return 'mailto:'.$contact; },$contacts); } + + private function getIssuerCN($chain){ + $tmp=$this->splitChain($chain); + $ret=$this->parseCertificate(end($tmp)); + return $ret['issuer']['CN']; + } + + private function splitChain($chain){ + $delim='-----BEGIN CERTIFICATE-----'; + $parts=explode($delim,$chain); + $parts=array_map(function($item)use($delim){ + return $delim.$item; + },array_filter($parts)); + return $parts; + } + } class ACMEv2 { // Communication with Let's Encrypt via ACME v2 protocol @@ -662,7 +701,7 @@ private function http_request($url,$data=null){ } } $method=$data===false?'HEAD':($data===null?'GET':'POST'); - $user_agent='ACMECert v2.8.1 (+https://github.com/skoerfgen/ACMECert)'; + $user_agent='ACMECert v2.9.0 (+https://github.com/skoerfgen/ACMECert)'; $header=($data===null||$data===false)?array():array('Content-Type: application/jose+json'); if ($this->ch) { $headers=array(); @@ -712,7 +751,14 @@ function($carry,$item)use(&$code){ $carry=array(); }else{ list($k,$v)=$parts; - $carry[strtolower(trim($k))]=trim($v); + $k=strtolower(trim($k)); + if ($k==='link'){ + if (preg_match('/<(.*)>\s*;\s*rel=\"(.*)\"/',$v,$matches)){ + $carry[$k][$matches[2]][]=trim($matches[1]); + } + }else{ + $carry[$k]=trim($v); + } } return $carry; }, diff --git a/README.md b/README.md index 113c912..365f656 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ACMECert PHP client library for [Let's Encrypt](https://letsencrypt.org/) ([ACME v2 - RFC 8555](https://tools.ietf.org/html/rfc8555)) -Version: 2.8.1 +Version: 2.9.0 ## Description @@ -18,7 +18,7 @@ It is self contained and contains a set of functions allowing you to: 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), +finalize order, request certificate) into a single function [getCertificateChain](#acmecertgetcertificatechain) (or [getCertificateChains](#acmecertgetcertificatechains) to also get all alternate chains), 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: @@ -146,6 +146,15 @@ $fullchain=$ac->getCertificateChain('file://'.'cert_private_key.pem',$domain_con file_put_contents('fullchain.pem',$fullchain); ``` +#### Get alternate chains +```php +$ret=$ac->getCertificateChains('file://'.'cert_private_key.pem',$domain_config,$handler); +if (isset[$ret['ISRG Root X1']]){ // use alternate chain 'ISRG Root X1' + file_put_contents('fullchain.pem',$ret['ISRG Root X1']); +}else{ // use default chain if 'ISRG Root X1' is not present + file_put_contents('fullchain.pem',reset($ret)); +} +``` #### Get Certificate using all (`http-01`,`dns-01` and `tls-alpn-01`) challenge types together ```php @@ -451,7 +460,7 @@ public array ACMECert::deactivateAccount() ### ACMECert::getCertificateChain -Get certificate-chain (certificate + the intermediate certificate). +Get certificate-chain (certificate + the intermediate certificate(s)). *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 @@ -538,6 +547,23 @@ public string ACMECert::getCertificateChain ( mixed $pem, array $domain_config, --- +### ACMECert::getCertificateChains + +Get all (default and alternate) certificate-chains. +This function takes the same arguments as the [getCertificateChain](#acmecertgetcertificatechain) function above, but it returns an array of certificate chains instead of a single chain. + + +###### Return Values +> Returns an array of PEM encoded certificate chains. +> +> The keys of the returned array correspond to the issuer `Common Name` (CN) of the topmost (closest to the root certificate) intermediate certificate. +> +> The first element of the returned array is the default 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 chains. + +--- + ### ACMECert::revoke Revoke certificate.