feat: 规范化 JSON 与三语言互验加固;app-routes 生成器
- wire.CanonicalJSON 自己写字符串转义:Go 编码器即使在关闭 HTML 转义时仍会转义 U+2028/U+2029,而 Node 与 Foundation 输出字面量;两处拼写解码结果相同, 因此签名不同却看不出原因。孤立代理项改为显式拒绝(Go 会替换成 U+FFFD、 Node 保留转义文本、Foundation 直接解析失败,三种行为只能取交集为空) - 对象键排序按 UTF-8 字节序(Go 的 sort.Strings 语义);Node 参考实现改用 Buffer.compare,因为 Array.sort 的 UTF-16 码元序在星形平面边界上与之相反 - test-vectors.json 新增转义、键序与必须拒绝的输入;Go/Node 双向断言 - 新增 scripts/export-app-routes.swift 生成 contracts/app-routes.json, 补上此前 vendored 路由表靠手抄的一环 - README 记录 iOS 侧向量校验为何是'互相验证'而非'逐字节相同'(CryptoKit 的 Ed25519 签名随机化)
This commit is contained in:
+42
-1
@@ -136,7 +136,8 @@ func main() {
|
||||
Body: `{"phone_number":"+8613800000000","content":"hello"}`,
|
||||
Digest: hexStr(intent),
|
||||
},
|
||||
CanonicalJSON: jsonPairs(),
|
||||
CanonicalJSON: jsonPairs(),
|
||||
CanonicalRejected: rejectedPairs(),
|
||||
}
|
||||
|
||||
encoded, err := json.MarshalIndent(document, "", " ")
|
||||
@@ -167,6 +168,8 @@ type vectors struct {
|
||||
AEAD aeadVector `json:"aead"`
|
||||
Intent intentVector `json:"intent"`
|
||||
CanonicalJSON []jsonPairVector `json:"canonicalJson"`
|
||||
// CanonicalRejected are inputs every implementation must refuse; see rejectedPairs.
|
||||
CanonicalRejected []string `json:"canonicalJsonRejected"`
|
||||
}
|
||||
|
||||
type canonicalVector struct {
|
||||
@@ -229,6 +232,31 @@ func jsonPairs() []jsonPairVector {
|
||||
`{"x":100}`,
|
||||
`{"a":{"c":[1,{"b":2,"a":1}],"b":0},"A":9}`,
|
||||
`{"phone_number":"+8613800000000","content":"hello"}`,
|
||||
// Escaping edge cases. Each of these has been observed to differ between Go, Node and
|
||||
// Foundation, so they are pinned rather than assumed:
|
||||
//
|
||||
// - U+2028/U+2029: Go's encoder escapes them even with HTML escaping off; Node's
|
||||
// JSON.stringify and Foundation emit them literally. Both spellings decode to the same
|
||||
// string, so a signature over the escape differs while the payload looks identical.
|
||||
// - A paired surrogate: the three languages must agree on the astral character, which
|
||||
// means agreeing that it is written as literal UTF-8 and not as two \u escapes.
|
||||
// - Control characters below 0x20 with and without short forms.
|
||||
// - A literal backslash-u sequence, which is text and must not be read as an escape.
|
||||
`{"content":"a` + "\u2028" + `b"}`,
|
||||
`{"content":"a` + "\u2029" + `b"}`,
|
||||
`{"content":"` + "\U0001F600" + `"}`,
|
||||
`{"content":"\b\f\u0001"}`,
|
||||
`{"content":"\\u2028"}`,
|
||||
// Key ordering. The rule is UTF-8 byte order, which is what Go's sort.Strings does, and it
|
||||
// is not what either shortcut produces: Node's default sort compares UTF-16 code units
|
||||
// (so an astral key starting with a D800 surrogate sorts below U+FFFD, while its UTF-8
|
||||
// form starting with F0 sorts above), and Swift's default String comparison uses Unicode
|
||||
// collation (so "é" sorts next to "e" rather than after every ASCII byte).
|
||||
//
|
||||
// {"z","é"}: UTF-8 has z(0x7A) before é(0xC3A9); collation reverses that.
|
||||
// {"\uFFFD","\U00010000"}: UTF-16 puts the surrogate first; UTF-8 puts F0 first.
|
||||
"{\"\u00e9\":1,\"z\":2}",
|
||||
"{\"\U00010000\":1,\"\uFFFD\":2}",
|
||||
}
|
||||
pairs := make([]jsonPairVector, 0, len(inputs))
|
||||
for _, input := range inputs {
|
||||
@@ -239,6 +267,19 @@ func jsonPairs() []jsonPairVector {
|
||||
return pairs
|
||||
}
|
||||
|
||||
// rejectedPairs are inputs every implementation must refuse. They are in the vectors so the Swift
|
||||
// and Node sides have the same list to check against rather than each inventing its own tolerance:
|
||||
// a lone surrogate decodes to U+FFFD in Go, stays as escape text in Node, and fails to parse in
|
||||
// Foundation, so the only consistent rule is to accept none of them.
|
||||
func rejectedPairs() []string {
|
||||
return []string{
|
||||
`{"content":"\ud800"}`,
|
||||
`{"content":"\udc00"}`,
|
||||
`{"content":"\ud800\ud800"}`,
|
||||
`{"content":"\ud800x"}`,
|
||||
}
|
||||
}
|
||||
|
||||
func mustHex(value string) []byte {
|
||||
raw, err := hex.DecodeString(value)
|
||||
check(err)
|
||||
|
||||
Reference in New Issue
Block a user