@7h3/protocol 0.1.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/.dockerignore +19 -0
  2. package/.github/dependabot.yml +32 -0
  3. package/.github/workflows/ci.yml +31 -0
  4. package/.github/workflows/publish.yml +59 -0
  5. package/.github/workflows/scorecard.yml +37 -0
  6. package/7h3.example.yaml +125 -0
  7. package/CHANGELOG.md +92 -0
  8. package/CONTRIBUTING.md +82 -0
  9. package/Dockerfile +73 -0
  10. package/GOVERNANCE.md +62 -0
  11. package/README.md +323 -6
  12. package/SECURITY.md +70 -0
  13. package/bench-results/replay-cache-full-1777891033256.json +10 -0
  14. package/bench-results/replay-cache-full-1777896317488.json +10 -0
  15. package/bench-results/replay-cache-full-1777900993184.json +10 -0
  16. package/bench-results/replay-cache-full-1777901019285.json +10 -0
  17. package/bench-results/replay-cache-quick-1777870170126.json +10 -0
  18. package/bench-results/signature-profiles-quick-1775875160079.json +85 -0
  19. package/bench-results/signature-profiles-quick-1775983539716.json +85 -0
  20. package/bench-results/signature-profiles-quick-1776237913190.json +85 -0
  21. package/bench-results/wire-codecs-full-1777891019803.json +93 -0
  22. package/bench-results/wire-codecs-full-1777896260964.json +93 -0
  23. package/bench-results/wire-codecs-full-1777901004247.json +93 -0
  24. package/bench-results/wire-codecs-quick-1775972879056.json +93 -0
  25. package/bench-results/wire-codecs-quick-1775983541111.json +93 -0
  26. package/bench-results/wire-codecs-quick-1776237914299.json +93 -0
  27. package/bench-results/wire-codecs-quick-1777841285236.json +93 -0
  28. package/bench-results/wire-codecs-quick-1777841321772.json +93 -0
  29. package/bench-results/wire-codecs-quick-1777841330408.json +93 -0
  30. package/bench-results/wire-codecs-quick-1777852886082.json +93 -0
  31. package/bench-results/wire-codecs-quick-1777852988773.json +93 -0
  32. package/bench-results/wire-codecs-quick-1777870188095.json +93 -0
  33. package/bench-results/wire-codecs-quick-1777870263918.json +93 -0
  34. package/bench-results/wire-codecs-quick-1777870455034.json +93 -0
  35. package/bench-results/wire-codecs-quick-1778816163081.json +93 -0
  36. package/bench-results/wire-codecs-quick-1778843936130.json +93 -0
  37. package/bin/7h3.ts +385 -0
  38. package/conformance/7h3_v0_1.json +77 -0
  39. package/conformance/7h3_v0_1_binary.json +20 -0
  40. package/conformance/aip_v0_1_binary.json +20 -0
  41. package/docker-compose.yaml +77 -0
  42. package/docs/ADOPTION_PLAN.md +120 -0
  43. package/docs/AGENTS.md +77 -0
  44. package/docs/AIP_RFC_v0.1.md +97 -0
  45. package/docs/AI_DECISION_CARD.md +122 -0
  46. package/docs/AI_RUNTIME_POLICY.json +126 -0
  47. package/docs/AI_RUNTIME_POLICY.yaml +110 -0
  48. package/docs/BACKPRESSURE_TUNING.md +65 -0
  49. package/docs/BENCHMARK_CLAIM_MATRIX.md +42 -0
  50. package/docs/BENCHMARK_REPORT_TEMPLATE.md +169 -0
  51. package/docs/BINARY_CODEC_BENCH.md +23 -0
  52. package/docs/CLEAN_CLONE_RUNBOOK.md +36 -0
  53. package/docs/CLOCK_SKEW_POLICY.md +30 -0
  54. package/docs/DISTRIBUTED_REPLAY.md +142 -0
  55. package/docs/FUZZ_CAMPAIGN.md +121 -0
  56. package/docs/GATEWAY.md +195 -0
  57. package/docs/KEY_MANAGEMENT_POLICY.md +53 -0
  58. package/docs/KEY_REVOCATION.md +69 -0
  59. package/docs/MCP_WRAPPER.md +159 -0
  60. package/docs/MIGRATION_GUIDE.md +40 -0
  61. package/docs/OPERATORS.md +184 -0
  62. package/docs/PERF_REGRESSION_POLICY.md +34 -0
  63. package/docs/PROJECT_EXAMINATION_2026-05-31.md +219 -0
  64. package/docs/RELEASE_BENCHMARK_REPORT_2026-05-15.md +135 -0
  65. package/docs/RELEASE_GATE.md +25 -0
  66. package/docs/RELEASE_NOTES_v0.1.0.md +54 -0
  67. package/docs/SECURITY_REVIEW_2026-06-05.md +165 -0
  68. package/docs/TELEMETRY.md +41 -0
  69. package/docs/THREAT_MODEL.md +89 -0
  70. package/docs/VERSIONING_POLICY.md +30 -0
  71. package/docs/assets/banner.png +0 -0
  72. package/eslint.config.js +15 -0
  73. package/fuzz/ts/harness-decode.ts +136 -0
  74. package/fuzz/ts/harness-verify.ts +121 -0
  75. package/fuzz/ts/run.ts +35 -0
  76. package/mcp-server/README.md +38 -0
  77. package/mcp-server/package-lock.json +1187 -0
  78. package/mcp-server/package.json +35 -0
  79. package/mcp-server/src/index.ts +236 -0
  80. package/mcp-server/tsconfig.json +14 -0
  81. package/package.json +79 -13
  82. package/scripts/aip-framework-quickstart.ts +110 -0
  83. package/scripts/aip-mcp-gateway.ts +38 -0
  84. package/scripts/aip-mcp-wrap-demo.ts +72 -0
  85. package/scripts/aip-quickstart.ts +60 -0
  86. package/scripts/bench-diff.ts +118 -0
  87. package/scripts/bench-protocol-e2e.ts +937 -0
  88. package/scripts/bench-protocol-openloop.ts +1397 -0
  89. package/scripts/bench-replay-cache.ts +76 -0
  90. package/scripts/bench-signature-profiles.ts +180 -0
  91. package/scripts/bench-wire-codecs.ts +161 -0
  92. package/scripts/build-binary-conformance.ts +36 -0
  93. package/scripts/build-release-dashboard.ts +175 -0
  94. package/scripts/canary-rollout.ts +38 -0
  95. package/scripts/mcpGatewayCli.test.ts +116 -0
  96. package/scripts/prepare-aip-package.ts +88 -0
  97. package/scripts/regen-conformance-sigs.ts +18 -0
  98. package/scripts/release-gate.ts +19 -0
  99. package/scripts/validate-runtime-policy.ts +18 -0
  100. package/sdk/browser/index.test.ts +162 -0
  101. package/sdk/browser/index.ts +257 -0
  102. package/sdk/browser/package.json +13 -0
  103. package/sdk/go/go.mod +3 -0
  104. package/sdk/go/http.go +135 -0
  105. package/sdk/go/protocol.go +324 -0
  106. package/sdk/go/protocol_test.go +334 -0
  107. package/sdk/go/webhook.go +136 -0
  108. package/sdk/python/README.md +18 -0
  109. package/sdk/python/protocol_7h3/__init__.py +46 -0
  110. package/sdk/python/protocol_7h3/http.py +212 -0
  111. package/sdk/python/protocol_7h3/keys.py +149 -0
  112. package/sdk/python/protocol_7h3/protocol.py +525 -0
  113. package/sdk/python/protocol_7h3/queue.py +118 -0
  114. package/sdk/python/protocol_7h3/webhook.py +116 -0
  115. package/sdk/python/pyproject.toml +40 -0
  116. package/sdk/python/tests/test_conformance.py +110 -0
  117. package/sdk/python/tests/test_http.py +305 -0
  118. package/sdk/python/tests/test_keys.py +417 -0
  119. package/sdk/python/tests/test_queue.py +120 -0
  120. package/sdk/python/tests/test_webhook.py +345 -0
  121. package/sdk/rust/Cargo.lock +371 -0
  122. package/sdk/rust/Cargo.toml +25 -0
  123. package/sdk/rust/README.md +31 -0
  124. package/sdk/rust/fuzz/Cargo.toml +29 -0
  125. package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +46 -0
  126. package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +11 -0
  127. package/sdk/rust/src/bin/aip_mcp_gateway.rs +59 -0
  128. package/sdk/rust/src/http.rs +145 -0
  129. package/sdk/rust/src/keys.rs +161 -0
  130. package/sdk/rust/src/lib.rs +688 -0
  131. package/sdk/rust/src/queue.rs +79 -0
  132. package/sdk/rust/src/webhook.rs +86 -0
  133. package/sdk/rust/tests/conformance.rs +148 -0
  134. package/sdk/rust/tests/gateway.rs +130 -0
  135. package/sdk/rust/tests/http_webhook_queue.rs +201 -0
  136. package/sdk/rust/tests/keys.rs +189 -0
  137. package/src/agentAdapter.test.ts +48 -0
  138. package/src/agentAdapter.ts +56 -0
  139. package/src/auditLog.test.ts +145 -0
  140. package/src/auditLog.ts +147 -0
  141. package/src/conformance.test.ts +136 -0
  142. package/src/conformanceVectors.ts +99 -0
  143. package/src/frameworkAdapters.test.ts +290 -0
  144. package/src/frameworkAdapters.ts +261 -0
  145. package/src/gateway.test.ts +343 -0
  146. package/src/gateway.ts +171 -0
  147. package/src/grpcBinding.test.ts +211 -0
  148. package/src/grpcBinding.ts +103 -0
  149. package/src/httpBinding.test.ts +376 -0
  150. package/src/httpBinding.ts +163 -0
  151. package/src/index.ts +32 -0
  152. package/src/keyInfra.test.ts +278 -0
  153. package/src/keyInfra.ts +228 -0
  154. package/src/keyRegistry.ts +59 -0
  155. package/src/keyRotation.test.ts +78 -0
  156. package/src/keyRotation.ts +72 -0
  157. package/src/mcpGateway.test.ts +129 -0
  158. package/src/mcpGateway.ts +250 -0
  159. package/src/mcpTransports.test.ts +92 -0
  160. package/src/mcpTransports.ts +169 -0
  161. package/src/mcpWrapper.test.ts +179 -0
  162. package/src/mcpWrapper.ts +206 -0
  163. package/src/policyEnforcer.test.ts +99 -0
  164. package/src/policyEnforcer.ts +169 -0
  165. package/src/policyTelemetryFeedback.test.ts +25 -0
  166. package/src/policyTelemetryFeedback.ts +38 -0
  167. package/src/protocol.bench.ts +37 -0
  168. package/src/protocol.test.ts +155 -0
  169. package/src/protocol.ts +413 -0
  170. package/src/protocolAgent.test.ts +105 -0
  171. package/src/protocolAgent.ts +169 -0
  172. package/src/protocolBinary.test.ts +165 -0
  173. package/src/protocolBinary.ts +312 -0
  174. package/src/protocolCapabilities.ts +70 -0
  175. package/src/protocolFuzz.advanced.test.ts +235 -0
  176. package/src/protocolFuzz.test.ts +111 -0
  177. package/src/protocolNegative.test.ts +97 -0
  178. package/src/protocolReplay.test.ts +71 -0
  179. package/src/protocolReplay.ts +194 -0
  180. package/src/protocolTransport.test.ts +556 -0
  181. package/src/protocolTransport.ts +483 -0
  182. package/src/queueBinding.test.ts +130 -0
  183. package/src/queueBinding.ts +102 -0
  184. package/src/rateLimiter.test.ts +96 -0
  185. package/src/rateLimiter.ts +46 -0
  186. package/src/redisClient.ts +140 -0
  187. package/src/redisIntegration.test.ts +134 -0
  188. package/src/replayStores.test.ts +141 -0
  189. package/src/replayStores.ts +82 -0
  190. package/src/revocation.test.ts +98 -0
  191. package/src/revocation.ts +0 -0
  192. package/src/routePolicy.test.ts +87 -0
  193. package/src/routePolicy.ts +72 -0
  194. package/src/runtimePolicy.test.ts +49 -0
  195. package/src/runtimePolicy.ts +81 -0
  196. package/src/runtimePolicyManager.test.ts +29 -0
  197. package/src/runtimePolicyManager.ts +50 -0
  198. package/src/runtimePolicyPresets.ts +43 -0
  199. package/src/signedResponse.test.ts +111 -0
  200. package/src/signedResponse.ts +83 -0
  201. package/src/webhookBinding.test.ts +144 -0
  202. package/src/webhookBinding.ts +115 -0
  203. package/src/wsBinding.test.ts +221 -0
  204. package/src/wsBinding.ts +100 -0
  205. package/tsconfig.json +15 -0
  206. package/tsconfig.lib.json +23 -0
  207. package/vite.lib.config.ts +16 -0
  208. package/agentAdapter.d.ts +0 -26
  209. package/conformanceVectors.d.ts +0 -20
  210. package/frameworkAdapters.d.ts +0 -72
  211. package/index.d.ts +0 -20
  212. package/index.js +0 -1702
  213. package/keyRotation.d.ts +0 -20
  214. package/mcpGateway.d.ts +0 -37
  215. package/mcpTransports.d.ts +0 -62
  216. package/mcpWrapper.d.ts +0 -83
  217. package/policyEnforcer.d.ts +0 -50
  218. package/policyTelemetryFeedback.d.ts +0 -11
  219. package/protocol.d.ts +0 -66
  220. package/protocolAgent.d.ts +0 -58
  221. package/protocolBinary.d.ts +0 -8
  222. package/protocolCapabilities.d.ts +0 -24
  223. package/protocolReplay.d.ts +0 -35
  224. package/protocolTransport.d.ts +0 -73
  225. package/redisClient.d.ts +0 -49
  226. package/replayStores.d.ts +0 -32
  227. package/revocation.d.ts +0 -71
  228. package/runtimePolicy.d.ts +0 -24
  229. package/runtimePolicyManager.d.ts +0 -15
  230. package/runtimePolicyPresets.d.ts +0 -11
