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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""COSE Sign1 envelopes (RFC 9052) for memory commits.
|
|
2
|
+
|
|
3
|
+
Every memory commit gets wrapped in a COSE Sign1 envelope signed with
|
|
4
|
+
the agent's Ed25519 key. External verifiers can confirm the commit came
|
|
5
|
+
from a known agent without trusting the host's audit log.
|
|
6
|
+
|
|
7
|
+
The COSE Sign1 structure (per RFC 9052 §4.1):
|
|
8
|
+
|
|
9
|
+
[
|
|
10
|
+
protected: bstr .cbor {1: -8, "alg": "EdDSA"}, // header
|
|
11
|
+
unprotected: {}, // empty
|
|
12
|
+
payload: bstr, // commit hash + metadata
|
|
13
|
+
signature: bstr // Ed25519 sig
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
We serialize as JSON (CBOR is the spec-preferred format, but JSON works
|
|
17
|
+
for the prototype and is easier to inspect).
|
|
18
|
+
|
|
19
|
+
Payload format (JSON object, base64url-encoded as a bstr):
|
|
20
|
+
{
|
|
21
|
+
"commit_id": "...", // Trace commit ID
|
|
22
|
+
"chain_hash": "...", // BLAKE3 chain hash from the commit
|
|
23
|
+
"n_facts": int, // number of facts in the commit
|
|
24
|
+
"ts": "...", // ISO timestamp
|
|
25
|
+
"agent_did": "did:key:..." // agent identifier
|
|
26
|
+
}
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import base64
|
|
32
|
+
import hashlib
|
|
33
|
+
import json
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from datetime import datetime, timezone
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
from cortexm.provenance.agent import Ed25519AgentKey, get_default_agent
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _b64url(b: bytes) -> str:
|
|
42
|
+
return base64.urlsafe_b64encode(b).decode("ascii").rstrip("=")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _b64url_decode(s: str) -> bytes:
|
|
46
|
+
pad = "=" * (-len(s) % 4)
|
|
47
|
+
return base64.urlsafe_b64decode(s + pad)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class CoseSign1Envelope:
|
|
52
|
+
"""A COSE Sign1 envelope (RFC 9052) wrapping a memory commit."""
|
|
53
|
+
protected: dict # protected header (e.g. {"alg": "EdDSA"})
|
|
54
|
+
payload: dict # decoded payload (commit metadata)
|
|
55
|
+
signature: str = "" # base64url-encoded signature
|
|
56
|
+
agent_did: str = "" # did:key of the signing agent
|
|
57
|
+
ts: str = "" # ISO timestamp when signed
|
|
58
|
+
raw_b64: str = "" # the full envelope, base64url-encoded
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> dict:
|
|
61
|
+
return {
|
|
62
|
+
"protected": self.protected,
|
|
63
|
+
"payload": self.payload,
|
|
64
|
+
"signature": self.signature,
|
|
65
|
+
"agent_did": self.agent_did,
|
|
66
|
+
"ts": self.ts,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def to_json(self) -> str:
|
|
70
|
+
"""JSON serialization of the envelope."""
|
|
71
|
+
return json.dumps(self.to_dict(), sort_keys=True, indent=2)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def sign_commit(commit_id: str, chain_hash: str, n_facts: int,
|
|
75
|
+
agent: Ed25519AgentKey | None = None,
|
|
76
|
+
extra_payload: dict | None = None) -> CoseSign1Envelope:
|
|
77
|
+
"""Sign a memory commit with an Ed25519 agent key.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
commit_id: Trace commit ID (hex string)
|
|
81
|
+
chain_hash: BLAKE3 chain hash from the commit
|
|
82
|
+
n_facts: number of facts in the commit
|
|
83
|
+
agent: agent key to sign with. If None, uses the default agent
|
|
84
|
+
(auto-generated on first call; persisted if Config.
|
|
85
|
+
provenance_agent_key_path is set).
|
|
86
|
+
extra_payload: additional fields to include in the payload
|
|
87
|
+
(e.g. user_id, branch, message).
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
CoseSign1Envelope with the signature + payload + protected header.
|
|
91
|
+
"""
|
|
92
|
+
if agent is None:
|
|
93
|
+
agent = get_default_agent()
|
|
94
|
+
|
|
95
|
+
ts = datetime.now(timezone.utc).isoformat()
|
|
96
|
+
|
|
97
|
+
payload = {
|
|
98
|
+
"commit_id": commit_id,
|
|
99
|
+
"chain_hash": chain_hash,
|
|
100
|
+
"n_facts": n_facts,
|
|
101
|
+
"ts": ts,
|
|
102
|
+
"agent_did": agent.did,
|
|
103
|
+
"agent_label": agent.label,
|
|
104
|
+
}
|
|
105
|
+
if extra_payload:
|
|
106
|
+
payload.update(extra_payload)
|
|
107
|
+
|
|
108
|
+
protected = {
|
|
109
|
+
"alg": "EdDSA",
|
|
110
|
+
"kid": agent.did,
|
|
111
|
+
"typ": "cortexm-commit",
|
|
112
|
+
"alg_id": -8, # COSE alg ID for EdDSA
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
payload_bytes = json.dumps(payload, sort_keys=True).encode("utf-8")
|
|
116
|
+
protected_bytes = json.dumps(protected, sort_keys=True).encode("utf-8")
|
|
117
|
+
|
|
118
|
+
# COSE Sig_structure (RFC 9052 §4.4):
|
|
119
|
+
# ["Signature1", protected, external_aad (empty), payload]
|
|
120
|
+
sig_structure = json.dumps(
|
|
121
|
+
["Signature1",
|
|
122
|
+
_b64url(protected_bytes),
|
|
123
|
+
"", # external_aad (empty)
|
|
124
|
+
_b64url(payload_bytes)],
|
|
125
|
+
sort_keys=True).encode("utf-8")
|
|
126
|
+
|
|
127
|
+
signature = agent.sign(sig_structure)
|
|
128
|
+
|
|
129
|
+
envelope = CoseSign1Envelope(
|
|
130
|
+
protected=protected,
|
|
131
|
+
payload=payload,
|
|
132
|
+
signature=_b64url(signature),
|
|
133
|
+
agent_did=agent.did,
|
|
134
|
+
ts=ts,
|
|
135
|
+
)
|
|
136
|
+
# full envelope serialization for storage
|
|
137
|
+
envelope.raw_b64 = _b64url(
|
|
138
|
+
json.dumps(envelope.to_dict(), sort_keys=True).encode("utf-8"))
|
|
139
|
+
return envelope
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def verify_commit(envelope: CoseSign1Envelope,
|
|
143
|
+
agent: Ed25519AgentKey | None = None,
|
|
144
|
+
expected_commit_id: str | None = None,
|
|
145
|
+
expected_chain_hash: str | None = None) -> bool:
|
|
146
|
+
"""Verify a COSE Sign1 envelope.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
envelope: the envelope to verify
|
|
150
|
+
agent: agent key to verify with. If None, a key with the
|
|
151
|
+
envelope's agent_did must be registered (via
|
|
152
|
+
set_default_agent or a future DID resolver).
|
|
153
|
+
expected_commit_id: if set, the payload's commit_id must match
|
|
154
|
+
expected_chain_hash: if set, the payload's chain_hash must match
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
True if the signature is valid AND (if set) the expected
|
|
158
|
+
commit_id and chain_hash match.
|
|
159
|
+
"""
|
|
160
|
+
if agent is None:
|
|
161
|
+
agent = get_default_agent()
|
|
162
|
+
if agent.did != envelope.agent_did:
|
|
163
|
+
# in production: resolve the key from did:key
|
|
164
|
+
# for now: only verify if the registered agent matches
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
payload = envelope.payload
|
|
168
|
+
if expected_commit_id and payload.get("commit_id") != expected_commit_id:
|
|
169
|
+
return False
|
|
170
|
+
if expected_chain_hash and payload.get("chain_hash") != expected_chain_hash:
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
protected_bytes = json.dumps(envelope.protected, sort_keys=True).encode("utf-8")
|
|
174
|
+
payload_bytes = json.dumps(payload, sort_keys=True).encode("utf-8")
|
|
175
|
+
sig_structure = json.dumps(
|
|
176
|
+
["Signature1",
|
|
177
|
+
_b64url(protected_bytes),
|
|
178
|
+
"",
|
|
179
|
+
_b64url(payload_bytes)],
|
|
180
|
+
sort_keys=True).encode("utf-8")
|
|
181
|
+
|
|
182
|
+
signature = _b64url_decode(envelope.signature)
|
|
183
|
+
return agent.verify(sig_structure, signature)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def envelope_from_dict(d: dict) -> CoseSign1Envelope:
|
|
187
|
+
"""Reconstruct an envelope from a dict (e.g. loaded from JSON)."""
|
|
188
|
+
return CoseSign1Envelope(
|
|
189
|
+
protected=d.get("protected", {}),
|
|
190
|
+
payload=d.get("payload", {}),
|
|
191
|
+
signature=d.get("signature", ""),
|
|
192
|
+
agent_did=d.get("agent_did", ""),
|
|
193
|
+
ts=d.get("ts", ""),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
__all__ = [
|
|
198
|
+
"CoseSign1Envelope",
|
|
199
|
+
"sign_commit", "verify_commit",
|
|
200
|
+
"envelope_from_dict",
|
|
201
|
+
]
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""SCITT (Secure Coding Infrastructure Transparency) signed statements.
|
|
2
|
+
|
|
3
|
+
Forwards a COSE Sign1 envelope to a SCITT transparency log, returns a
|
|
4
|
+
receipt that can be verified by a third party.
|
|
5
|
+
|
|
6
|
+
In a production deployment, SCITT runs as an external service (e.g. an
|
|
7
|
+
Azure SCITT instance, or a notary.localhost fake service for dev). For
|
|
8
|
+
the prototype, we implement an in-process mock SCITT that:
|
|
9
|
+
- accepts a COSE Sign1 envelope
|
|
10
|
+
- appends it to an append-only log (hash-chained)
|
|
11
|
+
- returns a receipt with the leaf hash + the chain head at the time
|
|
12
|
+
of submission + the inclusion path (sibling hashes + direction)
|
|
13
|
+
- signs the receipt with the SCITT service's key
|
|
14
|
+
|
|
15
|
+
This is NOT a real transparency log (it doesn't gossip, doesn't publish
|
|
16
|
+
to a public append-only store) but it implements the API surface that
|
|
17
|
+
a real SCITT integration would expose. The signature is verifiable
|
|
18
|
+
by any third party that trusts the SCITT service's public key.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import base64
|
|
24
|
+
import hashlib
|
|
25
|
+
import json
|
|
26
|
+
import threading
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
|
|
30
|
+
from cortexm.provenance.agent import Ed25519AgentKey
|
|
31
|
+
from cortexm.provenance.cose import CoseSign1Envelope
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ScittReceipt:
|
|
36
|
+
"""A SCITT transparency log receipt."""
|
|
37
|
+
leaf_hash: str # hash of this statement in the log
|
|
38
|
+
tree_size: int # log size at time of submission
|
|
39
|
+
chain_head: str # root hash of the log at time of submission
|
|
40
|
+
# inclusion path: list of (sibling_hash, direction) where direction
|
|
41
|
+
# is 'L' (sibling is to the left of the current node) or 'R' (right)
|
|
42
|
+
inclusion_path: list[tuple[str, str]] = field(default_factory=list)
|
|
43
|
+
service_did: str = ""
|
|
44
|
+
service_signature: str = "" # base64url Ed25519 sig from SCITT service
|
|
45
|
+
ts: str = ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class ScittStatement:
|
|
50
|
+
"""A SCITT-signed statement (a COSE Sign1 + a SCITT receipt)."""
|
|
51
|
+
envelope: CoseSign1Envelope
|
|
52
|
+
receipt: ScittReceipt
|
|
53
|
+
service_did: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _h(data: bytes) -> str:
|
|
57
|
+
"""BLAKE3 or BLAKE2b fallback."""
|
|
58
|
+
try:
|
|
59
|
+
import blake3
|
|
60
|
+
return blake3.blake3(data).hexdigest()
|
|
61
|
+
except ImportError:
|
|
62
|
+
return hashlib.blake2b(data, digest_size=32).hexdigest()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _merkle_level(leaves: list[str]) -> list[str]:
|
|
66
|
+
"""Compute the next level up in the Merkle tree."""
|
|
67
|
+
if len(leaves) == 1:
|
|
68
|
+
return leaves
|
|
69
|
+
if len(leaves) % 2 == 1:
|
|
70
|
+
leaves = leaves + [leaves[-1]] # pad
|
|
71
|
+
out = []
|
|
72
|
+
for i in range(0, len(leaves), 2):
|
|
73
|
+
out.append(_h((leaves[i] + leaves[i + 1]).encode("utf-8")))
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _merkle_root(leaves: list[str]) -> str:
|
|
78
|
+
"""Compute the Merkle root over a list of leaf hashes."""
|
|
79
|
+
if not leaves:
|
|
80
|
+
return ""
|
|
81
|
+
level = list(leaves)
|
|
82
|
+
while len(level) > 1:
|
|
83
|
+
level = _merkle_level(level)
|
|
84
|
+
return level[0]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _inclusion_path(leaves: list[str], idx: int) -> list[tuple[str, str]]:
|
|
88
|
+
"""Build the Merkle inclusion proof path for leaf at idx.
|
|
89
|
+
|
|
90
|
+
Returns a list of (sibling_hash, direction) where direction is
|
|
91
|
+
'L' (sibling is to the left of the current node) or 'R' (right).
|
|
92
|
+
The verifier walks the path from the leaf up to the root.
|
|
93
|
+
"""
|
|
94
|
+
if idx < 0 or idx >= len(leaves):
|
|
95
|
+
return []
|
|
96
|
+
path: list[tuple[str, str]] = []
|
|
97
|
+
level = list(leaves)
|
|
98
|
+
cur_idx = idx
|
|
99
|
+
while len(level) > 1:
|
|
100
|
+
if len(level) % 2 == 1:
|
|
101
|
+
level = level + [level[-1]] # pad
|
|
102
|
+
if cur_idx % 2 == 0:
|
|
103
|
+
# current is the left child — sibling is to the right
|
|
104
|
+
sib_idx = cur_idx + 1
|
|
105
|
+
direction = "R"
|
|
106
|
+
else:
|
|
107
|
+
sib_idx = cur_idx - 1
|
|
108
|
+
direction = "L"
|
|
109
|
+
path.append((level[sib_idx], direction))
|
|
110
|
+
level = _merkle_level(level)
|
|
111
|
+
cur_idx //= 2
|
|
112
|
+
return path
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _verify_inclusion(leaf: str,
|
|
116
|
+
path: list[tuple[str, str]]) -> str:
|
|
117
|
+
"""Walk an inclusion path from a leaf, return the recomputed root."""
|
|
118
|
+
cur = leaf
|
|
119
|
+
for sib, direction in path:
|
|
120
|
+
if direction == "R":
|
|
121
|
+
cur = _h((cur + sib).encode("utf-8"))
|
|
122
|
+
else:
|
|
123
|
+
cur = _h((sib + cur).encode("utf-8"))
|
|
124
|
+
return cur
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class _ScittLog:
|
|
128
|
+
"""In-process mock SCITT transparency log.
|
|
129
|
+
|
|
130
|
+
A production SCITT service persists the log to disk + gossips.
|
|
131
|
+
This mock keeps an in-process append-only list. For tests/dev
|
|
132
|
+
only — restart the process and the log is empty (verifications of
|
|
133
|
+
prior receipts will fail since the service key has changed).
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(self) -> None:
|
|
137
|
+
self._entries: list[str] = []
|
|
138
|
+
self._service_key: Ed25519AgentKey = Ed25519AgentKey.generate(
|
|
139
|
+
label="scitt-mock-service")
|
|
140
|
+
self._service_did = self._service_key.did
|
|
141
|
+
self._lock = threading.Lock()
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def service_did(self) -> str:
|
|
145
|
+
return self._service_did
|
|
146
|
+
|
|
147
|
+
def submit(self, envelope: CoseSign1Envelope) -> ScittReceipt:
|
|
148
|
+
"""Submit a COSE Sign1 envelope to the log."""
|
|
149
|
+
with self._lock:
|
|
150
|
+
envelope_b64 = envelope.signature.encode("utf-8")
|
|
151
|
+
leaf = _h(envelope_b64)
|
|
152
|
+
self._entries.append(leaf)
|
|
153
|
+
tree_size = len(self._entries)
|
|
154
|
+
chain_head = _merkle_root(self._entries)
|
|
155
|
+
ts = datetime.now(timezone.utc).isoformat()
|
|
156
|
+
inclusion = _inclusion_path(self._entries, tree_size - 1)
|
|
157
|
+
|
|
158
|
+
receipt_data = json.dumps({
|
|
159
|
+
"leaf_hash": leaf,
|
|
160
|
+
"tree_size": tree_size,
|
|
161
|
+
"chain_head": chain_head,
|
|
162
|
+
"ts": ts,
|
|
163
|
+
"service_did": self._service_did,
|
|
164
|
+
}, sort_keys=True).encode("utf-8")
|
|
165
|
+
service_sig = self._service_key.sign(receipt_data)
|
|
166
|
+
|
|
167
|
+
return ScittReceipt(
|
|
168
|
+
leaf_hash=leaf,
|
|
169
|
+
tree_size=tree_size,
|
|
170
|
+
chain_head=chain_head,
|
|
171
|
+
inclusion_path=inclusion,
|
|
172
|
+
service_did=self._service_did,
|
|
173
|
+
service_signature=base64.urlsafe_b64encode(
|
|
174
|
+
service_sig).decode().rstrip("="),
|
|
175
|
+
ts=ts,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def verify_receipt(self, receipt: ScittReceipt) -> bool:
|
|
179
|
+
"""Verify a SCITT receipt against this log's service key."""
|
|
180
|
+
if not receipt.leaf_hash or not receipt.chain_head:
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
sig_b64 = receipt.service_signature
|
|
184
|
+
if not sig_b64:
|
|
185
|
+
return False
|
|
186
|
+
try:
|
|
187
|
+
sig = base64.urlsafe_b64decode(
|
|
188
|
+
sig_b64 + "=" * (-len(sig_b64) % 4))
|
|
189
|
+
except Exception:
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
receipt_data = json.dumps({
|
|
193
|
+
"leaf_hash": receipt.leaf_hash,
|
|
194
|
+
"tree_size": receipt.tree_size,
|
|
195
|
+
"chain_head": receipt.chain_head,
|
|
196
|
+
"ts": receipt.ts,
|
|
197
|
+
"service_did": receipt.service_did,
|
|
198
|
+
}, sort_keys=True).encode("utf-8")
|
|
199
|
+
|
|
200
|
+
# verify the service's signature on the receipt metadata
|
|
201
|
+
if receipt.service_did == self._service_did:
|
|
202
|
+
if not self._service_key.verify(receipt_data, sig):
|
|
203
|
+
return False
|
|
204
|
+
else:
|
|
205
|
+
# production: resolve did:key from a registry
|
|
206
|
+
# prototype: reject unknown service DIDs
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
# recompute the Merkle root from the leaf + inclusion path
|
|
210
|
+
recomputed = _verify_inclusion(receipt.leaf_hash,
|
|
211
|
+
receipt.inclusion_path)
|
|
212
|
+
return recomputed == receipt.chain_head
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# Process-global mock log
|
|
216
|
+
_mock_log = _ScittLog()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def submit_to_scitt(envelope: CoseSign1Envelope) -> ScittStatement:
|
|
220
|
+
"""Submit a COSE Sign1 envelope to the SCITT transparency log."""
|
|
221
|
+
receipt = _mock_log.submit(envelope)
|
|
222
|
+
return ScittStatement(
|
|
223
|
+
envelope=envelope,
|
|
224
|
+
receipt=receipt,
|
|
225
|
+
service_did=receipt.service_did,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def verify_receipt(statement: ScittStatement) -> bool:
|
|
230
|
+
"""Verify a SCITT statement's receipt.
|
|
231
|
+
|
|
232
|
+
Verifies:
|
|
233
|
+
1. The service's Ed25519 signature on the receipt metadata
|
|
234
|
+
2. The Merkle inclusion proof (leaf + path = chain_head)
|
|
235
|
+
|
|
236
|
+
Returns True only if both checks pass. Production deployments
|
|
237
|
+
would resolve the service key from a did:key registry rather than
|
|
238
|
+
trusting the in-process mock log.
|
|
239
|
+
"""
|
|
240
|
+
return _mock_log.verify_receipt(statement.receipt)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def reset_scitt_log() -> None:
|
|
244
|
+
"""Reset the in-process SCITT log (test-only)."""
|
|
245
|
+
global _mock_log
|
|
246
|
+
_mock_log = _ScittLog()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def get_scitt_service_did() -> str:
|
|
250
|
+
"""Return the SCITT service's DID (for external verification setup)."""
|
|
251
|
+
return _mock_log.service_did
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
__all__ = [
|
|
255
|
+
"ScittStatement", "ScittReceipt",
|
|
256
|
+
"submit_to_scitt", "verify_receipt",
|
|
257
|
+
"reset_scitt_log", "get_scitt_service_did",
|
|
258
|
+
]
|