@7h3/protocol 0.4.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 (248) hide show
  1. package/README.md +6 -323
  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 -79
  23. package/policyEnforcer.d.ts +50 -0
  24. package/policyTelemetryFeedback.d.ts +11 -0
  25. package/protocol.d.ts +66 -0
  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 -92
  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 -385
  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.png +0 -0
  115. package/eslint.config.js +0 -15
  116. package/fuzz/ts/harness-decode.ts +0 -136
  117. package/fuzz/ts/harness-verify.ts +0 -121
  118. package/fuzz/ts/run.ts +0 -35
  119. package/mcp-server/README.md +0 -38
  120. package/mcp-server/package-lock.json +0 -1187
  121. package/mcp-server/package.json +0 -35
  122. package/mcp-server/src/index.ts +0 -236
  123. package/mcp-server/tsconfig.json +0 -14
  124. package/scripts/aip-framework-quickstart.ts +0 -110
  125. package/scripts/aip-mcp-gateway.ts +0 -38
  126. package/scripts/aip-mcp-wrap-demo.ts +0 -72
  127. package/scripts/aip-quickstart.ts +0 -60
  128. package/scripts/bench-diff.ts +0 -118
  129. package/scripts/bench-protocol-e2e.ts +0 -937
  130. package/scripts/bench-protocol-openloop.ts +0 -1397
  131. package/scripts/bench-replay-cache.ts +0 -76
  132. package/scripts/bench-signature-profiles.ts +0 -180
  133. package/scripts/bench-wire-codecs.ts +0 -161
  134. package/scripts/build-binary-conformance.ts +0 -36
  135. package/scripts/build-release-dashboard.ts +0 -175
  136. package/scripts/canary-rollout.ts +0 -38
  137. package/scripts/mcpGatewayCli.test.ts +0 -116
  138. package/scripts/prepare-aip-package.ts +0 -88
  139. package/scripts/regen-conformance-sigs.ts +0 -18
  140. package/scripts/release-gate.ts +0 -19
  141. package/scripts/validate-runtime-policy.ts +0 -18
  142. package/sdk/browser/index.test.ts +0 -162
  143. package/sdk/browser/index.ts +0 -257
  144. package/sdk/browser/package.json +0 -13
  145. package/sdk/go/go.mod +0 -3
  146. package/sdk/go/http.go +0 -135
  147. package/sdk/go/protocol.go +0 -324
  148. package/sdk/go/protocol_test.go +0 -334
  149. package/sdk/go/webhook.go +0 -136
  150. package/sdk/python/README.md +0 -18
  151. package/sdk/python/protocol_7h3/__init__.py +0 -46
  152. package/sdk/python/protocol_7h3/http.py +0 -212
  153. package/sdk/python/protocol_7h3/keys.py +0 -149
  154. package/sdk/python/protocol_7h3/protocol.py +0 -525
  155. package/sdk/python/protocol_7h3/queue.py +0 -118
  156. package/sdk/python/protocol_7h3/webhook.py +0 -116
  157. package/sdk/python/pyproject.toml +0 -40
  158. package/sdk/python/tests/test_conformance.py +0 -110
  159. package/sdk/python/tests/test_http.py +0 -305
  160. package/sdk/python/tests/test_keys.py +0 -417
  161. package/sdk/python/tests/test_queue.py +0 -120
  162. package/sdk/python/tests/test_webhook.py +0 -345
  163. package/sdk/rust/Cargo.lock +0 -371
  164. package/sdk/rust/Cargo.toml +0 -25
  165. package/sdk/rust/README.md +0 -31
  166. package/sdk/rust/fuzz/Cargo.toml +0 -29
  167. package/sdk/rust/fuzz/fuzz_targets/fuzz_canonicalize.rs +0 -46
  168. package/sdk/rust/fuzz/fuzz_targets/fuzz_decode.rs +0 -11
  169. package/sdk/rust/src/bin/aip_mcp_gateway.rs +0 -59
  170. package/sdk/rust/src/http.rs +0 -145
  171. package/sdk/rust/src/keys.rs +0 -161
  172. package/sdk/rust/src/lib.rs +0 -688
  173. package/sdk/rust/src/queue.rs +0 -79
  174. package/sdk/rust/src/webhook.rs +0 -86
  175. package/sdk/rust/tests/conformance.rs +0 -148
  176. package/sdk/rust/tests/gateway.rs +0 -130
  177. package/sdk/rust/tests/http_webhook_queue.rs +0 -201
  178. package/sdk/rust/tests/keys.rs +0 -189
  179. package/src/agentAdapter.test.ts +0 -48
  180. package/src/agentAdapter.ts +0 -56
  181. package/src/auditLog.test.ts +0 -145
  182. package/src/auditLog.ts +0 -147
  183. package/src/conformance.test.ts +0 -136
  184. package/src/conformanceVectors.ts +0 -99
  185. package/src/frameworkAdapters.test.ts +0 -290
  186. package/src/frameworkAdapters.ts +0 -261
  187. package/src/gateway.test.ts +0 -343
  188. package/src/gateway.ts +0 -171
  189. package/src/grpcBinding.test.ts +0 -211
  190. package/src/grpcBinding.ts +0 -103
  191. package/src/httpBinding.test.ts +0 -376
  192. package/src/httpBinding.ts +0 -163
  193. package/src/index.ts +0 -32
  194. package/src/keyInfra.test.ts +0 -278
  195. package/src/keyInfra.ts +0 -228
  196. package/src/keyRegistry.ts +0 -59
  197. package/src/keyRotation.test.ts +0 -78
  198. package/src/keyRotation.ts +0 -72
  199. package/src/mcpGateway.test.ts +0 -129
  200. package/src/mcpGateway.ts +0 -250
  201. package/src/mcpTransports.test.ts +0 -92
  202. package/src/mcpTransports.ts +0 -169
  203. package/src/mcpWrapper.test.ts +0 -179
  204. package/src/mcpWrapper.ts +0 -206
  205. package/src/policyEnforcer.test.ts +0 -99
  206. package/src/policyEnforcer.ts +0 -169
  207. package/src/policyTelemetryFeedback.test.ts +0 -25
  208. package/src/policyTelemetryFeedback.ts +0 -38
  209. package/src/protocol.bench.ts +0 -37
  210. package/src/protocol.test.ts +0 -155
  211. package/src/protocol.ts +0 -413
  212. package/src/protocolAgent.test.ts +0 -105
  213. package/src/protocolAgent.ts +0 -169
  214. package/src/protocolBinary.test.ts +0 -165
  215. package/src/protocolBinary.ts +0 -312
  216. package/src/protocolCapabilities.ts +0 -70
  217. package/src/protocolFuzz.advanced.test.ts +0 -235
  218. package/src/protocolFuzz.test.ts +0 -111
  219. package/src/protocolNegative.test.ts +0 -97
  220. package/src/protocolReplay.test.ts +0 -71
  221. package/src/protocolReplay.ts +0 -194
  222. package/src/protocolTransport.test.ts +0 -556
  223. package/src/protocolTransport.ts +0 -483
  224. package/src/queueBinding.test.ts +0 -130
  225. package/src/queueBinding.ts +0 -102
  226. package/src/rateLimiter.test.ts +0 -96
  227. package/src/rateLimiter.ts +0 -46
  228. package/src/redisIntegration.test.ts +0 -134
  229. package/src/replayStores.test.ts +0 -141
  230. package/src/replayStores.ts +0 -82
  231. package/src/revocation.test.ts +0 -98
  232. package/src/revocation.ts +0 -0
  233. package/src/routePolicy.test.ts +0 -87
  234. package/src/routePolicy.ts +0 -72
  235. package/src/runtimePolicy.test.ts +0 -49
  236. package/src/runtimePolicy.ts +0 -81
  237. package/src/runtimePolicyManager.test.ts +0 -29
  238. package/src/runtimePolicyManager.ts +0 -50
  239. package/src/runtimePolicyPresets.ts +0 -43
  240. package/src/signedResponse.test.ts +0 -111
  241. package/src/signedResponse.ts +0 -83
  242. package/src/webhookBinding.test.ts +0 -144
  243. package/src/webhookBinding.ts +0 -115
  244. package/src/wsBinding.test.ts +0 -221
  245. package/src/wsBinding.ts +0 -100
  246. package/tsconfig.json +0 -15
  247. package/tsconfig.lib.json +0 -23
  248. package/vite.lib.config.ts +0 -16
