153 lines
7.3 KiB
PHP
153 lines
7.3 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | RSA 密钥自检 —— 排查下游报 errCode=0007「请求参数解析异常」(CLI)
|
||
// +----------------------------------------------------------------------
|
||
// | 0007 的实际含义是:我方用【平台私钥】解不开下游加密的 AES 密钥。
|
||
// | 报错里的「PEM routines:no start line」是 OpenSSL 错误队列的残留信息,
|
||
// | 具有误导性,真正原因几乎都是:下游加密时用的公钥 ≠ 我方平台私钥对应的公钥。
|
||
// |
|
||
// | 用法(站点根目录执行):
|
||
// | php deploy/rsa_check.php 自检密钥对 + 打印要发给下游的公钥
|
||
// | php deploy/rsa_check.php <appKey> 再检查该合作商的公钥(用于验签其请求)
|
||
// | php deploy/rsa_check.php <appKey> "<key密文>" 用请求里的 key 字段实测能否解开
|
||
// |
|
||
// | 其中「key密文」是下游请求体 {"key":"...","data":"..."} 里的 key 原值。
|
||
// +----------------------------------------------------------------------
|
||
declare (strict_types=1);
|
||
|
||
if (PHP_SAPI !== 'cli') {
|
||
http_response_code(403);
|
||
exit('仅限命令行执行');
|
||
}
|
||
|
||
$root = dirname(__DIR__);
|
||
$appKey = $argv[1] ?? '';
|
||
$encKey = $argv[2] ?? '';
|
||
|
||
function line(): void
|
||
{
|
||
echo str_repeat('-', 62) . "\n";
|
||
}
|
||
|
||
/** 取密钥指纹,便于和对方核对是不是同一把 */
|
||
function fingerprint($keyRes): string
|
||
{
|
||
$d = openssl_pkey_get_details($keyRes);
|
||
return isset($d['rsa']['n']) ? strtoupper(substr(md5($d['rsa']['n']), 0, 16)) : '(取不到)';
|
||
}
|
||
|
||
// ================================================================
|
||
// 1. 平台密钥对
|
||
// ================================================================
|
||
echo "【1】平台密钥对\n";
|
||
line();
|
||
|
||
$privPath = $root . '/rsa/private_key.pem';
|
||
$pubPath = $root . '/rsa/public_key.pem';
|
||
|
||
foreach ([$privPath, $pubPath] as $p) {
|
||
printf(" %-28s %s\n", basename($p), is_file($p) ? '存在(' . filesize($p) . ' 字节)' : '❌ 不存在');
|
||
}
|
||
if (!is_file($privPath) || !is_file($pubPath)) {
|
||
exit("\n❌ 密钥文件缺失,下游所有请求都会失败。\n");
|
||
}
|
||
|
||
$priv = openssl_pkey_get_private(file_get_contents($privPath));
|
||
$pub = openssl_pkey_get_public(file_get_contents($pubPath));
|
||
printf(" 平台私钥加载%s\n", $priv ? '成功' : '❌ 失败:' . openssl_error_string());
|
||
printf(" 平台公钥加载%s\n", $pub ? '成功' : '❌ 失败:' . openssl_error_string());
|
||
if (!$priv || !$pub) {
|
||
exit("\n❌ 密钥文件格式不对,请检查是否为完整 PEM(含 BEGIN/END 行)。\n");
|
||
}
|
||
|
||
$dPriv = openssl_pkey_get_details($priv);
|
||
$dPub = openssl_pkey_get_details($pub);
|
||
printf(" 位数:私钥 %d / 公钥 %d\n", $dPriv['bits'], $dPub['bits']);
|
||
$pairOk = $dPriv['key'] === $dPub['key'];
|
||
printf(" 两者是否同一对:%s\n", $pairOk ? '是' : '❌ 否(公私钥不配套,必须换成同一对)');
|
||
|
||
// 实测闭环
|
||
$aes = openssl_random_pseudo_bytes(32);
|
||
openssl_public_encrypt($aes, $enc, $pub, OPENSSL_PKCS1_PADDING);
|
||
$loopOk = openssl_private_decrypt($enc, $dec, $priv, OPENSSL_PKCS1_PADDING) && $dec === $aes;
|
||
printf(" 加密→解密闭环:%s\n", $loopOk ? '通过' : '❌ 失败');
|
||
printf(" 平台公钥指纹:%s\n", fingerprint($pub));
|
||
|
||
// ================================================================
|
||
// 2. 发给下游的公钥(他们必须用这一把加密 AES 密钥)
|
||
// ================================================================
|
||
echo "\n【2】请把下面这把【平台公钥】发给下游,让他们核对\n";
|
||
line();
|
||
echo trim(file_get_contents($pubPath)) . "\n\n";
|
||
echo "一行 base64(Java 可直接用):\n";
|
||
echo preg_replace('/\s+/', '', str_replace(['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----'], '', file_get_contents($pubPath))) . "\n";
|
||
|
||
// ================================================================
|
||
// 3. 合作商公钥(我方用它验签下游请求)
|
||
// ================================================================
|
||
if ($appKey !== '') {
|
||
echo "\n【3】合作商 {$appKey}\n";
|
||
line();
|
||
try {
|
||
$ini = @parse_ini_file($root . '/.env', true, INI_SCANNER_RAW) ?: [];
|
||
$db = $ini['DATABASE'] ?? [];
|
||
$pdo = new PDO(
|
||
'mysql:host=' . ($db['HOSTNAME'] ?? '127.0.0.1') . ';port=' . ($db['HOSTPORT'] ?? '3306')
|
||
. ';dbname=' . ($db['DATABASE'] ?? 'oil') . ';charset=' . ($db['CHARSET'] ?? 'utf8'),
|
||
$db['USERNAME'] ?? 'root', $db['PASSWORD'] ?? '',
|
||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||
);
|
||
$st = $pdo->prepare('SELECT partners_id, name, orgNo, appKey, publicKey, privateKey FROM yoshop_partners WHERE appKey = ? LIMIT 1');
|
||
$st->execute([$appKey]);
|
||
$p = $st->fetch();
|
||
if (!$p) {
|
||
echo " ❌ 未找到该 appKey 的合作商\n";
|
||
} else {
|
||
printf(" 合作商:%s(partners_id=%s,orgNo=%s)\n", $p['name'], $p['partners_id'], $p['orgNo']);
|
||
$ppub = openssl_pkey_get_public((string) $p['publicKey']);
|
||
printf(" 合作商公钥:%s%s\n", $ppub ? '可加载' : '❌ 无法加载:' . openssl_error_string(),
|
||
$ppub ? ' 指纹 ' . fingerprint($ppub) : '');
|
||
echo " 用途:我方用它验签下游请求。下游必须用与之配套的私钥加签,否则报 0006。\n";
|
||
if (!empty($p['privateKey'])) {
|
||
$ppriv = openssl_pkey_get_private((string) $p['privateKey']);
|
||
if ($ppub && $ppriv) {
|
||
$dd = openssl_pkey_get_details($ppriv);
|
||
printf(" 库里另存的合作商私钥与其公钥是否配套:%s\n",
|
||
$dd['key'] === openssl_pkey_get_details($ppub)['key'] ? '是' : '❌ 否');
|
||
echo " (这把私钥是发给下游用的,请确认下游手里就是它)\n";
|
||
}
|
||
}
|
||
}
|
||
} catch (Throwable $e) {
|
||
echo ' 查询失败:' . $e->getMessage() . "\n";
|
||
}
|
||
}
|
||
|
||
// ================================================================
|
||
// 4. 用真实请求里的 key 密文实测
|
||
// ================================================================
|
||
if ($encKey !== '') {
|
||
echo "\n【4】实测解密下游请求里的 key\n";
|
||
line();
|
||
$raw = base64_decode($encKey, true);
|
||
if ($raw === false) {
|
||
echo " ❌ 不是合法 base64\n";
|
||
} else {
|
||
printf(" 密文长度:%d 字节(%d 位密钥应为 %d 字节)\n", strlen($raw), $dPriv['bits'], $dPriv['bits'] / 8);
|
||
while (openssl_error_string() !== false) {
|
||
// 先清空 OpenSSL 错误队列,避免读到别处残留的 PEM 报错
|
||
}
|
||
$ok = openssl_private_decrypt($raw, $out, $priv, OPENSSL_PKCS1_PADDING);
|
||
if ($ok) {
|
||
printf(" ✅ 解密成功,AES 密钥长度 %d 字节(应为 32)\n", strlen($out));
|
||
echo " 说明密钥是配套的,0007 的原因在别处,请把完整请求发来再看。\n";
|
||
} else {
|
||
echo " ❌ 解密失败:" . (openssl_error_string() ?: '无附加信息') . "\n";
|
||
echo " 结论:下游加密用的公钥,与我方平台私钥不是一对。\n";
|
||
echo " 让下游换成【2】里打印的那把平台公钥即可。\n";
|
||
}
|
||
}
|
||
}
|
||
|
||
echo "\n";
|