自行车/ACME证书

Let's Encrypt(ACME v2 - RFC 8555)的PHP客户端库

2.8.1 2021-05-19 07:17 UTC

This package is not auto-updated.

Last update: 2024-10-03 21:31:15 UTC


README

Let's Encrypt(https://letsencrypt.openssl.ac.cn/)(ACME v2 - RFC 8555)的PHP客户端库
版本:2.8

描述

ACMECert旨在通过几行PHP代码帮助您设置自动化的SSL/TLS证书/续订流程。

它是独立的,包含一系列函数,允许您

请参阅函数参考以获取完整列表

它将获取证书的复杂性抽象到单个函数getCertificateChain中,在该函数中,您指定您希望获取证书的一组域以及要使用的挑战类型(支持所有挑战类型)。此函数将用户定义的回调函数作为第三个参数,每次需要满足挑战时都会调用该回调函数。您需要自行设置/删除挑战令牌

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

请参阅getCertificateChain的描述以了解有关回调函数的详细信息。

以下提供了获取证书的示例。

ACMECert中的每个函数在失败时,除了返回FALSE外,还会抛出一个异常,如果ACME服务器响应了错误消息,则抛出ACME_Exception

要求

用法示例

需要ACMECert

require 'ACMECert.php';

选择生产环境或测试环境

生产

$ac=new ACMECert();

测试

$ac=new ACMECert(false);

生成RSA私钥

$key=$ac->generateRSAKey(2048);
file_put_contents('account_key.pem',$key);

等同于:openssl genrsa -out account_key.pem 2048

生成EC私钥

$key=$ac->generateECKey('P-384');
file_put_contents('account_key.pem',$key);

等同于: openssl ecparam -name secp384r1 -genkey -noout -out account_key.pem

在Let's Encrypt中注册账户密钥

$ac->loadAccountKey('file://'.'account_key.pem');
$ret=$ac->register(true,'info@example.com');
print_r($ret);

警告:通过将第一个参数传递为TRUE,您同意Let's Encrypt的服务条款。有关更多信息,请参阅Let’s Encrypt订阅者协议

获取账户信息

$ac->loadAccountKey('file://'.'account_key.pem');
$ret=$ac->getAccount();
print_r($ret);

账户密钥滚动更新

$ac->loadAccountKey('file://'.'account_key.pem');
$ret=$ac->keyChange('file://'.'new_account_key.pem');
print_r($ret);

停用账户

$ac->loadAccountKey('file://'.'account_key.pem');
$ret=$ac->deactivateAccount();
print_r($ret);

撤销证书

$ac->loadAccountKey('file://'.'account_key.pem');
$ac->revoke('file://'.'fullchain.pem');

获取剩余天数

$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..

这允许您运行续订脚本,而无需精确计时,只需经常运行即可。(cronjob)

使用http-01挑战获取证书

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

使用所有(http-01dns-01tls-alpn-01)挑战类型一起获取证书

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

日志记录

ACMECert使用error_log记录其操作,默认情况下在PHP CLI中记录到stderr,因此易于记录到文件。

error_reporting(E_ALL);
ini_set('log_errors',1);
ini_set('error_log',dirname(__FILE__).'/ACMECert.log');

ACME_Exception

如果ACME-Server返回错误消息,则会抛出ACME_Exception。(ACME_Exception扩展Exception)

ACME_Exception有两个附加功能

  • getType() 获取ACME错误代码
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() 获取一个包含ACME_Exception的数组,如果ACME-Server返回多个错误
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
	}
}

函数参考

ACMECert::__construct

创建一个新的ACMECert实例。

public ACMECert::__construct ( bool $live = TRUE )
参数

live

FALSE时,使用ACME v2 预演环境,否则使用生产环境

返回值

返回一个新的ACMECert实例。

ACMECert::generateRSAKey

生成RSA私钥(用作账户密钥或证书的私钥)。

public string ACMECert::generateRSAKey ( int $bits = 2048 )
参数

bits

RSA密钥大小,以位为单位。

返回值

以PEM编码字符串的形式返回生成的RSA私钥。

错误/异常

如果无法生成RSA密钥,则抛出Exception

ACMECert::generateECKey

生成椭圆曲线(EC)私钥(用作账户密钥或证书的私钥)。

public string ACMECert::generateECKey ( string $curve_name = 'P-384' )
参数

