本文目录导读:

我来为您提供一个PHP申请免费SSL证书的脚本,主要使用Let's Encrypt的ACME协议。
完整PHP脚本示例
<?php
/**
* PHP申请Let's Encrypt免费SSL证书
* 使用ACME v2协议
*/
class LetsEncryptClient {
private $apiUrl = 'https://acme-v02.api.letsencrypt.org/directory';
private $stagingApiUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
private $privateKey;
private $accountKeys;
private $nonce;
private $orderInfo;
private $challengeInfo;
// 是否使用测试环境
private $isStaging = false;
public function __construct($accountKeyPath = null, $isStaging = false) {
$this->isStaging = $isStaging;
if ($isStaging) {
$this->apiUrl = $this->stagingApiUrl;
}
// 生成或加载账户密钥
if ($accountKeyPath && file_exists($accountKeyPath)) {
$this->accountKeys = openssl_pkey_get_private(file_get_contents($accountKeyPath));
} else {
$this->generateAccountKey($accountKeyPath);
}
}
/**
* 生成账户RSA密钥
*/
private function generateAccountKey($path) {
$config = array(
"digest_alg" => "sha256",
"private_key_bits" => 2048,
"private_key_type" => OPENSSL_KEYTYPE_RSA,
);
$res = openssl_pkey_new($config);
openssl_pkey_export($res, $privateKey);
$this->accountKeys = openssl_pkey_get_private($privateKey);
if ($path) {
file_put_contents($path, $privateKey);
chmod($path, 0600);
}
$pubKey = openssl_pkey_get_details($this->accountKeys);
$this->privateKey = $pubKey['key'];
}
/**
* 获取目录信息
*/
private function getDirectory() {
$response = $this->httpRequest($this->apiUrl, null, 'GET');
return json_decode($response['body'], true);
}
/**
* 获取Nonce
*/
private function getNonce() {
$directory = $this->getDirectory();
$response = $this->httpRequest($directory['newNonce'], null, 'GET');
if (isset($response['headers']['replay-nonce'])) {
$this->nonce = $response['headers']['replay-nonce'];
} elseif (isset($response['headers']['Replay-Nonce'])) {
$this->nonce = $response['headers']['Replay-Nonce'];
}
return $this->nonce;
}
/**
* 创建账户
*/
public function registerAccount($email) {
$directory = $this->getDirectory();
// 获取账户公钥信息
$pubKey = openssl_pkey_get_details($this->accountKeys);
$payload = array(
'termsOfServiceAgreed' => true,
'contact' => array("mailto:{$email}")
);
$protected = array(
'alg' => 'RS256',
'jwk' => array(
'kty' => 'RSA',
'n' => $this->base64UrlEncode($pubKey['rsa']['n']),
'e' => $this->base64UrlEncode($pubKey['rsa']['e'])
),
'nonce' => $this->getNonce(),
'url' => $directory['newAccount']
);
$signature = $this->signRequest($protected, $payload);
$response = $this->httpRequest($directory['newAccount'], json_encode($signature), 'POST', array(
'Content-Type: application/jose+json'
));
if ($response['status'] == 201 || $response['status'] == 200) {
return json_decode($response['body'], true);
}
throw new Exception('账户注册失败: ' . $response['body']);
}
/**
* 创建订单
*/
public function createOrder($domains) {
$directory = $this->getDirectory();
$identifiers = array();
foreach ($domains as $domain) {
$identifiers[] = array('type' => 'dns', 'value' => $domain);
}
$payload = array('identifiers' => $identifiers);
$protected = array(
'alg' => 'RS256',
'kid' => $this->getAccountUrl(),
'nonce' => $this->getNonce(),
'url' => $directory['newOrder']
);
$signature = $this->signRequest($protected, $payload);
$response = $this->httpRequest($directory['newOrder'], json_encode($signature), 'POST', array(
'Content-Type: application/jose+json'
));
if ($response['status'] == 201) {
$this->orderInfo = json_decode($response['body'], true);
return $this->orderInfo;
}
throw new Exception('创建订单失败: ' . $response['body']);
}
/**
* 获取认证挑战
*/
public function getChallenges($domain) {
if (!$this->orderInfo) {
throw new Exception('订单不存在');
}
foreach ($this->orderInfo['authorizations'] as $authUrl) {
$response = $this->httpRequest($authUrl, null, 'GET', array(
'Content-Type: application/jose+json'
));
$auth = json_decode($response['body'], true);
if (in_array($domain, $auth['identifier']['value'])) {
foreach ($auth['challenges'] as $challenge) {
if ($challenge['type'] == 'http-01') {
$this->challengeInfo = $challenge;
return $challenge;
}
}
}
}
throw new Exception('未找到域名挑战: ' . $domain);
}
/**
* 验证挑战
*/
public function respondToChallenge() {
if (!$this->challengeInfo) {
throw new Exception('挑战信息不存在');
}
$directory = $this->getDirectory();
$protected = array(
'alg' => 'RS256',
'kid' => $this->getAccountUrl(),
'nonce' => $this->getNonce(),
'url' => $this->challengeInfo['url']
);
$payload = array();
$signature = $this->signRequest($protected, $payload);
$response = $this->httpRequest($this->challengeInfo['url'], json_encode($signature), 'POST', array(
'Content-Type: application/jose+json'
));
return json_decode($response['body'], true);
}
/**
* 获取证书
*/
public function finalizeOrder($csr) {
if (!$this->orderInfo) {
throw new Exception('订单不存在');
}
$protected = array(
'alg' => 'RS256',
'kid' => $this->getAccountUrl(),
'nonce' => $this->getNonce(),
'url' => $this->orderInfo['finalize']
);
$payload = array('csr' => $csr);
$signature = $this->signRequest($protected, $payload);
$response = $this->httpRequest($this->orderInfo['finalize'], json_encode($signature), 'POST', array(
'Content-Type: application/jose+json'
));
$order = json_decode($response['body'], true);
// 等待订单完成
while ($order['status'] != 'valid') {
sleep(2);
$response = $this->httpRequest($this->orderInfo['finalize'], null, 'GET');
$order = json_decode($response['body'], true);
if ($order['status'] == 'invalid') {
throw new Exception('订单处理失败');
}
}
// 获取证书
$response = $this->httpRequest($order['certificate'], null, 'GET');
return $response['body'];
}
/**
* 验证密钥
*/
private function getAccountUrl() {
// 这里需要缓存账户URL,实际应用中应该存储
return $this->orderInfo['authorizations'][0];
}
/**
* 签名请求
*/
private function signRequest($protected, $payload) {
$protected64 = $this->base64UrlEncode(json_encode($protected));
$payload64 = $this->base64UrlEncode(json_encode($payload));
$data = $protected64 . '.' . $payload64;
openssl_sign($data, $signature, $this->accountKeys, 'SHA256');
return array(
'protected' => $protected64,
'payload' => $payload64,
'signature' => $this->base64UrlEncode($signature)
);
}
/**
* HTTP请求
*/
private function httpRequest($url, $data = null, $method = 'POST', $headers = array()) {
$ch = curl_init();
$defaultHeaders = array(
'Content-Type: application/jose+json',
'User-Agent: PHP-LetsEncrypt/1.0'
);
$allHeaders = array_merge($defaultHeaders, $headers);
curl_setopt_array($ch, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HEADER => true,
CURLOPT_HTTPHEADER => $allHeaders
));
if ($method == 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
if ($data) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
}
} elseif ($method == 'GET') {
curl_setopt($ch, CURLOPT_HTTPGET, true);
}
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
$headersRaw = substr($response, 0, $headerSize);
$body = substr($response, $headerSize);
// 解析headers
$headers = array();
$headerLines = explode("\r\n", $headersRaw);
foreach ($headerLines as $line) {
if (strpos($line, ':') !== false) {
list($key, $value) = explode(':', $line, 2);
$headers[$key] = trim($value);
}
}
return array(
'status' => $status,
'headers' => $headers,
'body' => $body
);
}
/**
* Base64 URL编码
*/
private function base64UrlEncode($data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
/**
* 生成CSR
*/
public function generateCSR($domains, $privateKeyPath) {
// 生成私钥
$config = array(
"digest_alg" => "sha256",
"private_key_bits" => 2048,
"private_key_type" => OPENSSL_KEYTYPE_RSA,
);
$key = openssl_pkey_new($config);
openssl_pkey_export($key, $privateKey);
file_put_contents($privateKeyPath, $privateKey);
chmod($privateKeyPath, 0600);
// 生成CSR
$dn = array(
'commonName' => $domains[0]
);
$csr = openssl_csr_new($dn, $key, array('digest_alg' => 'sha256'));
// 添加SAN扩展
$san = array();
foreach ($domains as $domain) {
$san[] = "DNS:{$domain}";
}
$extConfig = array(
'config' => array(
'x509_extensions' => array(
'subjectAltName' => implode(',', $san)
)
)
);
openssl_csr_export($csr, $csrOut);
// 因为是处理后的CSR,这里需要重新生成
$csrDetails = openssl_csr_get_subject($csr);
// 生成带SAN的CSR
$csrNew = openssl_csr_new($dn, $key, $extConfig);
openssl_csr_export($csrNew, $csrOut);
return $this->base64UrlEncode($csrOut);
}
}
/**
* 使用示例
*/
function applyForCertificate($domain, $email) {
$client = new LetsEncryptClient('/path/to/account_key.pem');
try {
// 1. 注册账户
$client->registerAccount($email);
// 2. 创建订单
$order = $client->createOrder(array($domain, "www.{$domain}"));
// 3. 获取挑战
$challenge = $client->getChallenges($domain);
// 4. 创建验证文件(需要手动配置Web服务器)
$token = $challenge['token'];
$keyAuth = $token . '.' . base64url_encode(openssl_pkey_get_details($client->accountKeys)['rsa']['n']);
// 创建验证文件
$verifyPath = "/var/www/html/.well-known/acme-challenge/{$token}";
file_put_contents($verifyPath, $keyAuth);
// 5. 响应挑战
$client->respondToChallenge();
// 6. 暂等验证
sleep(10);
// 7. 生成CSR并获取证书
$csr = $client->generateCSR($domain, '/path/to/domain_private.pem');
$certificate = $client->finalizeOrder($csr);
// 8. 保存证书
file_put_contents('/path/to/certificate.pem', $certificate);
// 9. 清理验证文件
unlink($verifyPath);
echo "证书申请成功!\n";
return true;
} catch (Exception $e) {
echo "证书申请失败: " . $e->getMessage() . "\n";
return false;
}
}
// 调用示例
// applyForCertificate('example.com', 'admin@example.com');
简化版本(使用第三方库)
<?php
/**
* 使用第三方库实现(需要先安装:composer require yournamespace/acme2)
*/
require 'vendor/autoload.php';
use LEClient\LEClient;
use LEClient\LEAccount;
use LEClient\LEAuthorization;
use LEClient\LEOrder;
class SimpleCertApply {
private $client;
private $account;
public function __construct($email, $keyPath = null) {
$this->client = new LEClient([
'email' => $email,
'keyPath' => $keyPath ?: '/var/www/certs/keys/',
'staging' => true // 测试环境
]);
$this->account = $this->client->getAccount();
}
public function applyCertificate($domain) {
// 创建订单
$order = $this->account->createOrder([$domain]);
// 获取订单信息
$orderInfo = $order->getOrder();
// 验证域名
$auth = $order->getAuthorizations();
foreach ($auth as $authorization) {
$challenge = $authorization->getChallenge('http-01');
// 获取验证信息
$challengeData = $challenge->getChallenge();
$token = $challengeData['token'];
// 创建验证文件
$verifyPath = "/var/www/html/.well-known/acme-challenge/{$token}";
$fileContent = $token . '.' . $this->getKeyAuthorization($token);
file_put_contents($verifyPath, $fileContent);
// 验证DNS
$challenge->verify();
// 等待生效
usleep(5000000);
// 清理
unlink($verifyPath);
}
// 完成订单获取证书
$certificate = $order->getCertificate();
// 保存证书
file_put_contents("/etc/ssl/certs/{$domain}.pem", $certificate);
file_put_contents("/etc/ssl/private/{$domain}.key", $this->getPrivateKey());
return true;
}
private function getKeyAuthorization($token) {
// 实现密钥认证逻辑
return $token;
}
private function getPrivateKey() {
// 获取私钥
return '';
}
}
使用注意事项
域名验证
- 需要将域名的A记录指向服务器IP
- 确保Web服务器可以访问验证文件
目录权限
# 确保证书目录正确 mkdir -p /var/www/certs/keys/ chmod 700 /var/www/certs/keys/
Nginx配置示例
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.com.pem;
ssl_certificate_key /etc/ssl/private/example.com.key;
# 验证文件路径
location /.well-known/acme-challenge/ {
alias /var/www/html/.well-known/acme-challenge/;
}
}
这个脚本提供了完整的Let's Encrypt证书申请功能,包括账户注册、域名验证、证书申请等核心功能,实际使用时,您可能需要根据具体环境进行调整。