cortexm 0.3.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.
- context_m.py +17 -0
- cortexm/__init__.py +45 -0
- cortexm/accel.py +403 -0
- cortexm/api/__init__.py +0 -0
- cortexm/api/chaos.py +118 -0
- cortexm/api/memory.py +635 -0
- cortexm/bench/__init__.py +0 -0
- cortexm/bench/abilities.py +311 -0
- cortexm/bench/baselines.py +89 -0
- cortexm/bench/beam_loader.py +317 -0
- cortexm/bench/generator.py +376 -0
- cortexm/bench/harness.py +211 -0
- cortexm/bench/messy.py +218 -0
- cortexm/bench/micro.py +251 -0
- cortexm/bench/ood.py +443 -0
- cortexm/bench/run.py +137 -0
- cortexm/bridge/__init__.py +0 -0
- cortexm/bridge/dates.py +178 -0
- cortexm/bridge/decoders.py +204 -0
- cortexm/bridge/enrich.py +255 -0
- cortexm/bridge/extractor.py +316 -0
- cortexm/bridge/fallback.py +332 -0
- cortexm/bridge/onnx_runtime.py +158 -0
- cortexm/bridge/patterns.py +760 -0
- cortexm/bridge/ppr.py +104 -0
- cortexm/bridge/prefilter.py +188 -0
- cortexm/bridge/query_extract.py +420 -0
- cortexm/bridge/reader.py +1174 -0
- cortexm/bridge/rerank.py +204 -0
- cortexm/bridge/writer.py +492 -0
- cortexm/cli.py +295 -0
- cortexm/cognition/__init__.py +53 -0
- cortexm/cognition/abstraction.py +192 -0
- cortexm/cognition/analogy.py +159 -0
- cortexm/cognition/engine.py +204 -0
- cortexm/cognition/gaps.py +365 -0
- cortexm/cognition/scanner.py +204 -0
- cortexm/config.py +375 -0
- cortexm/cortexm.py +8 -0
- cortexm/enterprise/__init__.py +0 -0
- cortexm/enterprise/audit.py +178 -0
- cortexm/enterprise/governance.py +239 -0
- cortexm/errors.py +35 -0
- cortexm/features/__init__.py +0 -0
- cortexm/features/git.py +204 -0
- cortexm/features/prefetch.py +88 -0
- cortexm/features/zk.py +105 -0
- cortexm/federation/__init__.py +39 -0
- cortexm/federation/crdt.py +275 -0
- cortexm/federation/fabric.py +109 -0
- cortexm/federation/hlc.py +80 -0
- cortexm/federation/node.py +145 -0
- cortexm/federation/schema_report.py +73 -0
- cortexm/federation/transport.py +164 -0
- cortexm/index/__init__.py +19 -0
- cortexm/index/nsg.py +386 -0
- cortexm/mcp/__init__.py +0 -0
- cortexm/mcp/server.py +985 -0
- cortexm/metrics.py +62 -0
- cortexm/migrate/__init__.py +0 -0
- cortexm/migrate/importers.py +192 -0
- cortexm/provenance/__init__.py +78 -0
- cortexm/provenance/agent.py +214 -0
- cortexm/provenance/cose.py +201 -0
- cortexm/provenance/scitt.py +258 -0
- cortexm/provenance/vc.py +250 -0
- cortexm/security/__init__.py +0 -0
- cortexm/security/crypto.py +162 -0
- cortexm/security/hashes.py +140 -0
- cortexm/security/injection.py +149 -0
- cortexm/security/mind.py +154 -0
- cortexm/security/pii.py +265 -0
- cortexm/security/rbac.py +169 -0
- cortexm/security/sandbox.py +131 -0
- cortexm/security/zk_hamming.py +142 -0
- cortexm/security/zk_sql.py +485 -0
- cortexm/server/__init__.py +0 -0
- cortexm/server/metrics.py +88 -0
- cortexm/server/rest.py +936 -0
- cortexm/server/sparql.py +984 -0
- cortexm/text/__init__.py +0 -0
- cortexm/text/dissim.py +252 -0
- cortexm/text/embedder.py +155 -0
- cortexm/text/fuzzy.py +218 -0
- cortexm/text/idiolect.py +253 -0
- cortexm/text/labse.py +374 -0
- cortexm/text/tokenizer.py +79 -0
- cortexm/trace/__init__.py +0 -0
- cortexm/trace/blob_arena.py +277 -0
- cortexm/trace/consolidate.py +337 -0
- cortexm/trace/contradictions.py +69 -0
- cortexm/trace/dedup.py +114 -0
- cortexm/trace/edges.py +214 -0
- cortexm/trace/fact.py +121 -0
- cortexm/trace/fade.py +245 -0
- cortexm/trace/lifecycle.py +112 -0
- cortexm/trace/rebuild.py +173 -0
- cortexm/trace/rules.py +171 -0
- cortexm/trace/store.py +680 -0
- cortexm/trace/structural.py +183 -0
- cortexm/trace/tmt.py +335 -0
- cortexm/util.py +148 -0
- cortexm/vsa/__init__.py +0 -0
- cortexm/vsa/attribution.py +149 -0
- cortexm/vsa/cleanup.py +161 -0
- cortexm/vsa/codecs.py +397 -0
- cortexm/vsa/hologram_overlay.py +139 -0
- cortexm/vsa/index.py +163 -0
- cortexm/vsa/ops.py +149 -0
- cortexm/vsa/palace.py +446 -0
- cortexm/vsa/role_vectors.py +236 -0
- cortexm/vsa/slb.py +78 -0
- cortexm/vsa/tlsh_trie.py +137 -0
- cortexm/vsa/working_memory.py +249 -0
- cortexm-0.3.0.dist-info/METADATA +482 -0
- cortexm-0.3.0.dist-info/RECORD +120 -0
- cortexm-0.3.0.dist-info/WHEEL +5 -0
- cortexm-0.3.0.dist-info/entry_points.txt +2 -0
- cortexm-0.3.0.dist-info/licenses/LICENSE +190 -0
- cortexm-0.3.0.dist-info/top_level.txt +2 -0
cortexm/security/rbac.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Role-based access control and API key management.
|
|
2
|
+
|
|
3
|
+
Enterprise procurement asks two questions before anything else: "who can
|
|
4
|
+
read the memory?" and "can we prove what they did?". This module answers
|
|
5
|
+
the first; ``cortexm.enterprise.audit`` answers the second.
|
|
6
|
+
|
|
7
|
+
Roles (least-privilege ladder):
|
|
8
|
+
admin — everything, including key management and erasure
|
|
9
|
+
operator — memory read/write, snapshots; no keys, no erasure
|
|
10
|
+
reader — search/get only
|
|
11
|
+
auditor — audit log and integrity verification only
|
|
12
|
+
|
|
13
|
+
API keys look like ``ctxm_<role>_<32 hex>`` and are stored ONLY as
|
|
14
|
+
BLAKE3 digests with a per-deployment pepper — a leaked database cannot
|
|
15
|
+
be used to authenticate. Verification is constant-time.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hmac
|
|
21
|
+
import secrets
|
|
22
|
+
import time
|
|
23
|
+
|
|
24
|
+
from cortexm.security.hashes import HashProvider
|
|
25
|
+
|
|
26
|
+
ROLES = ("admin", "operator", "reader", "auditor")
|
|
27
|
+
|
|
28
|
+
# action -> minimum role allowed (admin passes everything)
|
|
29
|
+
PERMISSIONS: dict[str, set[str]] = {
|
|
30
|
+
"memory.add": {"admin", "operator"},
|
|
31
|
+
"memory.search": {"admin", "operator", "reader"},
|
|
32
|
+
"memory.get": {"admin", "operator", "reader"},
|
|
33
|
+
"memory.get_all": {"admin", "operator", "reader"},
|
|
34
|
+
"memory.update": {"admin", "operator"},
|
|
35
|
+
"memory.delete": {"admin", "operator"},
|
|
36
|
+
"memory.delete_all": {"admin"}, # destructive
|
|
37
|
+
"memory.history": {"admin", "operator", "reader"},
|
|
38
|
+
"memory.stats": {"admin", "operator", "reader"},
|
|
39
|
+
"memory.verify": {"admin", "operator", "reader", "auditor"},
|
|
40
|
+
"memory.export": {"admin", "operator", "reader"}, # swappable decoder
|
|
41
|
+
"memory.chaos_ingest": {"admin", "operator"}, # EAM chaos mode
|
|
42
|
+
"audit.read": {"admin", "auditor"},
|
|
43
|
+
"audit.verify": {"admin", "auditor"},
|
|
44
|
+
"keys.create": {"admin"},
|
|
45
|
+
"keys.list": {"admin"},
|
|
46
|
+
"keys.revoke": {"admin"},
|
|
47
|
+
"sparql.query": {"admin", "operator", "reader"},
|
|
48
|
+
"governance.erase": {"admin"}, # GDPR right-to-erasure
|
|
49
|
+
"governance.retention": {"admin"},
|
|
50
|
+
"governance.snapshot": {"admin", "operator"},
|
|
51
|
+
"governance.restore": {"admin"},
|
|
52
|
+
"governance.pitr": {"admin", "operator", "reader"},
|
|
53
|
+
"governance.consolidate": {"admin"}, # dreaming pass trigger
|
|
54
|
+
"federation.digest": {"admin", "operator", "reader", "auditor"},
|
|
55
|
+
"federation.sync": {"admin", "operator"}, # CRDT envelope exchange
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_KEY_PREFIX = "ctxm"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RBACError(PermissionError):
|
|
62
|
+
def __init__(self, action: str, role: str) -> None:
|
|
63
|
+
super().__init__(
|
|
64
|
+
f"role {role!r} is not allowed to perform {action!r}")
|
|
65
|
+
self.action = action
|
|
66
|
+
self.role = role
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class APIKeyStore:
|
|
70
|
+
"""Persistent API key registry (kv-backed, peppered digests)."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, store, pepper: bytes | None = None) -> None:
|
|
73
|
+
self.store = store
|
|
74
|
+
self._hasher = HashProvider("blake2b")
|
|
75
|
+
self._pepper = pepper or self._load_pepper()
|
|
76
|
+
|
|
77
|
+
def _load_pepper(self) -> bytes:
|
|
78
|
+
raw = self.store.kv_get("rbac:pepper")
|
|
79
|
+
if raw:
|
|
80
|
+
return bytes.fromhex(raw)
|
|
81
|
+
p = secrets.token_bytes(16)
|
|
82
|
+
self.store.kv_set("rbac:pepper", p.hex())
|
|
83
|
+
return p
|
|
84
|
+
|
|
85
|
+
def _digest(self, key: str) -> str:
|
|
86
|
+
return self._hasher.hash_text(key + self._pepper.hex())
|
|
87
|
+
|
|
88
|
+
# ------------------------------------------------------------- lifecycle
|
|
89
|
+
def create(self, role: str, label: str = "", actor: str = "system",
|
|
90
|
+
ttl_seconds: int | None = None) -> dict:
|
|
91
|
+
if role not in ROLES:
|
|
92
|
+
raise ValueError(f"role must be one of {ROLES}")
|
|
93
|
+
secret = secrets.token_hex(16)
|
|
94
|
+
key = f"{_KEY_PREFIX}_{role}_{secret}"
|
|
95
|
+
kid = f"key_{secrets.token_hex(4)}"
|
|
96
|
+
import json
|
|
97
|
+
meta = {"id": kid, "role": role, "label": label,
|
|
98
|
+
"created_by": actor, "created_at": time.time(),
|
|
99
|
+
"digest": self._digest(key),
|
|
100
|
+
"expires_at": (time.time() + ttl_seconds) if ttl_seconds else None,
|
|
101
|
+
"revoked": False}
|
|
102
|
+
self.store.kv_set(f"rbac:key:{kid}", json.dumps(meta))
|
|
103
|
+
# index by digest for O(1) lookup at request time
|
|
104
|
+
self.store.kv_set(f"rbac:dgst:{meta['digest']}", kid)
|
|
105
|
+
return {"key": key, "id": kid, "role": role, "label": label,
|
|
106
|
+
"expires_at": meta["expires_at"]}
|
|
107
|
+
|
|
108
|
+
def verify(self, key: str) -> dict | None:
|
|
109
|
+
"""Return key metadata iff valid (exists, not revoked, not expired)."""
|
|
110
|
+
import json
|
|
111
|
+
if not key.startswith(f"{_KEY_PREFIX}_"):
|
|
112
|
+
return None
|
|
113
|
+
dgst = self._digest(key)
|
|
114
|
+
kid = self.store.kv_get(f"rbac:dgst:{dgst}")
|
|
115
|
+
if not kid:
|
|
116
|
+
return None
|
|
117
|
+
raw = self.store.kv_get(f"rbac:key:{kid}")
|
|
118
|
+
if not raw:
|
|
119
|
+
return None
|
|
120
|
+
try:
|
|
121
|
+
meta = json.loads(raw)
|
|
122
|
+
except Exception:
|
|
123
|
+
return None
|
|
124
|
+
if meta.get("revoked"):
|
|
125
|
+
return None
|
|
126
|
+
exp = meta.get("expires_at")
|
|
127
|
+
if exp and time.time() > exp:
|
|
128
|
+
return None
|
|
129
|
+
return meta
|
|
130
|
+
|
|
131
|
+
def revoke(self, kid: str) -> bool:
|
|
132
|
+
import json
|
|
133
|
+
raw = self.store.kv_get(f"rbac:key:{kid}")
|
|
134
|
+
if not raw:
|
|
135
|
+
return False
|
|
136
|
+
meta = json.loads(raw)
|
|
137
|
+
meta["revoked"] = True
|
|
138
|
+
meta["revoked_at"] = time.time()
|
|
139
|
+
self.store.kv_set(f"rbac:key:{kid}", json.dumps(meta))
|
|
140
|
+
dgst = meta.get("digest")
|
|
141
|
+
if dgst:
|
|
142
|
+
self.store.kv_set(f"rbac:dgst:{dgst}", "") # break the index
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
def list_keys(self) -> list[dict]:
|
|
146
|
+
import json
|
|
147
|
+
out = []
|
|
148
|
+
for k, v in self.store.iter_kv("rbac:key:"):
|
|
149
|
+
meta = json.loads(v)
|
|
150
|
+
meta.pop("digest", None) # never leak digests
|
|
151
|
+
out.append(meta)
|
|
152
|
+
return sorted(out, key=lambda m: m.get("created_at", 0))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def authorize(meta: dict | None, action: str) -> dict:
|
|
156
|
+
"""Raise RBACError unless ``meta`` (from verify()) may do ``action``."""
|
|
157
|
+
if meta is None:
|
|
158
|
+
raise RBACError(action, "anonymous")
|
|
159
|
+
role = meta.get("role", "reader")
|
|
160
|
+
allowed = PERMISSIONS.get(action)
|
|
161
|
+
if allowed is None:
|
|
162
|
+
raise RBACError(action, role)
|
|
163
|
+
if role not in allowed:
|
|
164
|
+
raise RBACError(action, role)
|
|
165
|
+
return meta
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def constant_time_eq(a: str, b: str) -> bool:
|
|
169
|
+
return hmac.compare_digest(a.encode(), b.encode())
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Memory scope sandbox — the InjecMEM isolation the plan demanded.
|
|
2
|
+
|
|
3
|
+
Threat model (InjecMEM, arXiv:2502.08589 + MINJA second-order contagion):
|
|
4
|
+
facts an AGENT writes while operating on a user's behalf are attacker-
|
|
5
|
+
influenced until proven otherwise. A poisoned fact written by a compromised
|
|
6
|
+
agent must never silently surface in the USER's memory view, and must never
|
|
7
|
+
steer another agent's behaviour, unless a human/system explicitly promotes
|
|
8
|
+
it.
|
|
9
|
+
|
|
10
|
+
Policy:
|
|
11
|
+
* WRITE — unchanged; facts carry their agent_id scope.
|
|
12
|
+
* READ — user-scope queries (agent_id=None) EXCLUDE agent-scoped facts.
|
|
13
|
+
Agent queries (agent_id=A) see A's facts plus user facts.
|
|
14
|
+
* PROMOTE — explicit, audited, gated:
|
|
15
|
+
- fact must be active and not quarantined
|
|
16
|
+
- confidence >= config.sandbox_promote_min_confidence
|
|
17
|
+
- source chunk re-scanned through InjecMEM + MINJA detectors;
|
|
18
|
+
high-risk sources are refused
|
|
19
|
+
- every promotion appends to the tamper-evident audit chain
|
|
20
|
+
|
|
21
|
+
Config knobs (Config):
|
|
22
|
+
sandbox_enabled (default True)
|
|
23
|
+
sandbox_promote_min_confidence (default 0.5)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from cortexm.security.injection import scan as injection_scan
|
|
29
|
+
from cortexm.security.injection import contagion_scan # type: ignore[attr-defined]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PromotionRefused(Exception):
|
|
33
|
+
"""Raised when a fact fails promotion policy."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, fact_id: str, reason: str):
|
|
36
|
+
super().__init__(f"promotion refused for {fact_id}: {reason}")
|
|
37
|
+
self.fact_id = fact_id
|
|
38
|
+
self.reason = reason
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ScopeSandbox:
|
|
42
|
+
def __init__(self, config, store, audit_log=None) -> None:
|
|
43
|
+
self.cfg = config
|
|
44
|
+
self.store = store
|
|
45
|
+
self.audit = audit_log
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------ policy
|
|
48
|
+
def visible(self, fact, agent_id: str | None) -> bool:
|
|
49
|
+
"""Read-visibility predicate used by the reader."""
|
|
50
|
+
if agent_id is not None:
|
|
51
|
+
# agent queries see their own scope + shared user scope
|
|
52
|
+
return fact.agent_id in (None, agent_id)
|
|
53
|
+
if not getattr(self.cfg, "sandbox_enabled", True):
|
|
54
|
+
return True
|
|
55
|
+
# user-scope query: agent-written facts are invisible until promoted
|
|
56
|
+
return fact.agent_id is None
|
|
57
|
+
|
|
58
|
+
def filter_facts(self, facts: list, agent_id: str | None) -> list:
|
|
59
|
+
return [f for f in facts if self.visible(f, agent_id)]
|
|
60
|
+
|
|
61
|
+
# ------------------------------------------------------------ promote
|
|
62
|
+
def promote(self, fact_ids: list[str], *, reviewed_by: str = "system",
|
|
63
|
+
force: bool = False) -> dict:
|
|
64
|
+
"""Explicitly promote agent-scoped facts into the user scope."""
|
|
65
|
+
promoted, refused = [], []
|
|
66
|
+
for fid in fact_ids:
|
|
67
|
+
facts = self.store.get_facts([fid])
|
|
68
|
+
fact = facts[0] if facts else None
|
|
69
|
+
if fact is None:
|
|
70
|
+
refused.append({"id": fid, "reason": "not found"})
|
|
71
|
+
continue
|
|
72
|
+
if fact.agent_id is None:
|
|
73
|
+
promoted.append(fid) # already user-scope; idempotent
|
|
74
|
+
continue
|
|
75
|
+
if not force:
|
|
76
|
+
if fact.quarantined or not fact.is_active:
|
|
77
|
+
self._audit("sandbox.promote", fid, "refused_quarantined")
|
|
78
|
+
refused.append({"id": fid, "reason": "quarantined/inactive"})
|
|
79
|
+
continue
|
|
80
|
+
min_conf = getattr(self.cfg, "sandbox_promote_min_confidence",
|
|
81
|
+
0.5)
|
|
82
|
+
if fact.confidence < min_conf:
|
|
83
|
+
self._audit("sandbox.promote", fid, "refused_low_confidence",
|
|
84
|
+
meta={"confidence": fact.confidence})
|
|
85
|
+
refused.append({"id": fid, "reason": f"confidence "
|
|
86
|
+
f"{fact.confidence:.2f} < {min_conf}"})
|
|
87
|
+
continue
|
|
88
|
+
verdict = self._rescan_source(fact)
|
|
89
|
+
if verdict is not None:
|
|
90
|
+
self._audit("sandbox.promote", fid,
|
|
91
|
+
"refused_injection_rescan",
|
|
92
|
+
meta={"risk": verdict})
|
|
93
|
+
refused.append({"id": fid,
|
|
94
|
+
"reason": f"source rescan risk={verdict}"})
|
|
95
|
+
continue
|
|
96
|
+
self.store.update_fact(fid, agent_id=None,
|
|
97
|
+
provenance={**fact.provenance,
|
|
98
|
+
"promoted_by": reviewed_by,
|
|
99
|
+
"promoted_from":
|
|
100
|
+
fact.agent_id})
|
|
101
|
+
self._audit("sandbox.promote", fid, "promoted",
|
|
102
|
+
meta={"reviewed_by": reviewed_by})
|
|
103
|
+
promoted.append(fid)
|
|
104
|
+
return {"promoted": promoted, "refused": refused,
|
|
105
|
+
"reviewed_by": reviewed_by}
|
|
106
|
+
|
|
107
|
+
# ------------------------------------------------------------ helpers
|
|
108
|
+
def _rescan_source(self, fact) -> str | None:
|
|
109
|
+
"""Re-run injection detection over the fact's source chunk."""
|
|
110
|
+
chunk = self.store.get_chunk(fact.source_id) if fact.source_id else None
|
|
111
|
+
if not chunk:
|
|
112
|
+
return None
|
|
113
|
+
verdict = injection_scan(chunk["text"],
|
|
114
|
+
quarantine_high=True)
|
|
115
|
+
if verdict.risk == "high":
|
|
116
|
+
return "high"
|
|
117
|
+
if getattr(self.cfg, "quarantine_contagion", True):
|
|
118
|
+
cv = contagion_scan(chunk["text"], [],
|
|
119
|
+
threshold=self.cfg.contagion_threshold)
|
|
120
|
+
if cv is not None:
|
|
121
|
+
return "contagion"
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
def _audit(self, action: str, resource: str, outcome: str,
|
|
125
|
+
meta: dict | None = None) -> None:
|
|
126
|
+
if self.audit is not None:
|
|
127
|
+
try:
|
|
128
|
+
self.audit.log(action, resource=resource, outcome=outcome,
|
|
129
|
+
meta=meta or {})
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Hamming-distance zero-knowledge-style proofs on binary vectors.
|
|
2
|
+
|
|
3
|
+
ZK proofs on HRR/convolution bind vectors are algebraically intractable
|
|
4
|
+
(arXiv:2405.09689 — the involution inverse is approximate, not exact).
|
|
5
|
+
But binary vectors have clean algebraic structure for attestation: a
|
|
6
|
+
prover can attest they hold a memory whose Hamming distance to a public
|
|
7
|
+
commitment is below threshold.
|
|
8
|
+
|
|
9
|
+
Construction: verifiable proximity proof (Sigma-protocol-inspired
|
|
10
|
+
non-interactive proof of knowledge + Hamming proximity). NOT full ZK —
|
|
11
|
+
the delta is revealed, but the witness v remains hidden behind the
|
|
12
|
+
delta mask. Sufficient for audit attestation; for full cryptographic
|
|
13
|
+
ZK, the prover would need to also sign with their identity key.
|
|
14
|
+
|
|
15
|
+
1. Prover holds v, public_vec is public.
|
|
16
|
+
2. delta = v XOR public_vec, weight(delta) = Hamming distance.
|
|
17
|
+
3. If weight > threshold: prover doesn't have a close v → reject.
|
|
18
|
+
4. Commitment c = H(delta || salt)
|
|
19
|
+
5. Reveal: delta, salt, c.
|
|
20
|
+
6. Verifier: check weight(delta) <= threshold AND H(delta||salt) = c.
|
|
21
|
+
|
|
22
|
+
For Context-M's binary codec tier, this proves storage-of-knowledge +
|
|
23
|
+
proximity to a commitment. Useful for:
|
|
24
|
+
* Audit compliance — prove you hold a memory the user asked about
|
|
25
|
+
* Federated attestation — peer proves they have a fact in expected range
|
|
26
|
+
* Tamper detection — checksum + Hamming proof together detect any bit flip
|
|
27
|
+
|
|
28
|
+
Pure Python. BLAKE2b hash for the commitment.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import hashlib
|
|
34
|
+
import os
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class HammingZKProof:
|
|
40
|
+
"""Non-interactive proof of Hamming proximity to a public commitment."""
|
|
41
|
+
commitment: str # H(delta || salt) hex
|
|
42
|
+
delta: bytes # v XOR public (revealed for verification)
|
|
43
|
+
salt: bytes # random salt for freshness
|
|
44
|
+
threshold: int # claimed max Hamming distance
|
|
45
|
+
weight: int # actual Hamming weight of delta
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class HammingZKProver:
|
|
49
|
+
"""Prove knowledge of a binary memory within Hamming distance of
|
|
50
|
+
a public commitment."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, dims: int, threshold: int = 32,
|
|
53
|
+
hash_fn: str = "blake2b") -> None:
|
|
54
|
+
self.dims = dims
|
|
55
|
+
self.threshold = threshold
|
|
56
|
+
self.hash_fn = hash_fn
|
|
57
|
+
|
|
58
|
+
def prove(self, public_vec: bytes, private_vec: bytes,
|
|
59
|
+
salt: bytes | None = None) -> HammingZKProof:
|
|
60
|
+
"""Generate non-interactive proof of Hamming proximity.
|
|
61
|
+
|
|
62
|
+
Returns a proof that the verifier can check without learning
|
|
63
|
+
private_vec itself. The delta (v XOR public) is revealed but
|
|
64
|
+
the private v stays hidden as long as public is public — the
|
|
65
|
+
verifier cannot recover v from delta XOR public... actually
|
|
66
|
+
they CAN recover v = delta XOR public. So this is NOT ZK;
|
|
67
|
+
it's a verifiable proximity proof. For true ZK, would need
|
|
68
|
+
a more involved construction (e.g., MPC-style commitments).
|
|
69
|
+
"""
|
|
70
|
+
if len(public_vec) != len(private_vec):
|
|
71
|
+
raise ValueError("vector length mismatch")
|
|
72
|
+
if salt is None:
|
|
73
|
+
salt = os.urandom(16)
|
|
74
|
+
delta = bytes(a ^ b for a, b in zip(public_vec, private_vec))
|
|
75
|
+
weight = _hamming_weight(delta)
|
|
76
|
+
commitment = self._hash(delta + salt)
|
|
77
|
+
return HammingZKProof(
|
|
78
|
+
commitment=commitment,
|
|
79
|
+
delta=delta,
|
|
80
|
+
salt=salt,
|
|
81
|
+
threshold=self.threshold,
|
|
82
|
+
weight=weight,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def verify(self, public_vec: bytes, proof: HammingZKProof) -> bool:
|
|
86
|
+
"""Verify a proof of Hamming proximity."""
|
|
87
|
+
if len(public_vec) != len(proof.delta):
|
|
88
|
+
return False
|
|
89
|
+
# recompute commitment
|
|
90
|
+
expected = self._hash(proof.delta + proof.salt)
|
|
91
|
+
if expected != proof.commitment:
|
|
92
|
+
return False
|
|
93
|
+
# check Hamming proximity claim
|
|
94
|
+
if proof.weight > proof.threshold:
|
|
95
|
+
return False
|
|
96
|
+
# the prover committed to delta = v XOR public; if they know v,
|
|
97
|
+
# they can recompute delta; the commitment binds them to delta
|
|
98
|
+
# before any challenge. The verifier can re-derive private as
|
|
99
|
+
# public XOR delta, so this is NOT zero-knowledge — it's a
|
|
100
|
+
# verifiable proximity attestation. For real ZK, would need
|
|
101
|
+
# more involved MPC-style commitments.
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
def _hash(self, data: bytes) -> str:
|
|
105
|
+
if self.hash_fn == "blake2b":
|
|
106
|
+
return hashlib.blake2b(data, digest_size=32).hexdigest()
|
|
107
|
+
return hashlib.sha256(data).hexdigest()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _hamming_weight(b: bytes) -> int:
|
|
111
|
+
"""Number of 1-bits in a byte string (Hamming weight, not distance)."""
|
|
112
|
+
return sum(bin(x).count("1") for x in b)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _hamming_distance(a: bytes, b: bytes) -> int:
|
|
116
|
+
"""Bit-level Hamming distance between two byte strings."""
|
|
117
|
+
if len(a) != len(b):
|
|
118
|
+
return max(len(a), len(b)) * 8
|
|
119
|
+
return sum(bin(x ^ y).count("1") for x, y in zip(a, b))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def checksum_prove_and_verify(public_vec: bytes, private_vec: bytes,
|
|
123
|
+
expected_hash: str) -> bool:
|
|
124
|
+
"""Combined checksum + proximity proof: prove storage-of-knowledge +
|
|
125
|
+
integrity. Both must pass for the proof to verify."""
|
|
126
|
+
# 1. Checksum: BLAKE2b hash of private matches expected
|
|
127
|
+
h = hashlib.blake2b(private_vec, digest_size=32).hexdigest()
|
|
128
|
+
if h != expected_hash:
|
|
129
|
+
return False
|
|
130
|
+
# 2. Proximity proof: private is within Hamming distance of public
|
|
131
|
+
prover = HammingZKProver(dims=len(public_vec) * 8)
|
|
132
|
+
proof = prover.prove(public_vec, private_vec)
|
|
133
|
+
return prover.verify(public_vec, proof)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
__all__ = [
|
|
137
|
+
"HammingZKProver",
|
|
138
|
+
"HammingZKProof",
|
|
139
|
+
"checksum_prove_and_verify",
|
|
140
|
+
"_hamming_distance",
|
|
141
|
+
"_hamming_weight",
|
|
142
|
+
]
|