From ec89fd685b35c5ce3e6a5972aa471de2c0d06f76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Sun, 18 May 2025 16:16:15 +0800 Subject: [PATCH] =?UTF-8?q?Google=20EAB=E6=94=AF=E6=8C=81=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E8=8E=B7=E5=8F=96=E5=8F=8A=E6=8E=A5=E5=8F=A3=E5=8F=8D?= =?UTF-8?q?=E5=90=91=E4=BB=A3=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/lib/CertHelper.php | 730 ++++++++++++++++++++------------------ app/lib/acme/ACMECert.php | 2 +- app/lib/acme/ACMEv2.php | 50 ++- app/lib/cert/google.php | 260 ++++++++------ 4 files changed, 563 insertions(+), 479 deletions(-) diff --git a/app/lib/CertHelper.php b/app/lib/CertHelper.php index 81debda..98ccef9 100644 --- a/app/lib/CertHelper.php +++ b/app/lib/CertHelper.php @@ -1,355 +1,375 @@ - [ - 'name' => 'Let\'s Encrypt', - 'class' => 1, - 'icon' => 'letsencrypt.ico', - 'wildcard' => true, - 'max_domains' => 100, - 'cname' => true, - 'note' => null, - 'inputs' => [ - 'email' => [ - 'name' => '邮箱地址', - 'type' => 'input', - 'placeholder' => '用于注册Let\'s Encrypt账号', - 'required' => true, - ], - 'mode' => [ - 'name' => '环境选择', - 'type' => 'radio', - 'options' => [ - 'live' => '正式环境', - 'staging' => '测试环境', - ], - 'value' => 'live' - ], - 'proxy' => [ - 'name' => '使用代理服务器', - 'type' => 'radio', - 'options' => [ - '0' => '否', - '1' => '是', - ], - 'value' => '0' - ], - ] - ], - 'zerossl' => [ - 'name' => 'ZeroSSL', - 'class' => 1, - 'icon' => 'zerossl.ico', - 'wildcard' => true, - 'max_domains' => 100, - 'cname' => true, - 'note' => null, - 'inputs' => [ - 'email' => [ - 'name' => '邮箱地址', - 'type' => 'input', - 'placeholder' => 'EAB申请邮箱', - 'required' => true, - ], - 'proxy' => [ - 'name' => '使用代理服务器', - 'type' => 'radio', - 'options' => [ - '0' => '否', - '1' => '是', - ], - 'value' => '0' - ], - ] - ], - 'google' => [ - 'name' => 'Google SSL', - 'class' => 1, - 'icon' => 'google.ico', - 'wildcard' => true, - 'max_domains' => 100, - 'cname' => true, - 'note' => '查看Google SSL账户配置说明', - 'inputs' => [ - 'email' => [ - 'name' => '邮箱地址', - 'type' => 'input', - 'placeholder' => 'EAB申请邮箱', - 'required' => true, - ], - 'kid' => [ - 'name' => 'keyId', - 'type' => 'input', - 'placeholder' => '', - 'required' => true, - ], - 'key' => [ - 'name' => 'b64MacKey', - 'type' => 'input', - 'placeholder' => '', - 'required' => true, - ], - 'mode' => [ - 'name' => '环境选择', - 'type' => 'radio', - 'options' => [ - 'live' => '正式环境', - 'staging' => '测试环境', - ], - 'value' => 'live' - ], - 'proxy' => [ - 'name' => '使用代理服务器', - 'type' => 'radio', - 'options' => [ - '0' => '否', - '1' => '是', - ], - 'value' => '0' - ], - ] - ], - 'tencent' => [ - 'name' => '腾讯云免费SSL', - 'class' => 2, - 'icon' => 'tencent.ico', - 'wildcard' => false, - 'max_domains' => 1, - 'cname' => false, - 'note' => '一个账号有50张免费证书额度,证书到期或吊销可释放额度。腾讯云免费SSL简介与额度说明', - 'inputs' => [ - 'SecretId' => [ - 'name' => 'SecretId', - 'type' => 'input', - 'placeholder' => '', - 'required' => true, - ], - 'SecretKey' => [ - 'name' => 'SecretKey', - 'type' => 'input', - 'placeholder' => '', - 'required' => true, - ], - 'email' => [ - 'name' => '邮箱地址', - 'type' => 'input', - 'placeholder' => '申请证书时填写的邮箱', - 'required' => true, - ], - 'proxy' => [ - 'name' => '使用代理服务器', - 'type' => 'radio', - 'options' => [ - '0' => '否', - '1' => '是', - ], - 'value' => '0' - ], - ] - ], - 'aliyun' => [ - 'name' => '阿里云免费SSL', - 'class' => 2, - 'icon' => 'aliyun.ico', - 'wildcard' => false, - 'max_domains' => 1, - 'cname' => false, - 'note' => '每个自然年有20张免费证书额度,证书到期或吊销不释放额度。需要先进入阿里云控制台-数字证书管理服务,购买个人测试证书资源包。', - 'inputs' => [ - 'AccessKeyId' => [ - 'name' => 'AccessKeyId', - 'type' => 'input', - 'placeholder' => '', - 'required' => true, - ], - 'AccessKeySecret' => [ - 'name' => 'AccessKeySecret', - 'type' => 'input', - 'placeholder' => '', - 'required' => true, - ], - 'username' => [ - 'name' => '姓名', - 'type' => 'input', - 'placeholder' => '申请联系人的姓名', - 'required' => true, - ], - 'phone' => [ - 'name' => '手机号码', - 'type' => 'input', - 'placeholder' => '申请联系人的手机号码', - 'required' => true, - ], - 'email' => [ - 'name' => '邮箱地址', - 'type' => 'input', - 'placeholder' => '申请联系人的邮箱地址', - 'required' => true, - ], - 'proxy' => [ - 'name' => '使用代理服务器', - 'type' => 'radio', - 'options' => [ - '0' => '否', - '1' => '是', - ], - 'value' => '0' - ], - ] - ], - 'ucloud' => [ - 'name' => 'UCloud免费SSL', - 'class' => 2, - 'icon' => 'ucloud.ico', - 'wildcard' => false, - 'max_domains' => 1, - 'cname' => false, - 'note' => '一个账号有40张免费证书额度,证书到期或吊销可释放额度。', - 'inputs' => [ - 'PublicKey' => [ - 'name' => '公钥', - 'type' => 'input', - 'placeholder' => '', - 'required' => true, - ], - 'PrivateKey' => [ - 'name' => '私钥', - 'type' => 'input', - 'placeholder' => '', - 'required' => true, - ], - 'username' => [ - 'name' => '姓名', - 'type' => 'input', - 'placeholder' => '申请联系人的姓名', - 'required' => true, - ], - 'phone' => [ - 'name' => '手机号码', - 'type' => 'input', - 'placeholder' => '申请联系人的手机号码', - 'required' => true, - ], - 'email' => [ - 'name' => '邮箱地址', - 'type' => 'input', - 'placeholder' => '申请联系人的邮箱地址', - 'required' => true, - ], - ] - ], - 'customacme' => [ - 'name' => '自定义ACME', - 'class' => 1, - 'icon' => 'ssl.ico', - 'wildcard' => true, - 'max_domains' => 100, - 'cname' => true, - 'note' => null, - 'inputs' => [ - 'directory' => [ - 'name' => 'ACME地址', - 'type' => 'input', - 'placeholder' => 'ACME Directory 地址', - 'required' => true, - ], - 'email' => [ - 'name' => '邮箱地址', - 'type' => 'input', - 'placeholder' => '证书申请邮箱', - 'required' => true, - ], - 'kid' => [ - 'name' => 'EAB KID', - 'type' => 'input', - 'placeholder' => '留空则不使用EAB认证', - ], - 'key' => [ - 'name' => 'EAB HMAC Key', - 'type' => 'input', - 'placeholder' => '留空则不使用EAB认证', - ], - 'proxy' => [ - 'name' => '使用代理服务器', - 'type' => 'radio', - 'options' => [ - '0' => '否', - '1' => '是', - ], - 'value' => '0' - ], - ] - ], - ]; - - public static $class_config = [ - 1 => '基于ACME的SSL证书', - 2 => '云服务商的SSL证书', - ]; - - public static function getList() - { - return self::$cert_config; - } - - private static function getConfig($aid) - { - $account = Db::name('cert_account')->where('id', $aid)->find(); - if (!$account) return false; - return $account; - } - - public static function getInputs($type, $config = null) - { - $config = $config ? json_decode($config, true) : []; - $inputs = self::$cert_config[$type]['inputs']; - foreach ($inputs as &$input) { - if (isset($config[$input['name']])) { - $input['value'] = $config[$input['name']]; - } - } - return $inputs; - } - - /** - * @return CertInterface|bool - */ - public static function getModel($aid) - { - $account = self::getConfig($aid); - if (!$account) return false; - $type = $account['type']; - $class = "\\app\\lib\\cert\\{$type}"; - if (class_exists($class)) { - $config = json_decode($account['config'], true); - $ext = $account['ext'] ? json_decode($account['ext'], true) : null; - $model = new $class($config, $ext); - return $model; - } - return false; - } - - /** - * @return CertInterface|bool - */ - public static function getModel2($type, $config, $ext = null) - { - $class = "\\app\\lib\\cert\\{$type}"; - if (class_exists($class)) { - $model = new $class($config, $ext); - return $model; - } - return false; - } - - public static function getPfx($fullchain, $privatekey, $pwd = '123456'){ - openssl_pkcs12_export($fullchain, $pfx, $privatekey, $pwd); - return $pfx; - } -} + [ + 'name' => 'Let\'s Encrypt', + 'class' => 1, + 'icon' => 'letsencrypt.ico', + 'wildcard' => true, + 'max_domains' => 100, + 'cname' => true, + 'note' => null, + 'inputs' => [ + 'email' => [ + 'name' => '邮箱地址', + 'type' => 'input', + 'placeholder' => '用于注册Let\'s Encrypt账号', + 'required' => true, + ], + 'mode' => [ + 'name' => '环境选择', + 'type' => 'radio', + 'options' => [ + 'live' => '正式环境', + 'staging' => '测试环境', + ], + 'value' => 'live' + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ] + ], + 'zerossl' => [ + 'name' => 'ZeroSSL', + 'class' => 1, + 'icon' => 'zerossl.ico', + 'wildcard' => true, + 'max_domains' => 100, + 'cname' => true, + 'note' => null, + 'inputs' => [ + 'email' => [ + 'name' => '邮箱地址', + 'type' => 'input', + 'placeholder' => 'EAB申请邮箱', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ] + ], + 'google' => [ + 'name' => 'Google SSL', + 'class' => 1, + 'icon' => 'google.ico', + 'wildcard' => true, + 'max_domains' => 100, + 'cname' => true, + 'note' => 'EAB支持通过第三方接口(耗子面板提供)自动获取(不支持测试环境)或手动输入,查看Google SSL账户手动配置说明', + 'inputs' => [ + 'email' => [ + 'name' => '邮箱地址', + 'type' => 'input', + 'placeholder' => 'EAB申请邮箱', + 'required' => true, + ], + 'eabMode' => [ + 'name' => 'EAB获取方式', + 'type' => 'radio', + 'options' => [ + 'auto' => '自动获取', + 'manual' => '手动输入', + ], + 'value' => 'auto' + ], + 'kid' => [ + 'name' => 'keyId', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + 'show' => 'eabMode==\'manual\'', + ], + 'key' => [ + 'name' => 'b64MacKey', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + 'show' => 'eabMode==\'manual\'', + ], + 'mode' => [ + 'name' => '环境选择', + 'type' => 'radio', + 'options' => [ + 'live' => '正式环境', + 'staging' => '测试环境', + ], + 'value' => 'live' + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + '2' => '是(反向代理)' + ], + 'value' => '0' + ], + 'proxy_url' => [ + 'name' => '反向代理地址', + 'type' => 'input', + 'placeholder' => 'https://dv.acme-v02.api.pki.goog', + 'required' => true, + 'show' => 'proxy==2', + ], + ] + ], + 'tencent' => [ + 'name' => '腾讯云免费SSL', + 'class' => 2, + 'icon' => 'tencent.ico', + 'wildcard' => false, + 'max_domains' => 1, + 'cname' => false, + 'note' => '一个账号有50张免费证书额度,证书到期或吊销可释放额度。腾讯云免费SSL简介与额度说明', + 'inputs' => [ + 'SecretId' => [ + 'name' => 'SecretId', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'SecretKey' => [ + 'name' => 'SecretKey', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'email' => [ + 'name' => '邮箱地址', + 'type' => 'input', + 'placeholder' => '申请证书时填写的邮箱', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ] + ], + 'aliyun' => [ + 'name' => '阿里云免费SSL', + 'class' => 2, + 'icon' => 'aliyun.ico', + 'wildcard' => false, + 'max_domains' => 1, + 'cname' => false, + 'note' => '每个自然年有20张免费证书额度,证书到期或吊销不释放额度。需要先进入阿里云控制台-数字证书管理服务,购买个人测试证书资源包。', + 'inputs' => [ + 'AccessKeyId' => [ + 'name' => 'AccessKeyId', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'AccessKeySecret' => [ + 'name' => 'AccessKeySecret', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'username' => [ + 'name' => '姓名', + 'type' => 'input', + 'placeholder' => '申请联系人的姓名', + 'required' => true, + ], + 'phone' => [ + 'name' => '手机号码', + 'type' => 'input', + 'placeholder' => '申请联系人的手机号码', + 'required' => true, + ], + 'email' => [ + 'name' => '邮箱地址', + 'type' => 'input', + 'placeholder' => '申请联系人的邮箱地址', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ] + ], + 'ucloud' => [ + 'name' => 'UCloud免费SSL', + 'class' => 2, + 'icon' => 'ucloud.ico', + 'wildcard' => false, + 'max_domains' => 1, + 'cname' => false, + 'note' => '一个账号有40张免费证书额度,证书到期或吊销可释放额度。', + 'inputs' => [ + 'PublicKey' => [ + 'name' => '公钥', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'PrivateKey' => [ + 'name' => '私钥', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'username' => [ + 'name' => '姓名', + 'type' => 'input', + 'placeholder' => '申请联系人的姓名', + 'required' => true, + ], + 'phone' => [ + 'name' => '手机号码', + 'type' => 'input', + 'placeholder' => '申请联系人的手机号码', + 'required' => true, + ], + 'email' => [ + 'name' => '邮箱地址', + 'type' => 'input', + 'placeholder' => '申请联系人的邮箱地址', + 'required' => true, + ], + ] + ], + 'customacme' => [ + 'name' => '自定义ACME', + 'class' => 1, + 'icon' => 'ssl.ico', + 'wildcard' => true, + 'max_domains' => 100, + 'cname' => true, + 'note' => null, + 'inputs' => [ + 'directory' => [ + 'name' => 'ACME地址', + 'type' => 'input', + 'placeholder' => 'ACME Directory 地址', + 'required' => true, + ], + 'email' => [ + 'name' => '邮箱地址', + 'type' => 'input', + 'placeholder' => '证书申请邮箱', + 'required' => true, + ], + 'kid' => [ + 'name' => 'EAB KID', + 'type' => 'input', + 'placeholder' => '留空则不使用EAB认证', + ], + 'key' => [ + 'name' => 'EAB HMAC Key', + 'type' => 'input', + 'placeholder' => '留空则不使用EAB认证', + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ] + ], + ]; + + public static $class_config = [ + 1 => '基于ACME的SSL证书', + 2 => '云服务商的SSL证书', + ]; + + public static function getList() + { + return self::$cert_config; + } + + private static function getConfig($aid) + { + $account = Db::name('cert_account')->where('id', $aid)->find(); + if (!$account) return false; + return $account; + } + + public static function getInputs($type, $config = null) + { + $config = $config ? json_decode($config, true) : []; + $inputs = self::$cert_config[$type]['inputs']; + foreach ($inputs as &$input) { + if (isset($config[$input['name']])) { + $input['value'] = $config[$input['name']]; + } + } + return $inputs; + } + + /** + * @return CertInterface|bool + */ + public static function getModel($aid) + { + $account = self::getConfig($aid); + if (!$account) return false; + $type = $account['type']; + $class = "\\app\\lib\\cert\\{$type}"; + if (class_exists($class)) { + $config = json_decode($account['config'], true); + $ext = $account['ext'] ? json_decode($account['ext'], true) : null; + $model = new $class($config, $ext); + return $model; + } + return false; + } + + /** + * @return CertInterface|bool + */ + public static function getModel2($type, $config, $ext = null) + { + $class = "\\app\\lib\\cert\\{$type}"; + if (class_exists($class)) { + $model = new $class($config, $ext); + return $model; + } + return false; + } + + public static function getPfx($fullchain, $privatekey, $pwd = '123456') + { + openssl_pkcs12_export($fullchain, $pfx, $privatekey, $pwd); + return $pfx; + } +} diff --git a/app/lib/acme/ACMECert.php b/app/lib/acme/ACMECert.php index db6e275..5c7ab27 100644 --- a/app/lib/acme/ACMECert.php +++ b/app/lib/acme/ACMECert.php @@ -25,7 +25,7 @@ class ACMECert extends ACMEv2 $protected = array( 'alg' => 'HS256', 'kid' => $eab_kid, - 'url' => $this->resources['newAccount'] + 'url' => $this->unproxiedURL($this->resources['newAccount']) ); $payload = $this->jwk_header['jwk']; diff --git a/app/lib/acme/ACMEv2.php b/app/lib/acme/ACMEv2.php index 08ad00d..879b4db 100644 --- a/app/lib/acme/ACMEv2.php +++ b/app/lib/acme/ACMEv2.php @@ -8,13 +8,22 @@ class ACMEv2 { // Communication with Let's Encrypt via ACME v2 protocol protected - $ch = null, $logger = true, $bits, $sha_bits, $directory, $resources, $jwk_header, $kid_header, $account_key, $thumbprint, $nonce = null, $proxy; + $ch = null, $logger = true, $bits, $sha_bits, $directory, $resources, $jwk_header, $kid_header, $account_key, $thumbprint, $nonce = null, $proxy, $proxy_config = null; private $delay_until = null; - public function __construct($directory, $proxy = false) - { + /** + * @param $directory string ACME directory URL + * @param $proxy int 代理模式,0为不使用代理,1为使用系统代理,2为使用反向代理 + * @param null $proxy_config array 反向代理配置,proxy参数为2时必填 + * @throws Exception + */ + public function __construct($directory, $proxy = 0, $proxy_config = null) + { $this->directory = $directory; $this->proxy = $proxy; + if ($proxy == 2) { + $this->proxy_config = $proxy_config; + } } public function __destruct() @@ -190,7 +199,8 @@ class ACMEv2 } if (!$this->kid_header['kid'] && $type === 'newAccount') { - $this->kid_header['kid'] = $ret['headers']['location']; + // 反向替换反向代理配置,防止破坏签名 + $this->kid_header['kid'] = $this->unproxiedURL($ret['headers']['location']); $this->log('AccountID: ' . $this->kid_header['kid']); } @@ -218,7 +228,8 @@ class ACMEv2 throw new Exception('Resource "' . $type . '" not available.'); } - $protected['url'] = $this->resources[$type]; + // 反向替换反向代理配置,防止破坏签名 + $protected['url'] = $this->unproxiedURL($this->resources[$type]); $protected64 = $this->base64url(json_encode($protected, JSON_UNESCAPED_SLASHES)); $payload64 = $this->base64url(is_string($payload) ? $payload : json_encode($payload, JSON_UNESCAPED_SLASHES)); @@ -285,6 +296,9 @@ class ACMEv2 $this->delay_until = null; } + // 替换反向代理配置 + $url = $this->proxiedURL($url); + $method = $data === false ? 'HEAD' : ($data === null ? 'GET' : 'POST'); $user_agent = 'ACMECert v3.4.0 (+https://github.com/skoerfgen/ACMECert)'; $header = ($data === null || $data === false) ? array() : array('Content-Type: application/jose+json'); @@ -406,4 +420,30 @@ class ACMEv2 }, isset($error['subproblems']) ? $error['subproblems'] : array()) ); } + + // 替换反向代理配置 + protected function proxiedURL($url) + { + if ($this->proxy == 2) { + return str_replace( + $this->proxy_config['origin'], + $this->proxy_config['proxy'], + $url + ); + } + return $url; + } + + // 反向替换反向代理配置 + protected function unproxiedURL($url) + { + if ($this->proxy == 2) { + return str_replace( + $this->proxy_config['proxy'], + $this->proxy_config['origin'], + $url + ); + } + return $url; + } } diff --git a/app/lib/cert/google.php b/app/lib/cert/google.php index 8f10384..3045966 100644 --- a/app/lib/cert/google.php +++ b/app/lib/cert/google.php @@ -1,118 +1,142 @@ - 'https://dv.acme-v02.api.pki.goog/directory', - 'staging' => 'https://dv.acme-v02.test-api.pki.goog/directory' - ); - private $ac; - private $config; - private $ext; - - public function __construct($config, $ext = null) - { - $this->config = $config; - if (empty($config['mode'])) $config['mode'] = 'live'; - $this->ac = new ACMECert($this->directories[$config['mode']], $config['proxy']==1); - if ($ext) { - $this->ext = $ext; - $this->ac->loadAccountKey($ext['key']); - $this->ac->setAccount($ext['kid']); - } - } - - public function register() - { - if (empty($this->config['email'])) throw new Exception('邮件地址不能为空'); - if (empty($this->config['kid']) || empty($this->config['key'])) throw new Exception('必填参数不能为空'); - - if (!empty($this->ext['key'])) { - $kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']); - return ['kid' => $kid, 'key' => $this->ext['key']]; - } - - $key = $this->ac->generateRSAKey(2048); - $this->ac->loadAccountKey($key); - $kid = $this->ac->registerEAB(true, $this->config['kid'], $this->config['key'], $this->config['email']); - return ['kid' => $kid, 'key' => $key]; - } - - public function buyCert($domainList, &$order) - { - } - - public function createOrder($domainList, &$order, $keytype, $keysize) - { - $domain_config = []; - foreach ($domainList as $domain) { - if (empty($domain)) continue; - $domain_config[$domain] = ['challenge' => 'dns-01']; - } - if (empty($domain_config)) throw new Exception('域名列表不能为空'); - - $order = $this->ac->createOrder($domain_config); - - $dnsList = []; - if (!empty($order['challenges'])) { - foreach ($order['challenges'] as $opts) { - $mainDomain = getMainDomain($opts['domain']); - $name = str_replace('.' . $mainDomain, '', $opts['key']); - /*if (!array_key_exists($mainDomain, $dnsList)) { - $dnsList[$mainDomain][] = ['name' => '@', 'type' => 'CAA', 'value' => '0 issue "pki.goog"']; - }*/ - $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']]; - } - } - - return $dnsList; - } - - public function authOrder($domainList, $order) - { - $this->ac->authOrder($order); - } - - public function getAuthStatus($domainList, $order) - { - return true; - } - - public function finalizeOrder($domainList, $order, $keytype, $keysize) - { - if (empty($domainList)) throw new Exception('域名列表不能为空'); - - if ($keytype == 'ECC') { - if (empty($keysize)) $keysize = '384'; - $private_key = $this->ac->generateECKey($keysize); - } else { - if (empty($keysize)) $keysize = '2048'; - $private_key = $this->ac->generateRSAKey($keysize); - } - $fullchain = $this->ac->finalizeOrder($domainList, $order, $private_key); - - $certInfo = openssl_x509_parse($fullchain, true); - if (!$certInfo) throw new Exception('证书解析失败'); - return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $certInfo['issuer']['CN'], 'subject' => $certInfo['subject']['CN'], 'validFrom' => $certInfo['validFrom_time_t'], 'validTo' => $certInfo['validTo_time_t']]; - } - - public function revoke($order, $pem) - { - $this->ac->revoke($pem); - } - - public function cancel($order) - { - } - - public function setLogger($func) - { - $this->ac->setLogger($func); - } -} + 'https://dv.acme-v02.api.pki.goog', + 'staging' => 'https://dv.acme-v02.test-api.pki.goog' + ); + private $ac; + private $config; + private $ext; + + public function __construct($config, $ext = null) + { + $this->config = $config; + if (empty($config['mode'])) $config['mode'] = 'live'; + if (empty($config['proxy_url'])) $config['proxy_url'] = ''; + $this->ac = new ACMECert($this->directories[$config['mode']] . '/directory', (int)$config['proxy'], [ + 'origin' => $this->directories[$config['mode']], + 'proxy' => rtrim($config['proxy_url'], '/'), + ]); + if ($ext) { + $this->ext = $ext; + $this->ac->loadAccountKey($ext['key']); + $this->ac->setAccount($ext['kid']); + } + } + + public function register() + { + if (empty($this->config['email'])) throw new Exception('邮件地址不能为空'); + + if (isset($this->config['eabMode']) && $this->config['eabMode'] == 'auto') { + $eab = $this->getEAB(); + } else { + $eab = ['kid' => $this->config['kid'], 'key' => $this->config['key']]; + } + + if (!empty($this->ext['key'])) { + $kid = $this->ac->registerEAB(true, $eab['kid'], $eab['key'], $this->config['email']); + return ['kid' => $kid, 'key' => $this->ext['key']]; + } + + $key = $this->ac->generateRSAKey(2048); + $this->ac->loadAccountKey($key); + $kid = $this->ac->registerEAB(true, $eab['kid'], $eab['key'], $this->config['email']); + return ['kid' => $kid, 'key' => $key]; + } + + public function buyCert($domainList, &$order) + { + } + + public function createOrder($domainList, &$order, $keytype, $keysize) + { + $domain_config = []; + foreach ($domainList as $domain) { + if (empty($domain)) continue; + $domain_config[$domain] = ['challenge' => 'dns-01']; + } + if (empty($domain_config)) throw new Exception('域名列表不能为空'); + + $order = $this->ac->createOrder($domain_config); + + $dnsList = []; + if (!empty($order['challenges'])) { + foreach ($order['challenges'] as $opts) { + $mainDomain = getMainDomain($opts['domain']); + $name = str_replace('.' . $mainDomain, '', $opts['key']); + /*if (!array_key_exists($mainDomain, $dnsList)) { + $dnsList[$mainDomain][] = ['name' => '@', 'type' => 'CAA', 'value' => '0 issue "pki.goog"']; + }*/ + $dnsList[$mainDomain][] = ['name' => $name, 'type' => 'TXT', 'value' => $opts['value']]; + } + } + + return $dnsList; + } + + public function authOrder($domainList, $order) + { + $this->ac->authOrder($order); + } + + public function getAuthStatus($domainList, $order) + { + return true; + } + + public function finalizeOrder($domainList, $order, $keytype, $keysize) + { + if (empty($domainList)) throw new Exception('域名列表不能为空'); + + if ($keytype == 'ECC') { + if (empty($keysize)) $keysize = '384'; + $private_key = $this->ac->generateECKey($keysize); + } else { + if (empty($keysize)) $keysize = '2048'; + $private_key = $this->ac->generateRSAKey($keysize); + } + $fullchain = $this->ac->finalizeOrder($domainList, $order, $private_key); + + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + return ['private_key' => $private_key, 'fullchain' => $fullchain, 'issuer' => $certInfo['issuer']['CN'], 'subject' => $certInfo['subject']['CN'], 'validFrom' => $certInfo['validFrom_time_t'], 'validTo' => $certInfo['validTo_time_t']]; + } + + public function revoke($order, $pem) + { + $this->ac->revoke($pem); + } + + public function cancel($order) + { + } + + public function setLogger($func) + { + $this->ac->setLogger($func); + } + + private function getEAB() + { + $api = "https://gts.rat.dev/eab"; + $response = curl_client($api, null, null, null, null, $this->config['proxy'] == 1, 'GET', 10); + $result = json_decode($response['body'], true); + if (!isset($result['msg'])) { + throw new Exception('解析返回数据失败:' . $response['body']); + } elseif ($result['msg'] != 'success') { + throw new Exception('获取EAB失败:' . $result['msg']); + } elseif (empty($result['data']['key_id']) || empty($result['data']['mac_key'])) { + throw new Exception('获取EAB失败:返回数据不完整'); + } + return ['kid' => $result['data']['key_id'], 'key' => $result['data']['mac_key']]; + } +}