Skip to content

Commit

Permalink
added support for alternate chains (issue #34)
Browse files Browse the repository at this point in the history
  • Loading branch information
skoerfgen committed Oct 11, 2021
1 parent cc2859b commit 84dbdcc
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 6 deletions.
52 changes: 49 additions & 3 deletions ACMECert.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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().')');
Expand Down Expand Up @@ -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']);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
},
Expand Down
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 84dbdcc

Please sign in to comment.