@@ -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.4.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,305 +0,0 @@
1
- """Tests for protocol_7h3.http — HTTP binding."""
2
- from __future__ import annotations
3
-
4
- import json
5
- import time
6
- import unittest
7
-
8
- from protocol_7h3.protocol import sign_envelope_ed25519, sign_envelope_hmac
9
- from protocol_7h3.http import (
10
- DEFAULT_HEADER,
11
- KeyRegistry,
12
- StaticKeyRegistry,
13
- build_signed_request_headers,
14
- sign_http_request,
15
- verify_http_envelope,
16
- )
17
-
18
- # ED25519 test key pair from conformance vectors (SPKI public / PKCS8 private, base64url)
19
- _TEST_PUBLIC_KEY = "MCowBQYDK2VwAyEA-mUFiTQtcKN4nnD19V_-Wyy4q19OivnAutRUPhOcC78"
20
- _TEST_PRIVATE_KEY = "MC4CAQAwBQYDK2VwBCIEICheZbQGuDVb6hezIlcs0QnCHGxz6IhiLkC9M0qr8OOZ"
21
- _SENDER = "agent.ed"
22
-
23
-
24
- def _make_valid_envelope(ttl_ms: int = 60_000) -> dict:
25
- """Return an unsigned envelope with a current timestamp."""
26
- now = int(time.time() * 1000)
27
- return {
28
- "header": {
29
- "version": "7h3/0.1",
30
- "messageId": "test-msg-1",
31
- "timestampMs": now,
32
- "ttlMs": ttl_ms,
33
- "sender": _SENDER,
34
- "nonce": "test-nonce-1",
35
- },
36
- "body": {
37
- "intent": "TASK",
38
- "content": "hello http",
39
- },
40
- }
41
-
42
-
43
- def _signed_envelope(ttl_ms: int = 60_000) -> dict:
44
- env = _make_valid_envelope(ttl_ms=ttl_ms)
45
- return sign_envelope_ed25519(env, _TEST_PRIVATE_KEY)
46
-
47
-
48
- def _registry() -> StaticKeyRegistry:
49
- return StaticKeyRegistry({_SENDER: _TEST_PUBLIC_KEY})
50
-
51
-
52
- class TestVerifyHttpEnvelopeMissingHeader(unittest.TestCase):
53
- """1. verify_http_envelope: missing header."""
54
-
55
- def test_missing_header_returns_false_and_reason(self):
56
- ok, env, reason = verify_http_envelope({}, _registry())
57
- self.assertFalse(ok)
58
- self.assertIsNone(env)
59
- self.assertEqual(reason, "missing-header")
60
-
61
- def test_wrong_header_name_treated_as_missing(self):
62
- headers = {"x-other-header": "{}"}
63
- ok, env, reason = verify_http_envelope(headers, _registry())
64
- self.assertFalse(ok)
65
- self.assertEqual(reason, "missing-header")
66
-
67
-
68
- class TestVerifyHttpEnvelopeInvalidJson(unittest.TestCase):
69
- """2. verify_http_envelope: invalid JSON."""
70
-
71
- def test_malformed_json(self):
72
- headers = {DEFAULT_HEADER: "not-json{{{"}
73
- ok, env, reason = verify_http_envelope(headers, _registry())
74
- self.assertFalse(ok)
75
- self.assertIsNone(env)
76
- self.assertEqual(reason, "malformed-envelope")
77
-
78
- def test_json_without_required_keys(self):
79
- headers = {DEFAULT_HEADER: json.dumps({"foo": "bar"})}
80
- ok, env, reason = verify_http_envelope(headers, _registry())
81
- self.assertFalse(ok)
82
- self.assertEqual(reason, "malformed-envelope")
83
-
84
- def test_json_missing_signature_key(self):
85
- headers = {DEFAULT_HEADER: json.dumps({"header": {}, "body": {}})}
86
- ok, env, reason = verify_http_envelope(headers, _registry())
87
- self.assertFalse(ok)
88
- self.assertEqual(reason, "malformed-envelope")
89
-
90
-
91
- class TestVerifyHttpEnvelopeValidEd25519(unittest.TestCase):
92
- """3. verify_http_envelope: valid Ed25519 signed envelope."""
93
-
94
- def test_valid_signed_envelope(self):
95
- signed = _signed_envelope()
96
- headers = {DEFAULT_HEADER: json.dumps(signed)}
97
- ok, env, reason = verify_http_envelope(headers, _registry())
98
- self.assertTrue(ok)
99
- self.assertIsNotNone(env)
100
- self.assertIsNone(reason)
101
-
102
- def test_valid_envelope_mixed_case_header(self):
103
- """Headers are case-insensitive per HTTP spec."""
104
- signed = _signed_envelope()
105
- headers = {"X-7H3-Envelope": json.dumps(signed)}
106
- ok, env, reason = verify_http_envelope(headers, _registry())
107
- self.assertTrue(ok)
108
- self.assertIsNone(reason)
109
-
110
- def test_returns_envelope_dict_on_success(self):
111
- signed = _signed_envelope()
112
- headers = {DEFAULT_HEADER: json.dumps(signed)}
113
- ok, env, reason = verify_http_envelope(headers, _registry())
114
- self.assertTrue(ok)
115
- self.assertIn("header", env)
116
- self.assertIn("body", env)
117
- self.assertIn("signature", env)
118
-
119
-
120
- class TestVerifyHttpEnvelopeTampered(unittest.TestCase):
121
- """4. verify_http_envelope: tampered envelope."""
122
-
123
- def test_tampered_content_fails_verification(self):
124
- signed = _signed_envelope()
125
- # Tamper with the body content after signing
126
- signed["body"]["content"] = "tampered!"
127
- headers = {DEFAULT_HEADER: json.dumps(signed)}
128
- ok, env, reason = verify_http_envelope(headers, _registry())
129
- self.assertFalse(ok)
130
- self.assertEqual(reason, "invalid-signature")
131
-
132
- def test_tampered_sender_fails_verification(self):
133
- signed = _signed_envelope()
134
- signed["header"]["sender"] = "evil.agent"
135
- headers = {DEFAULT_HEADER: json.dumps(signed)}
136
- # evil.agent not in registry → unknown-sender
137
- ok, env, reason = verify_http_envelope(headers, _registry())
138
- self.assertFalse(ok)
139
- self.assertIn(reason, ("unknown-sender", "invalid-signature"))
140
-
141
- def test_tampered_signature_value_fails(self):
142
- signed = _signed_envelope()
143
- signed["signature"]["value"] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
144
- headers = {DEFAULT_HEADER: json.dumps(signed)}
145
- ok, env, reason = verify_http_envelope(headers, _registry())
146
- self.assertFalse(ok)
147
- self.assertEqual(reason, "invalid-signature")
148
-
149
-
150
- class TestVerifyHttpEnvelopeUnknownSender(unittest.TestCase):
151
- """5. verify_http_envelope: unknown sender."""
152
-
153
- def test_unknown_sender_ed25519(self):
154
- signed = _signed_envelope()
155
- # Empty registry — no known senders
156
- empty_registry = StaticKeyRegistry({})
157
- headers = {DEFAULT_HEADER: json.dumps(signed)}
158
- ok, env, reason = verify_http_envelope(headers, empty_registry)
159
- self.assertFalse(ok)
160
- self.assertIsNone(env)
161
- self.assertEqual(reason, "unknown-sender")
162
-
163
- def test_unknown_sender_hmac(self):
164
- env = _make_valid_envelope()
165
- signed = sign_envelope_hmac(env, "mysecret", "key-1")
166
- headers = {DEFAULT_HEADER: json.dumps(signed)}
167
-
168
- class NoSecretRegistry(KeyRegistry):
169
- def get_public_key(self, sender_id: str):
170
- return None
171
- def get_shared_secret(self, key_id: str):
172
- return None # key_id "key-1" unknown
173
-
174
- ok, result, reason = verify_http_envelope(headers, NoSecretRegistry())
175
- self.assertFalse(ok)
176
- self.assertEqual(reason, "unknown-sender")
177
-
178
-
179
- class TestSignHttpRequestRoundTrip(unittest.TestCase):
180
- """6. sign_http_request + verify_http_envelope round trip."""
181
-
182
- def test_round_trip_succeeds(self):
183
- unsigned = _make_valid_envelope()
184
- headers = sign_http_request(unsigned, _TEST_PRIVATE_KEY)
185
- ok, env, reason = verify_http_envelope(headers, _registry())
186
- self.assertTrue(ok)
187
- self.assertIsNone(reason)
188
- self.assertEqual(env["body"]["content"], "hello http")
189
-
190
- def test_round_trip_strict_ttl_expired(self):
191
- # Create envelope that is already expired
192
- past_ms = int(time.time() * 1000) - 120_000 # 2 minutes ago
193
- envelope = {
194
- "header": {
195
- "version": "7h3/0.1",
196
- "messageId": "expired-msg",
197
- "timestampMs": past_ms,
198
- "ttlMs": 1_000, # 1 second TTL → already expired
199
- "sender": _SENDER,
200
- "nonce": "nonce-x",
201
- },
202
- "body": {
203
- "intent": "TASK",
204
- "content": "stale",
205
- },
206
- }
207
- signed = sign_envelope_ed25519(envelope, _TEST_PRIVATE_KEY)
208
- headers = {DEFAULT_HEADER: json.dumps(signed)}
209
- ok, env, reason = verify_http_envelope(headers, _registry(), strict_ttl=True)
210
- self.assertFalse(ok)
211
- self.assertEqual(reason, "ttl-expired")
212
-
213
- def test_round_trip_strict_ttl_disabled(self):
214
- # Same expired envelope but strict_ttl=False → signature still valid
215
- past_ms = int(time.time() * 1000) - 120_000
216
- envelope = {
217
- "header": {
218
- "version": "7h3/0.1",
219
- "messageId": "expired-msg-2",
220
- "timestampMs": past_ms,
221
- "ttlMs": 1_000,
222
- "sender": _SENDER,
223
- "nonce": "nonce-y",
224
- },
225
- "body": {
226
- "intent": "TASK",
227
- "content": "stale but skip ttl check",
228
- },
229
- }
230
- signed = sign_envelope_ed25519(envelope, _TEST_PRIVATE_KEY)
231
- headers = {DEFAULT_HEADER: json.dumps(signed)}
232
- ok, env, reason = verify_http_envelope(headers, _registry(), strict_ttl=False)
233
- self.assertTrue(ok)
234
- self.assertIsNone(reason)
235
-
236
- def test_custom_header_name(self):
237
- unsigned = _make_valid_envelope()
238
- custom_hdr = "x-my-agent-envelope"
239
- headers = sign_http_request(unsigned, _TEST_PRIVATE_KEY, header_name=custom_hdr)
240
- self.assertIn(custom_hdr, headers)
241
- ok, env, reason = verify_http_envelope(headers, _registry(), header_name=custom_hdr)
242
- self.assertTrue(ok)
243
- self.assertIsNone(reason)
244
-
245
-
246
- class TestBuildSignedRequestHeaders(unittest.TestCase):
247
- """7. build_signed_request_headers produces correct header key."""
248
-
249
- def test_default_header_key_present(self):
250
- headers = build_signed_request_headers(
251
- sender=_SENDER,
252
- private_key=_TEST_PRIVATE_KEY,
253
- content="ping",
254
- )
255
- self.assertIn(DEFAULT_HEADER, headers)
256
-
257
- def test_custom_header_key(self):
258
- custom = "x-aip-envelope"
259
- headers = build_signed_request_headers(
260
- sender=_SENDER,
261
- private_key=_TEST_PRIVATE_KEY,
262
- content="ping",
263
- header_name=custom,
264
- )
265
- self.assertIn(custom, headers)
266
- self.assertNotIn(DEFAULT_HEADER, headers)
267
-
268
- def test_header_value_is_valid_json_envelope(self):
269
- headers = build_signed_request_headers(
270
- sender=_SENDER,
271
- private_key=_TEST_PRIVATE_KEY,
272
- content="check me",
273
- ttl_ms=30_000,
274
- )
275
- raw = headers[DEFAULT_HEADER]
276
- envelope = json.loads(raw)
277
- self.assertEqual(envelope["header"]["sender"], _SENDER)
278
- self.assertEqual(envelope["body"]["content"], "check me")
279
- self.assertEqual(envelope["signature"]["alg"], "ED25519")
280
-
281
- def test_round_trip_verify(self):
282
- headers = build_signed_request_headers(
283
- sender=_SENDER,
284
- private_key=_TEST_PRIVATE_KEY,
285
- content="full round trip",
286
- recipient="agent.receiver",
287
- )
288
- ok, env, reason = verify_http_envelope(headers, _registry())
289
- self.assertTrue(ok)
290
- self.assertIsNone(reason)
291
- self.assertEqual(env["body"]["content"], "full round trip")
292
-
293
- def test_recipient_is_included_when_provided(self):
294
- headers = build_signed_request_headers(
295
- sender=_SENDER,
296
- private_key=_TEST_PRIVATE_KEY,
297
- content="msg",
298
- recipient="agent.b",
299
- )
300
- envelope = json.loads(headers[DEFAULT_HEADER])
301
- self.assertEqual(envelope["header"].get("recipient"), "agent.b")
302
-
303
-
304
- if __name__ == "__main__":
305
- unittest.main()