curve_name

Let’s Encrypt支持曲线

  • P-256(prime256v1)
  • P-384(secp384r1)
  • P-521(secp521r1)
返回值

以PEM编码字符串的形式返回生成的EC私钥。

错误/异常

如果无法生成EC密钥,则抛出Exception

ACMECert::loadAccountKey

加载账户密钥。

public void ACMECert::loadAccountKey ( mixed $account_key_pem )
参数

account_key_pem

可以是以下之一

  • 包含PEM格式私钥的字符串。
  • file://开始的字符串,包含从其中读取PEM格式私钥的文件名。
返回值

没有返回值。

错误/异常

如果无法加载账户密钥,则抛出Exception

ACMECert::register

将加载的账户密钥与Let's Encrypt账户关联,并可选地指定联系人。

public array ACMECert::register ( bool $termsOfServiceAgreed = FALSE [, mixed $contacts = array() ] )
参数

termsOfServiceAgreed

警告:通过传递TRUE,您同意Let's Encrypt的服务条款。有关更多信息,请参阅Let’s Encrypt订阅者协议

为了成功注册账户,必须设置为TRUE

contacts

可以是以下之一

  1. 包含电子邮件地址的字符串
  2. 电子邮件地址数组
返回值

返回一个包含账户信息的数组。

错误/异常

如果服务器返回错误消息,则抛出ACME_Exception;如果发生其他注册错误,则抛出Exception

ACMECert::update

更新账户联系人。

public array ACMECert::update ( mixed $contacts = array() )
参数

contacts

可以是以下之一

  • 包含电子邮件地址的字符串
  • 电子邮件地址数组
返回值

返回一个包含账户信息的数组。

错误/异常

如果服务器返回错误消息或更新账户时发生其他错误,则抛出ACME_Exception

ACMECert::getAccount

获取账户信息。

public array ACMECert::getAccount()
返回值

返回一个包含账户信息的数组。

错误/异常

如果服务器返回错误消息或获取账户信息时发生其他错误,则抛出ACME_Exception

ACMECert::getAccountID

获取账户ID。

public string ACMECert::getAccountID()
返回值

返回账户ID

错误/异常

如果服务器返回了错误消息,将抛出 ACME_Exception;如果在获取账户 ID 时发生其他错误,将抛出 Exception

ACMECert::keyChange

账户密钥滚动(用另一个密钥交换当前账户密钥)。

如果账户密钥滚动成功,则新账户密钥将通过 loadAccountKey 自动加载。

public array ACMECert::keyChange ( mixed $new_account_key_pem )
参数

new_account_key_pem

可以是以下之一

  • 包含PEM格式私钥的字符串。
  • file://开始的字符串,包含从其中读取PEM格式私钥的文件名。
返回值

返回一个包含账户信息的数组。

错误/异常

如果服务器返回了错误消息或在密钥更改期间发生其他错误,将抛出 ACME_Exception

ACMECert::deactivateAccount

停用账户。

public array ACMECert::deactivateAccount()
返回值

返回一个包含账户信息的数组。

错误/异常

如果在账户停用期间服务器返回了错误消息或发生其他错误,将抛出 ACME_Exception

ACMECert::getCertificateChain

获取证书链(证书和中间证书)。

这是 Apache >= 2.4.8 需要的 SSLCertificateFile,也是 Nginx 需要的 ssl_certificate

public string ACMECert::getCertificateChain ( mixed $pem, array $domain_config, callable $callback, bool $authz_reuse = TRUE )
参数

pem

用于证书的私钥(在这种情况下,使用给定的密钥自动生成所需的 CSR)或以下格式之一的已存在 CSR:

  • 包含PEM格式私钥的字符串。
  • file:// 开头的字符串,包含读取 PEM 编码私钥的文件名。
  • file:// 开头的字符串,包含读取 PEM 编码 CSR 的文件名。
  • 包含 CSR 内容的字符串,PEM 编码,可能以 -----BEGIN CERTIFICATE REQUEST----- 开头。

domain_config

一个数组,定义了要获取证书的域及其相应的挑战类型(每张证书最多 100 个域)。

第一个用作证书的 Common Name

以下是一个示例结构

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

提示:通配符证书(*.example.com)仅支持 dns-01 挑战类型。

