PHP申请免费证书脚本

wen PHP项目 1

本文目录导读:

PHP申请免费证书脚本

  1. 完整PHP脚本示例
  2. 简化版本(使用第三方库)
  3. 使用注意事项

我来为您提供一个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证书申请功能,包括账户注册、域名验证、证书申请等核心功能,实际使用时,您可能需要根据具体环境进行调整。

抱歉,评论功能暂时关闭!