@7h3/protocol 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/README.md +6 -1317
  2. package/agentAdapter.d.ts +26 -0
  3. package/auditLog.d.ts +54 -0
  4. package/capability.d.ts +67 -0
  5. package/cborCodec.d.ts +34 -0
  6. package/conformanceVectors.d.ts +20 -0
  7. package/encryption.d.ts +85 -0
  8. package/envelopeCbor.d.ts +33 -0
  9. package/frameworkAdapters.d.ts +72 -0
  10. package/gateway.d.ts +53 -0
  11. package/grpcBinding.d.ts +29 -0
  12. package/httpBinding.d.ts +50 -0
  13. package/index.d.ts +39 -0
  14. package/index.js +3662 -0
  15. package/keyInfra.d.ts +64 -0
  16. package/keyRegistry.d.ts +9 -0
  17. package/keyRotation.d.ts +20 -0
  18. package/mcpGateway.d.ts +37 -0
  19. package/mcpTransports.d.ts +62 -0
  20. package/mcpWrapper.d.ts +83 -0
  21. package/otel.d.ts +65 -0
  22. package/package.json +13 -121
  23. package/policyEnforcer.d.ts +50 -0
  24. package/policyTelemetryFeedback.d.ts +11 -0
  25. package/{src/protocol.d.ts → protocol.d.ts} +1 -2
  26. package/protocolAgent.d.ts +58 -0
  27. package/protocolBinary.d.ts +8 -0
  28. package/protocolCapabilities.d.ts +24 -0
  29. package/protocolReplay.d.ts +35 -0
  30. package/protocolTransport.d.ts +73 -0
  31. package/queueBinding.d.ts +43 -0
  32. package/rateLimiter.d.ts +18 -0
  33. package/{src/redisClient.ts → redisClient.d.ts} +25 -75
  34. package/replayStores.d.ts +98 -0
  35. package/revocation.d.ts +71 -0
  36. package/routePolicy.d.ts +30 -0
  37. package/runtimePolicy.d.ts +24 -0
  38. package/runtimePolicyManager.d.ts +15 -0
  39. package/runtimePolicyPresets.d.ts +11 -0
  40. package/signedResponse.d.ts +21 -0
  41. package/stream.d.ts +59 -0
  42. package/telemetry.d.ts +79 -0
  43. package/webhookBinding.d.ts +30 -0
  44. package/wsBinding.d.ts +52 -0
  45. package/.dockerignore +0 -19
  46. package/.github/dependabot.yml +0 -32
  47. package/.github/workflows/ci.yml +0 -31
  48. package/.github/workflows/publish.yml +0 -59
  49. package/.github/workflows/scorecard.yml +0 -37
  50. package/7h3.example.yaml +0 -125
  51. package/CHANGELOG.md +0 -152
  52. package/CONTRIBUTING.md +0 -82
  53. package/Dockerfile +0 -73
  54. package/GOVERNANCE.md +0 -62
  55. package/SECURITY.md +0 -70
  56. package/bench-results/replay-cache-full-1777891033256.json +0 -10
  57. package/bench-results/replay-cache-full-1777896317488.json +0 -10
  58. package/bench-results/replay-cache-full-1777900993184.json +0 -10
  59. package/bench-results/replay-cache-full-1777901019285.json +0 -10
  60. package/bench-results/replay-cache-quick-1777870170126.json +0 -10
  61. package/bench-results/signature-profiles-quick-1775875160079.json +0 -85
  62. package/bench-results/signature-profiles-quick-1775983539716.json +0 -85
  63. package/bench-results/signature-profiles-quick-1776237913190.json +0 -85
  64. package/bench-results/wire-codecs-full-1777891019803.json +0 -93
  65. package/bench-results/wire-codecs-full-1777896260964.json +0 -93
  66. package/bench-results/wire-codecs-full-1777901004247.json +0 -93
  67. package/bench-results/wire-codecs-quick-1775972879056.json +0 -93
  68. package/bench-results/wire-codecs-quick-1775983541111.json +0 -93
  69. package/bench-results/wire-codecs-quick-1776237914299.json +0 -93
  70. package/bench-results/wire-codecs-quick-1777841285236.json +0 -93
  71. package/bench-results/wire-codecs-quick-1777841321772.json +0 -93
  72. package/bench-results/wire-codecs-quick-1777841330408.json +0 -93
  73. package/bench-results/wire-codecs-quick-1777852886082.json +0 -93
  74. package/bench-results/wire-codecs-quick-1777852988773.json +0 -93
  75. package/bench-results/wire-codecs-quick-1777870188095.json +0 -93
  76. package/bench-results/wire-codecs-quick-1777870263918.json +0 -93
  77. package/bench-results/wire-codecs-quick-1777870455034.json +0 -93
  78. package/bench-results/wire-codecs-quick-1778816163081.json +0 -93
  79. package/bench-results/wire-codecs-quick-1778843936130.json +0 -93
  80. package/bin/7h3.ts +0 -406
  81. package/conformance/7h3_v0_1.json +0 -77
  82. package/conformance/7h3_v0_1_binary.json +0 -20
  83. package/conformance/aip_v0_1_binary.json +0 -20
  84. package/docker-compose.yaml +0 -77
  85. package/docs/ADOPTION_PLAN.md +0 -120
  86. package/docs/AGENTS.md +0 -77
  87. package/docs/AIP_RFC_v0.1.md +0 -97
  88. package/docs/AI_DECISION_CARD.md +0 -122
  89. package/docs/AI_RUNTIME_POLICY.json +0 -126
  90. package/docs/AI_RUNTIME_POLICY.yaml +0 -110
  91. package/docs/BACKPRESSURE_TUNING.md +0 -65
  92. package/docs/BENCHMARK_CLAIM_MATRIX.md +0 -42
  93. package/docs/BENCHMARK_REPORT_TEMPLATE.md +0 -169
  94. package/docs/BINARY_CODEC_BENCH.md +0 -23
  95. package/docs/CLEAN_CLONE_RUNBOOK.md +0 -36
  96. package/docs/CLOCK_SKEW_POLICY.md +0 -30
  97. package/docs/DISTRIBUTED_REPLAY.md +0 -142
  98. package/docs/FUZZ_CAMPAIGN.md +0 -121
  99. package/docs/GATEWAY.md +0 -195
  100. package/docs/KEY_MANAGEMENT_POLICY.md +0 -53
  101. package/docs/KEY_REVOCATION.md +0 -69
  102. package/docs/MCP_WRAPPER.md +0 -159
  103. package/docs/MIGRATION_GUIDE.md +0 -40
  104. package/docs/OPERATORS.md +0 -184
  105. package/docs/PERF_REGRESSION_POLICY.md +0 -34
  106. package/docs/PROJECT_EXAMINATION_2026-05-31.md +0 -219
  107. package/docs/RELEASE_BENCHMARK_REPORT_2026-05-15.md +0 -135
  108. package/docs/RELEASE_GATE.md +0 -25
  109. package/docs/RELEASE_NOTES_v0.1.0.md +0 -54
  110. package/docs/SECURITY_REVIEW_2026-06-05.md +0 -165
  111. package/docs/TELEMETRY.md +0 -41
  112. package/docs/THREAT_MODEL.md +0 -89
  113. package/docs/VERSIONING_POLICY.md +0 -30
  114. package/docs/assets/banner-github.png +0 -0
  115. package/docs/assets/banner.png +0 -0
  116. package/docs/assets/banner.svg +0 -123
  117. package/eslint.config.js +0 -15
  118. package/fuzz/ts/harness-decode.ts +0 -136
  119. package/fuzz/ts/harness-verify.ts +0 -121
  120. package/fuzz/ts/run.ts +0 -35
  121. package/mcp-server/README.md +0 -38
  122. package/mcp-server/package-lock.json +0 -1187
  123. package/mcp-server/package.json +0 -35
  124. package/mcp-server/src/index.ts +0 -236
  125. package/mcp-server/tsconfig.json +0 -14
  126. package/scripts/aip-framework-quickstart.ts +0 -110
  127. package/scripts/aip-mcp-gateway.ts +0 -38
  128. package/scripts/aip-mcp-wrap-demo.ts +0 -72
  129. package/scripts/aip-quickstart.ts +0 -60
  130. package/scripts/bench-diff.ts +0 -118
  131. package/scripts/bench-protocol-e2e.ts +0 -937
  132. package/scripts/bench-protocol-openloop.ts +0 -1397
  133. package/scripts/bench-replay-cache.ts +0 -76
  134. package/scripts/bench-signature-profiles.ts +0 -180
  135. package/scripts/bench-wire-codecs.ts +0 -161
  136. package/scripts/build-binary-conformance.ts +0 -36
  137. package/scripts/build-release-dashboard.ts +0 -175
  138. package/scripts/canary-rollout.ts +0 -38
  139. package/scripts/mcpGatewayCli.test.ts +0 -116
  140. package/scripts/prepare-aip-package.ts +0 -88
  141. package/scripts/regen-conformance-sigs.ts +0 -18
  142. package/scripts/release-gate.ts +0 -19
  143. package/scripts/validate-runtime-policy.ts +0 -18
  144. package/sdk/browser/index.test.ts +0 -162
  145. package/sdk/browser/index.ts +0 -257
  146. package/sdk/browser/package.json +0 -13
  147. package/sdk/go/cbor.go +0 -551
  148. package/sdk/go/cbor_test.go +0 -232
  149. package/sdk/go/encryption.go +0 -280
  150. package/sdk/go/encryption_test.go +0 -318
  151. package/sdk/go/go.mod +0 -7
  152. package/sdk/go/go.sum +0 -4
  153. package/sdk/go/http.go +0 -135
  154. package/sdk/go/protocol.go +0 -324
  155. package/sdk/go/protocol_test.go +0 -334
  156. package/sdk/go/replay.go +0 -121
  157. package/sdk/go/replay_test.go +0 -149
  158. package/sdk/go/webhook.go +0 -136
  159. package/sdk/pq/package-lock.json +0 -1358
  160. package/sdk/pq/package.json +0 -42
  161. package/sdk/pq/src/index.test.ts +0 -143
  162. package/sdk/pq/src/index.ts +0 -166
  163. package/sdk/pq/tsconfig.json +0 -14
  164. package/sdk/pq/vitest.config.ts +0 -7
  165. package/sdk/python/README.md +0 -18
  166. package/sdk/python/protocol_7h3/__init__.py +0 -46
  167. package/sdk/python/protocol_7h3/encryption.py +0 -252
  168. package/sdk/python/protocol_7h3/http.py +0 -212
  169. package/sdk/python/protocol_7h3/keys.py +0 -149
  170. package/sdk/python/protocol_7h3/pq.py +0 -244
  171. package/sdk/python/protocol_7h3/protocol.py +0 -525
  172. package/sdk/python/protocol_7h3/queue.py +0 -118
  173. package/sdk/python/protocol_7h3/replay.py +0 -98
  174. package/sdk/python/protocol_7h3/webhook.py +0 -116
  175. package/sdk/python/pyproject.toml +0 -40
  176. package/sdk/python/tests/test_conformance.py +0 -110
  177. package/sdk/python/tests/test_encryption.py +0 -206
  178. package/sdk/python/tests/test_http.py +0 -305
  179. package/sdk/python/tests/test_keys.py +0 -417
  180. package/sdk/python/tests/test_queue.py +0 -120
  181. package/sdk/python/tests/test_webhook.py +0 -345
  182. package/sdk/rust/Cargo.lock +0 -371
  183. package/sdk/rust/Cargo.toml +0 -25
  184. package/sdk/rust/README.md +0 -31
  185. package/sdk/rust/fuzz/Cargo.toml +0 -29
  186. package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +0 -46
  187. package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +0 -11
  188. package/sdk/rust/src/bin/aip_mcp_gateway.rs +0 -59
  189. package/sdk/rust/src/http.rs +0 -145
  190. package/sdk/rust/src/keys.rs +0 -161
  191. package/sdk/rust/src/lib.rs +0 -688
  192. package/sdk/rust/src/queue.rs +0 -79
  193. package/sdk/rust/src/webhook.rs +0 -86
  194. package/sdk/rust/tests/conformance.rs +0 -148
  195. package/sdk/rust/tests/gateway.rs +0 -130
  196. package/sdk/rust/tests/http_webhook_queue.rs +0 -201
  197. package/sdk/rust/tests/keys.rs +0 -189
  198. package/sdk/threshold/index.d.ts +0 -68
  199. package/sdk/threshold/index.d.ts.map +0 -1
  200. package/sdk/threshold/index.js +0 -254
  201. package/sdk/threshold/package-lock.json +0 -1361
  202. package/sdk/threshold/package.json +0 -39
  203. package/sdk/threshold/src/index.d.ts +0 -68
  204. package/sdk/threshold/src/index.d.ts.map +0 -1
  205. package/sdk/threshold/src/index.js +0 -254
  206. package/sdk/threshold/src/index.test.ts +0 -238
  207. package/sdk/threshold/src/index.ts +0 -355
  208. package/sdk/threshold/tsconfig.json +0 -19
  209. package/sdk/threshold/vitest.config.ts +0 -12
  210. package/src/agentAdapter.test.ts +0 -48
  211. package/src/agentAdapter.ts +0 -56
  212. package/src/auditLog.test.ts +0 -145
  213. package/src/auditLog.ts +0 -147
  214. package/src/capability.test.ts +0 -504
  215. package/src/capability.ts +0 -380
  216. package/src/cborCodec.test.ts +0 -263
  217. package/src/cborCodec.ts +0 -339
  218. package/src/conformance.test.ts +0 -136
  219. package/src/conformanceVectors.ts +0 -99
  220. package/src/encryption.test.ts +0 -206
  221. package/src/encryption.ts +0 -245
  222. package/src/envelopeCbor.ts +0 -140
  223. package/src/frameworkAdapters.test.ts +0 -290
  224. package/src/frameworkAdapters.ts +0 -261
  225. package/src/gateway.test.ts +0 -343
  226. package/src/gateway.ts +0 -246
  227. package/src/grpcBinding.test.ts +0 -211
  228. package/src/grpcBinding.ts +0 -103
  229. package/src/httpBinding.test.ts +0 -376
  230. package/src/httpBinding.ts +0 -189
  231. package/src/index.ts +0 -39
  232. package/src/keyInfra.test.ts +0 -278
  233. package/src/keyInfra.ts +0 -228
  234. package/src/keyRegistry.ts +0 -59
  235. package/src/keyRotation.test.ts +0 -78
  236. package/src/keyRotation.ts +0 -72
  237. package/src/mcpGateway.test.ts +0 -129
  238. package/src/mcpGateway.ts +0 -250
  239. package/src/mcpTransports.test.ts +0 -92
  240. package/src/mcpTransports.ts +0 -169
  241. package/src/mcpWrapper.test.ts +0 -179
  242. package/src/mcpWrapper.ts +0 -206
  243. package/src/otel.ts +0 -136
  244. package/src/policyEnforcer.test.ts +0 -99
  245. package/src/policyEnforcer.ts +0 -169
  246. package/src/policyTelemetryFeedback.test.ts +0 -25
  247. package/src/policyTelemetryFeedback.ts +0 -38
  248. package/src/protocol.bench.ts +0 -37
  249. package/src/protocol.d.ts.map +0 -1
  250. package/src/protocol.js +0 -294
  251. package/src/protocol.test.ts +0 -155
  252. package/src/protocol.ts +0 -414
  253. package/src/protocolAgent.test.ts +0 -105
  254. package/src/protocolAgent.ts +0 -169
  255. package/src/protocolBinary.test.ts +0 -165
  256. package/src/protocolBinary.ts +0 -312
  257. package/src/protocolCapabilities.ts +0 -70
  258. package/src/protocolFuzz.advanced.test.ts +0 -235
  259. package/src/protocolFuzz.test.ts +0 -111
  260. package/src/protocolNegative.test.ts +0 -97
  261. package/src/protocolReplay.test.ts +0 -71
  262. package/src/protocolReplay.ts +0 -194
  263. package/src/protocolTransport.test.ts +0 -556
  264. package/src/protocolTransport.ts +0 -483
  265. package/src/queueBinding.test.ts +0 -130
  266. package/src/queueBinding.ts +0 -102
  267. package/src/rateLimiter.test.ts +0 -96
  268. package/src/rateLimiter.ts +0 -46
  269. package/src/redisIntegration.test.ts +0 -134
  270. package/src/replayStores.test.ts +0 -273
  271. package/src/replayStores.ts +0 -215
  272. package/src/revocation.test.ts +0 -98
  273. package/src/revocation.ts +0 -0
  274. package/src/routePolicy.test.ts +0 -87
  275. package/src/routePolicy.ts +0 -72
  276. package/src/runtimePolicy.test.ts +0 -49
  277. package/src/runtimePolicy.ts +0 -81
  278. package/src/runtimePolicyManager.test.ts +0 -29
  279. package/src/runtimePolicyManager.ts +0 -50
  280. package/src/runtimePolicyPresets.ts +0 -43
  281. package/src/signedResponse.test.ts +0 -111
  282. package/src/signedResponse.ts +0 -83
  283. package/src/stream.test.ts +0 -254
  284. package/src/stream.ts +0 -417
  285. package/src/telemetry.test.ts +0 -251
  286. package/src/telemetry.ts +0 -299
  287. package/src/webhookBinding.test.ts +0 -144
  288. package/src/webhookBinding.ts +0 -115
  289. package/src/wsBinding.test.ts +0 -221
  290. package/src/wsBinding.ts +0 -200
  291. package/tsconfig.json +0 -15
  292. package/tsconfig.lib.json +0 -23
  293. package/vite.lib.config.ts +0 -16
  294. package/vitest.config.ts +0 -11