challenge 是必需的,必须是 http-01dns-01tls-alpn-01 之一。所有其他键都是可选的,由您使用,并且在回调函数中作为 $opts['config'] 可用(参见 http-01 示例,其中以这种方式使用 docroot

callback

每次需要满足挑战时都会调用的回调函数。

callable callback ( array $opts )

在回调函数内部,您可以返回另一个回调函数,该函数在验证完成并且可以再次删除挑战令牌后调用。

提示:要在回调函数内部访问父作用域的变量,请使用 use 语言结构

$handler=function($opts) use ($variable_from_parent_scope){};
                         ^^^
参数

opts

$opts['domain']

要验证的域名。

$opts['config']

domain_config 数组对应的元素。

$opts['key']$opts['value']

根据所选的挑战类型包含以下内容:

authz_reuse(默认:TRUE

如果为 FALSE,则回调函数对于每个域都会被调用,不会因为可能已经复用的有效授权(authz)而跳过。这是通过在获取新的授权之前停用已有效的授权来实现的。

提示:在正常情况下,这仅在测试回调函数时需要,在生产环境中不需要!

返回值

返回 PEM 编码的证书链。

错误/异常

如果服务器返回了错误消息或在获取证书期间发生其他错误,将抛出 ACME_Exception

ACMECert::revoke

吊销证书。

public void ACMECert::revoke ( mixed $pem )
参数

pem

可以是以下之一

  • 一个以 file:// 开头的字符串,包含要读取的PEM编码证书或证书链的文件名。
  • 包含证书或证书链内容的字符串,PEM编码,可能以 -----BEGIN CERTIFICATE----- 开头
返回值

没有返回值。

如果函数在无异常的情况下完成,则证书已成功吊销。

错误/异常

如果服务器响应了错误消息,则抛出 ACME_Exception,如果在吊销证书时发生其他错误,则抛出 Exception

ACMECert::generateCSR

为一系列域名生成CSR。

public string ACMECert::generateCSR ( mixed $private_key, array $domains )
参数

private_key

可以是以下之一

  • 包含PEM格式私钥的字符串。
  • file://开始的字符串,包含从其中读取PEM格式私钥的文件名。

domains

域名数组

返回值

以字符串形式返回生成的CSR。

错误/异常

如果CSR无法生成,则抛出 Exception

ACMECert::generateALPNCertificate

生成包含用于 tls-alpn-01 挑战的acmeIdentifier扩展的自签名验证证书。

public string ACMECert::generateALPNCertificate ( mixed $private_key, string $domain, string $token )
参数

private_key

用于证书的私钥。

可以是以下之一

  • 包含PEM格式私钥的字符串。
  • file://开始的字符串,包含从其中读取PEM格式私钥的文件名。

domain

要验证的域名。

token

验证令牌。

返回值

返回PEM编码的验证证书。

错误/异常

如果证书无法生成,则抛出 Exception

ACMECert::parseCertificate

获取证书信息。

public array ACMECert::parseCertificate ( mixed $pem )
参数

pem

可以是以下之一

  • 一个以 file:// 开头的字符串,包含要读取的PEM编码证书或证书链的文件名。
  • 包含证书或证书链内容的字符串,PEM编码,可能以 -----BEGIN CERTIFICATE----- 开头
返回值

返回包含证书信息的数组。

错误/异常

如果证书无法解析,则抛出 Exception

ACMECert::getRemainingDays

获取证书仍有效的天数。

public float ACMECert::getRemainingDays ( mixed $pem )
参数

pem

可以是以下之一

  • 一个以 file:// 开头的字符串,包含要读取的PEM编码证书或证书链的文件名。
  • 包含证书或证书链内容的字符串,PEM编码,可能以 -----BEGIN CERTIFICATE----- 开头
返回值

返回证书仍有效的天数。

错误/异常

如果证书无法解析,则抛出 Exception

MIT许可协议

版权所有 (c) 2018 Stefan Körfgen

特此授予任何获得本软件及其相关文档副本(“软件”)的人权,可以在不受限制的情况下使用软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或出售软件的副本,并允许向软件提供者提供软件的人这样做,前提是符合以下条件

上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。

软件按“原样”提供,不提供任何明示或暗示的保证,包括但不限于适销性、针对特定目的的适用性和非侵权性保证。在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论此类责任源于合同、侵权或其他方式,无论是否与软件或软件的使用或其他方式有关。