diff --git a/app/controller/Cert.php b/app/controller/Cert.php index 33550cd..1693395 100644 --- a/app/controller/Cert.php +++ b/app/controller/Cert.php @@ -304,10 +304,6 @@ class Cert extends BaseController } } - if ($certInfo['keytype'] == 'ECC') { - $privatekey = CertHelper::ensureECPrivateKeyFormat($privatekey); - } - $order = [ 'aid' => 0, 'keytype' => $certInfo['keytype'], @@ -371,10 +367,6 @@ class Cert extends BaseController if ($certInfo['code'] == -1) return json($certInfo); $domains = $certInfo['domains']; - if ($certInfo['keytype'] == 'ECC') { - $privatekey = CertHelper::ensureECPrivateKeyFormat($privatekey); - } - $order = [ 'aid' => 0, 'keytype' => $certInfo['keytype'], diff --git a/app/lib/CertHelper.php b/app/lib/CertHelper.php index 7b3c3db..fa8f3ca 100644 --- a/app/lib/CertHelper.php +++ b/app/lib/CertHelper.php @@ -407,24 +407,6 @@ location / { return false; } - /** - * 确保ECC私钥使用EC专用格式标识 - * 某些程序需要EC标识才能正确识别ECC私钥 - */ - public static function ensureECPrivateKeyFormat($private_key) - { - if (strpos($private_key, '-----BEGIN EC PRIVATE KEY-----') !== false) { - return $private_key; - } - - if (strpos($private_key, '-----BEGIN PRIVATE KEY-----') !== false) { - $private_key = preg_replace('/^-----BEGIN PRIVATE KEY-----$/m', '-----BEGIN EC PRIVATE KEY-----', $private_key); - $private_key = preg_replace('/^-----END PRIVATE KEY-----$/m', '-----END EC PRIVATE KEY-----', $private_key); - } - - return $private_key; - } - public static function getPfx($fullchain, $privatekey, $pwd = '123456') { openssl_pkcs12_export($fullchain, $pfx, $privatekey, $pwd); diff --git a/app/lib/DeployHelper.php b/app/lib/DeployHelper.php index 2427ece..e08d3ca 100644 --- a/app/lib/DeployHelper.php +++ b/app/lib/DeployHelper.php @@ -1224,6 +1224,7 @@ ctrl+x 保存退出', ['value'=>'tse', 'label'=>'云原生API网关TSE'], ['value'=>'tcb', 'label'=>'云开发TCB'], ['value'=>'lighthouse', 'label'=>'轻量应用服务器'], + ['value'=>'update', 'label'=>'更新证书内容(证书ID不变)'], ], 'value' => 'cdn', 'required' => true, @@ -1327,6 +1328,14 @@ ctrl+x 保存退出', 'note' => 'CDN、EO、WAF多个域名可用,隔开,其他只能填写1个域名', 'required' => true, ], + 'cert_id' => [ + 'name' => '证书ID', + 'type' => 'input', + 'placeholder' => '要更新的证书ID,在我的证书列表查看', + 'show' => 'product==\'update\'', + 'required' => true, + 'note' => '当前接口需联系加白使用', + ], ], ], 'huawei' => [ diff --git a/app/lib/acme/ACMECert.php b/app/lib/acme/ACMECert.php index 1eada49..5c7ab27 100644 --- a/app/lib/acme/ACMECert.php +++ b/app/lib/acme/ACMECert.php @@ -4,7 +4,6 @@ namespace app\lib\acme; use Exception; use stdClass; -use app\lib\CertHelper; /** * ACMECert @@ -369,12 +368,10 @@ class ACMECert extends ACMEv2 if (version_compare(PHP_VERSION, '7.1.0') < 0) throw new Exception('PHP >= 7.1.0 required for EC keys !'); $map = array('256' => 'prime256v1', '384' => 'secp384r1', '521' => 'secp521r1'); if (isset($map[$curve_name])) $curve_name = $map[$curve_name]; - $pem = $this->generateKey(array( + return $this->generateKey(array( 'curve_name' => $curve_name, 'private_key_type' => OPENSSL_KEYTYPE_EC )); - - return CertHelper::ensureECPrivateKeyFormat($pem); } public function parseCertificate($cert_pem) diff --git a/app/lib/deploy/ssh.php b/app/lib/deploy/ssh.php index af61df9..bd964c0 100644 --- a/app/lib/deploy/ssh.php +++ b/app/lib/deploy/ssh.php @@ -159,9 +159,14 @@ class ssh implements DeployInterface file_put_contents($privateKeyPath, $this->config['privatekey']); file_put_contents($publicKeyPath, $publicKey); umask($umask); - $passphrase = $this->config['passphrase'] ?? null; // 私钥密码 - if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath, $passphrase)) { - throw new Exception('私钥认证失败'); + if (!empty($this->config['passphrase'])) { + if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath, $this->config['passphrase'])) { + throw new Exception('私钥认证失败'); + } + } else { + if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath)) { + throw new Exception('私钥认证失败'); + } } } else { if (!ssh2_auth_password($connection, $this->config['username'], $this->config['password'])) { diff --git a/app/lib/deploy/tencent.php b/app/lib/deploy/tencent.php index f290288..91f2d7c 100644 --- a/app/lib/deploy/tencent.php +++ b/app/lib/deploy/tencent.php @@ -31,6 +31,9 @@ class tencent implements DeployInterface public function deploy($fullchain, $privatekey, $config, &$info) { + if ($config['product'] == 'update') { + return $this->update_cert($fullchain, $privatekey, $config); + } $cert_id = $this->get_cert_id($fullchain, $privatekey); if (!$cert_id) throw new Exception('证书ID获取失败'); if ($config['product'] == 'cos') { @@ -281,6 +284,95 @@ class tencent implements DeployInterface $this->log('边缘安全加速域名 ' . $config['domain'] . ' 部署证书成功!'); } + private function update_cert($fullchain, $privatekey, $config) + { + if (empty($config['cert_id'])) throw new Exception('证书ID不能为空'); + + $param = [ + 'CertificateIds' => [$config['cert_id']], + 'IsCache' => 1, + ]; + try { + $data = $this->client->request('CreateCertificateBindResourceSyncTask', $param); + if (empty($data['CertTaskIds'])) throw new Exception('返回任务ID为空'); + } catch (Exception $e) { + throw new Exception('创建关联云资源查询任务失败:' . $e->getMessage()); + } + $task_id = $data['CertTaskIds'][0]['TaskId']; + $this->log('创建关联云资源查询任务成功 TaskId=' . $task_id); + + $retry = 0; + $resource_result = null; + while ($retry++ < 30) { + sleep(2); + $param = [ + 'TaskIds' => [$task_id], + ]; + try { + $data = $this->client->request('DescribeCertificateBindResourceTaskResult', $param); + if (empty($data['SyncTaskBindResourceResult'])) throw new Exception('返回结果为空'); + } catch (Exception $e) { + throw new Exception('查询关联云资源任务结果失败:' . $e->getMessage()); + } + $taskResult = $data['SyncTaskBindResourceResult'][0]; + if ($taskResult['Status'] == 1) { + $resource_result = $taskResult['BindResourceResult']; + break; + } elseif ($taskResult['Status'] == 2) { + throw new Exception('关联云资源查询任务执行失败:' . isset($taskResult['Error']) ? $taskResult['Error']['Message'] : '未知错误'); + } + }; + if (!$resource_result) { + throw new Exception('关联云资源查询任务超时未完成,请稍后重试'); + } + + $resourceTypes = []; + $resourceTypesRegions = []; + foreach ($resource_result as $res) { + if ($res['ResourceType'] != 'clb') continue; + $totalCount = 0; + $regions = []; + foreach ($res['BindResourceRegionResult'] as $regionRes) { + if ($regionRes['TotalCount'] > 0) { + $totalCount += $regionRes['TotalCount']; + if (!empty($regionRes['Region'])) { + $regions[] = $regionRes['Region']; + } + } + } + if ($totalCount > 0) { + $resourceTypes[] = $res['ResourceType']; + if (!empty($regions)) { + $resourceTypesRegions[] = [ + 'ResourceType' => $res['ResourceType'], + 'Regions' => $regions, + ]; + } + } + } + + $param = [ + 'OldCertificateId' => $config['cert_id'], + 'CertificatePublicKey' => $fullchain, + 'CertificatePrivateKey' => $privatekey, + 'ResourceTypes' => $resourceTypes, + 'ResourceTypesRegions' => $resourceTypesRegions, + ]; + $retry = 0; + while ($retry++ < 10) { + try { + $data = $this->client->request('UploadUpdateCertificateInstance', $param); + } catch (Exception $e) { + throw new Exception('更新证书内容失败:' . $e->getMessage()); + } + if ($data['DeployStatus'] == 1) { + break; + } + sleep(1); + } + $this->log('更新证书内容成功,可能需要一些时间完成各资源的证书更新部署'); + } + public function setLogger($func) { $this->logger = $func; diff --git a/app/lib/deploy/upyun.php b/app/lib/deploy/upyun.php index 77ae4e9..287689c 100644 --- a/app/lib/deploy/upyun.php +++ b/app/lib/deploy/upyun.php @@ -31,9 +31,15 @@ class upyun implements DeployInterface $this->login(); $url = 'https://console.upyun.com/api/https/certificate/'; + // 如果是 EC 证书,调整私钥头为 EC PRIVATE KEY + $privatekey_send = $privatekey; + if ($this->isEcCertificate($fullchain)) { + $privatekey_send = str_replace('-----BEGIN PRIVATE KEY-----', '-----BEGIN EC PRIVATE KEY-----', $privatekey_send); + $privatekey_send = str_replace('-----END PRIVATE KEY-----', '-----END EC PRIVATE KEY-----', $privatekey_send); + } $params = [ 'certificate' => $fullchain, - 'private_key' => $privatekey, + 'private_key' => $privatekey_send, ]; $response = http_request($url, http_build_query($params), null, $this->cookie, null, $this->proxy); $result = json_decode($response['body'], true); @@ -130,4 +136,22 @@ class upyun implements DeployInterface call_user_func($this->logger, $txt); } } + + /** + * 判断是否为 EC (ECDSA) 证书 + */ + private function isEcCertificate($fullchain) + { + // 提取第一个证书 + if (!preg_match('/-----BEGIN CERTIFICATE-----\s*(.+?)\s*-----END CERTIFICATE-----/s', $fullchain, $m)) { + return false; + } + + $pubKey = openssl_pkey_get_public($m[0]); + if (!$pubKey) return false; + + $details = openssl_pkey_get_details($pubKey); + + return $details && ($details['type'] ?? 0) === OPENSSL_KEYTYPE_EC; + } } diff --git a/config/app.php b/config/app.php index c81ffc9..9b89298 100644 --- a/config/app.php +++ b/config/app.php @@ -31,7 +31,7 @@ return [ 'show_error_msg' => true, 'exception_tmpl' => \think\facade\App::getAppPath() . 'view/exception.tpl', - 'version' => '1043', + 'version' => '1044', 'dbversion' => '1040' ];