vitnify 0.0.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.
- vitnify/__init__.py +0 -0
- vitnify/_vendor/__init__.py +0 -0
- vitnify/_vendor/pck/__init__.py +0 -0
- vitnify/_vendor/pck/cas.py +181 -0
- vitnify/capability.py +77 -0
- vitnify/certificate.py +388 -0
- vitnify/deterministic_llm.py +64 -0
- vitnify/engine.py +99 -0
- vitnify/events.py +131 -0
- vitnify/langgraph_adapter.py +120 -0
- vitnify/mcp_adapter.py +120 -0
- vitnify/react.py +93 -0
- vitnify/redact.py +142 -0
- vitnify/replayer.py +55 -0
- vitnify/viewer.py +328 -0
- vitnify-0.0.0.dist-info/METADATA +6 -0
- vitnify-0.0.0.dist-info/RECORD +21 -0
- vitnify-0.0.0.dist-info/WHEEL +5 -0
- vitnify-0.0.0.dist-info/licenses/LICENSE +202 -0
- vitnify-0.0.0.dist-info/licenses/NOTICE +14 -0
- vitnify-0.0.0.dist-info/top_level.txt +1 -0
vitnify/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Content-addressed store + Merkle ledger — the provenance backbone of PCK.
|
|
2
|
+
|
|
3
|
+
Every source chunk is addressed by its cryptographic hash. A Merkle tree over those
|
|
4
|
+
hashes yields a single ``root`` that attests the whole corpus. Any single chunk can
|
|
5
|
+
then carry an *inclusion proof*: a short path of sibling hashes that recomputes the
|
|
6
|
+
root. The guarantees this buys, and why they matter for AI answers:
|
|
7
|
+
|
|
8
|
+
* **Tamper-evidence** — change one byte of one chunk and the root changes.
|
|
9
|
+
* **Independent verifiability** — anyone holding the root can check a chunk's proof
|
|
10
|
+
without trusting the party that produced it (no model, no server, no secret key).
|
|
11
|
+
* **Fabrication is uncredentialed** — a made-up "source" has no leaf in the tree, so
|
|
12
|
+
it cannot produce a valid proof. This is the cryptographic half of catching a
|
|
13
|
+
hallucination: a grounded claim points at a chunk with a valid proof; an invented
|
|
14
|
+
one cannot.
|
|
15
|
+
|
|
16
|
+
This mirrors the BLAKE3 + Merkle design of a native runtime's ``cas_store``; here we
|
|
17
|
+
keep a dependency-light pure-Python implementation over ``blake3``. BLAKE3 is a hard
|
|
18
|
+
dependency and DEFINES the wire format: a ``blake2b`` digest is a different value under
|
|
19
|
+
the same ``hash_name``, so there is NO silent stdlib fallback -- one would make honest
|
|
20
|
+
proofs indistinguishable from forgeries. A missing blake3 fails loudly (matching
|
|
21
|
+
``events.py`` / ``certificate.py``), never silently downgrades.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
import blake3 as _blake3
|
|
29
|
+
|
|
30
|
+
_HASH_NAME = "blake3"
|
|
31
|
+
|
|
32
|
+
def _digest(b: bytes) -> str:
|
|
33
|
+
return _blake3.blake3(b).hexdigest()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
HASH_NAME = _HASH_NAME
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def hash_bytes(b: bytes) -> str:
|
|
40
|
+
"""Content address of raw bytes (hex)."""
|
|
41
|
+
return _digest(b)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def hash_text(s: str) -> str:
|
|
45
|
+
"""Content address of a text chunk (UTF-8, hex)."""
|
|
46
|
+
return _digest(s.encode("utf-8"))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _hash_pair(left: str, right: str) -> str:
|
|
50
|
+
"""Internal Merkle node = hash of the concatenated child hex digests."""
|
|
51
|
+
return _digest((left + right).encode("utf-8"))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# --- inclusion proof -------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class ProofStep:
|
|
58
|
+
"""One level of a Merkle inclusion path."""
|
|
59
|
+
|
|
60
|
+
sibling: str # hex digest of the sibling node
|
|
61
|
+
on_right: bool # True if the *current* node is the right child at this level
|
|
62
|
+
|
|
63
|
+
def to_json(self) -> dict:
|
|
64
|
+
return {"sibling": self.sibling, "on_right": self.on_right}
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def from_json(d: dict) -> "ProofStep":
|
|
68
|
+
return ProofStep(sibling=d["sibling"], on_right=bool(d["on_right"]))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class InclusionProof:
|
|
73
|
+
"""A self-contained proof that ``leaf`` is committed under ``root``.
|
|
74
|
+
|
|
75
|
+
Carries everything a third party needs to re-verify against a *published* root:
|
|
76
|
+
the leaf hash, the sibling path, and the index. No tree, server, or key required.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
leaf: str
|
|
80
|
+
index: int
|
|
81
|
+
path: list[ProofStep]
|
|
82
|
+
hash_name: str = HASH_NAME
|
|
83
|
+
|
|
84
|
+
def verify(self, root: str) -> bool:
|
|
85
|
+
# A proof carries the hash suite it was built under. This module only speaks
|
|
86
|
+
# BLAKE3, so a proof committed under any other suite cannot be recomputed here
|
|
87
|
+
# -- reject rather than silently recompute under the wrong hash.
|
|
88
|
+
if self.hash_name != HASH_NAME:
|
|
89
|
+
return False
|
|
90
|
+
return verify_proof(self.leaf, self.path, root)
|
|
91
|
+
|
|
92
|
+
def to_json(self) -> dict:
|
|
93
|
+
return {
|
|
94
|
+
"leaf": self.leaf,
|
|
95
|
+
"index": self.index,
|
|
96
|
+
"hash": self.hash_name,
|
|
97
|
+
"path": [s.to_json() for s in self.path],
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def from_json(d: dict) -> "InclusionProof":
|
|
102
|
+
return InclusionProof(
|
|
103
|
+
leaf=d["leaf"],
|
|
104
|
+
index=int(d["index"]),
|
|
105
|
+
path=[ProofStep.from_json(s) for s in d["path"]],
|
|
106
|
+
hash_name=d.get("hash", HASH_NAME),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def verify_proof(leaf: str, path: list[ProofStep], root: str) -> bool:
|
|
111
|
+
"""Recompute the root from ``leaf`` and ``path``; True iff it equals ``root``."""
|
|
112
|
+
h = leaf
|
|
113
|
+
for step in path:
|
|
114
|
+
h = _hash_pair(step.sibling, h) if step.on_right else _hash_pair(h, step.sibling)
|
|
115
|
+
return h == root
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# --- the tree / store ------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class MerkleCAS:
|
|
122
|
+
"""Content-addressed store with a Merkle commitment over its chunks.
|
|
123
|
+
|
|
124
|
+
Odd layers duplicate the final node (Bitcoin-style), so a proof always exists for
|
|
125
|
+
every leaf. Construction is deterministic in the chunk order.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
chunks: list[str]
|
|
129
|
+
leaves: list[str] = field(default_factory=list)
|
|
130
|
+
layers: list[list[str]] = field(default_factory=list)
|
|
131
|
+
_index_of_hash: dict[str, int] = field(default_factory=dict)
|
|
132
|
+
|
|
133
|
+
def __post_init__(self) -> None:
|
|
134
|
+
if not self.chunks:
|
|
135
|
+
raise ValueError("MerkleCAS requires at least one chunk")
|
|
136
|
+
self.leaves = [hash_text(c) for c in self.chunks]
|
|
137
|
+
# first occurrence wins if a chunk repeats
|
|
138
|
+
for i, h in enumerate(self.leaves):
|
|
139
|
+
self._index_of_hash.setdefault(h, i)
|
|
140
|
+
self.layers = self._build(self.leaves)
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def _build(leaves: list[str]) -> list[list[str]]:
|
|
144
|
+
layers = [leaves[:]]
|
|
145
|
+
while len(layers[-1]) > 1:
|
|
146
|
+
cur = layers[-1]
|
|
147
|
+
nxt = [
|
|
148
|
+
_hash_pair(cur[i], cur[i + 1] if i + 1 < len(cur) else cur[i])
|
|
149
|
+
for i in range(0, len(cur), 2)
|
|
150
|
+
]
|
|
151
|
+
layers.append(nxt)
|
|
152
|
+
return layers
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def root(self) -> str:
|
|
156
|
+
return self.layers[-1][0]
|
|
157
|
+
|
|
158
|
+
def index_of(self, chunk_or_hash: str) -> Optional[int]:
|
|
159
|
+
"""Index of a chunk by its raw text or by its hash; None if absent."""
|
|
160
|
+
if chunk_or_hash in self._index_of_hash:
|
|
161
|
+
return self._index_of_hash[chunk_or_hash]
|
|
162
|
+
return self._index_of_hash.get(hash_text(chunk_or_hash))
|
|
163
|
+
|
|
164
|
+
def contains(self, chunk_or_hash: str) -> bool:
|
|
165
|
+
return self.index_of(chunk_or_hash) is not None
|
|
166
|
+
|
|
167
|
+
def prove_index(self, idx: int) -> InclusionProof:
|
|
168
|
+
if not 0 <= idx < len(self.leaves):
|
|
169
|
+
raise IndexError(idx)
|
|
170
|
+
path: list[ProofStep] = []
|
|
171
|
+
i = idx
|
|
172
|
+
for layer in self.layers[:-1]:
|
|
173
|
+
sib = i ^ 1 if (i ^ 1) < len(layer) else i # duplicated node is its own sibling
|
|
174
|
+
path.append(ProofStep(sibling=layer[sib], on_right=bool(i & 1)))
|
|
175
|
+
i //= 2
|
|
176
|
+
return InclusionProof(leaf=self.leaves[idx], index=idx, path=path)
|
|
177
|
+
|
|
178
|
+
def prove(self, chunk_or_hash: str) -> Optional[InclusionProof]:
|
|
179
|
+
"""Inclusion proof for a chunk (by text or hash); None if not in the corpus."""
|
|
180
|
+
idx = self.index_of(chunk_or_hash)
|
|
181
|
+
return None if idx is None else self.prove_index(idx)
|
vitnify/capability.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Userspace, kernel-free capability broker. Containment by construction: every
|
|
2
|
+
side-effecting tool call routes through here, and a tool the run wasn't granted is
|
|
3
|
+
simply unreachable -- a jailbreak that reaches an ungranted tool hits a wall, not a
|
|
4
|
+
classifier. Every call (allow or deny) is recorded to the event log.
|
|
5
|
+
|
|
6
|
+
SAFE BY DEFAULT (0.4.0). The broker **redacts**: it commits a SALTED hash of each
|
|
7
|
+
tool's args/results into the receipt and keeps the cleartext in an org-held
|
|
8
|
+
:class:`~vitnify.redact.Vault` (``broker.vault``), so PHI/secrets never enter the
|
|
9
|
+
signed record -- on allow AND deny. Disclose one event at a time with an inclusion
|
|
10
|
+
proof (see :mod:`vitnify.redact`).
|
|
11
|
+
|
|
12
|
+
Pass ``allow_cleartext=True`` to record payloads in cleartext instead (the pre-0.4.0
|
|
13
|
+
behaviour) -- ONLY where the arguments and results are not sensitive; a redacted log
|
|
14
|
+
is the safe choice for regulated data.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
import hashlib
|
|
18
|
+
import os
|
|
19
|
+
from .events import EventLog, Kind
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _rh(x) -> str:
|
|
23
|
+
return hashlib.blake2b(repr(x).encode(), digest_size=16).hexdigest()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Broker:
|
|
27
|
+
def __init__(self, capabilities, tools: dict, log: EventLog, *, vault=None,
|
|
28
|
+
allow_cleartext: bool = False, replay=None):
|
|
29
|
+
self.caps = set(capabilities)
|
|
30
|
+
self.tools = tools
|
|
31
|
+
self.log = log
|
|
32
|
+
self.replay = replay # if set: recorded ALLOW results to re-inject
|
|
33
|
+
self.allow_cleartext = allow_cleartext
|
|
34
|
+
if allow_cleartext:
|
|
35
|
+
self.vault = None
|
|
36
|
+
else:
|
|
37
|
+
from .redact import Vault # lazy: avoids an import cycle (redact imports Broker)
|
|
38
|
+
self.vault = vault if vault is not None else Vault()
|
|
39
|
+
|
|
40
|
+
def call(self, tool: str, *args):
|
|
41
|
+
args = list(args)
|
|
42
|
+
allowed = tool in self.caps
|
|
43
|
+
|
|
44
|
+
if self.allow_cleartext:
|
|
45
|
+
# OPT-OUT: record payloads in cleartext (only for non-sensitive data).
|
|
46
|
+
if not allowed:
|
|
47
|
+
self.log.append(Kind.TOOL_CALL, {"tool": tool, "args": args, "decision": "DENY"})
|
|
48
|
+
return False, None
|
|
49
|
+
result = self.replay.pop(0) if self.replay is not None else self.tools[tool](*args)
|
|
50
|
+
self.log.append(Kind.TOOL_CALL, {"tool": tool, "args": args, "decision": "ALLOW",
|
|
51
|
+
"result": result, "result_hash": _rh(result)})
|
|
52
|
+
return True, result
|
|
53
|
+
|
|
54
|
+
# SAFE DEFAULT: commit SALTED hashes; cleartext goes to the vault, not the receipt.
|
|
55
|
+
from .redact import _commit
|
|
56
|
+
idx = len(self.log)
|
|
57
|
+
asalt = os.urandom(16)
|
|
58
|
+
if not allowed:
|
|
59
|
+
self.log.append(Kind.TOOL_CALL,
|
|
60
|
+
{"tool": tool, "args_commit": _commit(asalt, args), "decision": "DENY"})
|
|
61
|
+
self.vault.put(idx, args_salt=asalt, args=args)
|
|
62
|
+
return False, None
|
|
63
|
+
result = self.replay.pop(0) if self.replay is not None else self.tools[tool](*args)
|
|
64
|
+
rsalt = os.urandom(16)
|
|
65
|
+
self.log.append(Kind.TOOL_CALL,
|
|
66
|
+
{"tool": tool, "args_commit": _commit(asalt, args),
|
|
67
|
+
"decision": "ALLOW", "result_commit": _commit(rsalt, result)})
|
|
68
|
+
self.vault.put(idx, args_salt=asalt, args=args, result_salt=rsalt, result=result)
|
|
69
|
+
return True, result
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def recorded_tool_results(log: EventLog):
|
|
73
|
+
"""ALLOW results for a replay, from a CLEARTEXT-mode log. For a redacted log the
|
|
74
|
+
results are in the vault -- use :func:`vitnify.redact.recorded_tool_results(log, vault)`."""
|
|
75
|
+
return [e.payload["result"] for e in log.events
|
|
76
|
+
if e.kind == Kind.TOOL_CALL.value and e.payload.get("decision") == "ALLOW"
|
|
77
|
+
and "result" in e.payload]
|
vitnify/certificate.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""vitnify-receipt v1 -- the signed, self-verifying execution receipt.
|
|
2
|
+
|
|
3
|
+
One receipt binds an agent run into a single object:
|
|
4
|
+
|
|
5
|
+
program hash | granted capabilities | hash-chained event log (Merkle root + head)
|
|
6
|
+
| the engine's per-step model-computation digests
|
|
7
|
+
|
|
8
|
+
so an action can never be detached from the computation and context that produced
|
|
9
|
+
it. The receipt digest is BLAKE3 over a canonical body (matching the vitni-tensor
|
|
10
|
+
engine and the event log); the receipt is signed with ed25519 and embeds its own
|
|
11
|
+
public key, so ANYONE can verify it offline with no model, no network, and no secret.
|
|
12
|
+
|
|
13
|
+
Two verification levels:
|
|
14
|
+
* level 1 (integrity): recompute the Merkle root, chain head, and event count from
|
|
15
|
+
the raw events, and check the ed25519 signature. No model required.
|
|
16
|
+
* level 2 (recomputation): re-run each bound model step through vitni-tensor and
|
|
17
|
+
confirm every model_digest reproduces bit-for-bit.
|
|
18
|
+
|
|
19
|
+
verify_certificate() implements level 1 and is fully independent. HMAC signing is
|
|
20
|
+
retained only as a fallback for environments without an asymmetric key.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
import os, sys, json, hmac
|
|
24
|
+
from dataclasses import dataclass, field, asdict
|
|
25
|
+
from .events import EventLog, Kind
|
|
26
|
+
|
|
27
|
+
from ._vendor.pck.cas import MerkleCAS # noqa: E402
|
|
28
|
+
|
|
29
|
+
FORMAT = "vitnify-receipt v2"
|
|
30
|
+
# A published receipt format is frozen; older receipts must stay verifiable across a
|
|
31
|
+
# verifier upgrade. The verifier accepts every version it has ever issued and
|
|
32
|
+
# reconstructs each receipt's signed body per its own `v` (see body()).
|
|
33
|
+
SUPPORTED_FORMATS = frozenset({"vitnify-receipt v1", FORMAT})
|
|
34
|
+
|
|
35
|
+
# The tool decisions that count as GATED -- the call passed through the capability wall
|
|
36
|
+
# (allow) or was refused by it (deny). Any other label ("observed", ...) is watch-only,
|
|
37
|
+
# not enforcement. This is the SINGLE SOURCE OF TRUTH for the containment predicate:
|
|
38
|
+
# verify_certificate and the human-facing viewer both call it, so the rule cannot drift
|
|
39
|
+
# between the dict a machine checks and the page a person reads.
|
|
40
|
+
GATED_DECISIONS = frozenset({"allow", "deny"})
|
|
41
|
+
|
|
42
|
+
# The mandatory integrity checks folded into `integrity_ok` -- always produced by
|
|
43
|
+
# verify_certificate. Module-level so a test can assert the verifier produces every one
|
|
44
|
+
# (a dropped key would otherwise weaken integrity_ok silently). `program_matches` is
|
|
45
|
+
# NOT here: it is optional (only when `program=` is supplied).
|
|
46
|
+
_INTEGRITY_KEYS = ("format", "kinds_known", "root_matches", "head_matches", "count_matches",
|
|
47
|
+
"model_digests_match", "sig_valid", "caps_consistent", "fields_match_version")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def decision_is_gated(decision) -> bool:
|
|
51
|
+
"""True if a tool decision was gated by the capability wall (allow/deny), not merely
|
|
52
|
+
observed. Case- and whitespace-insensitive; fails closed on anything unrecognised."""
|
|
53
|
+
return str(decision).strip().lower() in GATED_DECISIONS
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _now_iso() -> str:
|
|
57
|
+
"""Issuer-asserted UTC timestamp (second precision). Places the receipt in time
|
|
58
|
+
and, with the nonce, makes every receipt unique. NOT a trusted timestamp -- an
|
|
59
|
+
RFC 3161 token or the Verification Authority countersignature is what makes the
|
|
60
|
+
time non-repudiable; this defeats silent replay, not a determined backdater."""
|
|
61
|
+
from datetime import datetime, timezone
|
|
62
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
63
|
+
|
|
64
|
+
# BLAKE3 is a hard dependency and DEFINES the wire format: a blake2b digest is a
|
|
65
|
+
# different value, so there is no silent stdlib fallback (see events.py). A missing
|
|
66
|
+
# blake3 fails loudly rather than producing incompatible digests.
|
|
67
|
+
import blake3 as _blake3
|
|
68
|
+
def _digest32(b: bytes) -> str:
|
|
69
|
+
return _blake3.blake3(b).hexdigest()
|
|
70
|
+
|
|
71
|
+
# `cryptography` is imported lazily inside the ed25519 helpers, so HMAC-only signing
|
|
72
|
+
# works without it installed. (blake3, above, is a hard dependency -- there is no
|
|
73
|
+
# hash-free path; the module will not import without it.)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _canon(o) -> str:
|
|
77
|
+
return json.dumps(o, sort_keys=True, separators=(",", ":"))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def gen_ed25519():
|
|
81
|
+
"""Return (private_key, public_key_hex). Persist the private key to sign future runs."""
|
|
82
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
83
|
+
priv = Ed25519PrivateKey.generate()
|
|
84
|
+
return priv, priv.public_key().public_bytes_raw().hex()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def derive_program_hash(program, root=None) -> str:
|
|
88
|
+
"""Derive a ``program_hash`` from the ACTUAL program, so the receipt binds what ran
|
|
89
|
+
instead of a caller-asserted label -- ``"literally anything I type"`` no longer
|
|
90
|
+
verifies against real code.
|
|
91
|
+
|
|
92
|
+
``program`` is a file path, an iterable of paths, or raw ``bytes``. Returns
|
|
93
|
+
``"sha256:<hex>"``. Pass the same value to :func:`issue_certificate` at issue time
|
|
94
|
+
and to :func:`verify_certificate` (``program=``) at verify time.
|
|
95
|
+
|
|
96
|
+
INJECTIVE by construction, matching the tier-1 digest discipline ("fields are
|
|
97
|
+
length-prefixed so concatenation is injective"): each entry binds its
|
|
98
|
+
**length-prefixed relative path** then its **length-prefixed content**. A raw
|
|
99
|
+
``\\x00`` delimiter is unsafe because file content can contain ``\\x00`` (two files
|
|
100
|
+
then collide with one crafted file); a length prefix cannot. Entries are sorted by
|
|
101
|
+
**relative path** -- a TOTAL order, unlike basename, so a program with several
|
|
102
|
+
``__init__.py`` is argument-order-independent -- and the relative path is bound, so
|
|
103
|
+
moving a file between directories changes the hash. Pass ``root`` to control what the
|
|
104
|
+
paths are relative to; otherwise paths are bound as given (pass relative paths for a
|
|
105
|
+
machine-independent hash).
|
|
106
|
+
"""
|
|
107
|
+
import hashlib
|
|
108
|
+
import struct
|
|
109
|
+
|
|
110
|
+
def _lp(b: bytes) -> bytes: # length-prefixed: injective concatenation
|
|
111
|
+
return struct.pack(">Q", len(b)) + b
|
|
112
|
+
|
|
113
|
+
hsh = hashlib.sha256()
|
|
114
|
+
if isinstance(program, (bytes, bytearray)):
|
|
115
|
+
hsh.update(_lp(b"")) # anonymous single blob: empty relative path
|
|
116
|
+
hsh.update(_lp(bytes(program)))
|
|
117
|
+
else:
|
|
118
|
+
paths = [program] if isinstance(program, (str, os.PathLike)) else list(program)
|
|
119
|
+
entries = []
|
|
120
|
+
for p in paths:
|
|
121
|
+
rel = os.path.relpath(str(p), root) if root is not None else str(p)
|
|
122
|
+
rel = rel.replace(os.sep, "/") # stable across platforms
|
|
123
|
+
with open(p, "rb") as f:
|
|
124
|
+
entries.append((rel, f.read()))
|
|
125
|
+
for rel, data in sorted(entries, key=lambda e: e[0]): # total order by rel path
|
|
126
|
+
hsh.update(_lp(rel.encode("utf-8")))
|
|
127
|
+
hsh.update(_lp(data))
|
|
128
|
+
return "sha256:" + hsh.hexdigest()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class ExecutionCertificate:
|
|
133
|
+
program_hash: str
|
|
134
|
+
capabilities: list
|
|
135
|
+
event_root: str
|
|
136
|
+
n_events: int
|
|
137
|
+
head_hash: str
|
|
138
|
+
model_digests: list = field(default_factory=list) # engine per-step digests, bound by the log
|
|
139
|
+
issued_at: str | None = None # ISO-8601 UTC, issuer-asserted (see _now_iso)
|
|
140
|
+
nonce: str | None = None # random per receipt -- makes each receipt unique, non-replayable
|
|
141
|
+
run_id: str | None = None # identifies THIS run; distinct runs get distinct receipts
|
|
142
|
+
v: str = FORMAT
|
|
143
|
+
sig: str | None = None
|
|
144
|
+
sig_alg: str = "none"
|
|
145
|
+
pubkey: str | None = None # ed25519 public key (hex) -- verifier needs no secret
|
|
146
|
+
|
|
147
|
+
def body(self) -> dict:
|
|
148
|
+
# Everything here is signed, and the body is VERSIONED: v2+ additionally
|
|
149
|
+
# binds issued_at/nonce/run_id (which place the receipt in time and make it
|
|
150
|
+
# unique). Reconstructing the body per the receipt's OWN `v` is what lets a
|
|
151
|
+
# v2 verifier still verify a receipt signed under v1 -- a published format is
|
|
152
|
+
# frozen, so older receipts must remain verifiable.
|
|
153
|
+
b = {"v": self.v, "program_hash": self.program_hash,
|
|
154
|
+
"capabilities": sorted(self.capabilities), "event_root": self.event_root,
|
|
155
|
+
"n_events": self.n_events, "head_hash": self.head_hash,
|
|
156
|
+
"model_digests": list(self.model_digests)}
|
|
157
|
+
if self.v != "vitnify-receipt v1":
|
|
158
|
+
b["issued_at"] = self.issued_at
|
|
159
|
+
b["nonce"] = self.nonce
|
|
160
|
+
b["run_id"] = self.run_id
|
|
161
|
+
return b
|
|
162
|
+
|
|
163
|
+
def digest(self) -> str:
|
|
164
|
+
return _digest32(_canon(self.body()).encode())
|
|
165
|
+
|
|
166
|
+
def sign_ed25519(self, priv) -> "ExecutionCertificate":
|
|
167
|
+
self.sig = priv.sign(bytes.fromhex(self.digest())).hex()
|
|
168
|
+
self.pubkey = priv.public_key().public_bytes_raw().hex()
|
|
169
|
+
self.sig_alg = "ed25519"
|
|
170
|
+
return self
|
|
171
|
+
|
|
172
|
+
def sign_hmac(self, key: bytes) -> "ExecutionCertificate":
|
|
173
|
+
import hashlib
|
|
174
|
+
self.sig = hmac.new(key, self.digest().encode(), hashlib.blake2b).hexdigest()
|
|
175
|
+
self.sig_alg = "hmac-blake2b"
|
|
176
|
+
return self
|
|
177
|
+
|
|
178
|
+
def to_json(self) -> str:
|
|
179
|
+
return _canon(asdict(self))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def issue_certificate(program_hash, capabilities, log: EventLog, priv=None, key: bytes | None = None,
|
|
183
|
+
run_id: str | None = None):
|
|
184
|
+
"""Build and sign a vitnify-receipt for a completed run.
|
|
185
|
+
|
|
186
|
+
The model-computation digests are read from the log's llm_call events, so the
|
|
187
|
+
receipt binds exactly the model steps that were recorded. `model_digests` is
|
|
188
|
+
EMPTY for a run with no recomputable model step (a hosted API, or a torch /
|
|
189
|
+
non-deterministic backend) -- such a receipt is integrity-only: it binds the
|
|
190
|
+
transcript, not the computation.
|
|
191
|
+
|
|
192
|
+
`program_hash` is CALLER-ASSERTED -- the receipt binds whatever string you pass;
|
|
193
|
+
it does not itself hash your agent's code. Pass a hash of the exact program or
|
|
194
|
+
config that ran if you want the receipt to pin it.
|
|
195
|
+
|
|
196
|
+
Each receipt is stamped with an issuer-asserted UTC time, a random nonce, and a
|
|
197
|
+
run id (pass `run_id` to set your own; a random one is used otherwise) so distinct
|
|
198
|
+
runs produce distinct, time-placeable receipts. ed25519 (self-verifying) is the
|
|
199
|
+
canonical signing path; pass `priv`.
|
|
200
|
+
"""
|
|
201
|
+
if not log.events:
|
|
202
|
+
raise ValueError("cannot issue a receipt for an empty event log: record at "
|
|
203
|
+
"least one event first (there is nothing to commit)")
|
|
204
|
+
cas = MerkleCAS(log.chunks())
|
|
205
|
+
cert = ExecutionCertificate(
|
|
206
|
+
program_hash, sorted(set(capabilities)), cas.root, len(log), log.head(),
|
|
207
|
+
model_digests=log.model_digests(),
|
|
208
|
+
issued_at=_now_iso(), nonce=os.urandom(16).hex(),
|
|
209
|
+
run_id=run_id if run_id is not None else os.urandom(16).hex(),
|
|
210
|
+
)
|
|
211
|
+
if priv is not None:
|
|
212
|
+
cert.sign_ed25519(priv)
|
|
213
|
+
elif key is not None:
|
|
214
|
+
cert.sign_hmac(key)
|
|
215
|
+
return cert, cas
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def verify_certificate(cert: ExecutionCertificate, log: EventLog, key: bytes | None = None,
|
|
219
|
+
pinned_pubkeys=None, program=None, require_authority: bool = True) -> dict:
|
|
220
|
+
"""Level-1 verification: recompute everything from the raw events; trust nothing.
|
|
221
|
+
|
|
222
|
+
Fails CLOSED. A receipt is only `ok` if a signature was actually checked and
|
|
223
|
+
passed: an unsigned receipt (`sig_alg="none"`), an unknown algorithm, or an
|
|
224
|
+
HMAC receipt presented without its key all yield `sig_valid=False`, so a
|
|
225
|
+
receipt that cannot be cryptographically verified can never report `ok=True`.
|
|
226
|
+
|
|
227
|
+
ed25519 receipts verify against their OWN embedded public key -- no secret
|
|
228
|
+
required -- but that proves signer continuity, not signer authority. Pass
|
|
229
|
+
`pinned_pubkeys` (an allow-list of hex ed25519 keys) to additionally require
|
|
230
|
+
the receipt was signed by a key you trust. (Level 2 -- recomputing each
|
|
231
|
+
model_digest through vitni-tensor -- is a separate step that additionally
|
|
232
|
+
needs the model weights.)
|
|
233
|
+
"""
|
|
234
|
+
# An empty event log has no Merkle commitment, so no legitimately issued receipt
|
|
235
|
+
# carries one (issue_certificate commits >=1 event). A verifier must FAIL CLOSED on
|
|
236
|
+
# such degenerate/hostile input, never crash: compute the root only when there are
|
|
237
|
+
# events and treat its absence as a non-match, instead of letting MerkleCAS raise.
|
|
238
|
+
cas_root = MerkleCAS(log.chunks()).root if log.events else None
|
|
239
|
+
# Every event kind must be recognised. The semantic checks below filter events
|
|
240
|
+
# by an EXACT kind match, so an unknown kind ("TOOL_CALL", "toolcall", ...) would
|
|
241
|
+
# slip past them silently -- the same self-declared-label class as an unknown
|
|
242
|
+
# sig_alg or an unknown decision string. Fail closed here so the whole class is
|
|
243
|
+
# gone: an unrecognised kind (or decision, or algorithm, or format) never verifies.
|
|
244
|
+
known_kinds = {k.value for k in Kind}
|
|
245
|
+
checks = {
|
|
246
|
+
"format": cert.v in SUPPORTED_FORMATS,
|
|
247
|
+
"kinds_known": all(e.kind in known_kinds for e in log.events),
|
|
248
|
+
"root_matches": cas_root is not None and cas_root == cert.event_root,
|
|
249
|
+
"head_matches": log.head() == cert.head_hash,
|
|
250
|
+
"count_matches": len(log) == cert.n_events,
|
|
251
|
+
"model_digests_match": log.model_digests() == list(cert.model_digests),
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# Signature -- fail closed. Only an actually-checked, passing signature sets
|
|
255
|
+
# this True; "none", an unknown alg, or keyless HMAC all leave it False, so an
|
|
256
|
+
# unverifiable receipt can never reach ok=True.
|
|
257
|
+
sig_valid = False
|
|
258
|
+
if cert.sig_alg == "ed25519" and cert.sig and cert.pubkey:
|
|
259
|
+
try:
|
|
260
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
261
|
+
Ed25519PublicKey.from_public_bytes(bytes.fromhex(cert.pubkey)).verify(
|
|
262
|
+
bytes.fromhex(cert.sig), bytes.fromhex(cert.digest()))
|
|
263
|
+
sig_valid = True
|
|
264
|
+
except Exception:
|
|
265
|
+
sig_valid = False
|
|
266
|
+
elif cert.sig_alg == "hmac-blake2b" and cert.sig and key is not None:
|
|
267
|
+
import hashlib
|
|
268
|
+
sig_valid = hmac.compare_digest(
|
|
269
|
+
cert.sig, hmac.new(key, cert.digest().encode(), hashlib.blake2b).hexdigest())
|
|
270
|
+
checks["sig_valid"] = sig_valid
|
|
271
|
+
|
|
272
|
+
# Capability consistency -- the receipt must PROVE no ungranted tool executed,
|
|
273
|
+
# not just carry a capability list. Every tool call must be within the declared,
|
|
274
|
+
# signed-over capability set OR a CLEAN DENIAL (decision exactly "deny",
|
|
275
|
+
# case/space-insensitive, with no result). Keying only off decision=="allow"
|
|
276
|
+
# let a forged log slip an ungranted, result-bearing call through by relabelling
|
|
277
|
+
# the decision ("PERMIT", " allow", omitted...) -- F11. Failing closed on
|
|
278
|
+
# anything that is not a clean denial removes that, whatever string it carries.
|
|
279
|
+
granted = set(cert.capabilities)
|
|
280
|
+
|
|
281
|
+
def _clean_denial(p: dict) -> bool:
|
|
282
|
+
# A denial is clean ONLY if it carries no result key AT ALL. This is a
|
|
283
|
+
# security boundary, not ergonomics: the most dangerous tool shape is a
|
|
284
|
+
# side-effecting call that returns None (send_email, wire_transfer,
|
|
285
|
+
# delete_record). An UNGRANTED one that actually executed and is logged
|
|
286
|
+
# {"decision":"deny","result":None} must NOT pass -- yet an explicit None
|
|
287
|
+
# and an absent key are indistinguishable by value, so accepting None (the
|
|
288
|
+
# 0.2.13 loosening, now reverted) let a real side effect masquerade as a
|
|
289
|
+
# block through any non-Broker wrapper. Every enforced deny site already
|
|
290
|
+
# OMITS the key; a genuine block never writes it. If the ergonomics are
|
|
291
|
+
# wanted later, the sound form is a POSITIVE Broker assertion
|
|
292
|
+
# (``executed: false``), never an absence you cannot distinguish from a
|
|
293
|
+
# None return. Guarded by tests/test_attack_matrix.py::...none_result.
|
|
294
|
+
return (str(p.get("decision", "")).strip().lower() == "deny"
|
|
295
|
+
and "result" not in p and "result_hash" not in p)
|
|
296
|
+
|
|
297
|
+
checks["caps_consistent"] = all(
|
|
298
|
+
e.payload.get("tool") in granted or _clean_denial(e.payload)
|
|
299
|
+
for e in log.events
|
|
300
|
+
if e.kind == Kind.TOOL_CALL.value
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# A verified receipt must carry NO data field its version does not sign. The v1
|
|
304
|
+
# body binds none of the v2 issuance fields, so a v1 receipt with issued_at /
|
|
305
|
+
# nonce / run_id set is carrying an unsigned, forgeable value (e.g. a backdated
|
|
306
|
+
# issued_at) -- reject it, so ok=True never blesses data outside the signature.
|
|
307
|
+
checks["fields_match_version"] = not (
|
|
308
|
+
cert.v == "vitnify-receipt v1"
|
|
309
|
+
and any(x is not None for x in (cert.issued_at, cert.nonce, cert.run_id)))
|
|
310
|
+
|
|
311
|
+
# Program binding -- an INTEGRITY check (computable offline, no trust root).
|
|
312
|
+
# `program_hash` is caller-asserted by default; supplying `program` (the ACTUAL code:
|
|
313
|
+
# a path, iterable of paths, or bytes) recomputes derive_program_hash and CONFIRMS the
|
|
314
|
+
# receipt names that program, turning a self-asserted label into a checked binding.
|
|
315
|
+
if program is not None:
|
|
316
|
+
checks["program_matches"] = (cert.program_hash == derive_program_hash(program))
|
|
317
|
+
|
|
318
|
+
# -------------------------------------------------------------- the verdict, SPLIT
|
|
319
|
+
# A receipt answers TWO distinct questions, and collapsing them into one boolean is
|
|
320
|
+
# wrong for somebody either way -- 0.3.x let a forgery read ok=True; 0.4.0 made an
|
|
321
|
+
# honest receipt read ok=False to a stranger with no trust root, indistinguishable
|
|
322
|
+
# from tampering. Keep them distinct:
|
|
323
|
+
#
|
|
324
|
+
# integrity_ok -- is this transcript internally consistent and validly signed by
|
|
325
|
+
# WHOEVER signed it (root/head/count, signature, caps, program)?
|
|
326
|
+
# Answerable by ANYONE, offline, with no model and no secret.
|
|
327
|
+
# authority_ok -- was the signer an APPROVED runtime? Needs a trust root, so it is
|
|
328
|
+
# True / False / None(unestablished). A stranger offline can never
|
|
329
|
+
# answer it; None must NOT be reported as a bare False that reads as
|
|
330
|
+
# "forged".
|
|
331
|
+
#
|
|
332
|
+
# The spec keeps signer pinning OPTIONAL (for integrity); this adds an explicit
|
|
333
|
+
# authority verdict on top, so the two never contradict.
|
|
334
|
+
# Mandatory integrity checks -- ALWAYS produced above. A missing one is a bug, so
|
|
335
|
+
# default to False (fail CLOSED, never open): a check silently dropped in a refactor,
|
|
336
|
+
# or a typo in this tuple, makes integrity_ok False, and the honest-receipt tests then
|
|
337
|
+
# fail LOUDLY rather than a hole passing quietly. Coverage is asserted in
|
|
338
|
+
# tests/test_safe_defaults.py::test_integrity_tuple_is_fully_produced.
|
|
339
|
+
integ = all(checks.get(k, False) for k in _INTEGRITY_KEYS)
|
|
340
|
+
if "program_matches" in checks: # optional: only when `program=` was supplied
|
|
341
|
+
integ = integ and checks["program_matches"]
|
|
342
|
+
checks["integrity_ok"] = integ
|
|
343
|
+
|
|
344
|
+
if pinned_pubkeys is not None:
|
|
345
|
+
authority_ok = bool(sig_valid and cert.sig_alg == "ed25519"
|
|
346
|
+
and cert.pubkey in set(pinned_pubkeys))
|
|
347
|
+
else:
|
|
348
|
+
authority_ok = None # no trust root -> UNESTABLISHED, not False
|
|
349
|
+
checks["authority_ok"] = authority_ok
|
|
350
|
+
checks["authority"] = ("verified" if authority_ok is True else
|
|
351
|
+
"rejected: signer not on the pinned allow-list" if authority_ok is False else
|
|
352
|
+
"unestablished: no trust root supplied (pass pinned_pubkeys)")
|
|
353
|
+
checks["signer_pinned"] = authority_ok is True # back-compat alias
|
|
354
|
+
|
|
355
|
+
# Enforcement vs observation (separate property, never folded into ok). An "allow"/
|
|
356
|
+
# "deny" decision was GATED by the wall; any other value ("observed", ...) was merely
|
|
357
|
+
# WATCHED, so the receipt is a tamper-evident transcript, not proof of containment.
|
|
358
|
+
checks["containment_enforced"] = all(
|
|
359
|
+
decision_is_gated(e.payload.get("decision"))
|
|
360
|
+
for e in log.events
|
|
361
|
+
if e.kind == Kind.TOOL_CALL.value
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# `ok` = integrity AND an authorised signer (default). require_authority=False makes
|
|
365
|
+
# `ok` the integrity-only verdict (authority is still reported, just not required) --
|
|
366
|
+
# the answer a stranger CAN compute offline.
|
|
367
|
+
checks["ok"] = bool(checks["integrity_ok"] and (authority_ok is True if require_authority else True))
|
|
368
|
+
return checks
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def verify_authorized(cert: ExecutionCertificate, log: EventLog, pinned_pubkeys, **kw) -> dict:
|
|
372
|
+
"""Production verification that requires an AUTHORISED signer.
|
|
373
|
+
|
|
374
|
+
`verify_certificate` proves integrity and signer *continuity* from a receipt's own
|
|
375
|
+
embedded key -- but a forger can re-sign an edited receipt with their OWN key and it
|
|
376
|
+
still self-verifies (the honest limitation, kept as an xfail in the attack matrix).
|
|
377
|
+
Authority requires anchoring the signer to keys you trust. This wrapper FAILS CLOSED
|
|
378
|
+
unless `pinned_pubkeys` is a non-empty allow-list AND the signer is on it, so a
|
|
379
|
+
re-signed forgery can never report `ok=True`, and neither can a call that simply
|
|
380
|
+
forgot to pin. Use this -- not the bare verifier -- anywhere signer authority matters.
|
|
381
|
+
|
|
382
|
+
The allow-list is the software anchor; the strongest form pins a TPM/enclave-resident
|
|
383
|
+
key so the private key cannot leave an approved runtime (future hardening).
|
|
384
|
+
"""
|
|
385
|
+
if not pinned_pubkeys:
|
|
386
|
+
return {"ok": False, "signer_pinned": False, "containment_enforced": False,
|
|
387
|
+
"error": "no pinned signer allow-list: signer authority cannot be established"}
|
|
388
|
+
return verify_certificate(cert, log, pinned_pubkeys=pinned_pubkeys, **kw)
|