150 lines
5.7 KiB
JavaScript
150 lines
5.7 KiB
JavaScript
/**
|
||||
|
|
* 部署适配层离线自测 —— 不联网,只验两块纯算法:
|
|||
|
|
* ① 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);
|