hypermind 0.11.0__py3-none-any.whl

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 (181) hide show
  1. hmctl/__init__.py +7 -0
  2. hmctl/cli.py +947 -0
  3. hypermind/__init__.py +208 -0
  4. hypermind/_deprecations.py +108 -0
  5. hypermind/agent.py +2896 -0
  6. hypermind/agent_card.py +127 -0
  7. hypermind/api/__init__.py +32 -0
  8. hypermind/api/app.py +686 -0
  9. hypermind/capability.py +923 -0
  10. hypermind/consult.py +493 -0
  11. hypermind/consult_bias.py +112 -0
  12. hypermind/contract_net.py +219 -0
  13. hypermind/crypto/__init__.py +21 -0
  14. hypermind/crypto/cose_encrypt.py +126 -0
  15. hypermind/crypto/dkg.py +246 -0
  16. hypermind/crypto/domain.py +59 -0
  17. hypermind/crypto/frost.py +771 -0
  18. hypermind/crypto/hashing.py +38 -0
  19. hypermind/crypto/hlc.py +163 -0
  20. hypermind/crypto/hpke.py +277 -0
  21. hypermind/crypto/kms.py +196 -0
  22. hypermind/crypto/kms_backends/__init__.py +56 -0
  23. hypermind/crypto/kms_backends/aws.py +128 -0
  24. hypermind/crypto/kms_backends/azure.py +114 -0
  25. hypermind/crypto/kms_backends/gcp.py +97 -0
  26. hypermind/crypto/kms_backends/vault.py +120 -0
  27. hypermind/crypto/namespace_root.py +204 -0
  28. hypermind/crypto/pq.py +152 -0
  29. hypermind/crypto/room_epoch.py +97 -0
  30. hypermind/crypto/signing.py +245 -0
  31. hypermind/deliberation.py +355 -0
  32. hypermind/did_web.py +256 -0
  33. hypermind/dispute.py +858 -0
  34. hypermind/errors.py +218 -0
  35. hypermind/eval/__init__.py +49 -0
  36. hypermind/eval/calibration.py +307 -0
  37. hypermind/eval/comparison.py +251 -0
  38. hypermind/eval/replay.py +202 -0
  39. hypermind/eval/swarm_iq.py +718 -0
  40. hypermind/knowledge/__init__.py +17 -0
  41. hypermind/knowledge/citation_graph.py +141 -0
  42. hypermind/knowledge/correlated_agreement.py +167 -0
  43. hypermind/knowledge/embedding_registry.py +115 -0
  44. hypermind/knowledge/kernel_store.py +197 -0
  45. hypermind/knowledge/rekor.py +173 -0
  46. hypermind/knowledge/reputation.py +364 -0
  47. hypermind/knowledge/swarm_memory.py +523 -0
  48. hypermind/knowledge/transparency.py +409 -0
  49. hypermind/mcp/__init__.py +60 -0
  50. hypermind/mcp/bridge.py +151 -0
  51. hypermind/mcp/client.py +142 -0
  52. hypermind/mcp/provenance.py +214 -0
  53. hypermind/mcp/relata_tools.py +152 -0
  54. hypermind/mcp/server.py +248 -0
  55. hypermind/mcp/transport.py +206 -0
  56. hypermind/mind/__init__.py +150 -0
  57. hypermind/mind/active.py +234 -0
  58. hypermind/mind/belief.py +369 -0
  59. hypermind/mind/deliberator.py +625 -0
  60. hypermind/mind/diversity.py +144 -0
  61. hypermind/mind/evolve.py +184 -0
  62. hypermind/mind/federation.py +154 -0
  63. hypermind/mind/goals.py +117 -0
  64. hypermind/mind/integration.py +190 -0
  65. hypermind/mind/mediator.py +74 -0
  66. hypermind/mind/memory_store.py +129 -0
  67. hypermind/mind/policy.py +307 -0
  68. hypermind/mind/records.py +260 -0
  69. hypermind/mind/roles.py +190 -0
  70. hypermind/mind/sybil_guard.py +41 -0
  71. hypermind/mind/world_model.py +166 -0
  72. hypermind/namespace.py +515 -0
  73. hypermind/namespace_policy.py +254 -0
  74. hypermind/observability/__init__.py +25 -0
  75. hypermind/observability/asgi.py +214 -0
  76. hypermind/observability/cost.py +277 -0
  77. hypermind/observability/otel_export.py +200 -0
  78. hypermind/observability/recorder.py +344 -0
  79. hypermind/observability/swarm_trace.py +438 -0
  80. hypermind/pin_policy.py +116 -0
  81. hypermind/py.typed +0 -0
  82. hypermind/responders/__init__.py +87 -0
  83. hypermind/responders/anthropic.py +159 -0
  84. hypermind/responders/base.py +162 -0
  85. hypermind/responders/openai.py +199 -0
  86. hypermind/responders/router.py +254 -0
  87. hypermind/responders/structured.py +287 -0
  88. hypermind/responders/tools.py +444 -0
  89. hypermind/revocation.py +270 -0
  90. hypermind/rule_ids.py +135 -0
  91. hypermind/schemas/encrypted_statement.cddl +28 -0
  92. hypermind/schemas/namespace_accept.cddl +20 -0
  93. hypermind/schemas/namespace_create.cddl +25 -0
  94. hypermind/schemas/namespace_founding_attest.cddl +30 -0
  95. hypermind/schemas/namespace_invite.cddl +22 -0
  96. hypermind/schemas/wire-v0.1.cddl +73 -0
  97. hypermind/simlab/__init__.py +15 -0
  98. hypermind/simlab/_persona_registry.py +93 -0
  99. hypermind/simlab/_scenario_impl.py +2778 -0
  100. hypermind/simlab/app.py +2538 -0
  101. hypermind/simlab/backends/__init__.py +27 -0
  102. hypermind/simlab/backends/anchor.py +88 -0
  103. hypermind/simlab/backends/local.py +32 -0
  104. hypermind/simlab/backends/server.py +79 -0
  105. hypermind/simlab/cli_command.py +108 -0
  106. hypermind/simlab/config.py +106 -0
  107. hypermind/simlab/deployment.py +26 -0
  108. hypermind/simlab/knowledge.py +716 -0
  109. hypermind/simlab/learning.py +295 -0
  110. hypermind/simlab/modes/__init__.py +115 -0
  111. hypermind/simlab/modes/_eval_real_llm.py +373 -0
  112. hypermind/simlab/modes/aar.py +611 -0
  113. hypermind/simlab/modes/backtest.py +610 -0
  114. hypermind/simlab/modes/binary_forecast.py +69 -0
  115. hypermind/simlab/modes/conformance.py +1869 -0
  116. hypermind/simlab/modes/evaluation.py +2271 -0
  117. hypermind/simlab/modes/governance.py +648 -0
  118. hypermind/simlab/modes/redteam.py +534 -0
  119. hypermind/simlab/modes/tabletop.py +797 -0
  120. hypermind/simlab/modes/tournament.py +448 -0
  121. hypermind/simlab/modes/whatif.py +523 -0
  122. hypermind/simlab/namespace.py +125 -0
  123. hypermind/simlab/personas.py +477 -0
  124. hypermind/simlab/prompt_generator.py +172 -0
  125. hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
  126. hypermind/simlab/question_sets/governance_q5.json +67 -0
  127. hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
  128. hypermind/simlab/question_sets/whatif_q3.json +38 -0
  129. hypermind/simlab/registry.py +325 -0
  130. hypermind/simlab/runner.py +239 -0
  131. hypermind/simlab/scenario.py +147 -0
  132. hypermind/simlab/swarms.py +180 -0
  133. hypermind/simlab/tool_handlers/__init__.py +4 -0
  134. hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
  135. hypermind/simlab/tool_handlers/brave.py +46 -0
  136. hypermind/simlab/tool_handlers/newsapi.py +50 -0
  137. hypermind/simlab/tool_handlers/openweather.py +46 -0
  138. hypermind/simlab/tool_handlers/pinecone.py +52 -0
  139. hypermind/simlab/tool_handlers/qdrant.py +57 -0
  140. hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
  141. hypermind/simlab/tool_handlers/relata_recall.py +61 -0
  142. hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
  143. hypermind/simlab/tool_handlers/serper.py +46 -0
  144. hypermind/simlab/tool_handlers/tavily.py +53 -0
  145. hypermind/simlab/tool_handlers/weaviate.py +65 -0
  146. hypermind/simlab/tool_handlers/wikipedia.py +64 -0
  147. hypermind/simlab/tool_handlers/wolfram.py +48 -0
  148. hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
  149. hypermind/simlab/tool_registry.py +568 -0
  150. hypermind/simlab/topic_adapter.py +338 -0
  151. hypermind/simlab/topic_questions.py +259 -0
  152. hypermind/storage/__init__.py +69 -0
  153. hypermind/storage/backend.py +71 -0
  154. hypermind/storage/relata.py +245 -0
  155. hypermind/storage/sqlite.py +258 -0
  156. hypermind/sync.py +191 -0
  157. hypermind/tal.py +175 -0
  158. hypermind/tasks.py +334 -0
  159. hypermind/testing.py +16 -0
  160. hypermind/tools/__init__.py +32 -0
  161. hypermind/tools/registry.py +61 -0
  162. hypermind/transport/__init__.py +22 -0
  163. hypermind/transport/anti_entropy.py +708 -0
  164. hypermind/transport/bus.py +206 -0
  165. hypermind/transport/knows_delta.py +204 -0
  166. hypermind/transport/libp2p.py +298 -0
  167. hypermind/transport/placement.py +58 -0
  168. hypermind/transport/tcp.py +797 -0
  169. hypermind/transport/tls_profile.py +417 -0
  170. hypermind/types.py +59 -0
  171. hypermind/uncertainty.py +222 -0
  172. hypermind/wire.py +897 -0
  173. hypermind/workflow/__init__.py +72 -0
  174. hypermind/workflow/engine.py +655 -0
  175. hypermind/workflow/plan.py +182 -0
  176. hypermind/workflow/repair.py +203 -0
  177. hypermind-0.11.0.dist-info/METADATA +524 -0
  178. hypermind-0.11.0.dist-info/RECORD +181 -0
  179. hypermind-0.11.0.dist-info/WHEEL +4 -0
  180. hypermind-0.11.0.dist-info/entry_points.txt +2 -0
  181. hypermind-0.11.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,204 @@
