mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
此前标成 flow="paste"(手填凭据)是**错的**:参考 login.ts/oauth.ts 默认走
新式 IAM OAuth —— 起本地回调服务器(端口须 ≥10000)、打开 portal 授权页、
收 code 后带 DPoP 证明向华为 STS 端点换 AK/SK/securityToken。
逐项对齐参考:
- authorize URL:code_challenge_method=SHA-256(**不是** S256,错值会让
portal 回退旧 ticket 流程)、theme=2、locale=zh-cn、plugin-name=snap_AIIDE、
plugin-version=5.2.0,且**不得**含 auth_callback_url
- 回调路径 /oauth/callback,端口 ≥10000(低于此 portal 拒绝)
- PKCE:verifier=48 随机字节 base64url,challenge=sha256(verifier) base64url
- DPoP:ES256 JWS,header{alg,typ,jwk} + payload{htm,htu,iat,jti},
签名为 **raw r||s 64 字节**(openssl 出 DER,必须转格式)
- token 请求:POST 表单 + DPoP 头;终态只认 invalid_grant /
ExpiredRefreshToken(**不认** InvalidDPoPHeader)
- 凭据含 refresh_token/code_verifier/dpop_private_key_jwk 以支持静默续期
- 旧 ticket 流程保留作回退(pollForCredential 语义)
本机实测的坑(已修):xxd 不存在(用 od);openssl ec -text 会截断 priv
(d 用 asn1parse 的 [HEX DUMP],tail -c 32 拿到的是公钥 y);min_port 参数
读到 argv[2](与 out_path 冲突)。
判据(4 条,全绿):
- TestCodeArtsDpopJwsVerifiesAsES256:用 Go crypto/ecdsa 当权威验签器,
验 JWS 是 64 字节 raw 且真实可验(只断言"能拼出字符串"防不住格式错)
- TestCodeArtsOAuthEndToEnd:假 STS 上游,检查交换请求形态(DPoP 头、
grant_type、code_verifier、redirect_uri)与凭据含续期材料
- TestCodeArtsLoopbackPortAtLeast10000:量实际端口与回调路径
- TestCodeArtsSignatureMatchesReference:既有签名判据
113 lines
3.3 KiB
Go
113 lines
3.3 KiB
Go
package core
|
||
|
||
import (
|
||
"crypto/ecdsa"
|
||
"crypto/elliptic"
|
||
"crypto/sha256"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"math/big"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// CodeArts 的 DPoP 证明是标准 ES256 JWS。用 Go 标准库(crypto/ecdsa)当权威
|
||
// 校验器验它 —— 只断言"能拼出字符串"是不够的:JWS 的 ES256 签名必须是
|
||
// **raw r||s(各 32 字节)**,而 openssl 默认输出 DER;写错格式服务端会拒,
|
||
// 而字符串看起来完全正常(这是最容易蒙混过去的一类缺陷)。
|
||
func TestCodeArtsDpopJwsVerifiesAsES256(t *testing.T) {
|
||
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)
|
||
}
|
||
|
||
got, err := ps.Call("channels", "__codearts_dpop_probe",
|
||
map[string]interface{}{"htm": "POST", "htu": "https://sts.cn-north-4.myhuaweicloud.com/v1/oauth2/tokens"})
|
||
if err != nil {
|
||
t.Fatalf("probe: %v", err)
|
||
}
|
||
w, _ := got.(map[string]interface{})
|
||
m, _ := w["json"].(map[string]interface{})
|
||
if m == nil {
|
||
t.Fatalf("无 json: %#v", got)
|
||
}
|
||
if m["error"] != nil {
|
||
t.Fatalf("探针报错: %v", m["error"])
|
||
}
|
||
jws, _ := m["jws"].(string)
|
||
if jws == "" {
|
||
t.Fatal("jws 为空")
|
||
}
|
||
parts := strings.Split(jws, ".")
|
||
if len(parts) != 3 {
|
||
t.Fatalf("JWS 应为三段,实得 %d 段", len(parts))
|
||
}
|
||
dec := func(s string) []byte {
|
||
b, err := base64.RawURLEncoding.DecodeString(s)
|
||
if err != nil {
|
||
t.Fatalf("base64url 解码失败: %v", err)
|
||
}
|
||
return b
|
||
}
|
||
// ---- header ----
|
||
var hdr map[string]interface{}
|
||
if err := json.Unmarshal(dec(parts[0]), &hdr); err != nil {
|
||
t.Fatalf("header 不是 JSON: %v", err)
|
||
}
|
||
if hdr["alg"] != "ES256" {
|
||
t.Errorf("alg 应为 ES256,实得 %v", hdr["alg"])
|
||
}
|
||
if hdr["typ"] != "dpop+jwt" {
|
||
t.Errorf("typ 应为 dpop+jwt,实得 %v", hdr["typ"])
|
||
}
|
||
jwk, _ := hdr["jwk"].(map[string]interface{})
|
||
if jwk == nil {
|
||
t.Fatal("header 必须内嵌 jwk(DPoP 要求公钥随证明走)")
|
||
}
|
||
// ---- 用 jwk 重建公钥 ----
|
||
xb, err := base64.RawURLEncoding.DecodeString(jwk["x"].(string))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
yb, err := base64.RawURLEncoding.DecodeString(jwk["y"].(string))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(xb) != 32 || len(yb) != 32 {
|
||
t.Fatalf("P-256 的 x/y 应各 32 字节,实得 %d/%d", len(xb), len(yb))
|
||
}
|
||
pub := &ecdsa.PublicKey{Curve: elliptic.P256(),
|
||
X: new(big.Int).SetBytes(xb), Y: new(big.Int).SetBytes(yb)}
|
||
|
||
// ---- payload ----
|
||
var pl map[string]interface{}
|
||
if err := json.Unmarshal(dec(parts[1]), &pl); err != nil {
|
||
t.Fatalf("payload 不是 JSON: %v", err)
|
||
}
|
||
for _, k := range []string{"htm", "htu", "iat", "jti"} {
|
||
if _, ok := pl[k]; !ok {
|
||
t.Errorf("payload 缺 %s(照参考 signDpopJws)", k)
|
||
}
|
||
}
|
||
if pl["htm"] != "POST" {
|
||
t.Errorf("htm = %v", pl["htm"])
|
||
}
|
||
|
||
// ---- 验签(关键:raw r||s,不是 DER)----
|
||
sig := dec(parts[2])
|
||
if len(sig) != 64 {
|
||
t.Fatalf("ES256 签名必须是 64 字节 raw r||s,实得 %d 字节(若是 70-72 字节,说明漏了 DER→raw 转换)", len(sig))
|
||
}
|
||
r := new(big.Int).SetBytes(sig[:32])
|
||
s := new(big.Int).SetBytes(sig[32:])
|
||
digest := sha256.Sum256([]byte(parts[0] + "." + parts[1]))
|
||
if !ecdsa.Verify(pub, digest[:], r, s) {
|
||
t.Fatal("ES256 验签失败 —— 签名内容或格式不对,服务端会拒绝这个 DPoP")
|
||
}
|
||
}
|