mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
## 问题:签名是实现者凭印象编的
原实现用 `SDK-HMAC-SHA256 Credential={ak}/{ts}/{nonce}/sdk_request` —— 那是另一套
协议(阿里风格)的形状,与参考的**华为云 SDK-HMAC-SHA256** 完全不同:
- canonical request 拼装错(参考是 METHOD\nURI\nQUERY\n排序后的 k:v 行\n\nSIGNEDHEADERS\nPAYLOADHASH)
- string-to-sign 格式错(参考是 "SDK-HMAC-SHA256\n{dateStamp}\n{sha256(canonical)}")
- Authorization 头用 Credential= 而非 **Access=**
- 少了 x-sdk-content-sha256 / x-security-token 参与签名
- URI 末尾未补 '/'
⇒ 服务端只会回 `APIG.0301 ... verify ak sk signature fail`,看起来像"账号/权限问题",
这个平台注定领不到额度。
## 修
逐行对齐参考 `sign.ts:signRequestHuawei` + `codearts-credits.ts`:
1. canonical request / string-to-sign / signature 全部照参考
2. 参与签名的头:host / x-sdk-date / x-sdk-content-sha256 / x-security-token
(非 GET 再加 content-type),按字典序拼 SignedHeaders
3. Authorization = `SDK-HMAC-SHA256 Access={ak},SignedHeaders=…,Signature=…`
4. Agent-Type / X-Language 是**签名后**追加的,绝不进 canonical(否则验签失败)
5. URI 末尾补 '/'
6. 完整领取 4 步(参考的 6 步判定顺序):账户类型 → 活动列表 → claim →(benefit.id
非空时)confirm;用 done 哨兵回给 claim_daily(它自己发多个请求,契约只发一个)
- 非积分账户 → inactive(Token 账户不该被报成"领取失败")
- activity.claimable=false 且状态属已领取态 → already-claimed(**唯一的幂等保护**,
本协议没有幂等键)
- confirm 失败不算整体失败(积分已在待确认态),只是降级提示
## 判据(差分,与参考逐字节一致)
`TestCodeArtsSignatureMatchesReference` + `tools/codearts_sig_expected.mjs`
(照抄参考函数生成期望值)。覆盖 canonical / string_to_sign / signature /
SignedHeaders / Authorization 形状 / canonical 不得含签名后追加的头。
变异验证:`Access=`→`Credential=`、去掉 content-type 参与签名、URI 不补斜杠
—— 三个全部被抓。
★ 修判据自身缺陷:首版探针**自己重算** canonical,导致真实签名被改坏时判据仍绿
(去掉 content-type 的变异存活)。改为让签名函数返回真实中间值,探针直接取用。
## 文档
新增 docs/channels-parity.md:与参考逐平台对齐清单(登录 / 额度 / 推理链路三张表
+ 差分判据表 + 未对齐项按影响排序),未对齐项**明示**而非含糊。
114 lines
3.4 KiB
JavaScript
114 lines
3.4 KiB
JavaScript
// 生成 CodeArts 华为云签名派生的差分期望值(供 internal/core/codearts_sign_test.go 比对)。
|
||
//
|
||
// 为什么需要:channels.lua 里的华为云 SDK-HMAC-SHA256 签名是**手工移植**
|
||
// (Lua 没有 webcrypto)。签名错一处,服务端只回一句
|
||
// `{"error_code":"APIG.0301","error_msg":"...verify ak sk signature fail"}`,
|
||
// 看起来像"账号/权限问题",极难定位。所以拿参考实现生成期望值做逐字节差分。
|
||
//
|
||
// 参考真值:vendor/channel-pack/src/sign.ts 的 signRequestHuawei。
|
||
// 这里**逐行照抄**那个函数(而非 import)—— 它依赖参考仓库的构建链,
|
||
// 抄这 40 行更能保证"同一算法"。
|
||
//
|
||
// 用法(改了参考或改了移植后都要重跑):
|
||
// node tools/codearts_sig_expected.mjs > internal/core/codearts_sig_expected.json
|
||
|
||
import { createHash, createHmac } from 'node:crypto'
|
||
|
||
const sha256Hex = (d) => createHash('sha256').update(d).digest('hex')
|
||
const hmacSha256Hex = (key, data) => createHmac('sha256', key).update(data).digest('hex')
|
||
|
||
/** 照抄 sign.ts:signRequestHuawei(dateStamp 提为参数以便固定时间)。 */
|
||
function signRequestHuawei(ak, sk, securityToken, method, urlStr, body, dateStamp) {
|
||
const url = new URL(urlStr)
|
||
let uri = url.pathname
|
||
if (!uri.endsWith('/')) uri += '/'
|
||
const query = url.search.slice(1)
|
||
const payloadHash = sha256Hex(body)
|
||
|
||
const headers = new Map()
|
||
headers.set('host', url.host)
|
||
headers.set('x-sdk-date', dateStamp)
|
||
headers.set('x-sdk-content-sha256', payloadHash)
|
||
headers.set('x-security-token', securityToken)
|
||
if (method.toUpperCase() !== 'GET') headers.set('content-type', 'application/json')
|
||
|
||
const signedHeaders = []
|
||
headers.forEach((_, k) => signedHeaders.push(k))
|
||
signedHeaders.sort()
|
||
const headerLines = signedHeaders.map((k) => `${k}:${headers.get(k) ?? ''}`)
|
||
const canonical = [
|
||
method,
|
||
uri,
|
||
query,
|
||
headerLines.join('\n'),
|
||
'',
|
||
signedHeaders.join(';'),
|
||
payloadHash,
|
||
].join('\n')
|
||
const stringToSign = `SDK-HMAC-SHA256\n${dateStamp}\n${sha256Hex(new TextEncoder().encode(canonical))}`
|
||
const signature = hmacSha256Hex(
|
||
new TextEncoder().encode(sk),
|
||
new TextEncoder().encode(stringToSign),
|
||
)
|
||
return {
|
||
canonical,
|
||
stringToSign,
|
||
signature,
|
||
signedHeaders: signedHeaders.join(';'),
|
||
}
|
||
}
|
||
|
||
// 覆盖三条真实路径(账户查询 / 活动列表带 query / 领取带 body),
|
||
// 以及有/无 security_token 两种形态。
|
||
const cases = [
|
||
{
|
||
m: 'GET',
|
||
u: 'https://snap-access.cn-north-4.myhuaweicloud.com/snap-manager/v1/statistics/plugin',
|
||
b: '',
|
||
ak: 'AK1',
|
||
sk: 'SK1',
|
||
st: 'TOK1',
|
||
},
|
||
{
|
||
m: 'GET',
|
||
u: 'https://snap-access.cn-north-4.myhuaweicloud.com/v1/ops/delivery?channel=IDE',
|
||
b: '',
|
||
ak: 'AK2',
|
||
sk: 'SK2',
|
||
st: '',
|
||
},
|
||
{
|
||
m: 'POST',
|
||
u: 'https://snap-access.cn-north-4.myhuaweicloud.com/v1/ops/claim',
|
||
b: '{"campaignId":"C1","channel":"IDE"}',
|
||
ak: 'AK3',
|
||
sk: 'SK3',
|
||
st: 'T3',
|
||
},
|
||
]
|
||
|
||
const out = cases.map((c) => {
|
||
const r = signRequestHuawei(
|
||
c.ak,
|
||
c.sk,
|
||
c.st,
|
||
c.m,
|
||
c.u,
|
||
Buffer.from(c.b, 'utf8'),
|
||
'20261013T150405Z',
|
||
)
|
||
return {
|
||
method: c.m,
|
||
url: c.u,
|
||
body: c.b,
|
||
ak: c.ak,
|
||
sk: c.sk,
|
||
st: c.st,
|
||
canonical: r.canonical,
|
||
string_to_sign: r.stringToSign,
|
||
signature: r.signature,
|
||
signed_headers: r.signedHeaders,
|
||
}
|
||
})
|
||
process.stdout.write(`${JSON.stringify(out, null, 1)}\n`)
|