zwartpet / php-certificate-toolbox
Let's Encrypt ACME v2 客户端
Requires
- php: ~7.0
- ext-openssl: *
- guzzlehttp/guzzle: ~6.0
- psr/log: ^1.0
Requires (Dev)
- phpunit/phpunit: ^6.5
- squizlabs/php_codesniffer: ^2.3 || ^3.0
Suggests
- psr/log-implementation: A PSR-3 compatible logger is recommended for troubleshooting
This package is auto-updated.
Last update: 2024-09-11 11:44:11 UTC
README
这是一个用于 ACME v2 的 Let's Encrypt 客户端库,允许使用 PHP 自动创建免费的 SSL/TLS 证书。这包括自 2018 年 2 月起由 Let's Encrypt 支持的通配符证书。
虽然它包括一个命令行工具,但这个库的真正目的是使其易于集成到需要颁发证书的现有 PHP 应用程序中。
有关 Let's Encrypt 和 ACME 的更多信息,请参阅 Let's Encrypt 文档。
起源和路线图
这是基于由 Youri van Weegberg 开发的客户端,但进行了以下改进
- 可由 composer 安装
- PSR-2 格式化
- PSR-3 日志记录兼容
- 单元测试(需要一些额外的重构以支持此功能)
- 支持替代存储后端
- 支持使用 DNS-over-HTTPS 验证 DNS 挑战
先决条件
所需的最小 PHP 版本是 7.0,但为了使用 ECDSA 实现,您需要 7.1
此客户端还依赖于 OpenSSL。
安装
通过 Composer
$ composer require zwartpet/php-certificate-toolbox
用法
这里显示了基本函数及其必要参数。每个类中还包括了扩展描述。
建议为脚本的执行时间设置一个较高的最大时间限制,以便给脚本一些执行上的宽容。有多种方法可以实现这一点。一种方法是在页面顶部添加以下内容
ini_set('max_execution_time', 120); // Maximum execution time in seconds.
初始化客户端
use Zwartpet\PHPCertificateToolbox\LEClient; // Initiating a basic LEClient with an array of string e-mail address(es). $client = new LEClient($email); // Iniitiate client with all properties, optionally create your own storage classes. $client = new LEClient( ['support@example.com'], LEClient::LE_PRODUCTION, new DiagnosticLogger(), new Client(), new FilesystemCertificateStorage('cert storage path'), new FilesystemAccountStorage('account storage path') );
如果没有找到账户,客户端将自动创建一个新账户。它将转发在初始化过程中提供的电子邮件地址,如上所示。
注意:由于 Let's Encrypt 清理其订单,证书颁发后应清理存储。应保留账户存储。
使用账户函数
$acct = $client->getAccount(); // Retrieves the LetsEncrypt Account instance created by the client. $acct->updateAccount($email); // Updates the account with new contact information. Supply an array of string e-mail address(es). $acct->changeAccountKeys(); // Generates a new RSA keypair for the account and updates the keys with LetsEncrypt. $acct->deactivateAccount(); // Deactivates the account with LetsEncrypt.
创建一个证书订单实例。如果找到已存储在本地的订单,则使用此订单。否则,将创建一个新的订单。如果提供的域名与订单不匹配,也将创建一个新的订单。构建 Let's Encrypt 订单实例
$order = $client->getOrCreateOrder($basename, $domains); // Get or create order. The basename is preferably the top domain name. This will be the directory in which the keys are stored. Supply an array of string domain names to create a certificate for. $order = $client->getOrCreateOrder($basename, $domains, $keyType); // Get or create order. keyType can be set to "ec" to get ECDSA certificate. "rsa" is default value. $order = $client->getOrCreateOrder($basename, $domains, $keyType, $notBefore); // Get or create order. Supply a notBefore date as a string similar to 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss). $order = $client->getOrCreateOrder($basename, $domains, $keyType, $notBefore, $notAfter); // Get or create order. Supply a notBefore and notAfter date as a string similar to 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss).
使用订单函数
$valid = $order->allAuthorizationsValid(); // Check whether all authorizations in this order instance are valid. $pending = $order->getPendingAuthorizations($type); // Get an array of pending authorizations. Performing authorizations is described further on. Type is LEOrder::CHALLENGE_TYPE_HTTP or LEOrder::CHALLENGE_TYPE_DNS. $verify = $order->verifyPendingOrderAuthorization($identifier, $type); // Verify a pending order. The identifier is a string domain name. Type is LEOrder::CHALLENGE_TYPE_HTTP or LEOrder::CHALLENGE_TYPE_DNS. $deactivate = $order->deactivateOrderAuthorization($identifier); // Deactivate an authorization. The identifier is a string domain name. $finalize = $order->finalizeOrder(); // Finalize the order and generate a Certificate Signing Request automatically. $finalize = $order->finalizeOrder($csr); // Finalize the order with a custom Certificate Signing Request string. $finalized = $order->isFinalized(); // Check whether the order is finalized. $cert = $order->getCertificate(); // Retrieves the certificate and stores it in the keys directory, under the specific order (basename). $revoke = $order->revokeCertificate(); // Revoke the certificate without a reason. $revoke = $order->revokeCertificate($reason); // Revoke the certificate with a reason integer as found in section 5.3.1 of RFC5280.
支持性函数
LEFunctions::RSAGenerateKeys($directory, $privateKeyFile, $publicKeyFile); // Generate a RSA keypair in the given directory. Variables privateKeyFile and publicKeyFile are optional and have default values private.pem and public.pem. LEFunctions::ECGenerateKeys($directory, $privateKeyFile, $publicKeyFile); // Generate a EC keypair in the given directory (PHP 7.1+ required). Variables privateKeyFile and publicKeyFile are optional and have default values private.pem and public.pem. LEFunctions::Base64UrlSafeEncode($input); // Encode the input string as a base64 URL safe string. LEFunctions::Base64UrlSafeDecode($input); // Decode a base64 URL safe encoded string. LEFunctions::log($data, $function); // Print the data. The function variable is optional and defaults to the calling function's name. LEFunctions::checkHTTPChallenge($domain, $token, $keyAuthorization); // Checks whether the HTTP challenge is valid. Performing authorizations is described further on. LEFunctions::checkDNSChallenge($domain, $DNSDigest); // Checks whether the DNS challenge is valid. Performing authorizations is described further on. LEFunctions::createhtaccess($directory); // Created a simple .htaccess file in the directory supplied, denying all visitors.
授权挑战
Let's Encrypt (ACME) 对您希望在证书中包含的域名执行授权,以验证您确实有权访问特定域名。因此,在创建订单时,为每个域名添加一个授权。如果某个域名在最近(在过去 30 天内)已被您的账户验证,例如在其他订单中,则不需要再次验证。此时,可以通过对文件的 HTTP 请求(http-01)或 DNS TXT 记录(dns-01)进行验证来验证域名。客户端通过调用 getPendingAuthorizations()
提供所选验证所需的数据。由于创建文件或 DNS 记录对于每个服务器都不同,因此客户端中没有实现这一点。在用户满足挑战要求后,需要调用 verifyPendingOrderAuthorization()
。此客户端将首先通过 checkHTTPChallenge()
或 checkDNSChallenge()
自行验证挑战,然后再通过 Let's Encrypt 启动验证。请注意,通配符域名只能通过 DNS 挑战进行验证。以下展示了两种挑战的示例。
HTTP 挑战
假设还有一个域名需要验证。
$pending = $order->getPendingAuthorizations(LEOrder::CHALLENGE_TYPE_HTTP);
这将返回一个数组
Array
(
[0] => Array
(
[type] => http-01
[identifier] => test.example.org
[filename] => A8Q1DAVcd_k_oKAC0D_y4ln2IWrRX51jmXnR9UMMtOb
[content] => A8Q1DAVcd_k_oKAC0D_y4ln2IWrRX51jmXnR9UMMtOb.C4kIiiwfcynb3i48AQVtZRtNrD51z4JiIrdQsgVqcL8
)
)
对于成功的验证,将向以下 URL 发送请求
http://test.example.org/.well-known/acme-challenge/A8Q1DAVcd_k_oKAC0D_y4ln2IWrRX51jmXnR9UMMtOb
此文件的内容应设置为上面数组中的内容。用户应在验证授权之前创建此文件。
DNS挑战
在此示例中,我们假设有两个域名需要验证。一个是通配符域名。本例中的第二个域名是为了演示目的添加的。将子域名添加到已由通配符域名覆盖的证书中并不增加多少价值。
$pending = $order->getPendingAuthorizations(LEOrder::CHALLENGE_TYPE_DNS);
这将返回一个数组
Array
(
[0] => Array
(
[type] => dns-01
[identifier] => example.org
[DNSDigest] => FV5HgbpjIYe1x9MkPI81Nffo2oA-Jo2S88gCL7-Ky5P
)
[1] => Array
(
[type] => dns-01
[identifier] => test.example.org
[DNSDigest] => WM5YIsgaZQv1b9DbRZ81EwCf2fi-Af2JlgxTC7-Up5D
)
)
为了成功验证,DNS记录应创建如下
如果需要,可以将TTL值设置得更高,但出于此目的,我更喜欢尽可能保持其低。为了确保验证成功,建议运行一个两部分的DNS挑战脚本,中间留出一定时间以允许DNS记录更新。用户本人应确保在记录可以验证之前设置此DNS记录。
DNS记录名称也取决于您的提供商,因此getPendingAuthorizations()
不会给您一个现成的记录名称。一些提供商只接受像_acme-challenge
这样的名称,不带顶级域名,对于_acme-challenge.example.org
。一些提供商接受(或要求)如上所示的完整名称。
通配符域名,如*.example.org
,将验证为example.org
,如上所示。这意味着DNS记录名称应该是_acme-challenge.example.org
完整示例
对于HTTP和DNS授权,项目的主代码目录中都有完整的示例。HTTP授权示例包含在一个文件中。如上所述,DNS授权示例分为两部分,以便在中间允许DNS记录更新。虽然记录的TTL可能很低,但在更改后,您的提供商可能需要一些时间才能更新您的DNS记录。
如果您无法获取这些示例或客户端库,请尝试查看上面提到的Let'sEncrypt文档。
变更日志
有关最近更改的更多信息,请参阅变更日志。
测试
单元测试如下执行
$ composer test
测试套件包括一些与外部依赖项的集成测试,例如验证每个支持的DNS-over-HTTP服务是否按预期工作。可以使用以下命令运行完整的测试套件
$ composer test-all
贡献
安全
安全是SSL/TLS证书的一个重要话题。当然,由于这个客户端是PHP脚本,它很可能运行在Web服务器上。显然,您的私钥应从不通过Web访问。您的私钥存储在Web服务器上,这是不言而喻的。
当客户端首次创建密钥目录时,它将在该目录中存储一个.htaccess文件,拒绝所有访客。请确保您自己的密钥不能从Web访问!我绝不对您的私钥公开负责。如果发生这种情况,最简单的解决方案是更改您的账户密钥(如上所述)或停用您的账户并创建一个新的账户。然后创建一个新的证书。
致谢
-
Paul Dixon 重构包含单元测试和存储接口
-
Youri van Weegberg 此项目基于的原始PHP ACME2客户端
-
wutno DNS-over-HTTPS支持
许可
MIT许可证(MIT)。有关更多信息,请参阅许可文件。