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:与参考逐平台对齐清单(登录 / 额度 / 推理链路三张表
+ 差分判据表 + 未对齐项按影响排序),未对齐项**明示**而非含糊。
98 lines
3.4 KiB
Go
98 lines
3.4 KiB
Go
package core
|
||
|
||
import (
|
||
"encoding/json"
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// CodeArts 的华为云 SDK-HMAC-SHA256 签名必须与参考**逐字节一致**,否则服务端
|
||
// 返回 `verify ak sk signature fail`,而这个平台的领取就永远失败 —— 且失败原因
|
||
// 会被误读成"账号问题"。此前插件里的实现是凭印象编的另一套形状
|
||
// (Credential={ak}/{ts}/{nonce}/sdk_request),必然验签失败。
|
||
//
|
||
// 期望值由参考实现(sign.ts:signRequestHuawei,Node + node:crypto)生成:
|
||
//
|
||
// node tools/codearts_sig_expected.mjs > internal/core/codearts_sig_expected.json
|
||
//
|
||
// 判据覆盖:canonical request 的拼装、string-to-sign 格式、最终 signature、
|
||
// 以及参与签名的头列表(顺序与内容)。
|
||
func TestCodeArtsSignatureMatchesReference(t *testing.T) {
|
||
raw, err := os.ReadFile("codearts_sig_expected.json")
|
||
if err != nil {
|
||
t.Skipf("缺差分数据(跳过): %v", err)
|
||
}
|
||
var cases []struct {
|
||
Method string `json:"method"`
|
||
URL string `json:"url"`
|
||
Body string `json:"body"`
|
||
AK string `json:"ak"`
|
||
SK string `json:"sk"`
|
||
ST string `json:"st"`
|
||
Canonical string `json:"canonical"`
|
||
StringToSign string `json:"string_to_sign"`
|
||
Signature string `json:"signature"`
|
||
SignedHeaders string `json:"signed_headers"`
|
||
}
|
||
if err := json.Unmarshal(raw, &cases); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(cases) == 0 {
|
||
t.Fatal("差分数据为空")
|
||
}
|
||
|
||
c := kernelTestCore(t)
|
||
ps := c.Plugins()
|
||
src, err := readBundledChannels(t)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := ps.LoadSource("channels", src); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
for i, tc := range cases {
|
||
got, err := ps.Call("channels", "__codearts_sign_probe", map[string]interface{}{
|
||
"method": tc.Method, "url": tc.URL, "body": tc.Body,
|
||
"ak": tc.AK, "sk": tc.SK, "st": tc.ST, "date": "20261013T150405Z",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("case %d probe: %v", i, err)
|
||
}
|
||
w, _ := got.(map[string]interface{})
|
||
m, _ := w["json"].(map[string]interface{})
|
||
if m == nil {
|
||
t.Fatalf("case %d 无 json: %#v", i, got)
|
||
}
|
||
if m["error"] != nil {
|
||
t.Fatalf("case %d 探针报错: %v", i, m["error"])
|
||
}
|
||
if s, _ := m["canonical"].(string); s != tc.Canonical {
|
||
t.Errorf("case %d canonical 不一致\n 实得: %q\n 参考: %q", i, s, tc.Canonical)
|
||
}
|
||
if s, _ := m["string_to_sign"].(string); s != tc.StringToSign {
|
||
t.Errorf("case %d string-to-sign 不一致\n 实得: %q\n 参考: %q", i, s, tc.StringToSign)
|
||
}
|
||
if s, _ := m["signature"].(string); s != tc.Signature {
|
||
t.Errorf("case %d signature 不一致: 实得 %s 参考 %s", i, s, tc.Signature)
|
||
}
|
||
if s, _ := m["signed_headers"].(string); s != tc.SignedHeaders {
|
||
t.Errorf("case %d SignedHeaders 不一致: 实得 %q 参考 %q", i, s, tc.SignedHeaders)
|
||
}
|
||
// Authorization 头必须是 `SDK-HMAC-SHA256 Access=...`(不是 Credential=)
|
||
if s, _ := m["authorization"].(string); len(s) == 0 ||
|
||
s[:len("SDK-HMAC-SHA256 Access=")] != "SDK-HMAC-SHA256 Access=" {
|
||
t.Errorf("case %d Authorization 形状应为 SDK-HMAC-SHA256 Access=…,实得 %q", i, s)
|
||
}
|
||
// 签名后追加的头绝不能进 canonical
|
||
if s, _ := m["canonical"].(string); len(s) > 0 {
|
||
for _, bad := range []string{"Agent-Type", "X-Language", "agent-type", "x-language"} {
|
||
if strings.Contains(s, bad) {
|
||
t.Errorf("case %d canonical 里出现了签名后追加的头 %s(会导致验签失败)", i, bad)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|