@@ -0,0 +1,142 @@
1
+ # Distributed Replay Defense
2
+
3
+ 7h3 Protocol AIP protects against replay through pluggable cache interfaces.
4
+
5
+ ## Interfaces
6
+
7
+ - `InMemoryReplayCache`: single-process replay protection.
8
+ - `DistributedReplayCache`: wraps a shared `DistributedReplayStore` for multi-node deployments.
9
+ - `createRedisReplayStore`: a production `DistributedReplayStore` backed by Redis-style `SET NX PX`.
10
+
11
+ Source: `src/protocolReplay.ts`, `src/replayStores.ts`.
12
+
13
+ ## Client-agnostic by design
14
+
15
+ The library ships **no Redis dependency**. You inject any client matching the
16
+ small `RedisLikeClient` surface (`set` with `nx`/`pxMs`, optional `get`/`del`,
17
+ optional `pipeline`). `ioredis`, `node-redis`, Upstash, or a cluster proxy all
18
+ adapt in a few lines. An `InMemoryRedisLikeClient` reference implementation is
19
+ provided for tests and local development.
20
+
21
+ ## Usage
22
+
23
+ ```ts
24
+ import { DistributedReplayCache, createRedisReplayStore, receiveEnvelope } from '@7h3/protocol'
25
+ import Redis from 'ioredis'
26
+
27
+ const redis = new Redis(process.env.REDIS_URL)
28
+
29
+ const replayCache = new DistributedReplayCache(
30
+ createRedisReplayStore(
31
+ // adapt ioredis' set(key, val, 'PX', ms, 'NX') to the RedisLikeClient shape:
32
+ {
33
+ set: async (key, value, opts = {}) => {
34
+ const args = []
35
+ if (opts.pxMs !== undefined) args.push('PX', opts.pxMs)
36
+ if (opts.nx) args.push('NX')
37
+ return (await redis.set(key, value, ...args)) === 'OK' ? 'OK' : null
38
+ },
39
+ },
40
+ {
41
+ keyPrefix: 'aip:replay:',
42
+ errorBehavior: 'fallback', // degrade to local store on a Redis outage
43
+ onDegraded: (err) => metrics.increment('aip.replay.degraded'),
44
+ },
45
+ ),
46
+ )
47
+
48
+ await receiveEnvelope(rawEnvelope, { replayCache, secretResolver: async () => sharedSecret })
49
+ ```
50
+
51
+ ## Shared store contract
52
+
53
+ `DistributedReplayStore.reserve(key, expiresAtMs, nowMs)` must be atomic:
54
+
55
+ - Return `true` when the key is newly reserved.
56
+ - Return `false` when the key is already reserved and still valid.
57
+
58
+ `key` format is `sender|messageId|nonce`. The optional `reserveMany(entries, nowMs)`
59
+ performs a batched reserve (one round-trip via a client pipeline) and is used
60
+ automatically by `DistributedReplayCache.consumeMany` when present.
61
+
62
+ ## Behavior on Redis outage (`errorBehavior`)
63
+
64
+ | Value | On client error | Use when |
65
+ |---|---|---|
66
+ | `fallback` (default) | Degrade to a local in-memory store — traffic flows, single-node replay protection stays in force, and TTL still bounds replays | Default; balances safety and uptime |
67
+ | `reject` | Fail closed — deny the message | Strictest security posture |
68
+ | `allow` | Fail open — accept the message | Uptime outweighs the narrow replay risk |
69
+
70
+ Every degraded decision fires `onDegraded` so the condition is observable — degradation is never silent.
71
+
72
+ ## High-availability topologies
73
+
74
+ ### Redis Sentinel
75
+
76
+ ioredis connects to Sentinel transparently. The adapter wrapper is identical to standalone Redis:
77
+
78
+ ```ts
79
+ import Redis from 'ioredis'
80
+
81
+ const redis = new Redis({
82
+ sentinels: [
83
+ { host: 'sentinel-1', port: 26379 },
84
+ { host: 'sentinel-2', port: 26379 },
85
+ { host: 'sentinel-3', port: 26379 },
86
+ ],
87
+ name: 'mymaster',
88
+ })
89
+
90
+ const aipRedis = {
91
+ set: async (key: string, value: string, opts: { nx?: boolean; pxMs?: number } = {}) => {
92
+ const args: (string | number)[] = []
93
+ if (opts.pxMs !== undefined) args.push('PX', opts.pxMs)
94
+ if (opts.nx) args.push('NX')
95
+ return (await redis.set(key, value, ...(args as [string, number, string]))) === 'OK'
96
+ ? ('OK' as const)
97
+ : null
98
+ },
99
+ }
100
+
101
+ const replayCache = new DistributedReplayCache(
102
+ createRedisReplayStore(aipRedis, { errorBehavior: 'fallback' }),
103
+ )
104
+ ```
105
+
106
+ Sentinel handles leader election automatically. During failover (typically < 30 s), `errorBehavior` controls whether requests are rejected or degrade to local replay protection.
107
+
108
+ ### Redis Cluster
109
+
110
+ AIP replay keys (`aip:replay:{sender}:{messageId}:{nonce}`) and revocation keys (`aip:revoked:{keyId}`) are independent — no cross-slot transactions required. Cluster mode works without modification. Adapt a Cluster client the same way as standalone.
111
+
112
+ ```ts
113
+ const cluster = new Redis.Cluster([
114
+ { host: 'node-1', port: 7000 },
115
+ { host: 'node-2', port: 7001 },
116
+ { host: 'node-3', port: 7002 },
117
+ ])
118
+ // adapter wrapper identical to standalone
119
+ ```
120
+
121
+ ### Upstash (serverless / edge)
122
+
123
+ Upstash's `@upstash/redis` client's `set` method matches `RedisLikeClient` directly:
124
+
125
+ ```ts
126
+ import { Redis } from '@upstash/redis'
127
+
128
+ const redis = new Redis({ url: process.env.UPSTASH_URL!, token: process.env.UPSTASH_TOKEN! })
129
+
130
+ const aipRedis = {
131
+ set: async (key: string, value: string, opts: { nx?: boolean; pxMs?: number } = {}) =>
132
+ redis.set(key, value, { nx: opts.nx, px: opts.pxMs }),
133
+ }
134
+ ```
135
+
136
+ ## Operational guidance
137
+
138
+ - Use a distributed replay cache for any horizontally scaled gateway.
139
+ - Keep clocks synchronized (NTP / PTP) — AIP TTL checks require clocks within the configured skew window (default ±30 s) across all nodes and Sentinel/Cluster members.
140
+ - Monitor replay reject rate and `onDegraded` rate as security / health signals.
141
+ - During Redis failover, `errorBehavior: 'fallback'` keeps traffic flowing under single-node replay protection. `errorBehavior: 'reject'` is safer but means failover = downtime for inbound envelopes. Choose the posture that matches your threat model.
142
+ - The replay store is the only component AIP requires Redis for. The revocation store (`createRedisRevocationStore`) uses the same `RedisLikeClient` interface and the same HA patterns apply.
@@ -0,0 +1,121 @@
1
+ # Fuzz Campaign
2
+
3
+ AIP employs two complementary fuzzing strategies: mutation-based harnesses for TypeScript and coverage-guided libFuzzer targets for Rust.
4
+
5
+ ---
6
+
7
+ ## TypeScript — mutation-based harnesses
8
+
9
+ **Location:** `fuzz/ts/`
10
+
11
+ **Run:**
12
+ ```bash
13
+ npm run fuzz:ts # both harnesses, default rounds
14
+ FUZZ_ROUNDS=200000 npm run fuzz:ts # heavier run
15
+ npm run fuzz:ts:decode # decoder only
16
+ npm run fuzz:ts:verify # verifier only
17
+ ```
18
+
19
+ ### Harness: `harness-decode.ts`
20
+
21
+ **Target:** `decodeEnvelope`
22
+ **Invariant:** Must never throw on any input — garbage returns `{ok: false}`, not a crash.
23
+
24
+ **Strategy:**
25
+ 1. Seed corpus: conformance vectors (JSON full form + compact form) + known-bad inputs (`{}`, `[]`, `null`, `"string"`, `{`, etc.)
26
+ 2. Each round: pick a mutator, apply it to the current input
27
+ 3. Reset to a fresh corpus item every 500 rounds
28
+
29
+ **Mutators:**
30
+ - Bit flip at a random byte position
31
+ - Random byte insertion
32
+ - Random byte deletion
33
+ - Truncation to a random prefix length
34
+ - JSON field mutation (null, type change)
35
+ - Token substitution (e.g. `"aip/0.1"` → `"aip/0.2"`)
36
+
37
+ **Initial run result (2026-06-05):**
38
+ ```
39
+ [harness-decode] rounds=50000 ok=98 ok:false=49902 crashes=0
40
+ ```
41
+ No crashes. Decoder tolerates all 50,000 mutated inputs.
42
+
43
+ ---
44
+
45
+ ### Harness: `harness-verify.ts`
46
+
47
+ **Target:** `verifyEnvelopeHmac`
48
+ **Invariants:**
49
+ 1. Must never throw on a tampered envelope.
50
+ 2. Any tampered envelope must verify as `false` (no false positives).
51
+
52
+ **Strategy:**
53
+ 1. Sign a fresh baseline envelope
54
+ 2. Each round: deep-clone and randomly mutate one field in `header` or `body`
55
+ 3. Guard: if mutation produced no net change, force `body.content` to a sentinel value
56
+ 4. Every 200 rounds: test with a fully broken envelope (`{}` headers/body)
57
+
58
+ **Mutation operators:**
59
+ - String fields: bit flip a random byte, UTF-8 round-tripped (invalid sequences → replacement char)
60
+ - Numeric fields: add a non-zero delta (−500 to −1 or +1 to +500)
61
+ - Optional fields: set to `null`
62
+
63
+ **Initial run result (2026-06-05):**
64
+ ```
65
+ [harness-verify] rounds=20000 tamper-false-positives=0 crashes=0
66
+ ```
67
+ No crashes, no false positives. No tampered envelope verified as valid.
68
+
69
+ ---
70
+
71
+ ## Rust — cargo-fuzz (libFuzzer)
72
+
73
+ **Location:** `sdk/rust/fuzz/`
74
+
75
+ **Prerequisites:**
76
+ ```bash
77
+ rustup toolchain install nightly
78
+ cargo install cargo-fuzz
79
+ ```
80
+
81
+ **Run:**
82
+ ```bash
83
+ npm run fuzz:rust:decode # 60-second decode fuzzing session
84
+ npm run fuzz:rust:canonicalize # 60-second canonicalize fuzzing session
85
+
86
+ # Or run directly:
87
+ cargo +nightly fuzz run fuzz_decode --manifest-path sdk/rust/fuzz/Cargo.toml
88
+ cargo +nightly fuzz run fuzz_canonicalize --manifest-path sdk/rust/fuzz/Cargo.toml
89
+ ```
90
+
91
+ ### Target: `fuzz_decode`
92
+
93
+ **Target:** `aip7h3::decode_envelope`
94
+ **Invariant:** Must never panic on any UTF-8-valid input — errors are `Err(...)`, not panics.
95
+
96
+ Input: arbitrary byte sequences converted to UTF-8 via `std::str::from_utf8`.
97
+
98
+ ### Target: `fuzz_canonicalize`
99
+
100
+ **Target:** `aip7h3::canonicalize_envelope`
101
+ **Invariant:** Calling it twice on the same envelope returns the same string (idempotence / purity).
102
+
103
+ Input: fuzz bytes carved into envelope fields (`messageId`, `content`) to exercise the canonicalization path under arbitrary field content.
104
+
105
+ ---
106
+
107
+ ## Coverage gaps and known limitations
108
+
109
+ | Gap | Notes |
110
+ |---|---|
111
+ | TypeScript fuzzing is mutation-based, not coverage-guided | Coverage-guided fuzzing (e.g. jazzer.js) requires Java; out of scope for CI. The mutation harnesses provide meaningful parser boundary coverage without the dependency. |
112
+ | Binary decoder (`decodeEnvelope` with `Uint8Array`) | The existing property-based tests (fast-check) cover this path (`src/protocolFuzz.advanced.test.ts`). A dedicated fuzz harness is a future addition. |
113
+ | Ed25519 verification paths | These depend on WebCrypto internals; crash surface is on the envelope parsing/canonicalization layer (covered). |
114
+ | No crash corpus retained | Extend the harnesses with a `fuzz/corpus/` directory to persist and replay interesting inputs. |
115
+ | Python | No standalone fuzz harness — Python correctness is covered by conformance vectors and the property-based TS tests. |
116
+
117
+ ---
118
+
119
+ ## Pre-release fuzz policy
120
+
121
+ Per `docs/RELEASE_GATE.md`, at minimum the TypeScript harnesses run at default round counts as part of every release gate check. Rust fuzzing is optional for patch releases and required for minor/major bumps.
@@ -0,0 +1,195 @@
1
+ # 7h3 Protocol — Gateway Quick-Start
2
+
3
+ The 7h3 gateway is a verifying HTTP reverse proxy. It sits in front of your
4
+ service, intercepts every request, checks that the AIP envelope header carries
5
+ a valid Ed25519 (or HMAC) signature from a known sender, enforces per-route
6
+ policies and rate limits, optionally signs upstream responses, then forwards
7
+ clean HTTP to your application.
8
+
9
+ ---
10
+
11
+ ## 1. Install
12
+
13
+ ```bash
14
+ npm install @7h3/protocol
15
+ ```
16
+
17
+ `tsx` is required to run the CLI from source. It is listed as a dev
18
+ dependency and is also installed globally in the Docker image.
19
+
20
+ ```bash
21
+ npm install -g tsx # if running outside Docker
22
+ ```
23
+
24
+ ---
25
+
26
+ ## 2. In-Process Middleware
27
+
28
+ Drop the gateway into an existing Express (or any Node.js `http`) server in
29
+ three lines:
30
+
31
+ ```ts
32
+ import express from 'express'
33
+ import { createGateway } from '@7h3/protocol/gateway'
34
+ import { createStaticKeyRegistry } from '@7h3/protocol/key-registry'
35
+
36
+ const app = express()
37
+
38
+ const registry = createStaticKeyRegistry({
39
+ 'agent@example.com': process.env.AGENT_PUBLIC_KEY!,
40
+ })
41
+
42
+ const gateway = createGateway({
43
+ upstream: 'http://internal-api:3000',
44
+ keyRegistry: registry,
45
+ defaultPolicy: 'deny',
46
+ })
47
+
48
+ // Verify every request before it reaches your routes
49
+ app.use(async (req, res, next) => {
50
+ const headers: Record<string, string> = {}
51
+ for (const [k, v] of Object.entries(req.headers)) {
52
+ if (typeof v === 'string') headers[k] = v
53
+ else if (Array.isArray(v)) headers[k] = v[0]
54
+ }
55
+ const outcome = await gateway.verify({ method: req.method, path: req.path, headers })
56
+ if (!outcome.ok) return res.status(outcome.status).json({ error: outcome.reason })
57
+ res.locals.sender = outcome.sender
58
+ next()
59
+ })
60
+ ```
61
+
62
+ ---
63
+
64
+ ## 3. Gateway Mode (CLI / Standalone Proxy)
65
+
66
+ ### 3a. Generate a keypair
67
+
68
+ ```bash
69
+ 7h3 keygen --output keys.json
70
+ ```
71
+
72
+ This writes a JSON file with `publicKey` and `privateKey` (both base64url-encoded
73
+ Ed25519 SPKI/PKCS8). Store `privateKey` in a secret; share `publicKey` with
74
+ the agents that will call your service.
75
+
76
+ ### 3b. Start the gateway
77
+
78
+ ```bash
79
+ 7h3 gateway \
80
+ --upstream http://localhost:3000 \
81
+ --public-key <base64url-SPKI> \
82
+ --port 8080 \
83
+ --require ed25519
84
+ ```
85
+
86
+ Full options:
87
+
88
+ | Flag | Default | Description |
89
+ |---|---|---|
90
+ | `--upstream <url>` | _(required)_ | Upstream service URL |
91
+ | `--port <n>` | `8080` | Port to listen on |
92
+ | `--require ed25519\|none` | `ed25519` if `--public-key` is set, else `none` | Signature mode |
93
+ | `--public-key <key>` | — | Ed25519 SPKI public key (base64url) |
94
+ | `--sign-responses` | `false` | Sign every proxied response |
95
+ | `--private-key <key>` | — | Private key for response signing |
96
+ | `--sender <id>` | — | Sender ID attached to signed responses |
97
+
98
+ ---
99
+
100
+ ## 4. Docker
101
+
102
+ ```bash
103
+ # Bring up the gateway + example upstream
104
+ docker compose up --build
105
+
106
+ # Test with a health probe (no signature required)
107
+ curl http://localhost:8080/health
108
+
109
+ # Generate keys, then call a signed route
110
+ 7h3 keygen --output keys.json
111
+ PRIV=$(jq -r .privateKey keys.json)
112
+ PUB=$(jq -r .publicKey keys.json)
113
+
114
+ ENVELOPE=$(7h3 sign --private-key "$PRIV" --sender agent@example.com --payload '{"q":"hello"}')
115
+ curl -H "x-7h3-envelope: $ENVELOPE" http://localhost:8080/api/hello
116
+ ```
117
+
118
+ To enable response signing, pass environment variables to the gateway service
119
+ in `docker-compose.yaml`:
120
+
121
+ ```yaml
122
+ environment:
123
+ GATEWAY_PRIVATE_KEY: <your-private-key>
124
+ GATEWAY_SENDER: gateway@my-service.example.com
125
+ ```
126
+
127
+ ---
128
+
129
+ ## 5. Config Reference (YAML)
130
+
131
+ See `7h3.example.yaml` for a fully-annotated configuration file.
132
+
133
+ | Field | Type | Default | Description |
134
+ |---|---|---|---|
135
+ | `upstream` | string | _(required)_ | Upstream base URL |
136
+ | `port` | number | `8080` | Listening port |
137
+ | `sender` | string | — | Gateway sender identity for signed responses |
138
+ | `sign_responses` | boolean | `false` | Sign every proxied response |
139
+ | `default_policy` | `allow` \| `deny` | `allow` | Behaviour when no route policy matches |
140
+ | `keys.private_key` | string | — | Gateway Ed25519 private key (base64url PKCS8) |
141
+ | `keys.registry` | map | `{}` | `senderID → publicKey` (base64url SPKI) |
142
+ | `policies[].path` | glob string | _(required)_ | Route glob (`**` crosses slashes) |
143
+ | `policies[].require` | `ed25519` \| `hmac` \| `any` \| `none` | _(required)_ | Signature algorithm |
144
+ | `policies[].allowed_senders` | string[] | _(all)_ | Allowlist of sender IDs |
145
+ | `policies[].rate_limit.requests` | number | — | Max requests in window |
146
+ | `policies[].rate_limit.window_ms` | number | — | Sliding window in ms |
147
+ | `policies[].sign_response` | boolean | — | Per-route response signing override |
148
+
149
+ ---
150
+
151
+ ## 6. Client-Side Signing
152
+
153
+ Agents calling a 7h3-protected gateway must attach a signed AIP envelope to
154
+ every request. Use the TypeScript SDK:
155
+
156
+ ```ts
157
+ import { createEnvelope, signEnvelopeEd25519 } from '@7h3/protocol'
158
+ import { attachEnvelopeToHeaders } from '@7h3/protocol/http'
159
+
160
+ async function signedFetch(url: string, body: string, privateKey: string) {
161
+ const envelope = createEnvelope({
162
+ sender: 'agent@example.com',
163
+ recipient: 'gateway@my-service.example.com',
164
+ intent: 'REQUEST',
165
+ content: body,
166
+ ttlMs: 60_000,
167
+ })
168
+
169
+ const signed = await signEnvelopeEd25519(envelope, privateKey)
170
+ const headers = attachEnvelopeToHeaders(signed, {}) // adds x-7h3-envelope
171
+
172
+ return fetch(url, {
173
+ method: 'POST',
174
+ headers: { 'content-type': 'application/json', ...headers },
175
+ body,
176
+ })
177
+ }
178
+ ```
179
+
180
+ The gateway validates the envelope header, verifies the Ed25519 signature
181
+ against the sender's registered public key, checks replay protection, and only
182
+ then forwards the request to the upstream service.
183
+
184
+ ---
185
+
186
+ ## 7. Policy Examples
187
+
188
+ | Use Case | `path` | `require` | `allowed_senders` | `rate_limit` |
189
+ |---|---|---|---|---|
190
+ | Public health probe | `/health` | `none` | — | — |
191
+ | Open public API | `/public/**` | `none` | — | — |
192
+ | Any authenticated agent | `/api/**` | `ed25519` | — | 1 000 req / 60 s |
193
+ | Named admin agents only | `/api/admin/**` | `ed25519` | `admin-agent@…`, `orchestrator@…` | — |
194
+ | Partner integration | `/api/partner/**` | `ed25519` | `partner-bot@partner.example.com` | 200 req / 60 s |
195
+ | Webhook receiver | `/webhooks/**` | `hmac` | — | 500 req / 60 s |
@@ -0,0 +1,53 @@
1
+ # 7h3 Protocol AIP Key Management Policy (v1.0 draft)
2
+
3
+ ## Supported signature profiles
4
+
5
+ - `HS256` (shared secret)
6
+ - `ED25519` (asymmetric keypair)
7
+
8
+ ## Key ID rules
9
+
10
+ - Every signing key must have a unique `keyId`.
11
+ - Verifiers must resolve verification material by both `keyId` and `sender`.
12
+ - Reusing the same `keyId` for different senders is prohibited.
13
+
14
+ ## Rotation policy
15
+
16
+ - Rotation interval target:
17
+ - `HS256`: every 30 days
18
+ - `ED25519`: every 90 days
19
+ - Rotation process:
20
+ 1. Provision new key as active for signing.
21
+ 2. Keep previous key in verify-only mode during overlap window.
22
+ 3. End overlap and revoke old key.
23
+
24
+ ## Revocation policy
25
+
26
+ - Immediate revocation triggers:
27
+ - suspected credential leak
28
+ - host compromise
29
+ - failed integrity investigation
30
+ - Revoked keys must fail verification in resolver control plane within SLA.
31
+ - For multi-node deployments, back revocation with a shared `RevocationStore`
32
+ (`createRedisRevocationStore`) and wrap the verification resolver with
33
+ `withRevocationCheck` so a revoke on one node is enforced fleet-wide. The
34
+ store fails **closed** by default: if the revocation list is unreachable, the
35
+ key is treated as revoked. See `KEY_REVOCATION.md`.
36
+
37
+ ## Storage requirements
38
+
39
+ - Do not commit secrets/private keys to source control.
40
+ - Store key material in managed secrets/KMS.
41
+ - Restrict read access to signing services and verification control plane.
42
+
43
+ ## Runtime policy
44
+
45
+ - Production endpoints require signatures by default.
46
+ - `HS256` is recommended for tightly controlled private clusters.
47
+ - `ED25519` is recommended for cross-domain or multi-tenant federation.
48
+ - Use `RollingKeyring` (`src/keyRotation.ts`) for overlap windows, verify-only periods, and revocation enforcement.
49
+
50
+ ## Audit requirements
51
+
52
+ - Log key activation, rotation, revocation, and failed verification events.
53
+ - Retain audit logs for incident response and compliance windows.
@@ -0,0 +1,69 @@
1
+ # Fleet-Wide Key Revocation
2
+
3
+ `RollingKeyring` (`src/keyRotation.ts`) enforces revocation and `notBefore`/
4
+ `notAfter` expiry **in-process**. For horizontally scaled deployments, a shared
5
+ `RevocationStore` makes a revocation effective across every node: revoke a
6
+ `(sender, keyId)` on one gateway and all gateways that consult the same store
7
+ reject it.
8
+
9
+ Source: `src/revocation.ts`.
10
+
11
+ ## Interfaces
12
+
13
+ - `RevocationStore` — `isRevoked(sender, keyId, nowMs?)` and `revoke(sender, keyId, { untilMs? })`.
14
+ - `InMemoryRevocationStore` — single-process implementation (tests, local dev, single node).
15
+ - `createRedisRevocationStore` — Redis-backed shared store with cached reads.
16
+ - `withRevocationCheck(resolver, store)` — wraps a signature resolver so a revoked key resolves to **no material**, which makes verification fail and the envelope is rejected.
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ import {
22
+ RollingKeyring,
23
+ createKeyringSignatureResolver,
24
+ createRedisRevocationStore,
25
+ withRevocationCheck,
26
+ receiveEnvelope,
27
+ } from '@7h3/protocol'
28
+
29
+ const keyring = new RollingKeyring(records)
30
+ const revocations = createRedisRevocationStore(redisLikeClient, {
31
+ keyPrefix: 'aip:revoked:',
32
+ errorBehavior: 'reject', // fail closed (default)
33
+ cacheTtlMs: 5000,
34
+ onDegraded: (err, ctx) => metrics.increment('aip.revocation.degraded'),
35
+ })
36
+
37
+ const signatureResolver = withRevocationCheck(
38
+ createKeyringSignatureResolver(keyring),
39
+ revocations,
40
+ )
41
+
42
+ await receiveEnvelope(rawEnvelope, { signatureResolver })
43
+
44
+ // Operationally, on a suspected compromise:
45
+ await revocations.revoke('agent.worker', 'agent.worker-k1') // permanent
46
+ await revocations.revoke('agent.worker', 'agent.worker-k2', { untilMs: Date.now() + 86_400_000 }) // 24h
47
+ ```
48
+
49
+ ## Behavior on Redis outage (`errorBehavior`)
50
+
51
+ | Value | On client error | Rationale |
52
+ |---|---|---|
53
+ | `reject` (default) | Treat the key as **revoked** (fail closed) | A revoked key is a compromised key — never accept on uncertainty |
54
+ | `allow` | Treat the key as **not revoked** | Favor availability where the risk is acceptable |
55
+
56
+ Reads are served from a short-lived local cache (`cacheTtlMs`), so:
57
+
58
+ - the verify hot path does not hit Redis on every message (scalability), and
59
+ - known revocations keep enforcing through a brief Redis outage (a stale cached
60
+ `revoked: true` is still honored).
61
+
62
+ Every degraded decision fires `onDegraded` — degradation is never silent.
63
+
64
+ ## Operational guidance
65
+
66
+ - Wire `withRevocationCheck` into every verifying node's `signatureResolver`.
67
+ - Treat the `onDegraded` rate as a security signal; sustained degradation means
68
+ revocations may not be propagating.
69
+ - Keep `cacheTtlMs` short enough that a revocation propagates within your SLA.