Skip to content

Commit

Permalink
added ARI support
Browse files Browse the repository at this point in the history
  • Loading branch information
skoerfgen authored Jul 17, 2024
2 parents 8098735 + 95f23a1 commit e8e5d0d
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 8 deletions.
96 changes: 92 additions & 4 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/) and other [ACME v2 - RFC 8555](https://tools.ietf.org/html/rfc8555) compatible Certificate Authorities.
Version: 3.3.1
Version: 3.4.0

## Description

Expand All @@ -13,7 +13,8 @@ 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)/[External Account Binding (EAB)](#acmecertregistereab)/[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
- [parse certificates](#acmecertparsecertificate) / get the [remaining days](#acmecertgetremainingdays) or [percentage](#acmecertgetremainingpercent) a certificate is still valid
- get/use [ACME Renewal Information](#acmecertgetari) (ARI)
- and more..
> see [Function Reference](#function-reference) for a full list
Expand Down Expand Up @@ -44,7 +45,7 @@ Instead of returning `FALSE` on error, every function in ACMECert throws an [Exc
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] PHP 5.6 or higher (for EC keys PHP 7.1 or higher) (for ARI PHP 7.1.2 or higher)
- [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)

Expand Down Expand Up @@ -312,6 +313,30 @@ $ret=$ac->deactivateAccount();
print_r($ret);
```

#### Get/Use ACME Renewal Information
```php
$ret=$ac->getARI('file://'.'fullchain.pem',$ari_cert_id);
if ($ret['suggestedWindow']['start']-time()>0) {
die('Certificate still good, exiting..');
}

$settings=array(
'replaces'=>$ari_cert_id
);
$ac->getCertificateChain(..., ..., ..., $settings);
```

#### Get Remaining Percentage
```php
$percent=$ac->getRemainingPercent('file://'.'fullchain.pem'); // certificate or certificate-chain
if ($precent>33.333) { // certificate has still more than 1/3 (33.333%) of its lifetime left
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 Remaining Days
```php
$days=$ac->getRemainingDays('file://'.'fullchain.pem'); // certificate or certificate-chain
Expand All @@ -320,7 +345,6 @@ if ($days>30) { // renew 30 days before expiry
}
// 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)

## Logging

Expand Down Expand Up @@ -689,6 +713,15 @@ public string ACMECert::getCertificateChain ( mixed $pem, array $domain_config,
>> ```php
>> array( 'notAfter' => '1970-01-01T01:22:17+01:00' )
>> ```
>>
>> **`replaces`** (string)
>>
>> The ARI CertID uniquely identifying a previously-issued certificate which this order is intended to replace.
>>
>> Use: [getARI](#acmecertgetari) to get the ARI CertID for a certificate.
>>
>> Example: [Get/Use ACME Renewal Information](#getuse-acme-renewal-information)
###### Return Values
> Returns a PEM encoded certificate chain.
Expand Down Expand Up @@ -809,6 +842,26 @@ public array ACMECert::parseCertificate ( mixed $pem )
---

### ACMECert::getRemainingPercent

Get the percentage the certificate is still valid.

```php
public float ACMECert::getRemainingPercent( 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
> A float value containing the percentage the certificate is still valid.
###### Errors/Exceptions
> Throws an `Exception` if the certificate could not be parsed.
---

### ACMECert::getRemainingDays

Get the number of days the certificate is still valid.
Expand Down Expand Up @@ -913,6 +966,41 @@ public void ACMECert::setLogger( bool|callable $value = TRUE )
###### Errors/Exceptions
> Throws an `Exception` if the value provided is not boolean or a callable function.
---
### ACMECert::getARI
Get ACME Renewal Information (ARI) for a given certificate.
```php
public array ACMECert::getARI( mixed $pem, string &$ari_cert_id = null )
```
###### 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-----`
>
> **`ari_cert_id`**
>
> If this parameter is present, it will be set to the ARI CertID of the given certificate.
>
> See the documentation of [getCertificateChain](#acmecertgetcertificatechain) where the ARI CertID can be used to replace an existing certificate.
>
> Example: [Get/Use ACME Renewal Information](#getuse-acme-renewal-information)
###### Return Values
> Returns an Array with the following keys:
>
>> `suggestedWindow` (array)
>>
>> An Array with two keys, `start` and `end`, whose values are unix timestamps, which bound the window of time in which the CA recommends renewing the certificate.
>>
>> `explanationURL` (string, optional)
>>
>> A URL pointing to a page which may explain why the suggested renewal window is what it is. For example, it may be a page explaining the CA's dynamic load-balancing strategy, or a page documenting which certificates are affected by a mass revocation event.
###### Errors/Exceptions
> Throws an `ACME_Exception` if the server responded with an error message or an `Exception` if an other error occured getting the ACME Renewal Information.
---

> MIT License
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "skoerfgen/acmecert",
"version": "3.3.1",
"version": "3.4.0",
"description": "PHP client library for Let's Encrypt and other ACME v2 - RFC 8555 compatible Certificate Authorities",
"license": "MIT",
"authors": [
Expand Down
64 changes: 63 additions & 1 deletion src/ACMECert.php
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,13 @@ public function getRemainingDays($cert_pem){
return ($ret['validTo_time_t']-time())/86400;
}

public function getRemainingPercent($cert_pem){
$ret=$this->parseCertificate($cert_pem);
$total=$ret['validTo_time_t']-$ret['validFrom_time_t'];
$used=time()-$ret['validFrom_time_t'];
return (1-max(0,min(1,$used/$total)))*100;
}

public function generateALPNCertificate($domain_key_pem,$domain,$token){
$domains=array($domain);
$csr=$this->generateCSR($domain_key_pem,$domains);
Expand All @@ -438,14 +445,63 @@ public function generateALPNCertificate($domain_key_pem,$domain,$token){
return $out;
}

public function getARI($pem,&$ari_cert_id=null){
$ari_cert_id=null;
$id=$this->getARICertID($pem);

if (!$this->resources) $this->readDirectory();
if (!isset($this->resources['renewalInfo'])) throw new Exception('ARI not supported');

$ret=$this->http_request($this->resources['renewalInfo'].'/'.$id);

if (!is_array($ret['body']['suggestedWindow'])) throw new Exception('ARI suggestedWindow not present');

$sw=&$ret['body']['suggestedWindow'];

if (!isset($sw['start'])) throw new Exception('ARI suggestedWindow start not present');
if (!isset($sw['end'])) throw new Exception('ARI suggestedWindow end not present');

$sw=array_map(array($this,'parseDate'),$sw);

$ari_cert_id=$id;
return $ret['body'];
}

private function getARICertID($pem){
if (version_compare(PHP_VERSION,'7.1.2','<')){
throw new Exception('PHP Version >= 7.1.2 required for ARI'); // serialNumberHex - https://github.com/php/php-src/pull/1755
}
$ret=$this->parseCertificate($pem);

if (!isset($ret['extensions']['authorityKeyIdentifier'])) {
throw new Exception('authorityKeyIdentifier missing');
}
$aki=hex2bin(str_replace(':','',substr(trim($ret['extensions']['authorityKeyIdentifier']),6)));
if (!$aki) throw new Exception('Failed to parse authorityKeyIdentifier');

if (!isset($ret['serialNumberHex'])) {
throw new Exception('serialNumberHex missing');
}
$ser=hex2bin(trim($ret['serialNumberHex']));
if (!$ser) throw new Exception('Failed to parse serialNumberHex');

return $this->base64url($aki).'.'.$this->base64url($ser);
}

private function parseDate($str){
$ret=strtotime(preg_replace('/(\.\d\d)\d+/','$1',$str));
if ($ret===false) throw new Exception('Failed to parse date: '.$str);
return $ret;
}

private function parseSettings($opts){
// authz_reuse: backwards compatibility to ACMECert v3.1.2 or older
if (!is_array($opts)) $opts=array('authz_reuse'=>(bool)$opts);
if (!isset($opts['authz_reuse'])) $opts['authz_reuse']=true;

$diff=array_diff_key(
$opts,
array_flip(array('authz_reuse','notAfter','notBefore'))
array_flip(array('authz_reuse','notAfter','notBefore','replaces'))
);

if (!empty($diff)){
Expand Down Expand Up @@ -474,6 +530,12 @@ function($domain){
);
$this->setRFC3339Date($order,'notAfter',$opts);
$this->setRFC3339Date($order,'notBefore',$opts);

if (isset($opts['replaces'])) { // ARI
$order['replaces']=$opts['replaces'];
$this->log('Replacing Certificate: '.$opts['replaces']);
}

return $order;
}

Expand Down
4 changes: 2 additions & 2 deletions src/ACMEv2.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ private function json_decode($str){
return $ret;
}

private function http_request($url,$data=null){
protected function http_request($url,$data=null){
if ($this->ch===null) {
if (extension_loaded('curl') && $this->ch=curl_init()) {
$this->log('Using cURL');
Expand All @@ -309,7 +309,7 @@ private function http_request($url,$data=null){
}

$method=$data===false?'HEAD':($data===null?'GET':'POST');
$user_agent='ACMECert v3.3.1 (+https://github.com/skoerfgen/ACMECert)';
$user_agent='ACMECert v3.4.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

0 comments on commit e8e5d0d

Please sign in to comment.