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.
- hmctl/__init__.py +7 -0
- hmctl/cli.py +947 -0
- hypermind/__init__.py +208 -0
- hypermind/_deprecations.py +108 -0
- hypermind/agent.py +2896 -0
- hypermind/agent_card.py +127 -0
- hypermind/api/__init__.py +32 -0
- hypermind/api/app.py +686 -0
- hypermind/capability.py +923 -0
- hypermind/consult.py +493 -0
- hypermind/consult_bias.py +112 -0
- hypermind/contract_net.py +219 -0
- hypermind/crypto/__init__.py +21 -0
- hypermind/crypto/cose_encrypt.py +126 -0
- hypermind/crypto/dkg.py +246 -0
- hypermind/crypto/domain.py +59 -0
- hypermind/crypto/frost.py +771 -0
- hypermind/crypto/hashing.py +38 -0
- hypermind/crypto/hlc.py +163 -0
- hypermind/crypto/hpke.py +277 -0
- hypermind/crypto/kms.py +196 -0
- hypermind/crypto/kms_backends/__init__.py +56 -0
- hypermind/crypto/kms_backends/aws.py +128 -0
- hypermind/crypto/kms_backends/azure.py +114 -0
- hypermind/crypto/kms_backends/gcp.py +97 -0
- hypermind/crypto/kms_backends/vault.py +120 -0
- hypermind/crypto/namespace_root.py +204 -0
- hypermind/crypto/pq.py +152 -0
- hypermind/crypto/room_epoch.py +97 -0
- hypermind/crypto/signing.py +245 -0
- hypermind/deliberation.py +355 -0
- hypermind/did_web.py +256 -0
- hypermind/dispute.py +858 -0
- hypermind/errors.py +218 -0
- hypermind/eval/__init__.py +49 -0
- hypermind/eval/calibration.py +307 -0
- hypermind/eval/comparison.py +251 -0
- hypermind/eval/replay.py +202 -0
- hypermind/eval/swarm_iq.py +718 -0
- hypermind/knowledge/__init__.py +17 -0
- hypermind/knowledge/citation_graph.py +141 -0
- hypermind/knowledge/correlated_agreement.py +167 -0
- hypermind/knowledge/embedding_registry.py +115 -0
- hypermind/knowledge/kernel_store.py +197 -0
- hypermind/knowledge/rekor.py +173 -0
- hypermind/knowledge/reputation.py +364 -0
- hypermind/knowledge/swarm_memory.py +523 -0
- hypermind/knowledge/transparency.py +409 -0
- hypermind/mcp/__init__.py +60 -0
- hypermind/mcp/bridge.py +151 -0
- hypermind/mcp/client.py +142 -0
- hypermind/mcp/provenance.py +214 -0
- hypermind/mcp/relata_tools.py +152 -0
- hypermind/mcp/server.py +248 -0
- hypermind/mcp/transport.py +206 -0
- hypermind/mind/__init__.py +150 -0
- hypermind/mind/active.py +234 -0
- hypermind/mind/belief.py +369 -0
- hypermind/mind/deliberator.py +625 -0
- hypermind/mind/diversity.py +144 -0
- hypermind/mind/evolve.py +184 -0
- hypermind/mind/federation.py +154 -0
- hypermind/mind/goals.py +117 -0
- hypermind/mind/integration.py +190 -0
- hypermind/mind/mediator.py +74 -0
- hypermind/mind/memory_store.py +129 -0
- hypermind/mind/policy.py +307 -0
- hypermind/mind/records.py +260 -0
- hypermind/mind/roles.py +190 -0
- hypermind/mind/sybil_guard.py +41 -0
- hypermind/mind/world_model.py +166 -0
- hypermind/namespace.py +515 -0
- hypermind/namespace_policy.py +254 -0
- hypermind/observability/__init__.py +25 -0
- hypermind/observability/asgi.py +214 -0
- hypermind/observability/cost.py +277 -0
- hypermind/observability/otel_export.py +200 -0
- hypermind/observability/recorder.py +344 -0
- hypermind/observability/swarm_trace.py +438 -0
- hypermind/pin_policy.py +116 -0
- hypermind/py.typed +0 -0
- hypermind/responders/__init__.py +87 -0
- hypermind/responders/anthropic.py +159 -0
- hypermind/responders/base.py +162 -0
- hypermind/responders/openai.py +199 -0
- hypermind/responders/router.py +254 -0
- hypermind/responders/structured.py +287 -0
- hypermind/responders/tools.py +444 -0
- hypermind/revocation.py +270 -0
- hypermind/rule_ids.py +135 -0
- hypermind/schemas/encrypted_statement.cddl +28 -0
- hypermind/schemas/namespace_accept.cddl +20 -0
- hypermind/schemas/namespace_create.cddl +25 -0
- hypermind/schemas/namespace_founding_attest.cddl +30 -0
- hypermind/schemas/namespace_invite.cddl +22 -0
- hypermind/schemas/wire-v0.1.cddl +73 -0
- hypermind/simlab/__init__.py +15 -0
- hypermind/simlab/_persona_registry.py +93 -0
- hypermind/simlab/_scenario_impl.py +2778 -0
- hypermind/simlab/app.py +2538 -0
- hypermind/simlab/backends/__init__.py +27 -0
- hypermind/simlab/backends/anchor.py +88 -0
- hypermind/simlab/backends/local.py +32 -0
- hypermind/simlab/backends/server.py +79 -0
- hypermind/simlab/cli_command.py +108 -0
- hypermind/simlab/config.py +106 -0
- hypermind/simlab/deployment.py +26 -0
- hypermind/simlab/knowledge.py +716 -0
- hypermind/simlab/learning.py +295 -0
- hypermind/simlab/modes/__init__.py +115 -0
- hypermind/simlab/modes/_eval_real_llm.py +373 -0
- hypermind/simlab/modes/aar.py +611 -0
- hypermind/simlab/modes/backtest.py +610 -0
- hypermind/simlab/modes/binary_forecast.py +69 -0
- hypermind/simlab/modes/conformance.py +1869 -0
- hypermind/simlab/modes/evaluation.py +2271 -0
- hypermind/simlab/modes/governance.py +648 -0
- hypermind/simlab/modes/redteam.py +534 -0
- hypermind/simlab/modes/tabletop.py +797 -0
- hypermind/simlab/modes/tournament.py +448 -0
- hypermind/simlab/modes/whatif.py +523 -0
- hypermind/simlab/namespace.py +125 -0
- hypermind/simlab/personas.py +477 -0
- hypermind/simlab/prompt_generator.py +172 -0
- hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
- hypermind/simlab/question_sets/governance_q5.json +67 -0
- hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
- hypermind/simlab/question_sets/whatif_q3.json +38 -0
- hypermind/simlab/registry.py +325 -0
- hypermind/simlab/runner.py +239 -0
- hypermind/simlab/scenario.py +147 -0
- hypermind/simlab/swarms.py +180 -0
- hypermind/simlab/tool_handlers/__init__.py +4 -0
- hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
- hypermind/simlab/tool_handlers/brave.py +46 -0
- hypermind/simlab/tool_handlers/newsapi.py +50 -0
- hypermind/simlab/tool_handlers/openweather.py +46 -0
- hypermind/simlab/tool_handlers/pinecone.py +52 -0
- hypermind/simlab/tool_handlers/qdrant.py +57 -0
- hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
- hypermind/simlab/tool_handlers/relata_recall.py +61 -0
- hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
- hypermind/simlab/tool_handlers/serper.py +46 -0
- hypermind/simlab/tool_handlers/tavily.py +53 -0
- hypermind/simlab/tool_handlers/weaviate.py +65 -0
- hypermind/simlab/tool_handlers/wikipedia.py +64 -0
- hypermind/simlab/tool_handlers/wolfram.py +48 -0
- hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
- hypermind/simlab/tool_registry.py +568 -0
- hypermind/simlab/topic_adapter.py +338 -0
- hypermind/simlab/topic_questions.py +259 -0
- hypermind/storage/__init__.py +69 -0
- hypermind/storage/backend.py +71 -0
- hypermind/storage/relata.py +245 -0
- hypermind/storage/sqlite.py +258 -0
- hypermind/sync.py +191 -0
- hypermind/tal.py +175 -0
- hypermind/tasks.py +334 -0
- hypermind/testing.py +16 -0
- hypermind/tools/__init__.py +32 -0
- hypermind/tools/registry.py +61 -0
- hypermind/transport/__init__.py +22 -0
- hypermind/transport/anti_entropy.py +708 -0
- hypermind/transport/bus.py +206 -0
- hypermind/transport/knows_delta.py +204 -0
- hypermind/transport/libp2p.py +298 -0
- hypermind/transport/placement.py +58 -0
- hypermind/transport/tcp.py +797 -0
- hypermind/transport/tls_profile.py +417 -0
- hypermind/types.py +59 -0
- hypermind/uncertainty.py +222 -0
- hypermind/wire.py +897 -0
- hypermind/workflow/__init__.py +72 -0
- hypermind/workflow/engine.py +655 -0
- hypermind/workflow/plan.py +182 -0
- hypermind/workflow/repair.py +203 -0
- hypermind-0.11.0.dist-info/METADATA +524 -0
- hypermind-0.11.0.dist-info/RECORD +181 -0
- hypermind-0.11.0.dist-info/WHEEL +4 -0
- hypermind-0.11.0.dist-info/entry_points.txt +2 -0
- hypermind-0.11.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Hashing primitives — SHA-256 for kids, BLAKE3 for fingerprints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
|
|
7
|
+
import blake3 as _blake3
|
|
8
|
+
|
|
9
|
+
from hypermind.crypto.domain import DomainTag, domain_separated_hash
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def sha256_kid(payload: bytes) -> bytes:
|
|
13
|
+
"""Compute the kid (knowledge id) for a payload.
|
|
14
|
+
|
|
15
|
+
`kid = sha256(0x02 || payload)` — domain tag KID enforces that this
|
|
16
|
+
32-byte digest cannot be confused with any other hash operation in
|
|
17
|
+
the protocol.
|
|
18
|
+
"""
|
|
19
|
+
return domain_separated_hash(DomainTag.KID, payload)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def sha256_with_tag(tag: DomainTag, payload: bytes) -> bytes:
|
|
23
|
+
"""Generic domain-tagged SHA-256."""
|
|
24
|
+
return domain_separated_hash(tag, payload)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def blake3_32(payload: bytes) -> bytes:
|
|
28
|
+
"""32-byte BLAKE3 digest. Used for envelope schema hashes and
|
|
29
|
+
anti-entropy fingerprints (truncation-safe at 64 bits).
|
|
30
|
+
"""
|
|
31
|
+
return _blake3.blake3(payload).digest()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def hmac_sha256(key: bytes, msg: bytes) -> bytes:
|
|
35
|
+
"""HMAC-SHA-256 — used for keyed-pseudonym salts (HMAC custody)."""
|
|
36
|
+
import hmac
|
|
37
|
+
|
|
38
|
+
return hmac.new(key, msg, hashlib.sha256).digest()
|
hypermind/crypto/hlc.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Hybrid Logical Clock (Kulkarni et al. 2014).
|
|
2
|
+
|
|
3
|
+
`HLC = (logical_ms: u64, counter: u32, node_id: bytes[8])`
|
|
4
|
+
|
|
5
|
+
v0.1 normative pin (closes spec/06-conformance.md gap #2):
|
|
6
|
+
- logical_ms = CLOCK_REALTIME snapshot in ms, single read at envelope-validate entry
|
|
7
|
+
- NTP sync ≤500 ms drift required
|
|
8
|
+
- clamp on receive: l_stored = min(l_received, now_ms + hlc_max_skew_ms)
|
|
9
|
+
- reject if l_received > now_ms + 2 × hlc_max_skew_ms (anomaly: clock_drift)
|
|
10
|
+
|
|
11
|
+
Clock-source contract (WS-H finding C5):
|
|
12
|
+
- `ClockSource` Protocol pairs a wall-clock with a monotonic clock.
|
|
13
|
+
- `SystemClock` (the default) detects >100ms divergence between calls and
|
|
14
|
+
emits `ClockSkewWarning`. This is the early-warning signal for NTP step,
|
|
15
|
+
VM pause, or backward leap second — all of which can cause `merge()` to
|
|
16
|
+
reject HLCs as exceeding `2 * HLC_MAX_SKEW_MS`.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import time
|
|
22
|
+
import warnings
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import ClassVar, Protocol, runtime_checkable
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HLCSkewError(Exception):
|
|
28
|
+
"""Received HLC exceeds 2× hlc_max_skew_ms — reject, do not clamp."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ClockSkewWarning(UserWarning):
|
|
32
|
+
"""Emitted when wall-clock and monotonic-clock diverge by more than 100ms between calls."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@runtime_checkable
|
|
36
|
+
class ClockSource(Protocol):
|
|
37
|
+
"""Contract for HLC time sources.
|
|
38
|
+
|
|
39
|
+
Implementations must provide:
|
|
40
|
+
- now_ms(): wall-clock-anchored milliseconds (allowed to jump under NTP step)
|
|
41
|
+
- monotonic_ms(): monotonic milliseconds (must never go backwards)
|
|
42
|
+
|
|
43
|
+
Default impl pairs time.time_ns() with time.monotonic_ns(); detects skew jumps
|
|
44
|
+
>100ms between calls and emits ClockSkewWarning.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def now_ms(self) -> int: ...
|
|
48
|
+
def monotonic_ms(self) -> int: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SystemClock:
|
|
52
|
+
"""Default ClockSource using time.time_ns() + time.monotonic_ns()."""
|
|
53
|
+
|
|
54
|
+
def __init__(self) -> None:
|
|
55
|
+
self._last_wall: int | None = None
|
|
56
|
+
self._last_mono: int | None = None
|
|
57
|
+
|
|
58
|
+
def now_ms(self) -> int:
|
|
59
|
+
wall = time.time_ns() // 1_000_000
|
|
60
|
+
mono = time.monotonic_ns() // 1_000_000
|
|
61
|
+
if self._last_wall is not None and self._last_mono is not None:
|
|
62
|
+
wall_delta = wall - self._last_wall
|
|
63
|
+
mono_delta = mono - self._last_mono
|
|
64
|
+
jump_ms = abs(wall_delta - mono_delta)
|
|
65
|
+
if jump_ms > 100:
|
|
66
|
+
warnings.warn(
|
|
67
|
+
f"ClockSkewWarning: wall-clock jumped {jump_ms}ms relative to monotonic "
|
|
68
|
+
"(NTP step, VM pause, or backward leap second). HLC operations across "
|
|
69
|
+
"this boundary may be rejected by HLC_MAX_SKEW_MS.",
|
|
70
|
+
ClockSkewWarning,
|
|
71
|
+
stacklevel=2,
|
|
72
|
+
)
|
|
73
|
+
self._last_wall = wall
|
|
74
|
+
self._last_mono = mono
|
|
75
|
+
return wall
|
|
76
|
+
|
|
77
|
+
def monotonic_ms(self) -> int:
|
|
78
|
+
return time.monotonic_ns() // 1_000_000
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True, order=True)
|
|
82
|
+
class HLC:
|
|
83
|
+
"""Lexicographically-orderable hybrid logical clock value."""
|
|
84
|
+
|
|
85
|
+
logical_ms: int
|
|
86
|
+
counter: int
|
|
87
|
+
node_id: bytes
|
|
88
|
+
# Non-comparable, non-hashed companion state; `frozen=True` requires object.__setattr__
|
|
89
|
+
# to mutate, which we only do for skew-rejection counting in merge().
|
|
90
|
+
clock: ClockSource = field(default=None, compare=False, repr=False, hash=False) # type: ignore[assignment]
|
|
91
|
+
_skew_rejections: list = field(default_factory=list, compare=False, repr=False, hash=False)
|
|
92
|
+
|
|
93
|
+
HLC_MAX_SKEW_MS: ClassVar[int] = 300_000 # 5 min, normative v0.1
|
|
94
|
+
|
|
95
|
+
def __post_init__(self) -> None:
|
|
96
|
+
if len(self.node_id) != 8:
|
|
97
|
+
raise ValueError("node_id must be 8 bytes")
|
|
98
|
+
if self.logical_ms < 0 or self.counter < 0:
|
|
99
|
+
raise ValueError("HLC components must be non-negative")
|
|
100
|
+
if self.clock is None:
|
|
101
|
+
object.__setattr__(self, "clock", SystemClock())
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def now(cls, node_id: bytes, clock: ClockSource | None = None) -> HLC:
|
|
105
|
+
"""Snapshot current wall-clock as logical_ms."""
|
|
106
|
+
c = clock if clock is not None else SystemClock()
|
|
107
|
+
return cls(
|
|
108
|
+
logical_ms=c.now_ms(),
|
|
109
|
+
counter=0,
|
|
110
|
+
node_id=node_id[:8].ljust(8, b"\x00"),
|
|
111
|
+
clock=c,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def tick(self) -> HLC:
|
|
115
|
+
"""Advance for a local event."""
|
|
116
|
+
now_ms = self.clock.now_ms()
|
|
117
|
+
if now_ms > self.logical_ms:
|
|
118
|
+
return HLC(now_ms, 0, self.node_id, clock=self.clock)
|
|
119
|
+
return HLC(self.logical_ms, self.counter + 1, self.node_id, clock=self.clock)
|
|
120
|
+
|
|
121
|
+
def merge(self, received: HLC) -> HLC:
|
|
122
|
+
"""Merge a received HLC, applying clamp + skew reject."""
|
|
123
|
+
now_ms = self.clock.now_ms()
|
|
124
|
+
if received.logical_ms > now_ms + 2 * self.HLC_MAX_SKEW_MS:
|
|
125
|
+
# Increment skew-rejection counter (using list as a mutable bucket on a frozen dc).
|
|
126
|
+
self._skew_rejections.append(1)
|
|
127
|
+
raise HLCSkewError(
|
|
128
|
+
f"received HLC {received.logical_ms} exceeds 2× max skew "
|
|
129
|
+
f"({2 * self.HLC_MAX_SKEW_MS}ms) above local now {now_ms}"
|
|
130
|
+
)
|
|
131
|
+
clamped_l = min(received.logical_ms, now_ms + self.HLC_MAX_SKEW_MS)
|
|
132
|
+
new_l = max(self.logical_ms, clamped_l, now_ms)
|
|
133
|
+
if new_l == self.logical_ms == clamped_l:
|
|
134
|
+
new_c = max(self.counter, received.counter) + 1
|
|
135
|
+
elif new_l == self.logical_ms:
|
|
136
|
+
new_c = self.counter + 1
|
|
137
|
+
elif new_l == clamped_l:
|
|
138
|
+
new_c = received.counter + 1
|
|
139
|
+
else:
|
|
140
|
+
new_c = 0
|
|
141
|
+
# Carry the skew-rejection bucket forward so metrics persist across merges.
|
|
142
|
+
merged = HLC(new_l, new_c, self.node_id, clock=self.clock)
|
|
143
|
+
merged._skew_rejections.extend(self._skew_rejections)
|
|
144
|
+
return merged
|
|
145
|
+
|
|
146
|
+
def get_skew_metrics(self) -> dict[str, int]:
|
|
147
|
+
"""Return clock-skew telemetry for this HLC chain."""
|
|
148
|
+
return {
|
|
149
|
+
"skew_rejections_total": len(self._skew_rejections),
|
|
150
|
+
"current_logical_ms": self.logical_ms,
|
|
151
|
+
"current_counter": self.counter,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
def to_cbor(self) -> list:
|
|
155
|
+
"""CBOR triple per protected header label -65603."""
|
|
156
|
+
return [self.logical_ms, self.counter, self.node_id]
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def from_cbor(cls, triple: list) -> HLC:
|
|
160
|
+
return cls(int(triple[0]), int(triple[1]), bytes(triple[2]))
|
|
161
|
+
|
|
162
|
+
def __repr__(self) -> str:
|
|
163
|
+
return f"HLC({self.logical_ms}.{self.counter}@{self.node_id.hex()[:8]})"
|
hypermind/crypto/hpke.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""RFC 9180 HPKE (base mode) — DHKEM(X25519, HKDF-SHA256) + AES-256-GCM/ChaCha20-Poly1305.
|
|
2
|
+
|
|
3
|
+
Implements only HPKE base mode (no PSK, no auth), single-shot seal/open.
|
|
4
|
+
Used by L3 envelope encryption (`cose_encrypt.py`) and Room epoch keying.
|
|
5
|
+
|
|
6
|
+
Domain-separation note: a dedicated `DomainTag.HPKE_INFO` byte was considered
|
|
7
|
+
for `crypto/domain.py`, but the existing closed-set tag space (0x01..0x18) is
|
|
8
|
+
guarded by `reject_unallocated`. Adding 0x40 would create a hole and break the
|
|
9
|
+
contiguous-allocation invariant tested in `tests/test_crypto.py`. Caller-supplied
|
|
10
|
+
HPKE `info` strings already provide cross-protocol separation per RFC 9180 §9.6.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import struct
|
|
17
|
+
|
|
18
|
+
from cryptography.hazmat.primitives import hashes, hmac
|
|
19
|
+
from cryptography.hazmat.primitives.asymmetric import x25519
|
|
20
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305
|
|
21
|
+
from cryptography.hazmat.primitives.serialization import (
|
|
22
|
+
Encoding,
|
|
23
|
+
NoEncryption,
|
|
24
|
+
PrivateFormat,
|
|
25
|
+
PublicFormat,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# --------------------------------------------------------------------------- #
|
|
29
|
+
# Suite registry — RFC 9180 §7.
|
|
30
|
+
# --------------------------------------------------------------------------- #
|
|
31
|
+
SUITE_DHKEM_X25519_HKDF_SHA256_AES256GCM = 0x0001
|
|
32
|
+
SUITE_DHKEM_X25519_HKDF_SHA256_CHACHA20_POLY1305 = 0x0002
|
|
33
|
+
|
|
34
|
+
# RFC 9180 §7.1 KEM ID for DHKEM(X25519, HKDF-SHA256)
|
|
35
|
+
_KEM_ID_X25519 = 0x0020
|
|
36
|
+
# RFC 9180 §7.2 KDF ID for HKDF-SHA256
|
|
37
|
+
_KDF_ID_HKDF_SHA256 = 0x0001
|
|
38
|
+
# RFC 9180 §7.3 AEAD IDs
|
|
39
|
+
_AEAD_ID_AES256GCM = 0x0002
|
|
40
|
+
_AEAD_ID_CHACHA20POLY1305 = 0x0003
|
|
41
|
+
|
|
42
|
+
_MODE_BASE = 0x00
|
|
43
|
+
|
|
44
|
+
_NH = 32 # HKDF-SHA256 output length
|
|
45
|
+
_NSECRET = 32 # DHKEM(X25519) shared-secret length
|
|
46
|
+
_NPK = 32 # X25519 public-key length
|
|
47
|
+
_NSK = 32 # X25519 private-key length
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _suite_aead(suite_id: int) -> tuple[int, int]:
|
|
51
|
+
"""Return (Nk, Nn) for the suite's AEAD."""
|
|
52
|
+
if suite_id == SUITE_DHKEM_X25519_HKDF_SHA256_AES256GCM:
|
|
53
|
+
return 32, 12
|
|
54
|
+
if suite_id == SUITE_DHKEM_X25519_HKDF_SHA256_CHACHA20_POLY1305:
|
|
55
|
+
return 32, 12
|
|
56
|
+
raise ValueError(f"unsupported HPKE suite id: 0x{suite_id:04x}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _suite_aead_id(suite_id: int) -> int:
|
|
60
|
+
if suite_id == SUITE_DHKEM_X25519_HKDF_SHA256_AES256GCM:
|
|
61
|
+
return _AEAD_ID_AES256GCM
|
|
62
|
+
if suite_id == SUITE_DHKEM_X25519_HKDF_SHA256_CHACHA20_POLY1305:
|
|
63
|
+
return _AEAD_ID_CHACHA20POLY1305
|
|
64
|
+
raise ValueError(f"unsupported HPKE suite id: 0x{suite_id:04x}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _new_aead(suite_id: int, key: bytes) -> AESGCM | ChaCha20Poly1305:
|
|
68
|
+
aead_id = _suite_aead_id(suite_id)
|
|
69
|
+
if aead_id == _AEAD_ID_AES256GCM:
|
|
70
|
+
return AESGCM(key)
|
|
71
|
+
return ChaCha20Poly1305(key)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# --------------------------------------------------------------------------- #
|
|
75
|
+
# HKDF labelled extract/expand — RFC 9180 §4.1.
|
|
76
|
+
# --------------------------------------------------------------------------- #
|
|
77
|
+
_HPKE_VERSION = b"HPKE-v1"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _hmac_sha256(key: bytes, data: bytes) -> bytes:
|
|
81
|
+
h = hmac.HMAC(key, hashes.SHA256())
|
|
82
|
+
h.update(data)
|
|
83
|
+
return h.finalize()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _hkdf_extract(salt: bytes, ikm: bytes) -> bytes:
|
|
87
|
+
if not salt:
|
|
88
|
+
salt = b"\x00" * _NH
|
|
89
|
+
return _hmac_sha256(salt, ikm)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _hkdf_expand(prk: bytes, info: bytes, length: int) -> bytes:
|
|
93
|
+
n = (length + _NH - 1) // _NH
|
|
94
|
+
if n > 255:
|
|
95
|
+
raise ValueError("HKDF-Expand: requested length too large")
|
|
96
|
+
out = b""
|
|
97
|
+
t = b""
|
|
98
|
+
for i in range(1, n + 1):
|
|
99
|
+
t = _hmac_sha256(prk, t + info + bytes([i]))
|
|
100
|
+
out += t
|
|
101
|
+
return out[:length]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _suite_id_kem() -> bytes:
|
|
105
|
+
# "KEM" || I2OSP(kem_id, 2)
|
|
106
|
+
return b"KEM" + struct.pack(">H", _KEM_ID_X25519)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _suite_id_hpke(suite_id: int) -> bytes:
|
|
110
|
+
# "HPKE" || I2OSP(kem_id,2) || I2OSP(kdf_id,2) || I2OSP(aead_id,2)
|
|
111
|
+
return (
|
|
112
|
+
b"HPKE"
|
|
113
|
+
+ struct.pack(">H", _KEM_ID_X25519)
|
|
114
|
+
+ struct.pack(">H", _KDF_ID_HKDF_SHA256)
|
|
115
|
+
+ struct.pack(">H", _suite_aead_id(suite_id))
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _labeled_extract(salt: bytes, suite_id_bytes: bytes, label: bytes, ikm: bytes) -> bytes:
|
|
120
|
+
labeled_ikm = _HPKE_VERSION + suite_id_bytes + label + ikm
|
|
121
|
+
return _hkdf_extract(salt, labeled_ikm)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _labeled_expand(
|
|
125
|
+
prk: bytes, suite_id_bytes: bytes, label: bytes, info: bytes, length: int
|
|
126
|
+
) -> bytes:
|
|
127
|
+
labeled_info = struct.pack(">H", length) + _HPKE_VERSION + suite_id_bytes + label + info
|
|
128
|
+
return _hkdf_expand(prk, labeled_info, length)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# --------------------------------------------------------------------------- #
|
|
132
|
+
# DHKEM(X25519, HKDF-SHA256) — RFC 9180 §4.1.
|
|
133
|
+
# --------------------------------------------------------------------------- #
|
|
134
|
+
def _serialize_pk(pk: x25519.X25519PublicKey) -> bytes:
|
|
135
|
+
return pk.public_bytes(Encoding.Raw, PublicFormat.Raw)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _deserialize_pk(raw: bytes) -> x25519.X25519PublicKey:
|
|
139
|
+
return x25519.X25519PublicKey.from_public_bytes(raw)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _serialize_sk(sk: x25519.X25519PrivateKey) -> bytes:
|
|
143
|
+
return sk.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _deserialize_sk(raw: bytes) -> x25519.X25519PrivateKey:
|
|
147
|
+
return x25519.X25519PrivateKey.from_private_bytes(raw)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _extract_and_expand(dh: bytes, kem_context: bytes) -> bytes:
|
|
151
|
+
suite_id = _suite_id_kem()
|
|
152
|
+
eae_prk = _labeled_extract(b"", suite_id, b"eae_prk", dh)
|
|
153
|
+
return _labeled_expand(eae_prk, suite_id, b"shared_secret", kem_context, _NSECRET)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _kem_encap(
|
|
157
|
+
recipient_pub: bytes, eph_priv: x25519.X25519PrivateKey | None = None
|
|
158
|
+
) -> tuple[bytes, bytes]:
|
|
159
|
+
"""DHKEM Encap. Returns (shared_secret, enc)."""
|
|
160
|
+
pk_r = _deserialize_pk(recipient_pub)
|
|
161
|
+
if eph_priv is None:
|
|
162
|
+
eph_priv = x25519.X25519PrivateKey.generate()
|
|
163
|
+
enc = _serialize_pk(eph_priv.public_key())
|
|
164
|
+
dh = eph_priv.exchange(pk_r)
|
|
165
|
+
kem_context = enc + recipient_pub
|
|
166
|
+
shared_secret = _extract_and_expand(dh, kem_context)
|
|
167
|
+
return shared_secret, enc
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _kem_decap(recipient_priv: bytes, enc: bytes) -> bytes:
|
|
171
|
+
sk_r = _deserialize_sk(recipient_priv)
|
|
172
|
+
pk_e = _deserialize_pk(enc)
|
|
173
|
+
dh = sk_r.exchange(pk_e)
|
|
174
|
+
pk_r_bytes = _serialize_pk(sk_r.public_key())
|
|
175
|
+
kem_context = enc + pk_r_bytes
|
|
176
|
+
return _extract_and_expand(dh, kem_context)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# --------------------------------------------------------------------------- #
|
|
180
|
+
# Key schedule (base mode) — RFC 9180 §5.1.
|
|
181
|
+
# --------------------------------------------------------------------------- #
|
|
182
|
+
def _key_schedule_base(suite_id: int, shared_secret: bytes, info: bytes) -> tuple[bytes, bytes]:
|
|
183
|
+
"""Return (key, base_nonce) for base-mode single-shot AEAD."""
|
|
184
|
+
suite_bytes = _suite_id_hpke(suite_id)
|
|
185
|
+
psk_id_hash = _labeled_extract(b"", suite_bytes, b"psk_id_hash", b"")
|
|
186
|
+
info_hash = _labeled_extract(b"", suite_bytes, b"info_hash", info)
|
|
187
|
+
key_schedule_context = bytes([_MODE_BASE]) + psk_id_hash + info_hash
|
|
188
|
+
|
|
189
|
+
secret = _labeled_extract(shared_secret, suite_bytes, b"secret", b"")
|
|
190
|
+
nk, nn = _suite_aead(suite_id)
|
|
191
|
+
key = _labeled_expand(secret, suite_bytes, b"key", key_schedule_context, nk)
|
|
192
|
+
base_nonce = _labeled_expand(secret, suite_bytes, b"base_nonce", key_schedule_context, nn)
|
|
193
|
+
return key, base_nonce
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# --------------------------------------------------------------------------- #
|
|
197
|
+
# Public API.
|
|
198
|
+
# --------------------------------------------------------------------------- #
|
|
199
|
+
def seal(
|
|
200
|
+
suite_id: int,
|
|
201
|
+
recipient_pub: bytes,
|
|
202
|
+
info: bytes,
|
|
203
|
+
aad: bytes,
|
|
204
|
+
plaintext: bytes,
|
|
205
|
+
*,
|
|
206
|
+
_test_eph_priv: bytes | None = None,
|
|
207
|
+
) -> tuple[bytes, bytes]:
|
|
208
|
+
"""HPKE base-mode single-shot seal.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
suite_id: One of the SUITE_* constants in this module.
|
|
212
|
+
recipient_pub: Recipient X25519 public key (32 bytes).
|
|
213
|
+
info: Application-bound contextual string per RFC 9180 §5.1.
|
|
214
|
+
aad: Additional authenticated data (bound by AEAD).
|
|
215
|
+
plaintext: Bytes to encrypt.
|
|
216
|
+
_test_eph_priv: Test-only override for the ephemeral private key. Production
|
|
217
|
+
callers MUST omit this; a fresh random ephemeral is generated per call.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Tuple `(enc, ciphertext)`. `enc` is the encapsulated key (32 bytes for
|
|
221
|
+
X25519). `ciphertext` includes the 16-byte AEAD tag.
|
|
222
|
+
"""
|
|
223
|
+
if len(recipient_pub) != _NPK:
|
|
224
|
+
raise ValueError(f"recipient_pub must be {_NPK} bytes")
|
|
225
|
+
eph = _deserialize_sk(_test_eph_priv) if _test_eph_priv is not None else None
|
|
226
|
+
shared_secret, enc = _kem_encap(recipient_pub, eph_priv=eph)
|
|
227
|
+
key, base_nonce = _key_schedule_base(suite_id, shared_secret, info)
|
|
228
|
+
ct = _new_aead(suite_id, key).encrypt(base_nonce, plaintext, aad)
|
|
229
|
+
return enc, ct
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def open(
|
|
233
|
+
suite_id: int,
|
|
234
|
+
recipient_priv: bytes,
|
|
235
|
+
enc: bytes,
|
|
236
|
+
info: bytes,
|
|
237
|
+
aad: bytes,
|
|
238
|
+
ciphertext: bytes,
|
|
239
|
+
) -> bytes:
|
|
240
|
+
"""HPKE base-mode single-shot open.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
suite_id: One of the SUITE_* constants in this module.
|
|
244
|
+
recipient_priv: Recipient X25519 private key (32 bytes).
|
|
245
|
+
enc: Encapsulated key from `seal()`.
|
|
246
|
+
info: Same `info` passed to `seal()`.
|
|
247
|
+
aad: Same `aad` passed to `seal()`.
|
|
248
|
+
ciphertext: Output of `seal()`.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Decrypted plaintext bytes.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
cryptography.exceptions.InvalidTag: AEAD verification failed.
|
|
255
|
+
"""
|
|
256
|
+
if len(recipient_priv) != _NSK:
|
|
257
|
+
raise ValueError(f"recipient_priv must be {_NSK} bytes")
|
|
258
|
+
if len(enc) != _NPK:
|
|
259
|
+
raise ValueError(f"enc must be {_NPK} bytes")
|
|
260
|
+
shared_secret = _kem_decap(recipient_priv, enc)
|
|
261
|
+
key, base_nonce = _key_schedule_base(suite_id, shared_secret, info)
|
|
262
|
+
return _new_aead(suite_id, key).decrypt(base_nonce, ciphertext, aad)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def generate_keypair() -> tuple[bytes, bytes]:
|
|
266
|
+
"""Generate a fresh X25519 keypair. Returns `(private_raw, public_raw)`."""
|
|
267
|
+
sk = x25519.X25519PrivateKey.generate()
|
|
268
|
+
return _serialize_sk(sk), _serialize_pk(sk.public_key())
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# Pure-Python fallback gate (RFC 9180 implementations exist but the project pins
|
|
272
|
+
# `cryptography>=44`; any host missing native primitives can opt-in here).
|
|
273
|
+
if os.environ.get("HM_PURE_PY_HPKE") == "1": # pragma: no cover - opt-in path
|
|
274
|
+
raise RuntimeError(
|
|
275
|
+
"HM_PURE_PY_HPKE=1 set but no pure-Python HPKE backend is bundled. "
|
|
276
|
+
"Install cryptography>=44 or remove the env var."
|
|
277
|
+
)
|
hypermind/crypto/kms.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Signer abstraction — pluggable signing backends for v0.8 production profile.
|
|
2
|
+
|
|
3
|
+
The SDK historically embedded `Keypair` directly in the agent. That is fine
|
|
4
|
+
for sandbox / single-tenant deployments but blocks the v0.8 production
|
|
5
|
+
profile where signing keys are expected to live in:
|
|
6
|
+
|
|
7
|
+
* AWS KMS — keys created with `KeySpec=ECC_NIST_P256` cannot be used
|
|
8
|
+
directly (Ed25519 is not yet GA in KMS as of 2026-04-27); the planned
|
|
9
|
+
backend wraps a customer-supplied `Sign` API call and validates the
|
|
10
|
+
returned DER signature against the published kid.
|
|
11
|
+
* GCP Cloud KMS — `EC_SIGN_ED25519` algorithm; the backend issues an
|
|
12
|
+
`AsymmetricSign` RPC and unwraps the IETF-encoded signature.
|
|
13
|
+
* HashiCorp Vault Transit — Ed25519 keys via the `transit/sign/<key>`
|
|
14
|
+
endpoint; backend uses the leased AppRole token, refreshes via the
|
|
15
|
+
sidecar.
|
|
16
|
+
|
|
17
|
+
All three back-ends are wired in v0.8.5 (see ROADMAP). v0.8.0 ships
|
|
18
|
+
the `Signer` Protocol and the `LocalSigner` adapter so the rest of the
|
|
19
|
+
SDK can already program against the abstraction.
|
|
20
|
+
|
|
21
|
+
Security invariants (production profile):
|
|
22
|
+
|
|
23
|
+
* `kid` is always the 32-byte Ed25519 public key half — never the secret.
|
|
24
|
+
This matches the wire format's `COSE_KID` discriminator.
|
|
25
|
+
* `sign(msg)` MUST return a raw signature (no envelope, no DER wrapper)
|
|
26
|
+
matching `crypto.signing.verify_signature(alg, public_key, sig, msg)`.
|
|
27
|
+
* The Signer MUST refuse `sign()` after the underlying key material has
|
|
28
|
+
been rotated; callers detect this via a stable `kid` plus monotonic
|
|
29
|
+
rotation epoch.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from typing import Protocol, runtime_checkable
|
|
35
|
+
|
|
36
|
+
from hypermind.crypto.signing import ALG_ED25519, Keypair
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@runtime_checkable
|
|
40
|
+
class Signer(Protocol):
|
|
41
|
+
"""Pluggable signing backend.
|
|
42
|
+
|
|
43
|
+
Implementations MUST be safe to call concurrently from multiple
|
|
44
|
+
coroutines; the production AWS/GCP/Vault adapters serialise inside
|
|
45
|
+
the SDK they wrap.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def public_key(self) -> bytes:
|
|
50
|
+
"""Full algorithm-specific public-key material.
|
|
51
|
+
|
|
52
|
+
For Ed25519: the 32-byte raw public key.
|
|
53
|
+
For composite (alg=-49): `ed_pk(32) || mldsa_pk(1952)`.
|
|
54
|
+
"""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def kid(self) -> bytes:
|
|
59
|
+
"""Stable 32-byte agent identity (Ed25519 public-key half)."""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def alg(self) -> int:
|
|
64
|
+
"""COSE algorithm identifier (e.g. ALG_ED25519 = -8)."""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
def sign(self, message: bytes) -> bytes:
|
|
68
|
+
"""Return a raw signature over `message` (no envelope)."""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LocalSigner:
|
|
73
|
+
"""In-process Signer backed by a `Keypair`.
|
|
74
|
+
|
|
75
|
+
This is the v0.1–v0.7 path: the secret is held in memory by the
|
|
76
|
+
agent. Suitable for sandbox/testbed and for self-hosted deployments
|
|
77
|
+
where operators have decided that an HSM/KMS dependency is not
|
|
78
|
+
required.
|
|
79
|
+
|
|
80
|
+
The constructor accepts either a `Keypair` (preferred) or a 32-byte
|
|
81
|
+
Ed25519 seed for deterministic test fixtures.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, keypair: Keypair) -> None:
|
|
85
|
+
self._kp = keypair
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_seed(cls, seed: bytes) -> LocalSigner:
|
|
89
|
+
return cls(Keypair.from_seed(seed))
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def generate(cls, *, alg: int = ALG_ED25519) -> LocalSigner:
|
|
93
|
+
return cls(Keypair.generate(alg=alg))
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def keypair(self) -> Keypair:
|
|
97
|
+
"""Escape hatch for code paths that still need the raw Keypair.
|
|
98
|
+
|
|
99
|
+
Removed in v0.9; callers should switch to `sign()` and `kid`.
|
|
100
|
+
"""
|
|
101
|
+
return self._kp
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def public_key(self) -> bytes:
|
|
105
|
+
return self._kp.public_bytes
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def kid(self) -> bytes:
|
|
109
|
+
return self._kp.kid
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def alg(self) -> int:
|
|
113
|
+
return self._kp.alg
|
|
114
|
+
|
|
115
|
+
def sign(self, message: bytes) -> bytes:
|
|
116
|
+
return self._kp.sign(message)
|
|
117
|
+
|
|
118
|
+
def __repr__(self) -> str:
|
|
119
|
+
return f"LocalSigner(kid={self.kid.hex()[:16]}..., alg={self.alg})"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class KMSSigner:
|
|
123
|
+
"""Stub Signer for cloud KMS / Vault back-ends.
|
|
124
|
+
|
|
125
|
+
**Not yet wired** — instantiating and calling `sign()` raises
|
|
126
|
+
`NotImplementedError`. The class exists so downstream code can
|
|
127
|
+
program against the contract today and the ROADMAP 0.8.5 backends
|
|
128
|
+
can drop in without an API break.
|
|
129
|
+
|
|
130
|
+
Construction parameters (anticipated):
|
|
131
|
+
|
|
132
|
+
* `provider` — one of `"aws-kms"`, `"gcp-kms"`, `"vault-transit"`.
|
|
133
|
+
* `key_uri` — provider-native key reference. Examples:
|
|
134
|
+
- AWS: ``arn:aws:kms:us-east-1:123456789012:key/abcd-…``
|
|
135
|
+
- GCP: ``projects/p/locations/l/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1``
|
|
136
|
+
- Vault: ``transit/keys/<key-name>``
|
|
137
|
+
* `published_kid` — the 32-byte Ed25519 public key half, fetched
|
|
138
|
+
out-of-band from the provider's `GetPublicKey` API and pinned at
|
|
139
|
+
construction. The Signer refuses to sign if the provider rotates
|
|
140
|
+
the underlying material without the operator updating this pin.
|
|
141
|
+
* `auth` — provider auth context (boto3 session, GCP credentials,
|
|
142
|
+
Vault token). Held by reference; the SDK does not persist it.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(
|
|
146
|
+
self,
|
|
147
|
+
*,
|
|
148
|
+
provider: str,
|
|
149
|
+
key_uri: str,
|
|
150
|
+
published_kid: bytes,
|
|
151
|
+
auth: object | None = None,
|
|
152
|
+
alg: int = ALG_ED25519,
|
|
153
|
+
) -> None:
|
|
154
|
+
if len(published_kid) != 32:
|
|
155
|
+
raise ValueError("published_kid must be 32 bytes (Ed25519 public)")
|
|
156
|
+
if provider not in ("aws-kms", "gcp-kms", "vault-transit"):
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"unknown KMS provider {provider!r}; expected aws-kms | gcp-kms | vault-transit"
|
|
159
|
+
)
|
|
160
|
+
self._provider = provider
|
|
161
|
+
self._key_uri = key_uri
|
|
162
|
+
self._kid = published_kid
|
|
163
|
+
self._auth = auth
|
|
164
|
+
self._alg = alg
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def public_key(self) -> bytes:
|
|
168
|
+
# Today we only know the Ed25519 half (the published kid). When
|
|
169
|
+
# the v0.8.5 backends land, the constructor will fetch the full
|
|
170
|
+
# public-key bytes (composite case includes the ML-DSA-65 half)
|
|
171
|
+
# via the provider's GetPublicKey RPC.
|
|
172
|
+
return self._kid
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def kid(self) -> bytes:
|
|
176
|
+
return self._kid
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def alg(self) -> int:
|
|
180
|
+
return self._alg
|
|
181
|
+
|
|
182
|
+
def sign(self, message: bytes) -> bytes:
|
|
183
|
+
raise NotImplementedError(
|
|
184
|
+
"KMS backend not yet wired; see ROADMAP 0.8.5. "
|
|
185
|
+
f"Provider={self._provider!r}, key_uri={self._key_uri!r}. "
|
|
186
|
+
"Use LocalSigner for now."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def __repr__(self) -> str:
|
|
190
|
+
return (
|
|
191
|
+
f"KMSSigner(provider={self._provider!r}, "
|
|
192
|
+
f"kid={self._kid.hex()[:16]}..., alg={self._alg})"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
__all__ = ["KMSSigner", "LocalSigner", "Signer"]
|