@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.
- package/.dockerignore +19 -0
- package/.github/dependabot.yml +32 -0
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/publish.yml +59 -0
- package/.github/workflows/scorecard.yml +37 -0
- package/7h3.example.yaml +125 -0
- package/CHANGELOG.md +92 -0
- package/CONTRIBUTING.md +82 -0
- package/Dockerfile +73 -0
- package/GOVERNANCE.md +62 -0
- package/README.md +323 -6
- package/SECURITY.md +70 -0
- package/bench-results/replay-cache-full-1777891033256.json +10 -0
- package/bench-results/replay-cache-full-1777896317488.json +10 -0
- package/bench-results/replay-cache-full-1777900993184.json +10 -0
- package/bench-results/replay-cache-full-1777901019285.json +10 -0
- package/bench-results/replay-cache-quick-1777870170126.json +10 -0
- package/bench-results/signature-profiles-quick-1775875160079.json +85 -0
- package/bench-results/signature-profiles-quick-1775983539716.json +85 -0
- package/bench-results/signature-profiles-quick-1776237913190.json +85 -0
- package/bench-results/wire-codecs-full-1777891019803.json +93 -0
- package/bench-results/wire-codecs-full-1777896260964.json +93 -0
- package/bench-results/wire-codecs-full-1777901004247.json +93 -0
- package/bench-results/wire-codecs-quick-1775972879056.json +93 -0
- package/bench-results/wire-codecs-quick-1775983541111.json +93 -0
- package/bench-results/wire-codecs-quick-1776237914299.json +93 -0
- package/bench-results/wire-codecs-quick-1777841285236.json +93 -0
- package/bench-results/wire-codecs-quick-1777841321772.json +93 -0
- package/bench-results/wire-codecs-quick-1777841330408.json +93 -0
- package/bench-results/wire-codecs-quick-1777852886082.json +93 -0
- package/bench-results/wire-codecs-quick-1777852988773.json +93 -0
- package/bench-results/wire-codecs-quick-1777870188095.json +93 -0
- package/bench-results/wire-codecs-quick-1777870263918.json +93 -0
- package/bench-results/wire-codecs-quick-1777870455034.json +93 -0
- package/bench-results/wire-codecs-quick-1778816163081.json +93 -0
- package/bench-results/wire-codecs-quick-1778843936130.json +93 -0
- package/bin/7h3.ts +385 -0
- package/conformance/7h3_v0_1.json +77 -0
- package/conformance/7h3_v0_1_binary.json +20 -0
- package/conformance/aip_v0_1_binary.json +20 -0
- package/docker-compose.yaml +77 -0
- package/docs/ADOPTION_PLAN.md +120 -0
- package/docs/AGENTS.md +77 -0
- package/docs/AIP_RFC_v0.1.md +97 -0
- package/docs/AI_DECISION_CARD.md +122 -0
- package/docs/AI_RUNTIME_POLICY.json +126 -0
- package/docs/AI_RUNTIME_POLICY.yaml +110 -0
- package/docs/BACKPRESSURE_TUNING.md +65 -0
- package/docs/BENCHMARK_CLAIM_MATRIX.md +42 -0
- package/docs/BENCHMARK_REPORT_TEMPLATE.md +169 -0
- package/docs/BINARY_CODEC_BENCH.md +23 -0
- package/docs/CLEAN_CLONE_RUNBOOK.md +36 -0
- package/docs/CLOCK_SKEW_POLICY.md +30 -0
- package/docs/DISTRIBUTED_REPLAY.md +142 -0
- package/docs/FUZZ_CAMPAIGN.md +121 -0
- package/docs/GATEWAY.md +195 -0
- package/docs/KEY_MANAGEMENT_POLICY.md +53 -0
- package/docs/KEY_REVOCATION.md +69 -0
- package/docs/MCP_WRAPPER.md +159 -0
- package/docs/MIGRATION_GUIDE.md +40 -0
- package/docs/OPERATORS.md +184 -0
- package/docs/PERF_REGRESSION_POLICY.md +34 -0
- package/docs/PROJECT_EXAMINATION_2026-05-31.md +219 -0
- package/docs/RELEASE_BENCHMARK_REPORT_2026-05-15.md +135 -0
- package/docs/RELEASE_GATE.md +25 -0
- package/docs/RELEASE_NOTES_v0.1.0.md +54 -0
- package/docs/SECURITY_REVIEW_2026-06-05.md +165 -0
- package/docs/TELEMETRY.md +41 -0
- package/docs/THREAT_MODEL.md +89 -0
- package/docs/VERSIONING_POLICY.md +30 -0
- package/docs/assets/banner.png +0 -0
- package/eslint.config.js +15 -0
- package/fuzz/ts/harness-decode.ts +136 -0
- package/fuzz/ts/harness-verify.ts +121 -0
- package/fuzz/ts/run.ts +35 -0
- package/mcp-server/README.md +38 -0
- package/mcp-server/package-lock.json +1187 -0
- package/mcp-server/package.json +35 -0
- package/mcp-server/src/index.ts +236 -0
- package/mcp-server/tsconfig.json +14 -0
- package/package.json +79 -13
- package/scripts/aip-framework-quickstart.ts +110 -0
- package/scripts/aip-mcp-gateway.ts +38 -0
- package/scripts/aip-mcp-wrap-demo.ts +72 -0
- package/scripts/aip-quickstart.ts +60 -0
- package/scripts/bench-diff.ts +118 -0
- package/scripts/bench-protocol-e2e.ts +937 -0
- package/scripts/bench-protocol-openloop.ts +1397 -0
- package/scripts/bench-replay-cache.ts +76 -0
- package/scripts/bench-signature-profiles.ts +180 -0
- package/scripts/bench-wire-codecs.ts +161 -0
- package/scripts/build-binary-conformance.ts +36 -0
- package/scripts/build-release-dashboard.ts +175 -0
- package/scripts/canary-rollout.ts +38 -0
- package/scripts/mcpGatewayCli.test.ts +116 -0
- package/scripts/prepare-aip-package.ts +88 -0
- package/scripts/regen-conformance-sigs.ts +18 -0
- package/scripts/release-gate.ts +19 -0
- package/scripts/validate-runtime-policy.ts +18 -0
- package/sdk/browser/index.test.ts +162 -0
- package/sdk/browser/index.ts +257 -0
- package/sdk/browser/package.json +13 -0
- package/sdk/go/go.mod +3 -0
- package/sdk/go/http.go +135 -0
- package/sdk/go/protocol.go +324 -0
- package/sdk/go/protocol_test.go +334 -0
- package/sdk/go/webhook.go +136 -0
- package/sdk/python/README.md +18 -0
- package/sdk/python/protocol_7h3/__init__.py +46 -0
- package/sdk/python/protocol_7h3/http.py +212 -0
- package/sdk/python/protocol_7h3/keys.py +149 -0
- package/sdk/python/protocol_7h3/protocol.py +525 -0
- package/sdk/python/protocol_7h3/queue.py +118 -0
- package/sdk/python/protocol_7h3/webhook.py +116 -0
- package/sdk/python/pyproject.toml +40 -0
- package/sdk/python/tests/test_conformance.py +110 -0
- package/sdk/python/tests/test_http.py +305 -0
- package/sdk/python/tests/test_keys.py +417 -0
- package/sdk/python/tests/test_queue.py +120 -0
- package/sdk/python/tests/test_webhook.py +345 -0
- package/sdk/rust/Cargo.lock +371 -0
- package/sdk/rust/Cargo.toml +25 -0
- package/sdk/rust/README.md +31 -0
- package/sdk/rust/fuzz/Cargo.toml +29 -0
- package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +46 -0
- package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +11 -0
- package/sdk/rust/src/bin/aip_mcp_gateway.rs +59 -0
- package/sdk/rust/src/http.rs +145 -0
- package/sdk/rust/src/keys.rs +161 -0
- package/sdk/rust/src/lib.rs +688 -0
- package/sdk/rust/src/queue.rs +79 -0
- package/sdk/rust/src/webhook.rs +86 -0
- package/sdk/rust/tests/conformance.rs +148 -0
- package/sdk/rust/tests/gateway.rs +130 -0
- package/sdk/rust/tests/http_webhook_queue.rs +201 -0
- package/sdk/rust/tests/keys.rs +189 -0
- package/src/agentAdapter.test.ts +48 -0
- package/src/agentAdapter.ts +56 -0
- package/src/auditLog.test.ts +145 -0
- package/src/auditLog.ts +147 -0
- package/src/conformance.test.ts +136 -0
- package/src/conformanceVectors.ts +99 -0
- package/src/frameworkAdapters.test.ts +290 -0
- package/src/frameworkAdapters.ts +261 -0
- package/src/gateway.test.ts +343 -0
- package/src/gateway.ts +171 -0
- package/src/grpcBinding.test.ts +211 -0
- package/src/grpcBinding.ts +103 -0
- package/src/httpBinding.test.ts +376 -0
- package/src/httpBinding.ts +163 -0
- package/src/index.ts +32 -0
- package/src/keyInfra.test.ts +278 -0
- package/src/keyInfra.ts +228 -0
- package/src/keyRegistry.ts +59 -0
- package/src/keyRotation.test.ts +78 -0
- package/src/keyRotation.ts +72 -0
- package/src/mcpGateway.test.ts +129 -0
- package/src/mcpGateway.ts +250 -0
- package/src/mcpTransports.test.ts +92 -0
- package/src/mcpTransports.ts +169 -0
- package/src/mcpWrapper.test.ts +179 -0
- package/src/mcpWrapper.ts +206 -0
- package/src/policyEnforcer.test.ts +99 -0
- package/src/policyEnforcer.ts +169 -0
- package/src/policyTelemetryFeedback.test.ts +25 -0
- package/src/policyTelemetryFeedback.ts +38 -0
- package/src/protocol.bench.ts +37 -0
- package/src/protocol.test.ts +155 -0
- package/src/protocol.ts +413 -0
- package/src/protocolAgent.test.ts +105 -0
- package/src/protocolAgent.ts +169 -0
- package/src/protocolBinary.test.ts +165 -0
- package/src/protocolBinary.ts +312 -0
- package/src/protocolCapabilities.ts +70 -0
- package/src/protocolFuzz.advanced.test.ts +235 -0
- package/src/protocolFuzz.test.ts +111 -0
- package/src/protocolNegative.test.ts +97 -0
- package/src/protocolReplay.test.ts +71 -0
- package/src/protocolReplay.ts +194 -0
- package/src/protocolTransport.test.ts +556 -0
- package/src/protocolTransport.ts +483 -0
- package/src/queueBinding.test.ts +130 -0
- package/src/queueBinding.ts +102 -0
- package/src/rateLimiter.test.ts +96 -0
- package/src/rateLimiter.ts +46 -0
- package/src/redisClient.ts +140 -0
- package/src/redisIntegration.test.ts +134 -0
- package/src/replayStores.test.ts +141 -0
- package/src/replayStores.ts +82 -0
- package/src/revocation.test.ts +98 -0
- package/src/revocation.ts +0 -0
- package/src/routePolicy.test.ts +87 -0
- package/src/routePolicy.ts +72 -0
- package/src/runtimePolicy.test.ts +49 -0
- package/src/runtimePolicy.ts +81 -0
- package/src/runtimePolicyManager.test.ts +29 -0
- package/src/runtimePolicyManager.ts +50 -0
- package/src/runtimePolicyPresets.ts +43 -0
- package/src/signedResponse.test.ts +111 -0
- package/src/signedResponse.ts +83 -0
- package/src/webhookBinding.test.ts +144 -0
- package/src/webhookBinding.ts +115 -0
- package/src/wsBinding.test.ts +221 -0
- package/src/wsBinding.ts +100 -0
- package/tsconfig.json +15 -0
- package/tsconfig.lib.json +23 -0
- package/vite.lib.config.ts +16 -0
- package/agentAdapter.d.ts +0 -26
- package/conformanceVectors.d.ts +0 -20
- package/frameworkAdapters.d.ts +0 -72
- package/index.d.ts +0 -20
- package/index.js +0 -1702
- package/keyRotation.d.ts +0 -20
- package/mcpGateway.d.ts +0 -37
- package/mcpTransports.d.ts +0 -62
- package/mcpWrapper.d.ts +0 -83
- package/policyEnforcer.d.ts +0 -50
- package/policyTelemetryFeedback.d.ts +0 -11
- package/protocol.d.ts +0 -66
- package/protocolAgent.d.ts +0 -58
- package/protocolBinary.d.ts +0 -8
- package/protocolCapabilities.d.ts +0 -24
- package/protocolReplay.d.ts +0 -35
- package/protocolTransport.d.ts +0 -73
- package/redisClient.d.ts +0 -49
- package/replayStores.d.ts +0 -32
- package/revocation.d.ts +0 -71
- package/runtimePolicy.d.ts +0 -24
- package/runtimePolicyManager.d.ts +0 -15
- 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.
|
package/docs/GATEWAY.md
ADDED
|
@@ -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.
|