@@ -1,98 +0,0 @@
1
- """
2
- Redis-backed replay store for the 7h3 Protocol Python SDK.
3
-
4
- Provides atomic nonce deduplication using Redis SET NX PX semantics.
5
- The ``redis`` package is optional — an ImportError is raised at construction
6
- time (not at module import) when it is absent, so the rest of the SDK remains
7
- usable without installing Redis dependencies.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- from typing import Optional, Any
13
-
14
-
15
- class RedisReplayStore:
16
- """
17
- Atomic replay detection backed by Redis.
18
-
19
- Uses ``SET key value NX PX ttl_ms`` so the first caller atomically claims
20
- the nonce and all subsequent callers are identified as replays — safe
21
- across multiple horizontally-scaled gateway instances.
22
-
23
- Parameters
24
- ----------
25
- redis_url:
26
- Redis connection URL passed to ``redis.from_url()``. Ignored when
27
- *client* is provided.
28
- key_prefix:
29
- Namespace prefix prepended to every nonce key. Default ``'7h3:nonce:'``.
30
- client:
31
- Pre-constructed Redis client (any object that exposes ``set`` and
32
- ``close``). When supplied, *redis_url* is ignored.
33
-
34
- Raises
35
- ------
36
- ImportError
37
- At construction time when *redis_url* is provided but the ``redis``
38
- package is not installed.
39
- """
40
-
41
- def __init__(
42
- self,
43
- redis_url: Optional[str] = None,
44
- key_prefix: str = "7h3:nonce:",
45
- client: Optional[Any] = None,
46
- ) -> None:
47
- self.key_prefix = key_prefix
48
-
49
- if client is not None:
50
- self._client = client
51
- else:
52
- try:
53
- import redis # type: ignore[import]
54
- except ImportError as exc:
55
- raise ImportError(
56
- "The 'redis' package is required to use RedisReplayStore. "
57
- "Install it with: pip install redis"
58
- ) from exc
59
-
60
- url = redis_url or "redis://localhost:6379"
61
- self._client = redis.from_url(url, decode_responses=True)
62
-
63
- def check(self, key: str, ttl_ms: int) -> bool:
64
- """
65
- Atomically check and register a nonce.
66
-
67
- Returns
68
- -------
69
- bool
70
- ``False`` if the nonce is fresh (first time seen — the key was set
71
- in Redis), or ``True`` if the nonce is a replay (the key already
72
- existed in Redis and SET NX was blocked).
73
- """
74
- redis_key = f"{self.key_prefix}{key}"
75
- px = max(1, ttl_ms)
76
- result = self._client.set(redis_key, "1", nx=True, px=px)
77
- # redis-py returns True when the key was set, None when NX blocked it
78
- return result is None
79
-
80
- def close(self) -> None:
81
- """Close the underlying Redis connection."""
82
- close = getattr(self._client, "close", None)
83
- if close is not None:
84
- close()
85
-
86
-
87
- def create_redis_replay_store(redis_url: str, key_prefix: str = "7h3:nonce:") -> RedisReplayStore:
88
- """
89
- Convenience factory — create a :class:`RedisReplayStore` from a Redis URL.
90
-
91
- Parameters
92
- ----------
93
- redis_url:
94
- Redis connection URL, e.g. ``'redis://localhost:6379'``.
95
- key_prefix:
96
- Key namespace prefix. Default ``'7h3:nonce:'``.
97
- """
98
- return RedisReplayStore(redis_url=redis_url, key_prefix=key_prefix)
@@ -1,116 +0,0 @@
1
- """Webhook binding for 7h3 Protocol — lightweight per-payload signing."""
2
- from __future__ import annotations
3
- import json
4
- import time
5
- from typing import Any, Dict, Optional, TypeVar, Union
6
-
7
- WEBHOOK_SIG_HEADER = "x-7h3-sig"
8
- WEBHOOK_TS_HEADER = "x-7h3-ts"
9
- WEBHOOK_DEFAULT_TTL_MS = 300_000 # 5 minutes
10
-
11
-
12
- def _webhook_signing_payload(timestamp_ms: int, body: str) -> str:
13
- return f"{timestamp_ms}.{body}"
14
-
15
-
16
- def sign_webhook(
17
- payload: Union[str, bytes],
18
- private_key: str,
19
- *,
20
- ttl_ms: int = WEBHOOK_DEFAULT_TTL_MS,
21
- ) -> Dict[str, str]:
22
- """Sign a webhook payload. Returns headers dict with x-7h3-sig and x-7h3-ts."""
23
- from .protocol import sign_canonical_payload_ed25519
24
-
25
- body = payload.decode("utf-8") if isinstance(payload, bytes) else payload
26
- ts = int(time.time() * 1000)
27
- signing_payload = _webhook_signing_payload(ts, body)
28
- sig = sign_canonical_payload_ed25519(signing_payload, private_key)
29
- return {WEBHOOK_SIG_HEADER: sig, WEBHOOK_TS_HEADER: str(ts)}
30
-
31
-
32
- def sign_webhook_hmac(
33
- payload: Union[str, bytes],
34
- secret: str,
35
- *,
36
- ttl_ms: int = WEBHOOK_DEFAULT_TTL_MS,
37
- ) -> Dict[str, str]:
38
- """Sign a webhook payload with HMAC-SHA256 shared secret."""
39
- from .protocol import sign_canonical_payload_hmac
40
-
41
- body = payload.decode("utf-8") if isinstance(payload, bytes) else payload
42
- ts = int(time.time() * 1000)
43
- signing_payload = _webhook_signing_payload(ts, body)
44
- sig = sign_canonical_payload_hmac(signing_payload, secret)
45
- return {WEBHOOK_SIG_HEADER: sig, WEBHOOK_TS_HEADER: str(ts)}
46
-
47
-
48
- def verify_webhook(
49
- payload: Union[str, bytes],
50
- headers: Dict[str, str],
51
- public_key: str,
52
- *,
53
- max_age_ms: int = WEBHOOK_DEFAULT_TTL_MS,
54
- ) -> bool:
55
- """Verify an Ed25519 webhook signature. Returns True/False."""
56
- from .protocol import verify_canonical_payload_ed25519
57
-
58
- body = payload.decode("utf-8") if isinstance(payload, bytes) else payload
59
- # normalise header keys
60
- normalised = {k.lower(): v for k, v in headers.items()}
61
- sig = normalised.get(WEBHOOK_SIG_HEADER.lower())
62
- ts_str = normalised.get(WEBHOOK_TS_HEADER.lower())
63
- if not sig or not ts_str:
64
- return False
65
- try:
66
- ts = int(ts_str)
67
- except ValueError:
68
- return False
69
- now_ms = int(time.time() * 1000)
70
- if now_ms - ts > max_age_ms:
71
- return False
72
- signing_payload = _webhook_signing_payload(ts, body)
73
- return verify_canonical_payload_ed25519(signing_payload, sig, public_key)
74
-
75
-
76
- def verify_webhook_hmac(
77
- payload: Union[str, bytes],
78
- headers: Dict[str, str],
79
- secret: str,
80
- *,
81
- max_age_ms: int = WEBHOOK_DEFAULT_TTL_MS,
82
- ) -> bool:
83
- """Verify an HMAC webhook signature."""
84
- from .protocol import verify_canonical_payload_hmac
85
-
86
- body = payload.decode("utf-8") if isinstance(payload, bytes) else payload
87
- normalised = {k.lower(): v for k, v in headers.items()}
88
- sig = normalised.get(WEBHOOK_SIG_HEADER.lower())
89
- ts_str = normalised.get(WEBHOOK_TS_HEADER.lower())
90
- if not sig or not ts_str:
91
- return False
92
- try:
93
- ts = int(ts_str)
94
- except ValueError:
95
- return False
96
- now_ms = int(time.time() * 1000)
97
- if now_ms - ts > max_age_ms:
98
- return False
99
- signing_payload = _webhook_signing_payload(ts, body)
100
- return verify_canonical_payload_hmac(signing_payload, sig, secret)
101
-
102
-
103
- T = TypeVar("T")
104
-
105
-
106
- def consume_webhook(
107
- payload: str,
108
- headers: Dict[str, str],
109
- public_key: str,
110
- *,
111
- max_age_ms: int = WEBHOOK_DEFAULT_TTL_MS,
112
- ) -> Any:
113
- """Parse and verify a JSON webhook payload. Raises ValueError on failure."""
114
- if not verify_webhook(payload, headers, public_key, max_age_ms=max_age_ms):
115
- raise ValueError("7h3: webhook signature verification failed")
116
- return json.loads(payload)
@@ -1,40 +0,0 @@
1
- [build-system]
2
- requires = ["hatchling"]
3
- build-backend = "hatchling.build"
4
-
5
- [project]
6
- name = "7h3-protocol"
7
- version = "0.5.0"
8
- description = "7h3 Protocol — Python SDK. Deterministic, signed, replay-safe AI-to-AI messaging. Wire version 7h3/0.1."
9
- readme = "README.md"
10
- license = { text = "MIT" }
11
- requires-python = ">=3.9"
12
- keywords = ["7h3", "agent", "protocol", "ed25519", "mcp", "signing", "replay-protection"]
13
- classifiers = [
14
- "Development Status :: 4 - Beta",
15
- "Intended Audience :: Developers",
16
- "License :: OSI Approved :: MIT License",
17
- "Programming Language :: Python :: 3",
18
- "Programming Language :: Python :: 3.9",
19
- "Programming Language :: Python :: 3.10",
20
- "Programming Language :: Python :: 3.11",
21
- "Programming Language :: Python :: 3.12",
22
- "Programming Language :: Python :: 3.13",
23
- "Topic :: Security :: Cryptography",
24
- "Topic :: Software Development :: Libraries :: Python Modules",
25
- "Topic :: Internet",
26
- ]
27
- dependencies = []
28
-
29
- [project.optional-dependencies]
30
- crypto = ["cryptography>=41.0"]
31
- nacl = ["PyNaCl>=1.5"]
32
-
33
- [project.urls]
34
- Homepage = "https://github.com/IceMasterT/7h3-protocol"
35
- Repository = "https://github.com/IceMasterT/7h3-protocol"
36
- Documentation = "https://github.com/IceMasterT/7h3-protocol/tree/main/sdk/python"
37
- Changelog = "https://github.com/IceMasterT/7h3-protocol/blob/main/CHANGELOG.md"
38
-
39
- [tool.hatch.build.targets.wheel]
40
- packages = ["protocol_7h3"]
@@ -1,110 +0,0 @@
1
- import json
2
- import pathlib
3
- import unittest
4
-
5
- from protocol_7h3.protocol import (
6
- canonicalize_envelope,
7
- decode_envelope,
8
- encode_envelope_compact,
9
- sign_canonical_payload_ed25519,
10
- sign_canonical_payload_hmac,
11
- sign_envelope_ed25519,
12
- sign_envelope_hmac,
13
- validate_envelope,
14
- verify_canonical_payload_ed25519,
15
- verify_canonical_payload_hmac,
16
- verify_envelope_ed25519,
17
- verify_envelope_hmac,
18
- )
19
-
20
-
21
- ROOT = pathlib.Path(__file__).resolve().parents[3]
22
- VECTORS_PATH = ROOT / "conformance" / "7h3_v0_1.json"
23
-
24
-
25
- class ConformanceTests(unittest.TestCase):
26
- @classmethod
27
- def setUpClass(cls) -> None:
28
- with VECTORS_PATH.open("r", encoding="utf-8") as handle:
29
- cls.payload = json.load(handle)
30
-
31
- def test_vectors_match_canonical_and_signatures(self) -> None:
32
- for vector in self.payload["vectors"]:
33
- envelope = vector["envelope"]
34
- canonical = canonicalize_envelope(envelope)
35
- self.assertEqual(canonical, vector["canonical"])
36
-
37
- signature = sign_canonical_payload_hmac(canonical, vector["secret"])
38
- self.assertEqual(signature, vector["signature"])
39
- self.assertTrue(
40
- verify_canonical_payload_hmac(canonical, signature, vector["secret"])
41
- )
42
- self.assertFalse(
43
- verify_canonical_payload_hmac(
44
- canonical, signature, vector["secret"] + "-bad"
45
- )
46
- )
47
-
48
- signed = sign_envelope_hmac(envelope, vector["secret"], vector["keyId"])
49
- self.assertTrue(verify_envelope_hmac(signed, vector["secret"]))
50
- self.assertFalse(verify_envelope_hmac(signed, vector["secret"] + "-bad"))
51
-
52
- def test_compact_roundtrip(self) -> None:
53
- for vector in self.payload["vectors"]:
54
- signed = sign_envelope_hmac(
55
- vector["envelope"], vector["secret"], vector["keyId"]
56
- )
57
- raw = encode_envelope_compact(signed)
58
- decoded = decode_envelope(raw)
59
- self.assertEqual(decoded, signed)
60
-
61
- def test_ttl_validation(self) -> None:
62
- vector = self.payload["vectors"][0]
63
- envelope = vector["envelope"]
64
-
65
- valid = validate_envelope(
66
- envelope, now_ms=envelope["header"]["timestampMs"] + 1
67
- )
68
- self.assertFalse(any(d.message == "Message TTL expired" for d in valid))
69
-
70
- expired = validate_envelope(
71
- envelope,
72
- now_ms=envelope["header"]["timestampMs"] + envelope["header"]["ttlMs"] + 1,
73
- )
74
- self.assertTrue(any(d.message == "Message TTL expired" for d in expired))
75
-
76
- def test_ed25519_vectors_if_crypto_available(self) -> None:
77
- vectors = self.payload.get("ed25519Vectors", [])
78
- if not vectors:
79
- self.skipTest("No ED25519 vectors present")
80
-
81
- for vector in vectors:
82
- envelope = vector["envelope"]
83
- canonical = canonicalize_envelope(envelope)
84
- self.assertEqual(canonical, vector["canonical"])
85
-
86
- try:
87
- signature = sign_canonical_payload_ed25519(
88
- canonical, vector["privateKey"]
89
- )
90
- except RuntimeError as exc:
91
- if "cryptography package is required" in str(exc):
92
- self.skipTest("cryptography package unavailable for ED25519")
93
- raise
94
-
95
- self.assertEqual(signature, vector["signature"])
96
- self.assertTrue(
97
- verify_canonical_payload_ed25519(
98
- canonical, signature, vector["publicKey"]
99
- )
100
- )
101
-
102
- signed = sign_envelope_ed25519(
103
- envelope, vector["privateKey"], vector["keyId"]
104
- )
105
- self.assertTrue(verify_envelope_ed25519(signed, vector["publicKey"]))
106
- self.assertEqual(signed["signature"]["alg"], "ED25519")
107
-
108
-
109
- if __name__ == "__main__":
110
- unittest.main()
@@ -1,206 +0,0 @@
1
- """Tests for protocol_7h3.encryption — X25519 + ChaCha20-Poly1305 E2E encryption."""
2
- from __future__ import annotations
3
-
4
- import base64
5
- import json
6
- import time
7
- import unittest
8
-
9
- from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
10
- from cryptography.hazmat.primitives import serialization
11
-
12
- from protocol_7h3.encryption import (
13
- generate_x25519_keypair,
14
- seal_envelope,
15
- open_envelope,
16
- _encrypt_body,
17
- _decrypt_body,
18
- )
19
-
20
-
21
- # ---------------------------------------------------------------------------
22
- # Helpers
23
- # ---------------------------------------------------------------------------
24
-
25
-
26
- def _b64url_encode(data: bytes) -> str:
27
- return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
28
-
29
-
30
- def _generate_ed25519_keypair():
31
- """Generate Ed25519 keypair in PKCS8/SPKI DER format (base64url) — matches TS protocol.ts."""
32
- priv = Ed25519PrivateKey.generate()
33
- pub = priv.public_key()
34
- priv_der = priv.private_bytes(
35
- serialization.Encoding.DER,
36
- serialization.PrivateFormat.PKCS8,
37
- serialization.NoEncryption(),
38
- )
39
- pub_der = pub.public_bytes(
40
- serialization.Encoding.DER,
41
- serialization.PublicFormat.SubjectPublicKeyInfo,
42
- )
43
- return _b64url_encode(priv_der), _b64url_encode(pub_der)
44
-
45
-
46
- def _make_envelope(body: dict) -> dict:
47
- now_ms = int(time.time() * 1000)
48
- return {
49
- "header": {
50
- "version": "7h3/0.1",
51
- "messageId": f"msg-{now_ms}-test",
52
- "timestampMs": now_ms,
53
- "ttlMs": 60_000,
54
- "sender": "agent-alice",
55
- "recipient": "agent-bob",
56
- "nonce": _b64url_encode(__import__("os").urandom(12)),
57
- },
58
- "body": body,
59
- }
60
-
61
-
62
- # ---------------------------------------------------------------------------
63
- # Tests
64
- # ---------------------------------------------------------------------------
65
-
66
-
67
- class TestGenerateX25519Keypair(unittest.TestCase):
68
- """Test 1: generate_x25519_keypair returns 32-byte base64url keys."""
69
-
70
- def test_key_format(self):
71
- pub, priv = generate_x25519_keypair()
72
- # base64url of 32 bytes = 43 chars (no padding)
73
- self.assertRegex(pub, r"^[A-Za-z0-9_-]{43}$")
74
- self.assertRegex(priv, r"^[A-Za-z0-9_-]{43}$")
75
- # Decoded must be exactly 32 bytes
76
- padding = "=" * ((4 - (len(pub) % 4)) % 4)
77
- self.assertEqual(len(base64.urlsafe_b64decode(pub + padding)), 32)
78
- padding = "=" * ((4 - (len(priv) % 4)) % 4)
79
- self.assertEqual(len(base64.urlsafe_b64decode(priv + padding)), 32)
80
-
81
-
82
- class TestSealAndOpenEnvelope(unittest.TestCase):
83
- """Test 2: sealEnvelope + openEnvelope round-trip recovers original body exactly."""
84
-
85
- def test_round_trip(self):
86
- recipient_pub, recipient_priv = generate_x25519_keypair()
87
- sender_priv_ed, sender_pub_ed = _generate_ed25519_keypair()
88
-
89
- original_body = {
90
- "intent": "TASK",
91
- "content": "Hello encrypted world!",
92
- "capability": "some-cap",
93
- "correlationId": "corr-123",
94
- }
95
- envelope = _make_envelope(original_body)
96
-
97
- sealed = seal_envelope(envelope, recipient_pub, sender_priv_ed)
98
- result = open_envelope(sealed, recipient_priv, sender_pub_ed)
99
- body = result["body"]
100
-
101
- self.assertEqual(body["intent"], original_body["intent"])
102
- self.assertEqual(body["content"], original_body["content"])
103
- self.assertEqual(body.get("capability"), original_body["capability"])
104
- self.assertEqual(body.get("correlationId"), original_body["correlationId"])
105
-
106
-
107
- class TestWrongRecipientKey(unittest.TestCase):
108
- """Test 3: fails with wrong recipient private key (AEAD tag mismatch)."""
109
-
110
- def test_wrong_key_fails(self):
111
- recipient_pub, _ = generate_x25519_keypair()
112
- _, wrong_priv = generate_x25519_keypair()
113
- sender_priv_ed, sender_pub_ed = _generate_ed25519_keypair()
114
-
115
- envelope = _make_envelope({"intent": "PING", "content": "secret"})
116
- sealed = seal_envelope(envelope, recipient_pub, sender_priv_ed)
117
-
118
- # Use _decrypt_body directly to bypass signature check (wrong key scenario)
119
- with self.assertRaises(Exception):
120
- _decrypt_body(sealed["body"]["content"], wrong_priv)
121
-
122
-
123
- class TestTamperedSignature(unittest.TestCase):
124
- """Test 4: fails if envelope signature is tampered."""
125
-
126
- def test_tampered_signature(self):
127
- recipient_pub, recipient_priv = generate_x25519_keypair()
128
- sender_priv_ed, sender_pub_ed = _generate_ed25519_keypair()
129
-
130
- envelope = _make_envelope({"intent": "PING", "content": "secret"})
131
- sealed = seal_envelope(envelope, recipient_pub, sender_priv_ed)
132
-
133
- # Tamper with signature
134
- tampered = dict(sealed)
135
- tampered["signature"] = dict(sealed["signature"])
136
- tampered["signature"]["value"] = "A" * 86 # wrong Ed25519 sig
137
-
138
- with self.assertRaises(ValueError) as ctx:
139
- open_envelope(tampered, recipient_priv, sender_pub_ed)
140
- self.assertIn("signature", str(ctx.exception).lower())
141
-
142
-
143
- class TestTamperedCiphertext(unittest.TestCase):
144
- """Test 5: fails if ciphertext tampered (AEAD auth tag fails)."""
145
-
146
- def test_tampered_ciphertext(self):
147
- recipient_pub, recipient_priv = generate_x25519_keypair()
148
-
149
- body = {"intent": "PING", "content": "secret"}
150
- encrypted_payload_dict = _encrypt_body(body, recipient_pub)
151
-
152
- # Flip bits in ciphertext
153
- import base64
154
- ct_bytes = bytearray(base64.urlsafe_b64decode(
155
- encrypted_payload_dict["ciphertext"] + "=" * ((4 - len(encrypted_payload_dict["ciphertext"]) % 4) % 4)
156
- ))
157
- ct_bytes[0] ^= 0xFF
158
- encrypted_payload_dict["ciphertext"] = base64.urlsafe_b64encode(bytes(ct_bytes)).decode().rstrip("=")
159
-
160
- # Re-encode payload
161
- tampered_content = _b64url_encode(
162
- json.dumps(encrypted_payload_dict, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
163
- )
164
-
165
- with self.assertRaises(Exception):
166
- _decrypt_body(tampered_content, recipient_priv)
167
-
168
-
169
- class TestEphemeralRandomness(unittest.TestCase):
170
- """Test 6: Two seal calls on same body produce different ciphertexts."""
171
-
172
- def test_different_ciphertexts(self):
173
- recipient_pub, _ = generate_x25519_keypair()
174
- sender_priv_ed, _ = _generate_ed25519_keypair()
175
-
176
- envelope1 = _make_envelope({"intent": "PING", "content": "same content"})
177
- envelope2 = _make_envelope({"intent": "PING", "content": "same content"})
178
-
179
- sealed1 = seal_envelope(envelope1, recipient_pub, sender_priv_ed)
180
- sealed2 = seal_envelope(envelope2, recipient_pub, sender_priv_ed)
181
-
182
- self.assertNotEqual(sealed1["body"]["content"], sealed2["body"]["content"])
183
-
184
-
185
- class TestEncryptedContentOpaque(unittest.TestCase):
186
- """Test 7: Encrypted content is opaque (does not contain original body.content)."""
187
-
188
- def test_content_is_opaque(self):
189
- recipient_pub, _ = generate_x25519_keypair()
190
- sender_priv_ed, _ = _generate_ed25519_keypair()
191
-
192
- original_content = "super-secret-data-12345"
193
- envelope = _make_envelope({"intent": "TASK", "content": original_content})
194
-
195
- sealed = seal_envelope(envelope, recipient_pub, sender_priv_ed)
196
-
197
- # The encrypted content (as JSON) should not reveal the original string
198
- encrypted_content_decoded = base64.urlsafe_b64decode(
199
- sealed["body"]["content"] + "=" * ((4 - len(sealed["body"]["content"]) % 4) % 4)
200
- ).decode("utf-8")
201
- self.assertNotIn(original_content, encrypted_content_decoded)
202
- self.assertNotIn(original_content, sealed["body"]["content"])
203
-
204
-
205
- if __name__ == "__main__":
206
- unittest.main()