1
+ """Namespace root key — FROST k-of-n group public key for §24 namespace lifecycle.
2
+
3
+ The namespace root is a FROST (RFC 9591) threshold Schnorr group public
4
+ key produced by a DKG ceremony among the founding cohort. It signs the
5
+ `NAMESPACE_CREATE` and `NAMESPACE_FOUNDING_ATTEST` records, and the
6
+ DID:web rendezvous document (§24.6).
7
+
8
+ This module is a thin wrapper around :mod:`hypermind.crypto.frost` — it
9
+ does not duplicate FROST primitives. ``NamespaceRoot.from_dkg()`` runs
10
+ the trusted-dealer DKG variant and computes a transcript hash that is
11
+ bound into the founding attestation (§24.2).
12
+
13
+ v1.0 hooks:
14
+ - :class:`NamespaceEpoch` carries `epoch_id`, transcript hash, and
15
+ group public key. Root key rotation across transparency-log
16
+ epochs is reserved for v1.0 (spec §24.9 `epoch_rotation_proof`).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass, field
22
+ from typing import Any
23
+
24
+ import cbor2
25
+
26
+ from hypermind.crypto import frost
27
+ from hypermind.crypto.hashing import blake3_32
28
+ from hypermind.crypto.hlc import HLC
29
+ from hypermind.crypto.signing import Keypair
30
+
31
+ # Default FROST threshold — spec §24.2 "k = 5, n = 7".
32
+ DEFAULT_K = 5
33
+ DEFAULT_N = 7
34
+
35
+
36
+ def _canonical_dkg_transcript(
37
+ *,
38
+ member_kids: list[bytes],
39
+ k: int,
40
+ n: int,
41
+ group_public: bytes,
42
+ share_indices: list[int],
43
+ ) -> bytes:
44
+ """Build the canonical CBOR transcript whose BLAKE3 digest binds the DKG.
45
+
46
+ The transcript records (a) the founding member kids in declaration
47
+ order, (b) the threshold parameters, (c) the resulting group public
48
+ key, and (d) the share-index assignment. This is what the §24.3
49
+ ``dkg_transcript_hash`` field commits to.
50
+ """
51
+ return cbor2.dumps(
52
+ {
53
+ "v": 1,
54
+ "members": member_kids,
55
+ "k": k,
56
+ "n": n,
57
+ "group_public": group_public,
58
+ "share_indices": share_indices,
59
+ },
60
+ canonical=True,
61
+ )
62
+
63
+
64
+ @dataclass
65
+ class NamespaceEpoch:
66
+ """A single epoch of the namespace root key.
67
+
68
+ v0.9.x ships epoch 0 only. ``rotate_to_next_epoch()`` is a stub that
69
+ raises :class:`NotImplementedError`; the actual rotation protocol
70
+ (epoch_rotation_proof, transparency-log carry-over) is reserved for
71
+ v1.0 per spec §24.9.
72
+ """
73
+
74
+ epoch_id: int
75
+ dkg_transcript_hash: bytes
76
+ group_pubkey: bytes
77
+ created_at_hlc: HLC
78
+
79
+ def rotate_to_next_epoch(self) -> NamespaceEpoch:
80
+ raise NotImplementedError("v1.0 — see spec §24 reserved fields")
81
+
82
+
83
+ @dataclass
84
+ class NamespaceRoot:
85
+ """Aggregate FROST k-of-n key for a namespace.
86
+
87
+ Attributes
88
+ ----------
89
+ group_pubkey:
90
+ 32-byte Ed25519 group public key (FROST aggregate).
91
+ k, n:
92
+ Threshold parameters (default 5-of-7, spec §24.2).
93
+ member_kids:
94
+ Founding-cohort kids in declaration order. ``share_indices[i]``
95
+ is the FROST share index for ``member_kids[i]``.
96
+ dkg_transcript_hash:
97
+ BLAKE3-256 of the canonical DKG transcript, bound into
98
+ ``NAMESPACE_FOUNDING_ATTEST.dkg_transcript_hash``.
99
+ epoch:
100
+ Current :class:`NamespaceEpoch`. v0.9.x only ever sees epoch 0.
101
+
102
+ Secret shares are kept in ``_shares`` (private) — never exposed
103
+ on the wire and refused by ``__reduce__``.
104
+ """
105
+
106
+ group_pubkey: bytes
107
+ k: int
108
+ n: int
109
+ member_kids: list[bytes]
110
+ dkg_transcript_hash: bytes
111
+ epoch: NamespaceEpoch
112
+ _shares: list[frost.FrostShare] = field(default_factory=list, repr=False)
113
+
114
+ @classmethod
115
+ def from_dkg(
116
+ cls,
117
+ members: list[Keypair],
118
+ k: int = DEFAULT_K,
119
+ n: int | None = None,
120
+ ) -> NamespaceRoot:
121
+ """Run the trusted-dealer FROST DKG over ``members``.
122
+
123
+ Spec §24.2 `NS-LIFECYCLE-FROST`: ``k >= ceil(2n/3)`` and ``n >= 3``.
124
+ Default parameters are k=5, n=7. ``len(members)`` MUST equal n.
125
+ """
126
+ if n is None:
127
+ n = len(members)
128
+ if n < 3:
129
+ raise ValueError("namespace founding cohort requires n >= 3 (spec §24.2)")
130
+ if len(members) != n:
131
+ raise ValueError(f"members count {len(members)} != n={n}")
132
+ # ceil(2n/3) — integer arithmetic
133
+ min_k = (2 * n + 2) // 3
134
+ if k < min_k:
135
+ raise ValueError(
136
+ f"k={k} below ceil(2n/3)={min_k} for n={n} (spec §24.2 NS-LIFECYCLE-FROST)"
137
+ )
138
+ if k > n:
139
+ raise ValueError(f"k={k} cannot exceed n={n}")
140
+
141
+ shares = frost.generate_shares(t=k, n=n)
142
+ group_public = shares[0].group_public
143
+ member_kids = [m.kid for m in members]
144
+ share_indices = [s.index for s in shares]
145
+ transcript = _canonical_dkg_transcript(
146
+ member_kids=member_kids,
147
+ k=k,
148
+ n=n,
149
+ group_public=group_public,
150
+ share_indices=share_indices,
151
+ )
152
+ transcript_hash = blake3_32(transcript)
153
+ epoch = NamespaceEpoch(
154
+ epoch_id=0,
155
+ dkg_transcript_hash=transcript_hash,
156
+ group_pubkey=group_public,
157
+ created_at_hlc=HLC.now(node_id=member_kids[0][:8]),
158
+ )
159
+ return cls(
160
+ group_pubkey=group_public,
161
+ k=k,
162
+ n=n,
163
+ member_kids=member_kids,
164
+ dkg_transcript_hash=transcript_hash,
165
+ epoch=epoch,
166
+ _shares=list(shares),
167
+ )
168
+
169
+ def sign(self, message: bytes, signer_subset: list[int]) -> bytes:
170
+ """Produce an aggregated FROST signature over ``message``.
171
+
172
+ ``signer_subset`` is a list of 1-indexed share indices (matching
173
+ ``share_indices`` from the DKG). MUST contain at least ``k``
174
+ distinct indices. Returns the wire signature ``R || z`` (64
175
+ bytes — both 32-byte scalars/points).
176
+ """
177
+ if len(set(signer_subset)) != len(signer_subset):
178
+ raise ValueError("signer_subset must contain unique indices")
179
+ if len(signer_subset) < self.k:
180
+ raise ValueError(
181
+ f"FROST quorum unmet: need at least k={self.k} signers, got {len(signer_subset)}"
182
+ )
183
+ share_by_index = {s.index: s for s in self._shares}
184
+ chosen: list[frost.FrostShare] = []
185
+ for idx in signer_subset:
186
+ if idx not in share_by_index:
187
+ raise ValueError(f"signer index {idx} not in DKG share set")
188
+ chosen.append(share_by_index[idx])
189
+ R, z = frost.threshold_sign(chosen, message)
190
+ return R + z
191
+
192
+ def verify(self, message: bytes, signature: bytes) -> bool:
193
+ """Verify a FROST signature produced by :meth:`sign`."""
194
+ if len(signature) != 64:
195
+ return False
196
+ R = signature[:32]
197
+ z = signature[32:]
198
+ return frost.verify(self.group_pubkey, message, R, z, commitments=None)
199
+
200
+ # ---- safety -------------------------------------------------------
201
+
202
+ def __reduce__(self) -> Any:
203
+ # Refuse pickle; secret shares would leak.
204
+ raise TypeError("refusing to pickle NamespaceRoot; contains secret FROST shares")
hypermind/crypto/pq.py ADDED
@@ -0,0 +1,152 @@
1
+ """Dual-vendor ML-DSA-65 backend adapter.
2
+
3
+ WS-A supply-chain hardening (v0.6): the production path in
4
+ `hypermind.crypto.signing` uses ``quantcrypt`` (FIPS 204 reference
5
+ implementation) directly. To defend against a single-vendor compromise
6
+ of the post-quantum signature primitive, this module defines a backend
7
+ Protocol with two interchangeable implementations:
8
+
9
+ * :class:`QuantcryptBackend` — wraps ``quantcrypt.dss.MLDSA_65`` (the
10
+ current production dependency).
11
+ * :class:`OqsBackend` — wraps ``oqs.Signature("ML-DSA-65")`` from the
12
+ Open Quantum Safe ``liboqs`` Python bindings.
13
+
14
+ Both vendors implement the same NIST FIPS 204 ML-DSA-65 algorithm, so a
15
+ keypair generated by one MUST verify under the other and vice versa.
16
+ This dual-vendor cross-check is asserted in
17
+ ``tests/test_pq_dual_vendor.py``.
18
+
19
+ This module deliberately does **not** alter any production code path;
20
+ it only adds the Protocol + two adapters. The cutover is gated to a
21
+ later milestone once the cross-vendor cross-check has been running in
22
+ CI for at least one release cycle.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Protocol, runtime_checkable
28
+
29
+
30
+ @runtime_checkable
31
+ class MlDsaBackend(Protocol):
32
+ """ML-DSA-65 signing backend.
33
+
34
+ All implementations MUST be byte-for-byte interoperable on
35
+ ``verify`` — a signature emitted by one backend must verify under
36
+ any other backend with the matching public key.
37
+ """
38
+
39
+ name: str
40
+
41
+ def keygen(self, seed: bytes | None = None) -> tuple[bytes, bytes]:
42
+ """Generate a keypair. Returns ``(public_key, secret_key)``.
43
+
44
+ ``seed`` is accepted for API symmetry; backends MAY ignore it
45
+ if they do not expose deterministic key generation. Callers
46
+ SHOULD NOT rely on ``seed`` for determinism across backends.
47
+ """
48
+ ...
49
+
50
+ def sign(self, sk: bytes, msg: bytes) -> bytes:
51
+ """Sign ``msg`` under secret key ``sk``. Returns the raw signature."""
52
+ ...
53
+
54
+ def verify(self, pk: bytes, msg: bytes, sig: bytes) -> bool:
55
+ """Verify ``sig`` over ``msg`` under public key ``pk``.
56
+
57
+ Returns ``True`` on success, ``False`` on any verification
58
+ failure (bad signature, malformed input, length mismatch).
59
+ Implementations MUST NOT raise on cryptographic failure.
60
+ """
61
+ ...
62
+
63
+
64
+ class QuantcryptBackend:
65
+ """ML-DSA-65 backend backed by ``quantcrypt`` (FIPS 204 reference).
66
+
67
+ This is the production backend used by
68
+ :mod:`hypermind.crypto.signing` today.
69
+ """
70
+
71
+ name = "quantcrypt"
72
+
73
+ def __init__(self) -> None:
74
+ # Lazy import — quantcrypt is a runtime dep but importing it
75
+ # lazily keeps this module cheap to import for type-only users.
76
+ from quantcrypt.dss import MLDSA_65
77
+
78
+ self._impl = MLDSA_65()
79
+
80
+ def keygen(self, seed: bytes | None = None) -> tuple[bytes, bytes]:
81
+ # quantcrypt's MLDSA_65.keygen() does not expose seed input.
82
+ # The `seed` parameter is accepted for Protocol conformance.
83
+ del seed
84
+ pk, sk = self._impl.keygen()
85
+ return bytes(pk), bytes(sk)
86
+
87
+ def sign(self, sk: bytes, msg: bytes) -> bytes:
88
+ return bytes(self._impl.sign(sk, msg))
89
+
90
+ def verify(self, pk: bytes, msg: bytes, sig: bytes) -> bool:
91
+ from quantcrypt.errors import DSSVerifyFailedError
92
+
93
+ try:
94
+ return bool(self._impl.verify(pk, msg, sig))
95
+ except DSSVerifyFailedError:
96
+ return False
97
+ except Exception:
98
+ return False
99
+
100
+
101
+ class OqsBackend:
102
+ """ML-DSA-65 backend backed by ``liboqs`` (Open Quantum Safe).
103
+
104
+ Lazy-loaded: importing this class does not require ``oqs`` to be
105
+ installed, but instantiating it does. Install via the
106
+ ``crypto-pq-test`` optional extra:
107
+
108
+ .. code-block:: bash
109
+
110
+ pip install -e ".[crypto-pq-test]"
111
+
112
+ Note: ``liboqs`` is intentionally test-only — it is not on the
113
+ runtime install path. The dual-vendor invariant is asserted in
114
+ CI; production deployments only require ``quantcrypt``.
115
+ """
116
+
117
+ name = "liboqs"
118
+
119
+ def __init__(self) -> None:
120
+ try:
121
+ import oqs # type: ignore[import-not-found]
122
+ except ImportError as exc: # pragma: no cover — exercised in skip path
123
+ raise ImportError(
124
+ "OqsBackend requires the 'oqs' Python package (liboqs bindings). "
125
+ "Install it via the crypto-pq-test extra: "
126
+ 'pip install -e ".[crypto-pq-test]"'
127
+ ) from exc
128
+ # We hold the module reference, not a long-lived Signature
129
+ # object: oqs.Signature is stateful (holds a secret key after
130
+ # generate_keypair), so we instantiate fresh per call.
131
+ self._oqs = oqs
132
+
133
+ def keygen(self, seed: bytes | None = None) -> tuple[bytes, bytes]:
134
+ del seed # liboqs does not expose seeded keygen
135
+ with self._oqs.Signature("ML-DSA-65") as signer:
136
+ pk = bytes(signer.generate_keypair())
137
+ sk = bytes(signer.export_secret_key())
138
+ return pk, sk
139
+
140
+ def sign(self, sk: bytes, msg: bytes) -> bytes:
141
+ with self._oqs.Signature("ML-DSA-65", sk) as signer:
142
+ return bytes(signer.sign(msg))
143
+
144
+ def verify(self, pk: bytes, msg: bytes, sig: bytes) -> bool:
145
+ try:
146
+ with self._oqs.Signature("ML-DSA-65") as verifier:
147
+ return bool(verifier.verify(msg, sig, pk))
148
+ except Exception:
149
+ return False
150
+
151
+
152
+ __all__ = ["MlDsaBackend", "OqsBackend", "QuantcryptBackend"]
@@ -0,0 +1,97 @@
1
+ """Sender-derived Room epoch key chain.
2
+
3
+ Lightweight one-way ratchet used by Rooms in v0.9.x to bind ciphertexts to a
4
+ specific epoch boundary. Each epoch's key is derived from the previous via
5
+ HKDF-Expand, so a key from epoch N cannot decrypt traffic encrypted under the
6
+ AEAD key derived from epoch N+1's chain key (forward secrecy at the boundary).
7
+
8
+ # MLS-SWAP-POINT: in v1.0 this module is replaced by an MLS TreeKEM group state.
9
+ # Callers depend only on `RoomEpochKey.derive_aead_key()` returning a 32-byte
10
+ # AEAD key bound to the current epoch — keep that surface stable across the swap.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import struct
16
+ from dataclasses import dataclass, replace
17
+
18
+ from cryptography.hazmat.primitives import hashes, hmac
19
+
20
+ _HASH_LEN = 32 # SHA-256
21
+ _KEY_LEN = 32 # AEAD key length (AES-256 / ChaCha20)
22
+
23
+ _LABEL_ADVANCE = b"hm/room-epoch/advance"
24
+ _LABEL_AEAD = b"hm/room-epoch/aead-key"
25
+
26
+
27
+ def _hmac_sha256(key: bytes, data: bytes) -> bytes:
28
+ h = hmac.HMAC(key, hashes.SHA256())
29
+ h.update(data)
30
+ return h.finalize()
31
+
32
+
33
+ def _hkdf_extract(salt: bytes, ikm: bytes) -> bytes:
34
+ if not salt:
35
+ salt = b"\x00" * _HASH_LEN
36
+ return _hmac_sha256(salt, ikm)
37
+
38
+
39
+ def _hkdf_expand(prk: bytes, info: bytes, length: int) -> bytes:
40
+ n = (length + _HASH_LEN - 1) // _HASH_LEN
41
+ if n > 255:
42
+ raise ValueError("HKDF-Expand: requested length too large")
43
+ out = b""
44
+ t = b""
45
+ for i in range(1, n + 1):
46
+ t = _hmac_sha256(prk, t + info + bytes([i]))
47
+ out += t
48
+ return out[:length]
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class RoomEpochKey:
53
+ """A Room's chain key at a specific epoch.
54
+
55
+ Attributes:
56
+ room_id: Stable identifier of the Room (any byte string).
57
+ epoch: Monotonically increasing counter, starts at 0.
58
+ key: 32-byte chain key for this epoch. Use `derive_aead_key()` for
59
+ encryption — never use this field directly as an AEAD key.
60
+ """
61
+
62
+ room_id: bytes
63
+ epoch: int
64
+ key: bytes
65
+
66
+ @classmethod
67
+ def from_root(cls, room_root_secret: bytes, room_id: bytes) -> RoomEpochKey:
68
+ """Initialise epoch 0 from a Room root secret."""
69
+ if not isinstance(room_root_secret, bytes) or len(room_root_secret) == 0:
70
+ raise ValueError("room_root_secret must be non-empty bytes")
71
+ if not isinstance(room_id, bytes) or len(room_id) == 0:
72
+ raise ValueError("room_id must be non-empty bytes")
73
+ prk = _hkdf_extract(room_id, room_root_secret)
74
+ info = _LABEL_ADVANCE + room_id + struct.pack(">Q", 0)
75
+ key0 = _hkdf_expand(prk, info, _HASH_LEN)
76
+ return cls(room_id=room_id, epoch=0, key=key0)
77
+
78
+ def advance(self) -> RoomEpochKey:
79
+ """Return the next epoch's chain key.
80
+
81
+ The previous chain key is the HKDF PRK; the next-epoch info string binds
82
+ `(room_id, next_epoch)` so distinct rooms never collide and the chain is
83
+ deterministic given a fixed root.
84
+ """
85
+ next_epoch = self.epoch + 1
86
+ info = _LABEL_ADVANCE + self.room_id + struct.pack(">Q", next_epoch)
87
+ next_key = _hkdf_expand(self.key, info, _HASH_LEN)
88
+ return replace(self, epoch=next_epoch, key=next_key)
89
+
90
+ def derive_aead_key(self) -> bytes:
91
+ """Return a 32-byte AEAD key bound to the current epoch.
92
+
93
+ Distinct from the chain key so a leaked AEAD key does not let an attacker
94
+ ratchet forward.
95
+ """
96
+ info = _LABEL_AEAD + self.room_id + struct.pack(">Q", self.epoch)
97
+ return _hkdf_expand(self.key, info, _KEY_LEN)
@@ -0,0 +1,245 @@
1
+ """Signing primitives — Ed25519 (v0.1) + ML-DSA-65 hybrid (v0.2).
2
+
3
+ Composite signing follows draft-ietf-lamps-pq-composite-sigs-16
4
+ (`id-MLDSA65-Ed25519-SHA512`): the composite signature is the
5
+ concatenation `Ed25519_sig || ML-DSA-65_sig`, both signed over the
6
+ same canonical preimage. Verification requires BOTH signatures to
7
+ verify — defeats single-key compromise.
8
+
9
+ A-ALG-BOUND invariant: every envelope discriminator is in the signed
10
+ preimage. See `wire.canonical_sig_input`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import secrets
16
+ from dataclasses import dataclass
17
+
18
+ from nacl.exceptions import BadSignatureError
19
+ from nacl.signing import SigningKey, VerifyKey
20
+
21
+ # Real ML-DSA-65 from quantcrypt (NIST FIPS 204 reference impl).
22
+ from quantcrypt.dss import MLDSA_65
23
+ from quantcrypt.errors import DSSVerifyFailedError
24
+
25
+ # COSE algorithm identifiers (RFC 9053; LAMPS draft for composite)
26
+ ALG_ED25519 = -8
27
+ ALG_MLDSA65_ED25519 = -49 # composite — id-MLDSA65-Ed25519-SHA512
28
+
29
+ ED25519_SIG_LEN = 64
30
+ ED25519_PK_LEN = 32
31
+
32
+ # ML-DSA-65 lengths from FIPS 204
33
+ MLDSA65_SIG_LEN = 3309
34
+ MLDSA65_PK_LEN = 1952
35
+
36
+ _MLDSA = MLDSA_65()
37
+
38
+ # --------------------------------------------------------------------------- #
39
+ # LAMPS draft-ietf-lamps-pq-composite-sigs-16 §6 domain-separated preimage.
40
+ #
41
+ # Per draft §6, the message signed by each component algorithm is:
42
+ # M' = Domain || OID || len(ctx) || ctx || M
43
+ #
44
+ # where:
45
+ # * Domain = the ASCII string "CompositeAlgorithmSignatures2025"
46
+ # * OID = DER encoding of the composite algorithm OID
47
+ # (id-MLDSA65-Ed25519-SHA512 = 2.16.840.1.101.3.4.3.21)
48
+ # * ctx = caller-supplied context bytes (default empty)
49
+ # * M = the original application message
50
+ #
51
+ # This prefix prevents (a) signature stripping — taking only the Ed25519
52
+ # half and re-using it as a non-composite Ed25519 signature over M, and
53
+ # (b) cross-protocol substitution against any signer that signs the raw
54
+ # message with no domain separation.
55
+ #
56
+ # OID encoding: this is the canonical DER encoding of
57
+ # 2.16.840.1.101.3.4.3.21 (id-MLDSA65-Ed25519-SHA512), the assignment
58
+ # tracked by the LAMPS Composite ML-DSA draft. The previous constant
59
+ # (06092a864886f70d010721) was a documented placeholder pointing at an
60
+ # unrelated RSA arc; while internal verify worked (signer and verifier
61
+ # shared the wrong bytes), any LAMPS-conformant external verifier would
62
+ # have rejected our signatures and we would have rejected theirs.
63
+ # Closes self-heal Cycle-3 Finding #21.
64
+ #
65
+ # Wire compatibility: composite signatures created under the placeholder
66
+ # OID will NOT verify under the canonical OID. Operators rotating across
67
+ # this upgrade must accept both during cutover; see
68
+ # docs/migrate/v0.x-to-v1.0.md for the deprecation schedule.
69
+ # --------------------------------------------------------------------------- #
70
+ _LAMPS_DOMAIN: bytes = b"CompositeAlgorithmSignatures2025"
71
+ # Canonical DER for OID 2.16.840.1.101.3.4.3.21:
72
+ # 06 09 60 86 48 01 65 03 04 03 15
73
+ # tag 0x06 (OBJECT IDENTIFIER), length 0x09 (9 content bytes), then OID body.
74
+ _OID_MLDSA65_ED25519: bytes = bytes.fromhex("0609608648016503040315")
75
+
76
+
77
+ def _lamps_preimage(message: bytes, ctx: bytes = b"") -> bytes:
78
+ """Build the LAMPS §6 domain-separated preimage.
79
+
80
+ `ctx` is the optional context string (LAMPS §6); we cap it at 255
81
+ bytes per the draft's single-byte length field.
82
+ """
83
+ if len(ctx) > 255:
84
+ raise ValueError("LAMPS context must be ≤ 255 bytes")
85
+ return _LAMPS_DOMAIN + _OID_MLDSA65_ED25519 + len(ctx).to_bytes(1, "big") + ctx + message
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class Keypair:
90
+ """Signing keypair.
91
+
92
+ For Ed25519 (alg=-8): `secret_bytes` and `public_bytes` are the
93
+ raw 32-byte / 32-byte Ed25519 material. `kid` = public_bytes.
94
+
95
+ For composite (alg=-49): `secret_bytes` is `ed_sk(32) || mldsa_sk`,
96
+ `public_bytes` is `ed_pk(32) || mldsa_pk`. `kid` is the Ed25519
97
+ public-key half (first 32 bytes of `public_bytes`) — this preserves
98
+ the v0.1 → v0.2 address-stability contract so an agent's identity
99
+ survives algorithm rotation from Ed25519 to composite.
100
+ """
101
+
102
+ secret_bytes: bytes
103
+ public_bytes: bytes
104
+ alg: int = ALG_ED25519
105
+
106
+ @classmethod
107
+ def generate(cls, *, alg: int = ALG_ED25519) -> Keypair:
108
+ if alg == ALG_ED25519:
109
+ sk = SigningKey.generate()
110
+ return cls(
111
+ secret_bytes=bytes(sk),
112
+ public_bytes=bytes(sk.verify_key),
113
+ alg=alg,
114
+ )
115
+ if alg == ALG_MLDSA65_ED25519:
116
+ ed_sk = SigningKey.generate()
117
+ mldsa_pk, mldsa_sk = _MLDSA.keygen()
118
+ return cls(
119
+ secret_bytes=bytes(ed_sk) + mldsa_sk,
120
+ public_bytes=bytes(ed_sk.verify_key) + mldsa_pk,
121
+ alg=alg,
122
+ )
123
+ raise NotImplementedError(f"unsupported alg {alg}")
124
+
125
+ @classmethod
126
+ def from_seed(cls, seed: bytes) -> Keypair:
127
+ """Deterministic Ed25519 keypair from a 32-byte seed."""
128
+ if len(seed) != 32:
129
+ raise ValueError("seed must be 32 bytes")
130
+ sk = SigningKey(seed)
131
+ return cls(
132
+ secret_bytes=bytes(sk),
133
+ public_bytes=bytes(sk.verify_key),
134
+ alg=ALG_ED25519,
135
+ )
136
+
137
+ def sign(self, message: bytes) -> bytes:
138
+ """Sign — returns raw signature (no envelope).
139
+
140
+ Ed25519: 64-byte raw signature.
141
+ Composite: 64-byte Ed25519 sig || 3309-byte ML-DSA-65 sig.
142
+ """
143
+ if self.alg == ALG_ED25519:
144
+ sk = SigningKey(self.secret_bytes)
145
+ return sk.sign(message).signature
146
+ if self.alg == ALG_MLDSA65_ED25519:
147
+ ed_sk = SigningKey(self.secret_bytes[:32])
148
+ mldsa_sk = self.secret_bytes[32:]
149
+ # LAMPS draft-16 §6: BOTH component signatures are computed
150
+ # over the same domain-separated preimage. This blocks the
151
+ # "strip-and-reuse the Ed25519 half" downgrade attack.
152
+ preimage = _lamps_preimage(message)
153
+ ed_sig = ed_sk.sign(preimage).signature
154
+ mldsa_sig = _MLDSA.sign(mldsa_sk, preimage)
155
+ return ed_sig + mldsa_sig
156
+ raise NotImplementedError(f"sign: unsupported alg {self.alg}")
157
+
158
+ @property
159
+ def ed_public_bytes(self) -> bytes:
160
+ """Ed25519 public-key half (always 32 bytes)."""
161
+ return self.public_bytes[:ED25519_PK_LEN]
162
+
163
+ @property
164
+ def mldsa_public_bytes(self) -> bytes:
165
+ """ML-DSA-65 public-key half. Empty for Ed25519-only keys."""
166
+ if self.alg == ALG_MLDSA65_ED25519:
167
+ return self.public_bytes[ED25519_PK_LEN:]
168
+ return b""
169
+
170
+ @property
171
+ def kid(self) -> bytes:
172
+ """Stable agent identity = first 32 bytes of public-key bytes.
173
+
174
+ For Ed25519, this is the public key itself. For composite, this
175
+ is the Ed25519 half — preserves the v0.1 → v0.2 key-identity
176
+ contract so an agent's address survives algorithm rotation.
177
+ """
178
+ return self.public_bytes[:ED25519_PK_LEN]
179
+
180
+ @property
181
+ def did_key(self) -> str:
182
+ return f"did:key:z{self.kid.hex()[:24]}"
183
+
184
+ def __repr__(self) -> str:
185
+ # Never render `secret_bytes` — it would land in logs / tracebacks /
186
+ # `pytest -v` output. Show only the public kid and algorithm.
187
+ return f"Keypair(kid={self.kid.hex()[:16]}..., alg={self.alg}, secret=<redacted>)"
188
+
189
+ def __str__(self) -> str:
190
+ return self.__repr__()
191
+
192
+ def __reduce__(self):
193
+ # Pickle is a known leak vector — a serialized Keypair would carry
194
+ # `secret_bytes` to disk / over the wire. Refuse outright; callers
195
+ # who need cross-process sharing must use the in-memory key store.
196
+ raise TypeError("refusing to pickle secret material; use the in-memory store")
197
+
198
+
199
+ def verify_ed25519(public_key: bytes, signature: bytes, message: bytes) -> bool:
200
+ try:
201
+ VerifyKey(public_key).verify(message, signature)
202
+ return True
203
+ except BadSignatureError:
204
+ return False
205
+ except Exception:
206
+ return False
207
+
208
+
209
+ def verify_composite(composite_pk: bytes, composite_sig: bytes, message: bytes) -> bool:
210
+ """Hybrid verify: BOTH Ed25519 and ML-DSA-65 must validate.
211
+
212
+ composite_pk = Ed25519_pk(32) || ML-DSA-65_pk(1952)
213
+ composite_sig = Ed25519_sig(64) || ML-DSA-65_sig(3309)
214
+ """
215
+ if len(composite_pk) != ED25519_PK_LEN + MLDSA65_PK_LEN:
216
+ return False
217
+ if len(composite_sig) != ED25519_SIG_LEN + MLDSA65_SIG_LEN:
218
+ return False
219
+ ed_pk = composite_pk[:ED25519_PK_LEN]
220
+ mldsa_pk = composite_pk[ED25519_PK_LEN:]
221
+ ed_sig = composite_sig[:ED25519_SIG_LEN]
222
+ mldsa_sig = composite_sig[ED25519_SIG_LEN:]
223
+ # LAMPS draft-16 §6: verify both halves over the prefixed preimage.
224
+ preimage = _lamps_preimage(message)
225
+ if not verify_ed25519(ed_pk, ed_sig, preimage):
226
+ return False
227
+ try:
228
+ return _MLDSA.verify(mldsa_pk, preimage, mldsa_sig)
229
+ except DSSVerifyFailedError:
230
+ return False
231
+ except Exception:
232
+ return False
233
+
234
+
235
+ def verify_signature(alg: int, public_key: bytes, signature: bytes, message: bytes) -> bool:
236
+ """Algorithm-dispatched verify."""
237
+ if alg == ALG_ED25519:
238
+ return verify_ed25519(public_key, signature, message)
239
+ if alg == ALG_MLDSA65_ED25519:
240
+ return verify_composite(public_key, signature, message)
241
+ return False
242
+
243
+
244
+ def random_seed() -> bytes:
245
+ return secrets.token_bytes(32)