feat: 独立成仓库,自己拥有契约文件
agent 从 simadmin-relay 拆出来,作为公开仓库:安装命令要由节点执行,而节点没有凭据,所以 二进制必须放在匿名可读的地方,而 relay 源码与 IPA 继续私有。 拆出来之后有一处必须改,不是搬文件:`contracts/agent-policy.json` 原先由 relay 仓库拥有、 agent 侧是 vendored 副本(Makefile 从 `../contracts/` 拷、policy_test.go 断言两份相等)。 现在反过来——**agent 拥有策略**,因为 agent 是执行它的那一方,让它去拷贝一份住在别处的文件 本来就是别扭的。relay 保留 vendored 副本(它的限额从同一组数字推导),而两侧的一致性不再 靠构建期比较(跨仓库的构建期比较谁也做不了),改由运行时保证:agent 在握手时上报策略摘要, relay 拿自己那份比对,在 `/v1/status` 里逐节点给出 `policyMatches`。这比原来的测试更强, 因为它比的是**实际部署的那两份**,而不是两个 checkout 里的文件。 `contracts/test-vectors.json` 同理:Go 实现生成它(`make vectors`),relay 的 Node 参考实现 与 app 的 Swift 测试从 vendored 副本重算。实测重新生成的结果与 relay 那份逐字节相同。 systemd 单元的 `Documentation=` 指向本仓库。`VERSION` 提到 1.0.0。 20 个提交的历史完整保留(filter-branch 按子目录抽取)。
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"name": "msa-agent-policy",
|
||||
"comment": "Authorisation surface for relay-issued commands. Deliberately narrower than the web console proxy policy: this is what the iPhone app can actually ask for, nothing more.",
|
||||
"egress": {
|
||||
"mode": "loopback-only",
|
||||
"portOnly": true,
|
||||
"comment": "The agent config carries a port, never a host. Outbound connections are dialed to 127.0.0.1/::1 by the transport itself, so there is no host field to misconfigure and no DNS-rebinding path.",
|
||||
"dialedHosts": ["127.0.0.1", "::1"]
|
||||
},
|
||||
"authPaths": {
|
||||
"pattern": "^/api/(?:auth(?:/|$)|login(?:/|$)|logout(?:/|$))",
|
||||
"proxied": false,
|
||||
"comment": "The agent logs into the local SimAdmin itself and holds the credential. Login material never traverses the relay in either direction."
|
||||
},
|
||||
"read": [
|
||||
"/api/health",
|
||||
"/api/device",
|
||||
"/api/sim",
|
||||
"/api/network",
|
||||
"/api/network/signal-strength",
|
||||
"/api/stats",
|
||||
"/api/stats/cpu",
|
||||
"/api/connectivity",
|
||||
"/api/sms/list"
|
||||
],
|
||||
"write": {
|
||||
"/api/sms/send": ["POST"],
|
||||
"/api/sms/batch-delete": ["POST"]
|
||||
},
|
||||
"dangerous": ["/api/sms/send", "/api/sms/batch-delete"],
|
||||
"limits": {
|
||||
"dangerousWritesPerMinute": 6,
|
||||
"agentUpstreamTimeoutSeconds": 20,
|
||||
"maxRequestBytes": 262144,
|
||||
"maxResponseBytes": 262144,
|
||||
"requestTtlSeconds": 30,
|
||||
"clockSkewSeconds": 120,
|
||||
"replayCacheSize": 4096,
|
||||
"mailboxDepth": 8
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"generatedBy": "agent/cmd/msa-vectors",
|
||||
"note": "Deterministic cross-language vectors. The Node relay (test/wire.test.js) and the Swift app recompute these from this file; a mismatch means the envelope encoding drifted, not that one side is broken.",
|
||||
"suite": "X25519-ED25519-AES256GCM-HKDFSHA256",
|
||||
"version": 1,
|
||||
"canonical": {
|
||||
"comment": "Length-prefixed canonical bytes, then the deterministic Ed25519 signature over them. ek and p are raw decoded bytes, never their base64 text.",
|
||||
"input": {
|
||||
"v": 1,
|
||||
"node": "nd_7f3a9c2e",
|
||||
"req": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
|
||||
"app": "ap_5b1d",
|
||||
"ts": 1727289600,
|
||||
"ek": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo=",
|
||||
"alg": "X25519-ED25519-AES256GCM-HKDFSHA256",
|
||||
"p": "AAEC/v8="
|
||||
},
|
||||
"bytes": {
|
||||
"request": "6d73612d72656c61792d76312d726571000000000800000000000000010000000b6e645f37663361396332650000002433663235303465302d346638392d343164332d396130632d3033303565383263333330310000000761705f35623164000000080000000066f45900000000208520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a00000005000102feff",
|
||||
"response": "6d73612d72656c61792d76312d72657370000000000800000000000000010000000b6e645f37663361396332650000002433663235303465302d346638392d343164332d396130632d303330356538326333333031000000080000000066f45900000000208520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a00000005000102feff"
|
||||
},
|
||||
"signature": {
|
||||
"request": "f91fc4a40c4cb19643515894dba14a1baae602d0b307feafcf441a2d64690b94ac10e20982f8f3e8d2b48c74db11d250344256af92f7c22fa1b2c2fb6975fb0f",
|
||||
"response": "3977dc4ad544ed83d9921be9929c49f5b38cd618583d99d10d096d3997237c02110da17ffebd45656453d94cd07044d2b1348e0c45381fa193bf5fb08b30b50e"
|
||||
},
|
||||
"publicKey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
|
||||
"responsePublicKey": "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c"
|
||||
},
|
||||
"hkdf": {
|
||||
"comment": "RFC 5869 test case 1, pinning the KDF itself rather than only its callers.",
|
||||
"secret": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
|
||||
"salt": "000102030405060708090a0b0c",
|
||||
"info": "f0f1f2f3f4f5f6f7f8f9",
|
||||
"length": 42,
|
||||
"output": "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"
|
||||
},
|
||||
"x25519": {
|
||||
"comment": "RFC 7748 section 6.1, then the derived per-direction key and IV for the envelope above. The IV is bound to the request id and the direction, so a key that is ever reused still cannot reuse a nonce.",
|
||||
"appPrivate": "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a",
|
||||
"agentPublic": "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f",
|
||||
"sharedSecret": "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742",
|
||||
"requestKey": "14f4f1823bd68aabccf326e5a4ee69891428491477bfc6949a25dd69bff1b1e9",
|
||||
"requestIV": "e5be943a5b8ea89e34bbcb02",
|
||||
"responseKey": "e350511cf24be4045c770180276146ec647283695394dec608f1695b5f07524e",
|
||||
"responseIV": "44c5c0499337c5da62b463ad"
|
||||
},
|
||||
"aead": {
|
||||
"comment": "AES-256-GCM under the request key above; the 16-byte tag is appended.",
|
||||
"key": "14f4f1823bd68aabccf326e5a4ee69891428491477bfc6949a25dd69bff1b1e9",
|
||||
"iv": "e5be943a5b8ea89e34bbcb02",
|
||||
"plaintext": "7b226d223a22474554222c2270223a222f6170692f6865616c7468227d",
|
||||
"ciphertext": "6601340c304905a8308894ab9d050d5e2ea083ee7ad43df01b9c3e79daead5321099e7203abd5985cf2ef12114"
|
||||
},
|
||||
"intent": {
|
||||
"comment": "sha256(method | path | canonical body). The agent recomputes it from the request it is about to forward, so a relay cannot edit a confirmed write.",
|
||||
"method": "POST",
|
||||
"path": "/api/sms/send",
|
||||
"body": "{\"phone_number\":\"+8613800000000\",\"content\":\"hello\"}",
|
||||
"digest": "d0f584d844cf6e25d5b233a65e86fd80b82a396836cb80522e528b0f6ea5ac00"
|
||||
},
|
||||
"canonicalJson": [
|
||||
{
|
||||
"input": "{\"b\":1,\"a\":2}",
|
||||
"output": "{\"a\":2,\"b\":1}"
|
||||
},
|
||||
{
|
||||
"input": "[3,1,2]",
|
||||
"output": "[3,1,2]"
|
||||
},
|
||||
{
|
||||
"input": " null ",
|
||||
"output": "null"
|
||||
},
|
||||
{
|
||||
"input": "true",
|
||||
"output": "true"
|
||||
},
|
||||
{
|
||||
"input": "\"\u003c\u0026\u003e\"",
|
||||
"output": "\"\u003c\u0026\u003e\""
|
||||
},
|
||||
{
|
||||
"input": "{\"x\":100}",
|
||||
"output": "{\"x\":100}"
|
||||
},
|
||||
{
|
||||
"input": "{\"a\":{\"c\":[1,{\"b\":2,\"a\":1}],\"b\":0},\"A\":9}",
|
||||
"output": "{\"A\":9,\"a\":{\"b\":0,\"c\":[1,{\"a\":1,\"b\":2}]}}"
|
||||
},
|
||||
{
|
||||
"input": "{\"phone_number\":\"+8613800000000\",\"content\":\"hello\"}",
|
||||
"output": "{\"content\":\"hello\",\"phone_number\":\"+8613800000000\"}"
|
||||
},
|
||||
{
|
||||
"input": "{\"content\":\"a\u2028b\"}",
|
||||
"output": "{\"content\":\"a\u2028b\"}"
|
||||
},
|
||||
{
|
||||
"input": "{\"content\":\"a\u2029b\"}",
|
||||
"output": "{\"content\":\"a\u2029b\"}"
|
||||
},
|
||||
{
|
||||
"input": "{\"content\":\"😀\"}",
|
||||
"output": "{\"content\":\"😀\"}"
|
||||
},
|
||||
{
|
||||
"input": "{\"content\":\"\\b\\f\\u0001\"}",
|
||||
"output": "{\"content\":\"\\b\\f\\u0001\"}"
|
||||
},
|
||||
{
|
||||
"input": "{\"content\":\"\\\\u2028\"}",
|
||||
"output": "{\"content\":\"\\\\u2028\"}"
|
||||
},
|
||||
{
|
||||
"input": "{\"é\":1,\"z\":2}",
|
||||
"output": "{\"z\":2,\"é\":1}"
|
||||
},
|
||||
{
|
||||
"input": "{\"𐀀\":1,\"�\":2}",
|
||||
"output": "{\"�\":2,\"𐀀\":1}"
|
||||
}
|
||||
],
|
||||
"canonicalJsonRejected": [
|
||||
"{\"content\":\"\\ud800\"}",
|
||||
"{\"content\":\"\\udc00\"}",
|
||||
"{\"content\":\"\\ud800\\ud800\"}",
|
||||
"{\"content\":\"\\ud800x\"}"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user