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