Files
blog/blog-admin/tools/selftest-deploy.mjs
T

150 lines
5.7 KiB
JavaScript
Raw Normal View History

/**
* 部署适配层离线自测 —— 不联网,只验两块纯算法:
* ① 1Panel 的 `md5("1panel" + apiKey + timestamp)` 签名
* ② 多吉云 `HMAC-SHA1(secretKey, path+"\n"+body)` → hex 的签名
*
* 这两块是**最容易静默出错**的地方:签名算错了,服务端只会回一句
* 「签名错误」,看不出是哪一步错的。用 Node 的 crypto 独立复算一遍,
* 至少保证「算法本身」是对的。
*
* 跑法:node tools/selftest-deploy.mjs
*/
import crypto from 'node:crypto';
let pass = 0;
const fails = [];
function t(name, cond, extra = '') {
if (cond) {
pass++;
console.log(' ✓ ' + name);
} else {
fails.push(name + (extra ? ' → ' + extra : ''));
console.log(' ✗ ' + name + (extra ? ' → ' + extra : ''));
}
}
// ---- 复刻 deployer.ts 里的 md5(RFC 1321)----
function md5(bytes) {
const S = [
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14,
20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6,
10, 15, 21,
];
const K = new Uint32Array(64);
for (let i = 0; i < 64; i++) K[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 4294967296) >>> 0;
const len = bytes.length;
const withPad = new Uint8Array((((len + 8) >> 6) + 1) << 6);
withPad.set(bytes);
withPad[len] = 0x80;
const bitLen = len * 8;
const lo = bitLen >>> 0;
const hi = Math.floor(bitLen / 4294967296) >>> 0;
const dv = new DataView(withPad.buffer);
dv.setUint32(withPad.length - 8, lo, true);
dv.setUint32(withPad.length - 4, hi, true);
let a0 = 0x67452301,
b0 = 0xefcdab89,
c0 = 0x98badcfe,
d0 = 0x10325476;
const rotl = (x, c) => ((x << c) | (x >>> (32 - c))) >>> 0;
for (let off = 0; off < withPad.length; off += 64) {
const M = new Uint32Array(16);
for (let i = 0; i < 16; i++) M[i] = dv.getUint32(off + i * 4, true);
let A = a0,
B = b0,
C = c0,
D = d0;
for (let i = 0; i < 64; i++) {
let F, g;
if (i < 16) {
F = (B & C) | (~B & D);
g = i;
} else if (i < 32) {
F = (D & B) | (~D & C);
g = (5 * i + 1) % 16;
} else if (i < 48) {
F = B ^ C ^ D;
g = (3 * i + 5) % 16;
} else {
F = C ^ (B | ~D);
g = (7 * i) % 16;
}
F = (F + A + K[i] + M[g]) >>> 0;
A = D;
D = C;
C = B;
B = (B + rotl(F, S[i])) >>> 0;
}
a0 = (a0 + A) >>> 0;
b0 = (b0 + B) >>> 0;
c0 = (c0 + C) >>> 0;
d0 = (d0 + D) >>> 0;
}
return [a0, b0, c0, d0]
.map((x) => {
const b = new Uint8Array(4);
new DataView(b.buffer).setUint32(0, x, true);
return [...b].map((v) => v.toString(16).padStart(2, '0')).join('');
})
.join('');
}
const enc = (s) => new TextEncoder().encode(s);
console.log('\n[1] md5(1Panel 签名的核心)');
const vectors = [
['', 'd41d8cd98f00b204e9800998ecf8427e'],
['a', '0cc175b9c0f1b6a831c399e269772661'],
['abc', '900150983cd24fb0d6963f7d28e17f72'],
['message digest', 'f96b697d7cb7938d525a2f31aaf161d0'],
['12345678901234567890123456789012345678901234567890123456789012345678901234567890',
'57edf4a22be3c955ac49da2e2107b67a'],
];
for (const [input, want] of vectors) {
const got = md5(enc(input));
t(`md5("${input.slice(0, 20)}${input.length > 20 ? '…' : ''}")`, got === want, `got ${got} want ${want}`);
}
// 长度跨过 55/56/64 边界(补位逻辑最容易在这里错)
t('md5 在 55 字节输入下正确', md5(enc('a'.repeat(55))) === 'ef1772b6dff9a122358552954ad0df65', md5(enc('a'.repeat(55))));
t('md5 在 56 字节输入下正确', md5(enc('a'.repeat(56))) === '3b0c8ac703f828b04c6c197006d17218', md5(enc('a'.repeat(56))));
t('md5 在 64 字节输入下正确', md5(enc('a'.repeat(64))) === '014842d480b571495a4a0363793f7367', md5(enc('a'.repeat(64))));
console.log('\n[2] 1Panel 签名串格式');
const apiKey = 'test-api-key-1234';
const ts = '1767225600';
const mine = md5(enc(`1panel${apiKey}${ts}`));
const ref = crypto.createHash('md5').update(`1panel${apiKey}${ts}`).digest('hex');
t('自制 md5 与 Node crypto 一致', mine === ref, `${mine} vs ${ref}`);
t('签名是 32 位小写 hex', /^[0-9a-f]{32}$/.test(mine), mine);
t('★ 前缀必须是字面量 "1panel"', md5(enc(`1Panel${apiKey}${ts}`)) !== mine, '大小写不同应得到不同结果');
console.log('\n[3] 多吉云签名(HMAC-SHA1 → hex)');
function dogeSign(secretKey, path, body) {
return crypto.createHmac('sha1', secretKey).update(`${path}\n${body}`).digest('hex');
}
const sk = 'test-secret-key';
const path = '/cdn/cert/upload.json';
const body = '{"note":"usj.cc","cert":"-----BEGIN","private":"-----BEGIN"}';
const sig = dogeSign(sk, path, body);
t('签名是 40 位小写 hex(SHA1)', /^[0-9a-f]{40}$/.test(sig), sig);
t('★ 用的必须是 SHA1 不是 SHA256', sig.length === 40, `len=${sig.length}`);
t('★ stringToSign 是 path + "\\n" + body', dogeSign(sk, path, body) === dogeSign(sk, path, body), '');
t('body 变了签名必须变', dogeSign(sk, path, body + ' ') !== sig, 'trailing space 应改变签名');
t('path 变了签名必须变', dogeSign(sk, '/cdn/cert/bind.json', body) !== sig, '');
console.log('\n[4] 多吉云 Authorization 头格式');
const ak = 'AKIDtest1234567890';
const authHeader = `TOKEN ${ak}:${sig}`;
t('形如 `TOKEN <ak>:<sig>`', /^TOKEN [^:]+:[0-9a-f]{40}$/.test(authHeader), authHeader.slice(0, 30) + '…');
t('★ 分隔符是冒号不是空格', authHeader.includes(`${ak}:`), '');
console.log('\n' + '='.repeat(56));
console.log(`通过 ${pass} / ${pass + fails.length}`);
if (fails.length) {
console.log('\n失败项:');
for (const f of fails) console.log(' · ' + f);
}
process.exit(fails.length ? 1 : 0);