@7h3/protocol 0.5.0 → 0.5.2
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/README.md +6 -1317
- package/agentAdapter.d.ts +26 -0
- package/auditLog.d.ts +54 -0
- package/capability.d.ts +67 -0
- package/cborCodec.d.ts +34 -0
- package/conformanceVectors.d.ts +20 -0
- package/encryption.d.ts +85 -0
- package/envelopeCbor.d.ts +33 -0
- package/frameworkAdapters.d.ts +72 -0
- package/gateway.d.ts +53 -0
- package/grpcBinding.d.ts +29 -0
- package/httpBinding.d.ts +50 -0
- package/index.d.ts +39 -0
- package/index.js +3670 -0
- package/keyInfra.d.ts +64 -0
- package/keyRegistry.d.ts +9 -0
- package/keyRotation.d.ts +20 -0
- package/mcpGateway.d.ts +37 -0
- package/mcpTransports.d.ts +62 -0
- package/mcpWrapper.d.ts +83 -0
- package/otel.d.ts +65 -0
- package/package.json +13 -121
- package/policyEnforcer.d.ts +50 -0
- package/policyTelemetryFeedback.d.ts +11 -0
- package/{src/protocol.d.ts → protocol.d.ts} +6 -2
- package/protocolAgent.d.ts +58 -0
- package/protocolBinary.d.ts +8 -0
- package/protocolCapabilities.d.ts +24 -0
- package/protocolReplay.d.ts +35 -0
- package/protocolTransport.d.ts +73 -0
- package/queueBinding.d.ts +43 -0
- package/rateLimiter.d.ts +18 -0
- package/{src/redisClient.ts → redisClient.d.ts} +25 -75
- package/replayStores.d.ts +98 -0
- package/revocation.d.ts +71 -0
- package/routePolicy.d.ts +30 -0
- package/runtimePolicy.d.ts +24 -0
- package/runtimePolicyManager.d.ts +15 -0
- package/runtimePolicyPresets.d.ts +11 -0
- package/signedResponse.d.ts +21 -0
- package/stream.d.ts +59 -0
- package/telemetry.d.ts +79 -0
- package/webhookBinding.d.ts +30 -0
- package/wsBinding.d.ts +52 -0
- package/.dockerignore +0 -19
- package/.github/dependabot.yml +0 -32
- package/.github/workflows/ci.yml +0 -31
- package/.github/workflows/publish.yml +0 -59
- package/.github/workflows/scorecard.yml +0 -37
- package/7h3.example.yaml +0 -125
- package/CHANGELOG.md +0 -152
- package/CONTRIBUTING.md +0 -82
- package/Dockerfile +0 -73
- package/GOVERNANCE.md +0 -62
- package/SECURITY.md +0 -70
- package/bench-results/replay-cache-full-1777891033256.json +0 -10
- package/bench-results/replay-cache-full-1777896317488.json +0 -10
- package/bench-results/replay-cache-full-1777900993184.json +0 -10
- package/bench-results/replay-cache-full-1777901019285.json +0 -10
- package/bench-results/replay-cache-quick-1777870170126.json +0 -10
- package/bench-results/signature-profiles-quick-1775875160079.json +0 -85
- package/bench-results/signature-profiles-quick-1775983539716.json +0 -85
- package/bench-results/signature-profiles-quick-1776237913190.json +0 -85
- package/bench-results/wire-codecs-full-1777891019803.json +0 -93
- package/bench-results/wire-codecs-full-1777896260964.json +0 -93
- package/bench-results/wire-codecs-full-1777901004247.json +0 -93
- package/bench-results/wire-codecs-quick-1775972879056.json +0 -93
- package/bench-results/wire-codecs-quick-1775983541111.json +0 -93
- package/bench-results/wire-codecs-quick-1776237914299.json +0 -93
- package/bench-results/wire-codecs-quick-1777841285236.json +0 -93
- package/bench-results/wire-codecs-quick-1777841321772.json +0 -93
- package/bench-results/wire-codecs-quick-1777841330408.json +0 -93
- package/bench-results/wire-codecs-quick-1777852886082.json +0 -93
- package/bench-results/wire-codecs-quick-1777852988773.json +0 -93
- package/bench-results/wire-codecs-quick-1777870188095.json +0 -93
- package/bench-results/wire-codecs-quick-1777870263918.json +0 -93
- package/bench-results/wire-codecs-quick-1777870455034.json +0 -93
- package/bench-results/wire-codecs-quick-1778816163081.json +0 -93
- package/bench-results/wire-codecs-quick-1778843936130.json +0 -93
- package/bin/7h3.ts +0 -406
- package/conformance/7h3_v0_1.json +0 -77
- package/conformance/7h3_v0_1_binary.json +0 -20
- package/conformance/aip_v0_1_binary.json +0 -20
- package/docker-compose.yaml +0 -77
- package/docs/ADOPTION_PLAN.md +0 -120
- package/docs/AGENTS.md +0 -77
- package/docs/AIP_RFC_v0.1.md +0 -97
- package/docs/AI_DECISION_CARD.md +0 -122
- package/docs/AI_RUNTIME_POLICY.json +0 -126
- package/docs/AI_RUNTIME_POLICY.yaml +0 -110
- package/docs/BACKPRESSURE_TUNING.md +0 -65
- package/docs/BENCHMARK_CLAIM_MATRIX.md +0 -42
- package/docs/BENCHMARK_REPORT_TEMPLATE.md +0 -169
- package/docs/BINARY_CODEC_BENCH.md +0 -23
- package/docs/CLEAN_CLONE_RUNBOOK.md +0 -36
- package/docs/CLOCK_SKEW_POLICY.md +0 -30
- package/docs/DISTRIBUTED_REPLAY.md +0 -142
- package/docs/FUZZ_CAMPAIGN.md +0 -121
- package/docs/GATEWAY.md +0 -195
- package/docs/KEY_MANAGEMENT_POLICY.md +0 -53
- package/docs/KEY_REVOCATION.md +0 -69
- package/docs/MCP_WRAPPER.md +0 -159
- package/docs/MIGRATION_GUIDE.md +0 -40
- package/docs/OPERATORS.md +0 -184
- package/docs/PERF_REGRESSION_POLICY.md +0 -34
- package/docs/PROJECT_EXAMINATION_2026-05-31.md +0 -219
- package/docs/RELEASE_BENCHMARK_REPORT_2026-05-15.md +0 -135
- package/docs/RELEASE_GATE.md +0 -25
- package/docs/RELEASE_NOTES_v0.1.0.md +0 -54
- package/docs/SECURITY_REVIEW_2026-06-05.md +0 -165
- package/docs/TELEMETRY.md +0 -41
- package/docs/THREAT_MODEL.md +0 -89
- package/docs/VERSIONING_POLICY.md +0 -30
- package/docs/assets/banner-github.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/banner.svg +0 -123
- package/eslint.config.js +0 -15
- package/fuzz/ts/harness-decode.ts +0 -136
- package/fuzz/ts/harness-verify.ts +0 -121
- package/fuzz/ts/run.ts +0 -35
- package/mcp-server/README.md +0 -38
- package/mcp-server/package-lock.json +0 -1187
- package/mcp-server/package.json +0 -35
- package/mcp-server/src/index.ts +0 -236
- package/mcp-server/tsconfig.json +0 -14
- package/scripts/aip-framework-quickstart.ts +0 -110
- package/scripts/aip-mcp-gateway.ts +0 -38
- package/scripts/aip-mcp-wrap-demo.ts +0 -72
- package/scripts/aip-quickstart.ts +0 -60
- package/scripts/bench-diff.ts +0 -118
- package/scripts/bench-protocol-e2e.ts +0 -937
- package/scripts/bench-protocol-openloop.ts +0 -1397
- package/scripts/bench-replay-cache.ts +0 -76
- package/scripts/bench-signature-profiles.ts +0 -180
- package/scripts/bench-wire-codecs.ts +0 -161
- package/scripts/build-binary-conformance.ts +0 -36
- package/scripts/build-release-dashboard.ts +0 -175
- package/scripts/canary-rollout.ts +0 -38
- package/scripts/mcpGatewayCli.test.ts +0 -116
- package/scripts/prepare-aip-package.ts +0 -88
- package/scripts/regen-conformance-sigs.ts +0 -18
- package/scripts/release-gate.ts +0 -19
- package/scripts/validate-runtime-policy.ts +0 -18
- package/sdk/browser/index.test.ts +0 -162
- package/sdk/browser/index.ts +0 -257
- package/sdk/browser/package.json +0 -13
- package/sdk/go/cbor.go +0 -551
- package/sdk/go/cbor_test.go +0 -232
- package/sdk/go/encryption.go +0 -280
- package/sdk/go/encryption_test.go +0 -318
- package/sdk/go/go.mod +0 -7
- package/sdk/go/go.sum +0 -4
- package/sdk/go/http.go +0 -135
- package/sdk/go/protocol.go +0 -324
- package/sdk/go/protocol_test.go +0 -334
- package/sdk/go/replay.go +0 -121
- package/sdk/go/replay_test.go +0 -149
- package/sdk/go/webhook.go +0 -136
- package/sdk/pq/package-lock.json +0 -1358
- package/sdk/pq/package.json +0 -42
- package/sdk/pq/src/index.test.ts +0 -143
- package/sdk/pq/src/index.ts +0 -166
- package/sdk/pq/tsconfig.json +0 -14
- package/sdk/pq/vitest.config.ts +0 -7
- package/sdk/python/README.md +0 -18
- package/sdk/python/protocol_7h3/__init__.py +0 -46
- package/sdk/python/protocol_7h3/encryption.py +0 -252
- package/sdk/python/protocol_7h3/http.py +0 -212
- package/sdk/python/protocol_7h3/keys.py +0 -149
- package/sdk/python/protocol_7h3/pq.py +0 -244
- package/sdk/python/protocol_7h3/protocol.py +0 -525
- package/sdk/python/protocol_7h3/queue.py +0 -118
- package/sdk/python/protocol_7h3/replay.py +0 -98
- package/sdk/python/protocol_7h3/webhook.py +0 -116
- package/sdk/python/pyproject.toml +0 -40
- package/sdk/python/tests/test_conformance.py +0 -110
- package/sdk/python/tests/test_encryption.py +0 -206
- package/sdk/python/tests/test_http.py +0 -305
- package/sdk/python/tests/test_keys.py +0 -417
- package/sdk/python/tests/test_queue.py +0 -120
- package/sdk/python/tests/test_webhook.py +0 -345
- package/sdk/rust/Cargo.lock +0 -371
- package/sdk/rust/Cargo.toml +0 -25
- package/sdk/rust/README.md +0 -31
- package/sdk/rust/fuzz/Cargo.toml +0 -29
- package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +0 -46
- package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +0 -11
- package/sdk/rust/src/bin/aip_mcp_gateway.rs +0 -59
- package/sdk/rust/src/http.rs +0 -145
- package/sdk/rust/src/keys.rs +0 -161
- package/sdk/rust/src/lib.rs +0 -688
- package/sdk/rust/src/queue.rs +0 -79
- package/sdk/rust/src/webhook.rs +0 -86
- package/sdk/rust/tests/conformance.rs +0 -148
- package/sdk/rust/tests/gateway.rs +0 -130
- package/sdk/rust/tests/http_webhook_queue.rs +0 -201
- package/sdk/rust/tests/keys.rs +0 -189
- package/sdk/threshold/index.d.ts +0 -68
- package/sdk/threshold/index.d.ts.map +0 -1
- package/sdk/threshold/index.js +0 -254
- package/sdk/threshold/package-lock.json +0 -1361
- package/sdk/threshold/package.json +0 -39
- package/sdk/threshold/src/index.d.ts +0 -68
- package/sdk/threshold/src/index.d.ts.map +0 -1
- package/sdk/threshold/src/index.js +0 -254
- package/sdk/threshold/src/index.test.ts +0 -238
- package/sdk/threshold/src/index.ts +0 -355
- package/sdk/threshold/tsconfig.json +0 -19
- package/sdk/threshold/vitest.config.ts +0 -12
- package/src/agentAdapter.test.ts +0 -48
- package/src/agentAdapter.ts +0 -56
- package/src/auditLog.test.ts +0 -145
- package/src/auditLog.ts +0 -147
- package/src/capability.test.ts +0 -504
- package/src/capability.ts +0 -380
- package/src/cborCodec.test.ts +0 -263
- package/src/cborCodec.ts +0 -339
- package/src/conformance.test.ts +0 -136
- package/src/conformanceVectors.ts +0 -99
- package/src/encryption.test.ts +0 -206
- package/src/encryption.ts +0 -245
- package/src/envelopeCbor.ts +0 -140
- package/src/frameworkAdapters.test.ts +0 -290
- package/src/frameworkAdapters.ts +0 -261
- package/src/gateway.test.ts +0 -343
- package/src/gateway.ts +0 -246
- package/src/grpcBinding.test.ts +0 -211
- package/src/grpcBinding.ts +0 -103
- package/src/httpBinding.test.ts +0 -376
- package/src/httpBinding.ts +0 -189
- package/src/index.ts +0 -39
- package/src/keyInfra.test.ts +0 -278
- package/src/keyInfra.ts +0 -228
- package/src/keyRegistry.ts +0 -59
- package/src/keyRotation.test.ts +0 -78
- package/src/keyRotation.ts +0 -72
- package/src/mcpGateway.test.ts +0 -129
- package/src/mcpGateway.ts +0 -250
- package/src/mcpTransports.test.ts +0 -92
- package/src/mcpTransports.ts +0 -169
- package/src/mcpWrapper.test.ts +0 -179
- package/src/mcpWrapper.ts +0 -206
- package/src/otel.ts +0 -136
- package/src/policyEnforcer.test.ts +0 -99
- package/src/policyEnforcer.ts +0 -169
- package/src/policyTelemetryFeedback.test.ts +0 -25
- package/src/policyTelemetryFeedback.ts +0 -38
- package/src/protocol.bench.ts +0 -37
- package/src/protocol.d.ts.map +0 -1
- package/src/protocol.js +0 -294
- package/src/protocol.test.ts +0 -155
- package/src/protocol.ts +0 -414
- package/src/protocolAgent.test.ts +0 -105
- package/src/protocolAgent.ts +0 -169
- package/src/protocolBinary.test.ts +0 -165
- package/src/protocolBinary.ts +0 -312
- package/src/protocolCapabilities.ts +0 -70
- package/src/protocolFuzz.advanced.test.ts +0 -235
- package/src/protocolFuzz.test.ts +0 -111
- package/src/protocolNegative.test.ts +0 -97
- package/src/protocolReplay.test.ts +0 -71
- package/src/protocolReplay.ts +0 -194
- package/src/protocolTransport.test.ts +0 -556
- package/src/protocolTransport.ts +0 -483
- package/src/queueBinding.test.ts +0 -130
- package/src/queueBinding.ts +0 -102
- package/src/rateLimiter.test.ts +0 -96
- package/src/rateLimiter.ts +0 -46
- package/src/redisIntegration.test.ts +0 -134
- package/src/replayStores.test.ts +0 -273
- package/src/replayStores.ts +0 -215
- package/src/revocation.test.ts +0 -98
- package/src/revocation.ts +0 -0
- package/src/routePolicy.test.ts +0 -87
- package/src/routePolicy.ts +0 -72
- package/src/runtimePolicy.test.ts +0 -49
- package/src/runtimePolicy.ts +0 -81
- package/src/runtimePolicyManager.test.ts +0 -29
- package/src/runtimePolicyManager.ts +0 -50
- package/src/runtimePolicyPresets.ts +0 -43
- package/src/signedResponse.test.ts +0 -111
- package/src/signedResponse.ts +0 -83
- package/src/stream.test.ts +0 -254
- package/src/stream.ts +0 -417
- package/src/telemetry.test.ts +0 -251
- package/src/telemetry.ts +0 -299
- package/src/webhookBinding.test.ts +0 -144
- package/src/webhookBinding.ts +0 -115
- package/src/wsBinding.test.ts +0 -221
- package/src/wsBinding.ts +0 -200
- package/tsconfig.json +0 -15
- package/tsconfig.lib.json +0 -23
- package/vite.lib.config.ts +0 -16
- package/vitest.config.ts +0 -11
package/README.md
CHANGED
|
@@ -1,1327 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
<img src="./docs/assets/banner-github.png" alt="7h3 Protocol — Sign every message. Reject every replay." width="100%">
|
|
1
|
+
# @7h3/protocol
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
7h3 Protocol (wire version `7h3/0.1`):
|
|
4
|
+
deterministic, signed, replay-safe AI-to-AI message envelopes.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
[](https://www.npmjs.com/package/@7h3/protocol-browser)
|
|
8
|
-
[](https://pypi.org/project/7h3-protocol/)
|
|
9
|
-
[](https://crates.io/crates/protocol-7h3)
|
|
10
|
-
[](https://github.com/IceMasterT/7h3-protocol/tree/main/src)
|
|
11
|
-
[](./package.json)
|
|
12
|
-
[](./docs/VERSIONING_POLICY.md)
|
|
13
|
-
[](./LICENSE)
|
|
14
|
-
|
|
15
|
-
<br/>
|
|
16
|
-
|
|
17
|
-
**Cryptographic signing and replay protection for AI agent messages. One envelope. Every transport.**
|
|
18
|
-
|
|
19
|
-
<br/>
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
## The Problem
|
|
25
|
-
|
|
26
|
-
AI agent systems are moving fast, and the protocols underpinning them were not built with message-level security in mind.
|
|
27
|
-
|
|
28
|
-
**MCP (Model Context Protocol)** is plain JSON-RPC 2.0. A message in flight has no signature. Any intermediary — a rogue proxy, a compromised queue consumer, a misconfigured load balancer — can alter tool call parameters or replay a previously captured request. The MCP handler on the other end has no way to know.
|
|
29
|
-
|
|
30
|
-
**A2A (Agent-to-Agent)** improves on this by signing Agent Cards, giving agents a verifiable identity at the domain level. But Agent Cards are static configuration, not per-message traffic. Once an agent is "trusted," every message it sends thereafter is implicitly trusted regardless of whether the specific message was tampered with in transit or is a replay from ten minutes ago.
|
|
31
|
-
|
|
32
|
-
**HTTP APIs** default to IP-based rate limiting. IP addresses are trivially spoofed or shared — a single compromised NAT or cloud egress IP can represent thousands of agents. And there is no standard replay prevention: the same valid signed request can often be submitted multiple times, triggering duplicate writes, payments, or tool executions. There is no tamper-evident audit trail baked into the infrastructure — logs can be deleted or altered after the fact.
|
|
33
|
-
|
|
34
|
-
The gap these protocols share is identical: they authenticate *agents* at the connection or identity level, but they do not authenticate *individual messages* at the content level. 7h3 Protocol fills that gap without replacing any existing protocol.
|
|
35
|
-
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
## What 7h3 Protocol Does
|
|
39
|
-
|
|
40
|
-
7h3 Protocol wraps every message — regardless of transport — in a **signed envelope**. The envelope is compact, deterministic, and verifiable by any peer that holds the sender's public key (or shared secret).
|
|
41
|
-
|
|
42
|
-
The protocol provides four security primitives, all in one structure:
|
|
43
|
-
|
|
44
|
-
| Primitive | Mechanism |
|
|
45
|
-
|---|---|
|
|
46
|
-
| **Authentication** | Ed25519 or HMAC-SHA256 signature ties the message to a specific key |
|
|
47
|
-
| **Integrity** | Signature covers a canonical byte-identical serialization of the full envelope |
|
|
48
|
-
| **Replay prevention** | TTL expiry + unique nonce; a replayed envelope will fail `(messageId, nonce)` deduplication |
|
|
49
|
-
| **Non-repudiation** | Ed25519 asymmetric keys mean only the holder of the private key could have produced the signature |
|
|
50
|
-
|
|
51
|
-
The same envelope format works over HTTP, WebSocket, gRPC, message queues, and webhooks. One library. One wire format. Every transport.
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## How It Works
|
|
56
|
-
|
|
57
|
-
### Ed25519 Signing
|
|
58
|
-
|
|
59
|
-
Each sender generates an Ed25519 keypair. The private key signs messages; peers verify using the public key. Because Ed25519 is asymmetric, a peer that can verify your signatures cannot forge them — compromising one agent does not compromise the signing key of another.
|
|
60
|
-
|
|
61
|
-
For cases where key distribution is impractical, HMAC-SHA256 over a pre-shared secret is also supported, though it provides authentication without non-repudiation.
|
|
62
|
-
|
|
63
|
-
### Canonical Serialization
|
|
64
|
-
|
|
65
|
-
Signatures only mean something if everyone signs the same bytes. JSON object key order is not guaranteed by the spec, so the same message can serialize differently on different platforms. 7h3 Protocol solves this with deterministic JSON canonicalization: keys are sorted alphabetically at every nesting level, optional absent fields are omitted entirely (not set to `null` or `""`), and the result is UTF-8 encoded with no trailing whitespace.
|
|
66
|
-
|
|
67
|
-
The canonical form is identical byte-for-byte across TypeScript, Python, Rust, and Go. The conformance test suite proves this with shared test vectors.
|
|
68
|
-
|
|
69
|
-
### TTL and Nonce
|
|
70
|
-
|
|
71
|
-
Every envelope carries:
|
|
72
|
-
- `timestampMs` — when the message was created (Unix milliseconds)
|
|
73
|
-
- `ttlMs` — how long the message is valid
|
|
74
|
-
- `nonce` — a random value unique to this message
|
|
75
|
-
|
|
76
|
-
A receiver rejects the envelope if `now > timestampMs + ttlMs`. It then checks `(sender, messageId, nonce)` against a deduplication cache. A replayed envelope is rejected even if the signature is valid.
|
|
77
|
-
|
|
78
|
-
### The Envelope Structure
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
{"body":{"capability"?:"...","content":"...","correlationId"?:"...","intent":"..."},"header":{"messageId":"...","nonce":"...","recipient"?:"...","sender":"...","timestampMs":N,"ttlMs":N,"version":"7h3/0.1"}}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Optional fields (`capability`, `correlationId`, `recipient`) are omitted when absent — not set to `null` or `""`. This is load-bearing for the canonical form: any variation breaks the signature.
|
|
85
|
-
|
|
86
|
-
### Sequence Diagram
|
|
87
|
-
|
|
88
|
-
```mermaid
|
|
89
|
-
sequenceDiagram
|
|
90
|
-
participant S as Sender Agent
|
|
91
|
-
participant C as Canonical Serializer
|
|
92
|
-
participant K as Ed25519 Private Key
|
|
93
|
-
participant T as Transport (HTTP/WS/gRPC/Queue/Webhook)
|
|
94
|
-
participant G as Gateway / Receiver
|
|
95
|
-
participant V as Verifier
|
|
96
|
-
participant U as Upstream Service
|
|
97
|
-
|
|
98
|
-
S->>C: createEnvelope(sender, body, ttlMs)
|
|
99
|
-
C->>C: Deterministic JSON canonicalization
|
|
100
|
-
C->>K: sign(canonicalPayload)
|
|
101
|
-
K-->>C: Ed25519 signature (base64url)
|
|
102
|
-
C-->>S: SignedEnvelope {header, body, signature}
|
|
103
|
-
S->>T: Transmit via transport
|
|
104
|
-
T->>G: Request with envelope in header/metadata/wrapper
|
|
105
|
-
G->>V: verifyEnvelope(envelope, publicKey)
|
|
106
|
-
V->>V: Check TTL not expired
|
|
107
|
-
V->>V: Check nonce not replayed
|
|
108
|
-
V->>V: Verify Ed25519 signature
|
|
109
|
-
V->>V: Check allowedSenders + rate limit
|
|
110
|
-
V-->>G: {ok: true, sender: "agent.alpha"}
|
|
111
|
-
G->>U: Forward + inject x-7h3-sender header
|
|
112
|
-
U-->>G: Response
|
|
113
|
-
G-->>S: Response (optionally signed with x-7h3-response)
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
## 🔒 Security Guarantees
|
|
119
|
-
|
|
120
|
-
| Attack | Defense |
|
|
121
|
-
|---|---|
|
|
122
|
-
| **Impersonation** | Ed25519 signature — only the private key holder can produce a valid signature; no private key means no forgeable message |
|
|
123
|
-
| **Replay attacks** | `(messageId, nonce)` deduplication cache + TTL expiry — a captured valid message cannot be resubmitted |
|
|
124
|
-
| **Tampering** | Signature covers the canonical serialization of the full envelope body; any modification breaks verification |
|
|
125
|
-
| **Unauthorized access** | Per-route `allowedSenders` policy — envelopes from unlisted senders are rejected before reaching upstream |
|
|
126
|
-
| **Response spoofing** | Signed responses with `x-7h3-response` header; `correlationId` binding ties the response to the specific request |
|
|
127
|
-
| **Rate abuse** | `SlidingWindowRateLimiter` keyed by verified sender identity, not IP — VPN and NAT sharing does not grant extra quota |
|
|
128
|
-
| **Audit trail manipulation** | `InMemoryAuditLog` entries are themselves Ed25519-signed and chained; tampering with any entry breaks the chain |
|
|
129
|
-
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
## 🤖 Works with Claude (MCP)
|
|
133
|
-
|
|
134
|
-
Claude's tool-calling mechanism is MCP (Model Context Protocol), which uses plain JSON-RPC 2.0. 7h3 Protocol hardens MCP traffic without requiring any changes to your MCP handler.
|
|
135
|
-
|
|
136
|
-
`wrapMcpServer` wraps an existing MCP handler and enforces:
|
|
137
|
-
|
|
138
|
-
- **Signature verification** — every inbound JSON-RPC request must carry a valid 7h3 envelope
|
|
139
|
-
- **Replay protection** — `InMemoryReplayCache` injected automatically
|
|
140
|
-
- **Recipient binding** — the server rejects envelopes not addressed to its own `selfAgentId`, defeating cross-server relay attacks
|
|
141
|
-
- **Sender binding** — the client accepts responses only from the declared `peerAgentId`, defeating response spoofing
|
|
142
|
-
- **Correlation binding** — `correlationId` in every response must match the request's `messageId`, defeating response substitution
|
|
143
|
-
|
|
144
|
-
```mermaid
|
|
145
|
-
flowchart LR
|
|
146
|
-
CA[Claude Agent] -->|Signed JSON-RPC request| MW[7h3 MCP Wrapper]
|
|
147
|
-
MW -->|Verify signature\ncheck replay\nrecipient binding| MH[MCP Handler]
|
|
148
|
-
MH -->|Plain JSON-RPC response| MW
|
|
149
|
-
MW -->|Sign response\ncorrelation binding| CA
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
**Server side:**
|
|
153
|
-
|
|
154
|
-
```ts
|
|
155
|
-
import { wrapMcpServer, signEnvelopeEd25519 } from '@7h3/protocol'
|
|
156
|
-
|
|
157
|
-
const secureServer = wrapMcpServer(myMcpHandler, {
|
|
158
|
-
selfAgentId: 'my-mcp-server',
|
|
159
|
-
sign: (e) => signEnvelopeEd25519(e, serverPrivateKey, 'k1'),
|
|
160
|
-
})
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
**Client side:**
|
|
164
|
-
|
|
165
|
-
```ts
|
|
166
|
-
import { wrapMcpClient, signEnvelopeEd25519 } from '@7h3/protocol'
|
|
167
|
-
|
|
168
|
-
const { send } = wrapMcpClient({
|
|
169
|
-
selfAgentId: 'my-client',
|
|
170
|
-
peerAgentId: 'my-mcp-server',
|
|
171
|
-
sign: (e) => signEnvelopeEd25519(e, clientPrivateKey, 'k1'),
|
|
172
|
-
receive: {
|
|
173
|
-
signatureResolver: async ({ keyId }) => ({
|
|
174
|
-
alg: 'ED25519',
|
|
175
|
-
publicKey: serverPublicKey,
|
|
176
|
-
}),
|
|
177
|
-
},
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
const response = await send({ jsonrpc: '2.0', id: 1, method: 'tools/list' }, fetch)
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
The MCP handler itself is unchanged. The wrapper handles all envelope logic at the boundary.
|
|
184
|
-
|
|
185
|
-
---
|
|
186
|
-
|
|
187
|
-
## ⚡ Transport Coverage
|
|
188
|
-
|
|
189
|
-
7h3 Protocol covers all five common agent transports with the same signing primitives.
|
|
190
|
-
|
|
191
|
-
### HTTP / REST
|
|
192
|
-
|
|
193
|
-
Signed envelopes travel in the `x-7h3-envelope` request header. The gateway verifies before forwarding; upstream receives `x-7h3-sender` and `x-7h3-verified: true`.
|
|
194
|
-
|
|
195
|
-
```mermaid
|
|
196
|
-
flowchart LR
|
|
197
|
-
A[Client] -->|POST /api/action\nx-7h3-envelope: {...signed...}| B[API Gateway]
|
|
198
|
-
B -->|verify signature\nrate limit check| C{Policy match?}
|
|
199
|
-
C -->|✅ pass| D[Upstream Service\nx-7h3-sender: agent.alice\nx-7h3-verified: true]
|
|
200
|
-
C -->|❌ fail| E[401 / 403 / 429]
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
```ts
|
|
204
|
-
import { createEnvelope, signEnvelopeEd25519 } from '@7h3/protocol'
|
|
205
|
-
|
|
206
|
-
const envelope = await signEnvelopeEd25519(
|
|
207
|
-
createEnvelope({ sender: 'agent.alice', intent: 'write', content: JSON.stringify(payload) }),
|
|
208
|
-
privateKey, 'k1',
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
await fetch('https://api.example.com/action', {
|
|
212
|
-
method: 'POST',
|
|
213
|
-
headers: { 'x-7h3-envelope': JSON.stringify(envelope) },
|
|
214
|
-
body: JSON.stringify(payload),
|
|
215
|
-
})
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
### WebSocket
|
|
219
|
-
|
|
220
|
-
Each frame carries a signed JSON envelope with a monotonically increasing `sequenceNumber` to detect out-of-order or dropped frames.
|
|
221
|
-
|
|
222
|
-
```mermaid
|
|
223
|
-
flowchart LR
|
|
224
|
-
A[Agent] -->|ws.send signed frame| B[WebSocket Server]
|
|
225
|
-
B -->|verify per-frame signature\nsequence check| C[Handler]
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
```ts
|
|
229
|
-
import { wrapWebSocket } from '@7h3/protocol'
|
|
230
|
-
|
|
231
|
-
const ws = new WebSocket('wss://agent.example.com/stream')
|
|
232
|
-
|
|
233
|
-
const secure = wrapWebSocket(ws, {
|
|
234
|
-
sender: 'agent.alpha',
|
|
235
|
-
sign: (e) => signEnvelopeEd25519(e, privateKey, 'k1'),
|
|
236
|
-
verify: (e) => verifyEnvelopeEd25519(e, peerPublicKey),
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
secure.send({ intent: 'UPDATE', content: 'delta-payload' })
|
|
240
|
-
secure.onMessage((verified) => console.log(verified.body))
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### gRPC
|
|
244
|
-
|
|
245
|
-
Envelopes travel in the `7h3-envelope-bin` metadata key (binary-safe base64url). The interceptor verifies before the handler is invoked.
|
|
246
|
-
|
|
247
|
-
```mermaid
|
|
248
|
-
flowchart LR
|
|
249
|
-
A[gRPC Client] -->|metadata: 7h3-envelope-bin| B[gRPC Interceptor]
|
|
250
|
-
B -->|verify| C[gRPC Handler]
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
```ts
|
|
254
|
-
import { withGrpcVerification } from '@7h3/protocol'
|
|
255
|
-
|
|
256
|
-
const server = new grpc.Server()
|
|
257
|
-
server.addService(MyService, withGrpcVerification(myServiceImpl, {
|
|
258
|
-
verify: (e) => verifyEnvelopeEd25519(e, clientPublicKey),
|
|
259
|
-
}))
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
### Message Queues (SQS, RabbitMQ, etc.)
|
|
263
|
-
|
|
264
|
-
Messages are wrapped in `{ envelope, payload }` JSON. The consumer verifies the envelope before processing the payload. Works with any queue that accepts JSON message bodies.
|
|
265
|
-
|
|
266
|
-
```mermaid
|
|
267
|
-
flowchart LR
|
|
268
|
-
P[Producer] -->|{envelope, payload}| Q[Queue]
|
|
269
|
-
Q --> C[Consumer]
|
|
270
|
-
C -->|verify envelope\nthen process payload| H[Handler]
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
```ts
|
|
274
|
-
import { signQueueMessage, verifyQueueBatch } from '@7h3/protocol'
|
|
275
|
-
|
|
276
|
-
// Producer
|
|
277
|
-
const message = await signQueueMessage(
|
|
278
|
-
{ intent: 'PROCESS_ORDER', content: JSON.stringify(order) },
|
|
279
|
-
{ sender: 'order-service', sign: (e) => signEnvelopeEd25519(e, privateKey, 'k1') },
|
|
280
|
-
)
|
|
281
|
-
await sqs.sendMessage({ QueueUrl, MessageBody: JSON.stringify(message) }).promise()
|
|
282
|
-
|
|
283
|
-
// Consumer
|
|
284
|
-
const results = await verifyQueueBatch(messages, {
|
|
285
|
-
verify: (e) => verifyEnvelopeEd25519(e, producerPublicKey),
|
|
286
|
-
})
|
|
287
|
-
for (const { ok, payload, sender } of results) {
|
|
288
|
-
if (ok) await processOrder(payload)
|
|
289
|
-
}
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### Webhooks
|
|
293
|
-
|
|
294
|
-
Webhook requests carry two headers: `x-7h3-sig` (Ed25519 signature of the body) and `x-7h3-ts` (Unix timestamp). The receiver verifies both before processing. Timestamp checking prevents replay of captured webhook payloads.
|
|
295
|
-
|
|
296
|
-
```mermaid
|
|
297
|
-
flowchart LR
|
|
298
|
-
S[Sender] -->|POST /webhook\nx-7h3-sig: ...\nx-7h3-ts: ...| R[Receiver]
|
|
299
|
-
R -->|verify sig + ts freshness| H[Handler]
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
```ts
|
|
303
|
-
import { signWebhook, verifyWebhook, consumeWebhook } from '@7h3/protocol'
|
|
304
|
-
|
|
305
|
-
// Sender
|
|
306
|
-
const { headers } = await signWebhook(body, { privateKey, keyId: 'k1' })
|
|
307
|
-
await fetch('https://partner.example.com/webhook', {
|
|
308
|
-
method: 'POST',
|
|
309
|
-
headers: { 'content-type': 'application/json', ...headers },
|
|
310
|
-
body,
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
// Receiver (Express)
|
|
314
|
-
app.post('/webhook', async (req, res) => {
|
|
315
|
-
const result = await verifyWebhook(req.rawBody, req.headers, { publicKey })
|
|
316
|
-
if (!result.ok) return res.status(401).end()
|
|
317
|
-
await processWebhook(result.payload)
|
|
318
|
-
res.status(200).end()
|
|
319
|
-
})
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
---
|
|
323
|
-
|
|
324
|
-
## 🚀 Installation
|
|
325
|
-
|
|
326
|
-
### TypeScript / Node.js
|
|
6
|
+
Install:
|
|
327
7
|
|
|
328
8
|
```bash
|
|
329
9
|
npm install @7h3/protocol
|
|
330
|
-
# or
|
|
331
|
-
yarn add @7h3/protocol
|
|
332
|
-
# or
|
|
333
|
-
pnpm add @7h3/protocol
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
**Requirements:** Node.js ≥ 18, or any runtime with `globalThis.crypto` (Web Crypto API). Zero runtime dependencies.
|
|
337
|
-
|
|
338
|
-
### Browser / Edge (Cloudflare Workers, Deno, Bun)
|
|
339
|
-
|
|
340
|
-
```bash
|
|
341
|
-
npm install @7h3/protocol-browser
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
Pure Web Crypto API. No Node.js built-ins. Works in browsers, Cloudflare Workers, Deno, and Bun out of the box. Zero dependencies.
|
|
345
|
-
|
|
346
|
-
### Python
|
|
347
|
-
|
|
348
|
-
```bash
|
|
349
|
-
pip install 7h3-protocol
|
|
350
|
-
# or
|
|
351
|
-
uv add 7h3-protocol
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
**Requirements:** Python ≥ 3.9. Uses `cryptography` for Ed25519.
|
|
355
|
-
|
|
356
|
-
### Rust
|
|
357
|
-
|
|
358
|
-
```toml
|
|
359
|
-
[dependencies]
|
|
360
|
-
protocol-7h3 = "0.4"
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
Or via cargo:
|
|
364
|
-
|
|
365
|
-
```bash
|
|
366
|
-
cargo add protocol-7h3
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
Zero external dependencies. Pure stdlib + `ed25519-dalek`.
|
|
370
|
-
|
|
371
|
-
### Go
|
|
372
|
-
|
|
373
|
-
```bash
|
|
374
|
-
go get github.com/IceMasterT/7h3-protocol/sdk/go
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
Zero external dependencies. Pure stdlib (`crypto/ed25519`).
|
|
378
|
-
|
|
379
|
-
### CLI (`7h3` binary)
|
|
380
|
-
|
|
381
|
-
```bash
|
|
382
|
-
npm install -g @7h3/protocol
|
|
383
|
-
7h3 --help
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
### Docker
|
|
387
|
-
|
|
388
|
-
```bash
|
|
389
|
-
docker pull 7h3agency/gateway:latest
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
---
|
|
393
|
-
|
|
394
|
-
## Quick Start
|
|
395
|
-
|
|
396
|
-
The minimum to sign and verify a message.
|
|
397
|
-
|
|
398
|
-
### TypeScript
|
|
399
|
-
|
|
400
|
-
```ts
|
|
401
|
-
import {
|
|
402
|
-
generateEd25519KeypairBase64Url,
|
|
403
|
-
createEnvelope,
|
|
404
|
-
signEnvelopeEd25519,
|
|
405
|
-
verifyEnvelopeEd25519,
|
|
406
|
-
} from '@7h3/protocol'
|
|
407
|
-
|
|
408
|
-
const { privateKey, publicKey } = await generateEd25519KeypairBase64Url()
|
|
409
|
-
|
|
410
|
-
const envelope = await signEnvelopeEd25519(
|
|
411
|
-
createEnvelope({ sender: 'agent.alpha', intent: 'TASK', content: 'hello' }),
|
|
412
|
-
privateKey, 'k1',
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
const ok = await verifyEnvelopeEd25519(envelope, publicKey)
|
|
416
|
-
console.log(ok) // true
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
### Python
|
|
420
|
-
|
|
421
|
-
```python
|
|
422
|
-
from protocol_7h3 import generate_keypair, create_envelope, sign_envelope, verify_envelope
|
|
423
|
-
|
|
424
|
-
private_key, public_key = generate_keypair()
|
|
425
|
-
|
|
426
|
-
envelope = sign_envelope(
|
|
427
|
-
create_envelope(sender="agent.alpha", intent="TASK", content="hello"),
|
|
428
|
-
private_key, key_id="k1",
|
|
429
|
-
)
|
|
430
|
-
|
|
431
|
-
ok = verify_envelope(envelope, public_key)
|
|
432
|
-
print(ok) # True
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
### Rust
|
|
436
|
-
|
|
437
|
-
```rust
|
|
438
|
-
use protocol_7h3::{generate_keypair, create_envelope, sign_envelope, verify_envelope};
|
|
439
|
-
|
|
440
|
-
let (private_key, public_key) = generate_keypair();
|
|
441
|
-
|
|
442
|
-
let env = create_envelope("agent.alpha", "TASK", "hello", 30_000);
|
|
443
|
-
let signed = sign_envelope(&env, &private_key, "k1")?;
|
|
444
|
-
|
|
445
|
-
let ok = verify_envelope(&signed, &public_key)?;
|
|
446
|
-
assert!(ok);
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
### Go
|
|
450
|
-
|
|
451
|
-
```go
|
|
452
|
-
import "github.com/IceMasterT/7h3-protocol/sdk/go/protocol7h3"
|
|
453
|
-
|
|
454
|
-
privateKey, publicKey, _ := protocol7h3.GenerateKeypair()
|
|
455
|
-
|
|
456
|
-
env := protocol7h3.CreateEnvelope("agent.alpha", "TASK", "hello", 30000)
|
|
457
|
-
signed, _ := protocol7h3.SignEnvelope(env, privateKey, "k1")
|
|
458
|
-
|
|
459
|
-
ok, _ := protocol7h3.VerifyEnvelope(signed, publicKey)
|
|
460
|
-
fmt.Println(ok) // true
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
---
|
|
464
|
-
|
|
465
|
-
## HTTP Middleware
|
|
466
|
-
|
|
467
|
-
Drop-in middleware for common frameworks.
|
|
468
|
-
|
|
469
|
-
### Express.js (TypeScript)
|
|
470
|
-
|
|
471
|
-
```ts
|
|
472
|
-
import express from 'express'
|
|
473
|
-
import { createVerifyMiddleware } from '@7h3/protocol'
|
|
474
|
-
|
|
475
|
-
const app = express()
|
|
476
|
-
|
|
477
|
-
app.use(createVerifyMiddleware({
|
|
478
|
-
verify: (e) => verifyEnvelopeEd25519(e, agentPublicKey),
|
|
479
|
-
onFailure: (res, reason) => res.status(401).json({ error: reason }),
|
|
480
|
-
}))
|
|
481
|
-
|
|
482
|
-
app.post('/action', (req, res) => {
|
|
483
|
-
// req.headers['x-7h3-sender'] contains the verified sender identity
|
|
484
|
-
const sender = req.headers['x-7h3-sender']
|
|
485
|
-
res.json({ received: true, from: sender })
|
|
486
|
-
})
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
### Flask (Python)
|
|
490
|
-
|
|
491
|
-
```python
|
|
492
|
-
from flask import Flask, request, jsonify
|
|
493
|
-
from protocol_7h3 import verify_middleware
|
|
494
|
-
|
|
495
|
-
app = Flask(__name__)
|
|
496
|
-
|
|
497
|
-
@app.before_request
|
|
498
|
-
@verify_middleware(public_key=AGENT_PUBLIC_KEY)
|
|
499
|
-
def require_signed():
|
|
500
|
-
pass
|
|
501
|
-
|
|
502
|
-
@app.route('/action', methods=['POST'])
|
|
503
|
-
def action():
|
|
504
|
-
sender = request.headers.get('x-7h3-sender')
|
|
505
|
-
return jsonify(received=True, sender=sender)
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
### Go net/http
|
|
509
|
-
|
|
510
|
-
```go
|
|
511
|
-
import "github.com/IceMasterT/7h3-protocol/sdk/go/protocol7h3"
|
|
512
|
-
|
|
513
|
-
func verifyMiddleware(next http.Handler, pubKey []byte) http.Handler {
|
|
514
|
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
515
|
-
if err := protocol7h3.VerifyRequest(r, pubKey); err != nil {
|
|
516
|
-
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
517
|
-
return
|
|
518
|
-
}
|
|
519
|
-
next.ServeHTTP(w, r)
|
|
520
|
-
})
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
http.Handle("/action", verifyMiddleware(actionHandler, agentPublicKey))
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
---
|
|
527
|
-
|
|
528
|
-
## 🛡️ API Gateway
|
|
529
|
-
|
|
530
|
-
The zero-code path: drop the 7h3 gateway in front of any existing service. No application changes required.
|
|
531
|
-
|
|
532
|
-
**Step 1: Generate keys**
|
|
533
|
-
|
|
534
|
-
```bash
|
|
535
|
-
7h3 keygen --output my-keys.json
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
**Step 2: Configure `7h3.yaml`**
|
|
539
|
-
|
|
540
|
-
```yaml
|
|
541
|
-
gateway:
|
|
542
|
-
upstream: http://my-api:3000
|
|
543
|
-
port: 8080
|
|
544
|
-
|
|
545
|
-
auth:
|
|
546
|
-
require: ed25519
|
|
547
|
-
|
|
548
|
-
routes:
|
|
549
|
-
- path: "/admin/**"
|
|
550
|
-
allowedSenders: ["agent.admin"]
|
|
551
|
-
require: ed25519
|
|
552
|
-
|
|
553
|
-
- path: "/api/**"
|
|
554
|
-
rateLimit:
|
|
555
|
-
windowMs: 60000
|
|
556
|
-
max: 100
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
**Step 3: Run**
|
|
560
|
-
|
|
561
|
-
```bash
|
|
562
|
-
# CLI
|
|
563
|
-
7h3 gateway --upstream http://my-api:3000 --public-key MCowBQYDK2Vw...
|
|
564
|
-
|
|
565
|
-
# Docker
|
|
566
|
-
docker run -p 8080:8080 \
|
|
567
|
-
-e GATEWAY_PRIVATE_KEY=... \
|
|
568
|
-
7h3agency/gateway:latest \
|
|
569
|
-
--upstream http://my-api:3000 --require ed25519
|
|
570
|
-
|
|
571
|
-
# Docker Compose
|
|
572
|
-
docker compose up
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
### Gateway Architecture
|
|
576
|
-
|
|
577
|
-
```mermaid
|
|
578
|
-
flowchart TB
|
|
579
|
-
subgraph Clients
|
|
580
|
-
A1[AI Agent A]
|
|
581
|
-
A2[AI Agent B]
|
|
582
|
-
A3[Browser SDK]
|
|
583
|
-
end
|
|
584
|
-
subgraph "7h3 Gateway"
|
|
585
|
-
GW[Protocol7h3Gateway\nverify → rate-limit → policy]
|
|
586
|
-
AL[InMemoryAuditLog\nEd25519-signed entries]
|
|
587
|
-
end
|
|
588
|
-
subgraph "Your Services (unchanged)"
|
|
589
|
-
S1[API Service]
|
|
590
|
-
S2[Admin Service]
|
|
591
|
-
S3[Webhook Handler]
|
|
592
|
-
end
|
|
593
|
-
A1 -->|signed request| GW
|
|
594
|
-
A2 -->|signed request| GW
|
|
595
|
-
A3 -->|signed request| GW
|
|
596
|
-
GW -->|x-7h3-sender injected| S1
|
|
597
|
-
GW -->|policy: ed25519 + allowedSenders| S2
|
|
598
|
-
GW -.->|log every event| AL
|
|
599
|
-
S1 -.->|optional signed response| A1
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
The gateway exposes three methods in code:
|
|
603
|
-
|
|
604
|
-
```ts
|
|
605
|
-
import { createGateway } from '@7h3/protocol'
|
|
606
|
-
|
|
607
|
-
const gateway = createGateway(config)
|
|
608
|
-
|
|
609
|
-
// Verify an inbound envelope
|
|
610
|
-
const result = await gateway.verify(envelope)
|
|
611
|
-
|
|
612
|
-
// Handle a full HTTP request (verify + route + forward)
|
|
613
|
-
const response = await gateway.handle(request)
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
---
|
|
617
|
-
|
|
618
|
-
## Per-Route Policies
|
|
619
|
-
|
|
620
|
-
Each route can enforce independent authentication requirements, sender allowlists, and rate limits.
|
|
621
|
-
|
|
622
|
-
### TypeScript
|
|
623
|
-
|
|
624
|
-
```ts
|
|
625
|
-
import { RoutePolicy } from '@7h3/protocol'
|
|
626
|
-
|
|
627
|
-
const policies: RoutePolicy[] = [
|
|
628
|
-
{
|
|
629
|
-
path: '/admin/**',
|
|
630
|
-
require: 'ed25519',
|
|
631
|
-
allowedSenders: ['agent.admin', 'agent.operator'],
|
|
632
|
-
rateLimit: { windowMs: 60_000, max: 20 },
|
|
633
|
-
},
|
|
634
|
-
{
|
|
635
|
-
path: '/api/write',
|
|
636
|
-
require: 'ed25519',
|
|
637
|
-
rateLimit: { windowMs: 60_000, max: 100 },
|
|
638
|
-
},
|
|
639
|
-
{
|
|
640
|
-
path: '/api/read/**',
|
|
641
|
-
require: 'hmac',
|
|
642
|
-
},
|
|
643
|
-
]
|
|
644
|
-
```
|
|
645
|
-
|
|
646
|
-
### YAML
|
|
647
|
-
|
|
648
|
-
```yaml
|
|
649
|
-
routes:
|
|
650
|
-
- path: "/admin/**"
|
|
651
|
-
require: ed25519
|
|
652
|
-
allowedSenders:
|
|
653
|
-
- agent.admin
|
|
654
|
-
- agent.operator
|
|
655
|
-
rateLimit:
|
|
656
|
-
windowMs: 60000
|
|
657
|
-
max: 20
|
|
658
|
-
|
|
659
|
-
- path: "/api/write"
|
|
660
|
-
require: ed25519
|
|
661
|
-
rateLimit:
|
|
662
|
-
windowMs: 60000
|
|
663
|
-
max: 100
|
|
664
|
-
|
|
665
|
-
- path: "/api/read/**"
|
|
666
|
-
require: hmac
|
|
667
10
|
```
|
|
668
11
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
---
|
|
672
|
-
|
|
673
|
-
## Rate Limiting
|
|
674
|
-
|
|
675
|
-
The `SlidingWindowRateLimiter` is keyed by **verified sender identity**, not IP address. This means:
|
|
676
|
-
- NAT, VPNs, and shared cloud egress IPs do not inflate any agent's quota
|
|
677
|
-
- Quota follows the cryptographic identity of the agent regardless of network topology
|
|
678
|
-
- Each window is computed per sender as a sliding window (not a fixed bucket), preventing burst exploitation at window boundaries
|
|
12
|
+
Import:
|
|
679
13
|
|
|
680
14
|
```ts
|
|
681
|
-
import {
|
|
682
|
-
|
|
683
|
-
const limiter = new SlidingWindowRateLimiter({
|
|
684
|
-
windowMs: 60_000, // 1 minute
|
|
685
|
-
max: 100, // 100 requests per window per sender
|
|
686
|
-
})
|
|
687
|
-
|
|
688
|
-
// After verifying the envelope:
|
|
689
|
-
const allowed = await limiter.check(verifiedSender)
|
|
690
|
-
if (!allowed) {
|
|
691
|
-
return new Response('Too Many Requests', { status: 429 })
|
|
692
|
-
}
|
|
693
|
-
```
|
|
694
|
-
|
|
695
|
-
Rate limit state is per-instance. For distributed deployments, configure an external store (Redis adapter available) in `7h3.yaml`:
|
|
696
|
-
|
|
697
|
-
```yaml
|
|
698
|
-
rateLimit:
|
|
699
|
-
store: redis
|
|
700
|
-
redisUrl: redis://localhost:6379
|
|
15
|
+
import { createAipAgentAdapter, receiveEnvelope } from '@7h3/protocol'
|
|
701
16
|
```
|
|
702
|
-
|
|
703
|
-
---
|
|
704
|
-
|
|
705
|
-
## Signed Responses
|
|
706
|
-
|
|
707
|
-
Responses can optionally carry a server signature in the `x-7h3-response` header. This gives clients cryptographic proof that the response came from the expected server and corresponds to their specific request.
|
|
708
|
-
|
|
709
|
-
```ts
|
|
710
|
-
import { signResponse, verifyResponse } from '@7h3/protocol'
|
|
711
|
-
|
|
712
|
-
// Server: sign the response
|
|
713
|
-
const responseEnvelope = await signResponse(responseBody, {
|
|
714
|
-
sender: 'api-server',
|
|
715
|
-
correlationId: requestEnvelope.header.messageId,
|
|
716
|
-
sign: (e) => signEnvelopeEd25519(e, serverPrivateKey, 'k1'),
|
|
717
|
-
})
|
|
718
|
-
|
|
719
|
-
// Server sends: x-7h3-response: <JSON stringified responseEnvelope>
|
|
720
|
-
|
|
721
|
-
// Client: verify the response
|
|
722
|
-
const verified = await verifyResponse(responseEnvelope, {
|
|
723
|
-
expectedSender: 'api-server',
|
|
724
|
-
expectedCorrelationId: sentEnvelope.header.messageId,
|
|
725
|
-
verify: (e) => verifyEnvelopeEd25519(e, serverPublicKey),
|
|
726
|
-
})
|
|
727
|
-
|
|
728
|
-
if (!verified.ok) throw new Error(`Response rejected: ${verified.reason}`)
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
Bidirectional trust matters when agents can be instructed by servers to take further actions. Without signed responses, a compromised intermediary can send arbitrary instructions on behalf of a trusted server.
|
|
732
|
-
|
|
733
|
-
---
|
|
734
|
-
|
|
735
|
-
## 📋 Tamper-Evident Audit Log
|
|
736
|
-
|
|
737
|
-
`InMemoryAuditLog` records every verification event. Each log entry is itself Ed25519-signed, and each entry's signature covers the hash of the previous entry. Deleting or modifying any entry breaks the chain — the `verify()` call surfaces exactly which entries were tampered with.
|
|
738
|
-
|
|
739
|
-
```ts
|
|
740
|
-
import { createAuditLog } from '@7h3/protocol'
|
|
741
|
-
|
|
742
|
-
const auditLog = createAuditLog({
|
|
743
|
-
sign: (e) => signEnvelopeEd25519(e, auditPrivateKey, 'audit-k1'),
|
|
744
|
-
})
|
|
745
|
-
|
|
746
|
-
// Log an event
|
|
747
|
-
await auditLog.log({
|
|
748
|
-
event: 'VERIFY_OK',
|
|
749
|
-
sender: 'agent.alpha',
|
|
750
|
-
messageId: envelope.header.messageId,
|
|
751
|
-
route: '/api/action',
|
|
752
|
-
})
|
|
753
|
-
|
|
754
|
-
// Query events
|
|
755
|
-
const events = await auditLog.query({ sender: 'agent.alpha', since: Date.now() - 3600_000 })
|
|
756
|
-
|
|
757
|
-
// Verify chain integrity
|
|
758
|
-
const integrity = await auditLog.verify()
|
|
759
|
-
if (!integrity.ok) {
|
|
760
|
-
console.error('Audit log tampered at entry:', integrity.firstTamperedIndex)
|
|
761
|
-
}
|
|
762
|
-
```
|
|
763
|
-
|
|
764
|
-
```mermaid
|
|
765
|
-
flowchart LR
|
|
766
|
-
E1[Entry 1\n✅ valid sig] --> E2[Entry 2\n✅ valid sig] --> E3[Entry 3\n✅ valid sig]
|
|
767
|
-
E3 --> E4[Entry 4\n❌ TAMPERED\nsig broken]
|
|
768
|
-
style E4 fill:#ef4444,color:#fff
|
|
769
|
-
```
|
|
770
|
-
|
|
771
|
-
Each entry contains: `event`, `sender`, `messageId`, `route`, `timestampMs`, `signature`, and `prevHash`. The chain is append-only during normal operation; `verify()` is a read-only integrity check that can be run at any time.
|
|
772
|
-
|
|
773
|
-
---
|
|
774
|
-
|
|
775
|
-
## 🔑 Key Management
|
|
776
|
-
|
|
777
|
-
### Generate a Keypair
|
|
778
|
-
|
|
779
|
-
```bash
|
|
780
|
-
# CLI — writes to stdout or --output file
|
|
781
|
-
7h3 keygen
|
|
782
|
-
7h3 keygen --output my-keys.json
|
|
783
|
-
|
|
784
|
-
# TypeScript
|
|
785
|
-
const { privateKey, publicKey } = await generateEd25519KeypairBase64Url()
|
|
786
|
-
|
|
787
|
-
# Python
|
|
788
|
-
private_key, public_key = generate_keypair()
|
|
789
|
-
|
|
790
|
-
# Rust
|
|
791
|
-
let (private_key, public_key) = generate_keypair();
|
|
792
|
-
|
|
793
|
-
# Go
|
|
794
|
-
privateKey, publicKey, _ := protocol7h3.GenerateKeypair()
|
|
795
|
-
```
|
|
796
|
-
|
|
797
|
-
### Key Rotation
|
|
798
|
-
|
|
799
|
-
`KeyRotationManager` manages multiple active key versions. Old keys remain valid for verification until explicitly revoked; only the current key is used for signing.
|
|
800
|
-
|
|
801
|
-
```ts
|
|
802
|
-
import { KeyRotationManager } from '@7h3/protocol'
|
|
803
|
-
|
|
804
|
-
const manager = new KeyRotationManager()
|
|
805
|
-
await manager.addKey({ keyId: 'k2', privateKey: newPrivKey, publicKey: newPubKey })
|
|
806
|
-
await manager.setActive('k2')
|
|
807
|
-
// k1 still verifies existing messages; k2 signs new ones
|
|
808
|
-
```
|
|
809
|
-
|
|
810
|
-
### Revocation
|
|
811
|
-
|
|
812
|
-
```ts
|
|
813
|
-
import { RevocationRegistry } from '@7h3/protocol'
|
|
814
|
-
|
|
815
|
-
const registry = new RevocationRegistry()
|
|
816
|
-
await registry.revoke('k1', { reason: 'key-compromise', revokedAt: Date.now() })
|
|
817
|
-
|
|
818
|
-
// Verification now rejects envelopes signed with k1
|
|
819
|
-
const result = await verifyEnvelopeEd25519(envelope, publicKey, { registry })
|
|
820
|
-
```
|
|
821
|
-
|
|
822
|
-
### Public Key Discovery
|
|
823
|
-
|
|
824
|
-
Serve your public keys at `/.well-known/7h3-keys` for automatic discovery by peers:
|
|
825
|
-
|
|
826
|
-
```bash
|
|
827
|
-
# Serve via CLI
|
|
828
|
-
7h3 keys serve --public-key MCowBQYDK2Vw...
|
|
829
|
-
|
|
830
|
-
# Or configure in 7h3.yaml
|
|
831
|
-
keyDiscovery:
|
|
832
|
-
enabled: true
|
|
833
|
-
keys:
|
|
834
|
-
- keyId: k1
|
|
835
|
-
publicKey: MCowBQYDK2Vw...
|
|
836
|
-
algorithm: ed25519
|
|
837
|
-
```
|
|
838
|
-
|
|
839
|
-
Response format:
|
|
840
|
-
|
|
841
|
-
```json
|
|
842
|
-
{
|
|
843
|
-
"version": "7h3/0.1",
|
|
844
|
-
"keys": [
|
|
845
|
-
{ "keyId": "k1", "algorithm": "ed25519", "publicKey": "MCowBQYDK2Vw..." }
|
|
846
|
-
]
|
|
847
|
-
}
|
|
848
|
-
```
|
|
849
|
-
|
|
850
|
-
---
|
|
851
|
-
|
|
852
|
-
## WebSocket Usage
|
|
853
|
-
|
|
854
|
-
```ts
|
|
855
|
-
import { wrapWebSocket, signEnvelopeEd25519, verifyEnvelopeEd25519 } from '@7h3/protocol'
|
|
856
|
-
|
|
857
|
-
const ws = new WebSocket('wss://agent.example.com/stream')
|
|
858
|
-
|
|
859
|
-
const secure = wrapWebSocket(ws, {
|
|
860
|
-
sender: 'agent.alpha',
|
|
861
|
-
sign: (envelope) => signEnvelopeEd25519(envelope, privateKey, 'k1'),
|
|
862
|
-
verify: (envelope) => verifyEnvelopeEd25519(envelope, peerPublicKey),
|
|
863
|
-
})
|
|
864
|
-
|
|
865
|
-
// Send a signed frame
|
|
866
|
-
await secure.send({ intent: 'UPDATE', content: JSON.stringify({ delta: 42 }) })
|
|
867
|
-
|
|
868
|
-
// Receive verified frames
|
|
869
|
-
secure.onMessage((verified) => {
|
|
870
|
-
console.log('from:', verified.header.sender)
|
|
871
|
-
console.log('body:', verified.body)
|
|
872
|
-
})
|
|
873
|
-
|
|
874
|
-
// Sequence numbers are managed automatically; out-of-order frames throw
|
|
875
|
-
secure.onSequenceError((err) => console.error('Frame sequence gap:', err))
|
|
876
|
-
```
|
|
877
|
-
|
|
878
|
-
Each frame carries an auto-incremented `sequenceNumber` in the header. The receiver tracks the expected sequence and rejects frames that arrive out of order or with gaps.
|
|
879
|
-
|
|
880
|
-
---
|
|
881
|
-
|
|
882
|
-
## gRPC Usage
|
|
883
|
-
|
|
884
|
-
```ts
|
|
885
|
-
import * as grpc from '@grpc/grpc-js'
|
|
886
|
-
import { withGrpcVerification, signEnvelopeEd25519 } from '@7h3/protocol'
|
|
887
|
-
|
|
888
|
-
// Server: wrap handler with signature verification
|
|
889
|
-
const server = new grpc.Server()
|
|
890
|
-
server.addService(
|
|
891
|
-
AgentService,
|
|
892
|
-
withGrpcVerification(agentServiceImpl, {
|
|
893
|
-
metadataKey: '7h3-envelope-bin',
|
|
894
|
-
verify: (envelope) => verifyEnvelopeEd25519(envelope, clientPublicKey),
|
|
895
|
-
}),
|
|
896
|
-
)
|
|
897
|
-
|
|
898
|
-
// Client: attach signed envelope to outbound metadata
|
|
899
|
-
const metadata = new grpc.Metadata()
|
|
900
|
-
const envelope = await signEnvelopeEd25519(
|
|
901
|
-
createEnvelope({ sender: 'client', intent: 'CALL', content: '' }),
|
|
902
|
-
privateKey, 'k1',
|
|
903
|
-
)
|
|
904
|
-
metadata.set('7h3-envelope-bin', Buffer.from(JSON.stringify(envelope)).toString('base64'))
|
|
905
|
-
|
|
906
|
-
const stub = new AgentServiceClient(address, grpc.credentials.createInsecure())
|
|
907
|
-
stub.someMethod(request, metadata, callback)
|
|
908
|
-
```
|
|
909
|
-
|
|
910
|
-
---
|
|
911
|
-
|
|
912
|
-
## Queue Usage
|
|
913
|
-
|
|
914
|
-
```ts
|
|
915
|
-
import { signQueueMessage, verifyQueueBatch } from '@7h3/protocol'
|
|
916
|
-
import { SQS } from 'aws-sdk'
|
|
917
|
-
|
|
918
|
-
const sqs = new SQS()
|
|
919
|
-
|
|
920
|
-
// Producer
|
|
921
|
-
async function enqueue(order: Order) {
|
|
922
|
-
const message = await signQueueMessage(
|
|
923
|
-
{ intent: 'PROCESS_ORDER', content: JSON.stringify(order) },
|
|
924
|
-
{
|
|
925
|
-
sender: 'order-service',
|
|
926
|
-
sign: (e) => signEnvelopeEd25519(e, privateKey, 'k1'),
|
|
927
|
-
},
|
|
928
|
-
)
|
|
929
|
-
await sqs.sendMessage({
|
|
930
|
-
QueueUrl: QUEUE_URL,
|
|
931
|
-
MessageBody: JSON.stringify(message),
|
|
932
|
-
}).promise()
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// Consumer
|
|
936
|
-
async function consume(sqsMessages: SQS.Message[]) {
|
|
937
|
-
const results = await verifyQueueBatch(
|
|
938
|
-
sqsMessages.map((m) => JSON.parse(m.Body!)),
|
|
939
|
-
{ verify: (e) => verifyEnvelopeEd25519(e, producerPublicKey) },
|
|
940
|
-
)
|
|
941
|
-
|
|
942
|
-
for (const { ok, payload, sender, reason } of results) {
|
|
943
|
-
if (!ok) {
|
|
944
|
-
console.error('Rejected message from', sender, ':', reason)
|
|
945
|
-
continue
|
|
946
|
-
}
|
|
947
|
-
await processOrder(JSON.parse(payload.content))
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
```
|
|
951
|
-
|
|
952
|
-
---
|
|
953
|
-
|
|
954
|
-
## Webhook Usage
|
|
955
|
-
|
|
956
|
-
```ts
|
|
957
|
-
import { signWebhook, verifyWebhook } from '@7h3/protocol'
|
|
958
|
-
import express from 'express'
|
|
959
|
-
|
|
960
|
-
// Sender
|
|
961
|
-
async function sendWebhook(payload: object) {
|
|
962
|
-
const body = JSON.stringify(payload)
|
|
963
|
-
const { headers } = await signWebhook(body, {
|
|
964
|
-
privateKey,
|
|
965
|
-
keyId: 'k1',
|
|
966
|
-
})
|
|
967
|
-
|
|
968
|
-
await fetch('https://partner.example.com/webhook', {
|
|
969
|
-
method: 'POST',
|
|
970
|
-
headers: { 'content-type': 'application/json', ...headers },
|
|
971
|
-
body,
|
|
972
|
-
})
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// Receiver (Express, with raw body access)
|
|
976
|
-
const app = express()
|
|
977
|
-
app.use(express.raw({ type: 'application/json' }))
|
|
978
|
-
|
|
979
|
-
app.post('/webhook', async (req, res) => {
|
|
980
|
-
const result = await verifyWebhook(req.body, req.headers as Record<string, string>, {
|
|
981
|
-
publicKey: senderPublicKey,
|
|
982
|
-
maxAgeMs: 30_000, // reject payloads older than 30 seconds
|
|
983
|
-
})
|
|
984
|
-
|
|
985
|
-
if (!result.ok) {
|
|
986
|
-
return res.status(401).json({ error: result.reason })
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
await processWebhookPayload(result.payload)
|
|
990
|
-
res.status(200).end()
|
|
991
|
-
})
|
|
992
|
-
```
|
|
993
|
-
|
|
994
|
-
The `x-7h3-sig` header contains the Ed25519 signature of the raw request body. The `x-7h3-ts` header contains the Unix millisecond timestamp. Both must be present and valid. The timestamp check prevents replaying captured webhook payloads outside the `maxAgeMs` window.
|
|
995
|
-
|
|
996
|
-
---
|
|
997
|
-
|
|
998
|
-
## MCP Integration (Claude) — Full Example
|
|
999
|
-
|
|
1000
|
-
Complete setup for a hardened MCP server that Claude (or any MCP client) can call with signature verification on both sides.
|
|
1001
|
-
|
|
1002
|
-
### Server (`my-mcp-server.ts`)
|
|
1003
|
-
|
|
1004
|
-
```ts
|
|
1005
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
1006
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
1007
|
-
import {
|
|
1008
|
-
wrapMcpServer,
|
|
1009
|
-
signEnvelopeEd25519,
|
|
1010
|
-
verifyEnvelopeEd25519,
|
|
1011
|
-
generateEd25519KeypairBase64Url,
|
|
1012
|
-
} from '@7h3/protocol'
|
|
1013
|
-
|
|
1014
|
-
// Load or generate keys (in production, load from env/secrets)
|
|
1015
|
-
const { privateKey: serverPrivKey, publicKey: serverPubKey } = await generateEd25519KeypairBase64Url()
|
|
1016
|
-
const clientPubKey = process.env.CLIENT_PUBLIC_KEY!
|
|
1017
|
-
|
|
1018
|
-
const server = new Server({ name: 'my-mcp-server', version: '1.0.0' })
|
|
1019
|
-
|
|
1020
|
-
// Register tools normally
|
|
1021
|
-
server.setRequestHandler('tools/call', async (request) => {
|
|
1022
|
-
return { content: [{ type: 'text', text: `Executed: ${request.params.name}` }] }
|
|
1023
|
-
})
|
|
1024
|
-
|
|
1025
|
-
// Wrap with 7h3 verification — no other changes needed
|
|
1026
|
-
const secureServer = wrapMcpServer(server, {
|
|
1027
|
-
selfAgentId: 'my-mcp-server',
|
|
1028
|
-
sign: (e) => signEnvelopeEd25519(e, serverPrivKey, 'k1'),
|
|
1029
|
-
receive: {
|
|
1030
|
-
signatureResolver: async ({ keyId }) => ({
|
|
1031
|
-
alg: 'ED25519',
|
|
1032
|
-
publicKey: clientPubKey,
|
|
1033
|
-
}),
|
|
1034
|
-
},
|
|
1035
|
-
})
|
|
1036
|
-
|
|
1037
|
-
const transport = new StdioServerTransport()
|
|
1038
|
-
await secureServer.connect(transport)
|
|
1039
|
-
```
|
|
1040
|
-
|
|
1041
|
-
### Client (`my-mcp-client.ts`)
|
|
1042
|
-
|
|
1043
|
-
```ts
|
|
1044
|
-
import {
|
|
1045
|
-
wrapMcpClient,
|
|
1046
|
-
signEnvelopeEd25519,
|
|
1047
|
-
verifyEnvelopeEd25519,
|
|
1048
|
-
generateEd25519KeypairBase64Url,
|
|
1049
|
-
} from '@7h3/protocol'
|
|
1050
|
-
|
|
1051
|
-
const { privateKey: clientPrivKey } = await generateEd25519KeypairBase64Url()
|
|
1052
|
-
const serverPubKey = process.env.SERVER_PUBLIC_KEY!
|
|
1053
|
-
|
|
1054
|
-
const { send } = wrapMcpClient({
|
|
1055
|
-
selfAgentId: 'my-client',
|
|
1056
|
-
peerAgentId: 'my-mcp-server',
|
|
1057
|
-
sign: (e) => signEnvelopeEd25519(e, clientPrivKey, 'k1'),
|
|
1058
|
-
receive: {
|
|
1059
|
-
signatureResolver: async () => ({ alg: 'ED25519', publicKey: serverPubKey }),
|
|
1060
|
-
},
|
|
1061
|
-
})
|
|
1062
|
-
|
|
1063
|
-
// Use exactly like a normal MCP fetch call — signing is transparent
|
|
1064
|
-
const tools = await send({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }, fetch)
|
|
1065
|
-
const result = await send({
|
|
1066
|
-
jsonrpc: '2.0', id: 2, method: 'tools/call',
|
|
1067
|
-
params: { name: 'my_tool', arguments: { input: 'hello' } },
|
|
1068
|
-
}, fetch)
|
|
1069
|
-
```
|
|
1070
|
-
|
|
1071
|
-
The `send` function wraps the JSON-RPC request in a signed envelope, sends it, receives the signed response, verifies the response signature and correlation ID, and returns the unwrapped JSON-RPC result. The caller sees standard MCP semantics.
|
|
1072
|
-
|
|
1073
|
-
---
|
|
1074
|
-
|
|
1075
|
-
## Browser / Edge SDK
|
|
1076
|
-
|
|
1077
|
-
The `@7h3/protocol-browser` package uses the Web Crypto API exclusively. It has zero Node.js dependencies and works in browsers, Cloudflare Workers, Deno, and Bun.
|
|
1078
|
-
|
|
1079
|
-
```ts
|
|
1080
|
-
import {
|
|
1081
|
-
generateKeypair,
|
|
1082
|
-
signRequest,
|
|
1083
|
-
verifyResponseHeader,
|
|
1084
|
-
} from '@7h3/protocol-browser'
|
|
1085
|
-
|
|
1086
|
-
// Generate a keypair (stored in the browser's crypto key store)
|
|
1087
|
-
const { privateKey, publicKey, publicKeyBase64 } = await generateKeypair()
|
|
1088
|
-
|
|
1089
|
-
// Sign an outbound request
|
|
1090
|
-
const { headers: signedHeaders } = await signRequest(
|
|
1091
|
-
{ method: 'POST', url: 'https://api.example.com/action', body: payload },
|
|
1092
|
-
{
|
|
1093
|
-
sender: 'browser-agent',
|
|
1094
|
-
privateKey,
|
|
1095
|
-
keyId: 'k1',
|
|
1096
|
-
},
|
|
1097
|
-
)
|
|
1098
|
-
|
|
1099
|
-
const response = await fetch('https://api.example.com/action', {
|
|
1100
|
-
method: 'POST',
|
|
1101
|
-
headers: { 'content-type': 'application/json', ...signedHeaders },
|
|
1102
|
-
body: JSON.stringify(payload),
|
|
1103
|
-
})
|
|
1104
|
-
|
|
1105
|
-
// Verify the signed response
|
|
1106
|
-
const responseEnvelope = response.headers.get('x-7h3-response')
|
|
1107
|
-
if (responseEnvelope) {
|
|
1108
|
-
const verified = await verifyResponseHeader(responseEnvelope, {
|
|
1109
|
-
expectedSender: 'api-server',
|
|
1110
|
-
serverPublicKey: SERVER_PUBLIC_KEY,
|
|
1111
|
-
expectedCorrelationId: signedHeaders['x-7h3-message-id'],
|
|
1112
|
-
})
|
|
1113
|
-
if (!verified.ok) throw new Error('Response not from expected server')
|
|
1114
|
-
}
|
|
1115
|
-
```
|
|
1116
|
-
|
|
1117
|
-
**Cloudflare Workers example:**
|
|
1118
|
-
|
|
1119
|
-
```ts
|
|
1120
|
-
import { generateKeypair, signRequest } from '@7h3/protocol-browser'
|
|
1121
|
-
|
|
1122
|
-
export default {
|
|
1123
|
-
async fetch(request: Request, env: Env): Promise<Response> {
|
|
1124
|
-
const { privateKey } = await generateKeypair()
|
|
1125
|
-
const { headers } = await signRequest(
|
|
1126
|
-
{ method: request.method, url: request.url, body: await request.text() },
|
|
1127
|
-
{ sender: 'worker-agent', privateKey, keyId: 'k1' },
|
|
1128
|
-
)
|
|
1129
|
-
return fetch(env.UPSTREAM_URL, { headers: { ...Object.fromEntries(request.headers), ...headers } })
|
|
1130
|
-
},
|
|
1131
|
-
}
|
|
1132
|
-
```
|
|
1133
|
-
|
|
1134
|
-
---
|
|
1135
|
-
|
|
1136
|
-
## Conformance and Cross-SDK Compatibility
|
|
1137
|
-
|
|
1138
|
-
All SDKs share a test vector file at `conformance/7h3_v0_1.json`. Each vector contains an envelope, the canonical serialization expected, and the expected signature (produced with a fixed test keypair). An SDK passes conformance when it can:
|
|
1139
|
-
|
|
1140
|
-
1. Produce the identical canonical bytes from the input envelope
|
|
1141
|
-
2. Verify the pre-generated signature from any other SDK
|
|
1142
|
-
3. Produce a signature that any other SDK can verify
|
|
1143
|
-
|
|
1144
|
-
| SDK | Canonical form | Verify cross-SDK | Sign cross-SDK |
|
|
1145
|
-
|---|---|---|---|
|
|
1146
|
-
| TypeScript / Node.js | ✅ | ✅ | ✅ |
|
|
1147
|
-
| Browser / Edge | ✅ | ✅ | ✅ |
|
|
1148
|
-
| Python | ✅ | ✅ | ✅ |
|
|
1149
|
-
| Rust | ✅ | ✅ | ✅ |
|
|
1150
|
-
| Go | ✅ | ✅ | ✅ |
|
|
1151
|
-
|
|
1152
|
-
Run conformance tests:
|
|
1153
|
-
|
|
1154
|
-
```bash
|
|
1155
|
-
# TypeScript
|
|
1156
|
-
npm run test:conformance
|
|
1157
|
-
|
|
1158
|
-
# Python
|
|
1159
|
-
pytest sdk/python/tests/test_conformance.py
|
|
1160
|
-
|
|
1161
|
-
# Rust
|
|
1162
|
-
cargo test conformance -- --nocapture
|
|
1163
|
-
|
|
1164
|
-
# Go
|
|
1165
|
-
go test ./sdk/go/... -run TestConformance
|
|
1166
|
-
```
|
|
1167
|
-
|
|
1168
|
-
The canonical form rule is simple: serialize the `header` and `body` objects with keys sorted alphabetically at every level. Absent optional fields are excluded. Numbers are JSON numbers (not strings). The result is UTF-8 encoded with no trailing whitespace or newline.
|
|
1169
|
-
|
|
1170
|
-
---
|
|
1171
|
-
|
|
1172
|
-
## CLI Reference
|
|
1173
|
-
|
|
1174
|
-
```bash
|
|
1175
|
-
npm install -g @7h3/protocol
|
|
1176
|
-
```
|
|
1177
|
-
|
|
1178
|
-
| Command | Description |
|
|
1179
|
-
|---|---|
|
|
1180
|
-
| `7h3 keygen` | Generate an Ed25519 keypair, print to stdout |
|
|
1181
|
-
| `7h3 keygen --output <file>` | Write keypair to a JSON file |
|
|
1182
|
-
| `7h3 sign --private-key <k> --sender <id>` | Sign a message from stdin, print envelope |
|
|
1183
|
-
| `7h3 sign --private-key <k> --sender <id> --intent <intent>` | Sign with explicit intent |
|
|
1184
|
-
| `7h3 sign --private-key <k> --sender <id> --recipient <id>` | Sign with recipient binding |
|
|
1185
|
-
| `7h3 verify --public-key <k> --envelope <json>` | Verify an envelope's signature |
|
|
1186
|
-
| `7h3 verify --public-key <k> --envelope <json> --check-replay` | Verify and check replay cache |
|
|
1187
|
-
| `7h3 inspect --envelope <json>` | Pretty-print an envelope with decoded fields |
|
|
1188
|
-
| `7h3 gateway --upstream <url> --public-key <k>` | Start the HTTP proxy gateway |
|
|
1189
|
-
| `7h3 gateway --upstream <url> --public-key <k> --port <n>` | Gateway on a specific port |
|
|
1190
|
-
| `7h3 gateway --config 7h3.yaml` | Gateway with full config file |
|
|
1191
|
-
| `7h3 keys serve --public-key <k>` | Serve `/.well-known/7h3-keys` |
|
|
1192
|
-
| `7h3 keys serve --public-key <k> --port <n>` | Key server on a specific port |
|
|
1193
|
-
| `7h3 keys revoke --key-id <id>` | Add a key to the revocation list |
|
|
1194
|
-
| `7h3 --help` | Show all commands |
|
|
1195
|
-
| `7h3 <command> --help` | Show flags for a specific command |
|
|
1196
|
-
|
|
1197
|
-
**Common flags:**
|
|
1198
|
-
|
|
1199
|
-
| Flag | Description |
|
|
1200
|
-
|---|---|
|
|
1201
|
-
| `--private-key <base64url>` | Ed25519 private key (base64url encoded) |
|
|
1202
|
-
| `--public-key <base64url>` | Ed25519 public key (base64url encoded) |
|
|
1203
|
-
| `--key-id <id>` | Key identifier string (used in envelope header) |
|
|
1204
|
-
| `--sender <id>` | Sender identity string |
|
|
1205
|
-
| `--recipient <id>` | Recipient identity string (optional) |
|
|
1206
|
-
| `--intent <string>` | Intent label for the envelope body |
|
|
1207
|
-
| `--ttl <ms>` | TTL in milliseconds (default: 30000) |
|
|
1208
|
-
| `--output <file>` | Write output to file instead of stdout |
|
|
1209
|
-
| `--config <file>` | Load configuration from YAML file |
|
|
1210
|
-
| `--upstream <url>` | Upstream URL for gateway mode |
|
|
1211
|
-
| `--port <n>` | Port to listen on (default: 8080) |
|
|
1212
|
-
| `--require <alg>` | Required algorithm: `ed25519` or `hmac` |
|
|
1213
|
-
|
|
1214
|
-
---
|
|
1215
|
-
|
|
1216
|
-
## Docker Reference
|
|
1217
|
-
|
|
1218
|
-
### Gateway Image
|
|
1219
|
-
|
|
1220
|
-
```bash
|
|
1221
|
-
docker pull 7h3agency/gateway:latest
|
|
1222
|
-
```
|
|
1223
|
-
|
|
1224
|
-
**Run:**
|
|
1225
|
-
|
|
1226
|
-
```bash
|
|
1227
|
-
docker run -p 8080:8080 \
|
|
1228
|
-
-e GATEWAY_PRIVATE_KEY=<base64url-private-key> \
|
|
1229
|
-
-e GATEWAY_PUBLIC_KEY=<base64url-public-key> \
|
|
1230
|
-
7h3agency/gateway:latest \
|
|
1231
|
-
--upstream http://my-api:3000 \
|
|
1232
|
-
--require ed25519
|
|
1233
|
-
```
|
|
1234
|
-
|
|
1235
|
-
**Docker Compose (with config file):**
|
|
1236
|
-
|
|
1237
|
-
```yaml
|
|
1238
|
-
services:
|
|
1239
|
-
gateway:
|
|
1240
|
-
image: 7h3agency/gateway:latest
|
|
1241
|
-
ports:
|
|
1242
|
-
- "8080:8080"
|
|
1243
|
-
environment:
|
|
1244
|
-
GATEWAY_PRIVATE_KEY: ${GATEWAY_PRIVATE_KEY}
|
|
1245
|
-
GATEWAY_PUBLIC_KEY: ${GATEWAY_PUBLIC_KEY}
|
|
1246
|
-
volumes:
|
|
1247
|
-
- ./7h3.yaml:/app/7h3.yaml:ro
|
|
1248
|
-
command: ["--config", "/app/7h3.yaml"]
|
|
1249
|
-
healthcheck:
|
|
1250
|
-
test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
|
|
1251
|
-
interval: 10s
|
|
1252
|
-
timeout: 5s
|
|
1253
|
-
retries: 3
|
|
1254
|
-
```
|
|
1255
|
-
|
|
1256
|
-
**Environment variables:**
|
|
1257
|
-
|
|
1258
|
-
| Variable | Required | Description |
|
|
1259
|
-
|---|---|---|
|
|
1260
|
-
| `GATEWAY_PRIVATE_KEY` | Yes (if signing responses) | Base64url Ed25519 private key for signing gateway responses |
|
|
1261
|
-
| `GATEWAY_PUBLIC_KEY` | Yes | Base64url Ed25519 public key for incoming request verification |
|
|
1262
|
-
| `GATEWAY_UPSTREAM` | If no config file | Upstream service URL |
|
|
1263
|
-
| `GATEWAY_PORT` | No (default: 8080) | Port to listen on |
|
|
1264
|
-
| `GATEWAY_REQUIRE` | No (default: ed25519) | Required algorithm: `ed25519` or `hmac` |
|
|
1265
|
-
| `GATEWAY_LOG_LEVEL` | No (default: info) | Log level: `debug`, `info`, `warn`, `error` |
|
|
1266
|
-
| `GATEWAY_RATE_LIMIT_MAX` | No | Max requests per window per sender |
|
|
1267
|
-
| `GATEWAY_RATE_LIMIT_WINDOW_MS` | No | Rate limit window in milliseconds |
|
|
1268
|
-
| `REDIS_URL` | No | Redis URL for distributed rate limiting |
|
|
1269
|
-
|
|
1270
|
-
**Health check:**
|
|
1271
|
-
|
|
1272
|
-
```bash
|
|
1273
|
-
curl http://localhost:8080/health
|
|
1274
|
-
# {"status":"ok","uptime":12345,"version":"0.4.0"}
|
|
1275
|
-
```
|
|
1276
|
-
|
|
1277
|
-
**Metrics (Prometheus):**
|
|
1278
|
-
|
|
1279
|
-
```bash
|
|
1280
|
-
curl http://localhost:8080/metrics
|
|
1281
|
-
```
|
|
1282
|
-
|
|
1283
|
-
---
|
|
1284
|
-
|
|
1285
|
-
## Uninstall
|
|
1286
|
-
|
|
1287
|
-
```bash
|
|
1288
|
-
# Node.js
|
|
1289
|
-
npm uninstall @7h3/protocol
|
|
1290
|
-
npm uninstall @7h3/protocol-browser
|
|
1291
|
-
|
|
1292
|
-
# Python
|
|
1293
|
-
pip uninstall 7h3-protocol
|
|
1294
|
-
|
|
1295
|
-
# Rust
|
|
1296
|
-
cargo remove protocol-7h3
|
|
1297
|
-
|
|
1298
|
-
# Go
|
|
1299
|
-
go mod edit -droprequire github.com/IceMasterT/7h3-protocol/sdk/go
|
|
1300
|
-
go mod tidy
|
|
1301
|
-
```
|
|
1302
|
-
|
|
1303
|
-
**Note on deprecated packages:** The old package names `aip7h3` (PyPI) and `aip7h3` (crates.io) are tombstones — they re-export from the current packages and will not receive further updates. If you have them installed, uninstalling the tombstone and installing the current package is the correct migration path.
|
|
1304
|
-
|
|
1305
|
-
```bash
|
|
1306
|
-
# Python migration
|
|
1307
|
-
pip uninstall aip7h3
|
|
1308
|
-
pip install 7h3-protocol
|
|
1309
|
-
|
|
1310
|
-
# Rust migration
|
|
1311
|
-
cargo remove aip7h3
|
|
1312
|
-
cargo add protocol-7h3
|
|
1313
|
-
```
|
|
1314
|
-
|
|
1315
|
-
---
|
|
1316
|
-
|
|
1317
|
-
## Contributing
|
|
1318
|
-
|
|
1319
|
-
See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup, the test suite structure, how to add conformance vectors, and the pull request process.
|
|
1320
|
-
|
|
1321
|
-
The short version: open an issue before significant changes, run `npm test` (all 278 tests must pass), and ensure any new API is covered by conformance vectors if it affects the canonical form or signature behavior.
|
|
1322
|
-
|
|
1323
|
-
---
|
|
1324
|
-
|
|
1325
|
-
## License
|
|
1326
|
-
|
|
1327
|
-
MIT — see [LICENSE](./LICENSE).
|