Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ dist/
.env
*.log
coverage/
__pycache__/
*.pyc
4 changes: 2 additions & 2 deletions docs/EVIDENCE_CAPSULE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ python3 independent/python/verify_evidence_capsule.py capsule.json
python3 independent/python/verify_evidence_capsule.py signed-capsule.json trusted-policy.json
```

Unsigned capsules need one argument. Signed v1.1 capsules require a second JSON file with `trusted_receipt_fingerprints` (an array of SHA-256 SPKI DER fingerprints) and optional `revoked_receipt_fingerprints`. The embedded key is not a trust source. Legacy v1.0 signatures are unsupported in Python because their signer and signed time were not bound. The two implementations and their mutation cases are exercised with `node scripts/check-evidence-capsule-python.mjs` on CI, which publishes a JSON result artifact. Signed receipts with small and large exponents, boundary integers, the maximum finite JavaScript number, array-index-like keys, UTF-16-ordered keys, and escaped lone surrogates are checked there; changed numerical values, escaped duplicate keys, and non-finite numbers must fail. This is cross-runtime engineering evidence maintained by the project, not an independent external witness.
Unsigned capsules need one argument. Signed v1.1 capsules require a second JSON file with `trusted_receipt_fingerprints` (an array of SHA-256 SPKI DER fingerprints) and optional `revoked_receipt_fingerprints`. The embedded key is not a trust source. Legacy v1.0 signatures are unsupported in Python because their signer and signed time were not bound. The two implementations and their mutation cases are exercised with `node scripts/check-evidence-capsule-python.mjs` on CI, which publishes a JSON result artifact. Signed receipts with small and large exponents, boundary integers, the maximum finite JavaScript number, array-index-like keys, UTF-16-ordered keys, and escaped lone surrogates are checked there. A locally assembled, JavaScript-valid signed ledger also checks that three charges totaling beyond the safe-integer boundary use binary64 arithmetic. Boolean and negative-zero balance mutations must fail, while a negative-zero sequence spelling that JavaScript accepts must verify. Changed numerical values, escaped duplicate keys, and non-finite numbers must fail. This is cross-runtime engineering evidence maintained by the project, not an independent external witness.

For a saved response from the locked HTTP deployment endpoint, the [HTTP exchange pilot](../pilots/enterprise-http-gate/README.md#recheck-a-real-http-exchange-with-python) additionally checks host-owned expected inputs, nonce, HTTP envelope and effective decision in Python while reusing this signed capsule verifier. The capsule alone does not make those request and response claims.

## Frozen v1 contract

The capsule has exactly six top-level keys: `protocol`, `canonicalization`, `claim_boundary`, `summary`, `receipt`, and `digests`. The digest material consists of the first five keys, serialized with `canonicalJSONStringify` from `protocol/canonicalJson.js`: ordinary JSON values, object keys sorted by JavaScript UTF-16 code-unit order before `JSON.stringify`, no whitespace, JSON string escaping, and JSON number serialization. JavaScript enumerates array-index keys numerically ahead of other keys during that final serialization. The receipt's internal hashes use a separate, hand-built sorted JSON rule that keeps those keys in UTF-16 lexical order. The Python verifier reproduces both byte orders without changing either frozen JavaScript hash. `undefined`, non-finite numbers, executable values, and non-plain objects are rejected. Hash the capsule payload's UTF-8 bytes with SHA-256 and SHA-512 as lowercase hexadecimal.

The receipt's own v1.1 verification remains a separate contract. Both digest algorithms are required in this capsule version. New algorithms and formats should get a new version and test vectors; do not reinterpret old bytes. Do not overwrite an old capsule when migrating: retain it and record a new, separately verifiable link to its exact digest. The [renewal chain](EVIDENCE_RENEWAL.md) implements one such link with a verifier-owned trust policy. No verifier here establishes an external timestamp or independent validation. The Python implementation handles tested finite fractions, large integers and exponents through JavaScript's maximum finite number, rendering shortest round-trip digits at JavaScript's fixed/exponent boundary. JSON integer tokens above the safe-integer limit are rounded to binary64 before verification, as in JavaScript: distinct decimal spellings can denote the same signed Number. Represent precision-critical identifiers and amounts as strings with a separate validation rule. Non-finite numbers are rejected. The verifier also reproduces tested UTF-16 key order, array-index enumeration, and JSON escaping of lone surrogates. This is a tested interoperability subset, not a proof of parity for every JavaScript number or arbitrary source string. Supplied JSON with duplicate keys is rejected before verification.
The receipt's own v1.1 verification remains a separate contract. Both digest algorithms are required in this capsule version. New algorithms and formats should get a new version and test vectors; do not reinterpret old bytes. Do not overwrite an old capsule when migrating: retain it and record a new, separately verifiable link to its exact digest. The [renewal chain](EVIDENCE_RENEWAL.md) implements one such link with a verifier-owned trust policy. No verifier here establishes an external timestamp or independent validation. The Python implementation handles tested finite fractions, large integers and exponents through JavaScript's maximum finite number, rendering shortest round-trip digits at JavaScript's fixed/exponent boundary. JSON integer tokens above the safe-integer limit are rounded to binary64 before verification, as in JavaScript: distinct decimal spellings can denote the same signed Number. Attention-ledger arithmetic rounds after each binary64 operation; its before, after, and remaining balances use JavaScript `Object.is` semantics, including the sign of zero. Represent precision-critical identifiers and amounts as strings with a separate validation rule. Non-finite numbers are rejected. The verifier also reproduces tested UTF-16 key order, array-index enumeration, and JSON escaping of lone surrogates. This is a tested interoperability subset, not a proof of parity for every JavaScript number or arbitrary source string. Supplied JSON with duplicate keys is rejected before verification.

## Why preserve both views?

Expand Down
46 changes: 35 additions & 11 deletions independent/python/verify_evidence_capsule.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ def reject_constant(value):


def parse_js_int(raw):
# JSON.parse represents every JSON number as binary64. Preserve small integers
# for Python structural checks; round larger decimal tokens to the same value.
# JSON.parse represents every JSON number as binary64. Preserve small positive
# and negative integers for structural checks, but retain the sign of -0.
if raw == "-0":
return -0.0
number = int(raw)
if abs(number) <= MAX_SAFE_INTEGER:
return number
Expand Down Expand Up @@ -129,6 +131,11 @@ def hash_text(value):
return hashlib.sha256(value.encode("utf-8")).hexdigest()


def js_integer(value):
# Number.isInteger(-0) is true; Python's int type check would reject it.
return type(value) is int or (type(value) is float and math.isfinite(value) and value.is_integer())


def verify_signature(receipt, policy):
if not isinstance(policy, dict):
raise ValueError("external_receipt_trust_required")
Expand Down Expand Up @@ -175,21 +182,38 @@ def valid_ledger(ledger):
budget = ledger.get("initial_budget")
if budget is not None and (type(budget) not in (int, float) or budget < 0):
return False
remaining, consumed = budget, 0

def same_number(actual, expected, *, object_is=False):
# JS uses === for consumed values and Object.is for before/after/remaining.
# Python's True == 1 and -0.0 == 0.0 must not silently change that contract.
if actual is None or expected is None:
return actual is None and expected is None
if type(actual) not in (int, float) or type(expected) not in (int, float):
return False
if object_is and actual == expected == 0:
return math.copysign(1, actual) == math.copysign(1, expected)
return actual == expected

# JavaScript adds and subtracts binary64 Numbers, even when each input is a
# safe integer. Python integer arithmetic would diverge after the sum crosses
# 2**53, rejecting an otherwise valid signed receipt.
remaining, consumed = (None if budget is None else float(budget)), 0.0
for index, entry in enumerate(ledger["entries"]):
amount = entry.get("amount_requested")
if type(amount) not in (int, float) or amount < 0:
return False
allowed = budget is None or amount <= remaining
charged = amount if allowed else 0
after = None if budget is None else max(0, remaining - charged)
if (type(entry.get("sequence")) is not int or entry.get("sequence") != index or entry.get("allowed") is not allowed or
entry.get("budget_before") != remaining or entry.get("amount_consumed") != charged or
entry.get("budget_after") != after):
charged = float(amount) if allowed else 0.0
after = None if budget is None else max(0.0, remaining - charged)
if (not js_integer(entry.get("sequence")) or entry.get("sequence") != index or entry.get("allowed") is not allowed or
not same_number(entry.get("budget_before"), remaining, object_is=True) or
not same_number(entry.get("amount_consumed"), charged) or
not same_number(entry.get("budget_after"), after, object_is=True)):
return False
consumed += charged
remaining = after
return ledger.get("consumed") == consumed and ledger.get("remaining") == remaining
return (same_number(ledger.get("consumed"), consumed) and
same_number(ledger.get("remaining"), remaining, object_is=True))


def valid_audit(stream):
Expand All @@ -198,7 +222,7 @@ def valid_audit(stream):
previous = None
for index, entry in enumerate(stream["entries"]):
core = {key: entry[key] for key in ("sequence", "timestamp", "event_type", "payload", "previous_hash")}
if type(entry["sequence"]) is not int or entry["sequence"] != index or entry["previous_hash"] != previous or entry["entry_hash"] != hash_value(core):
if not js_integer(entry["sequence"]) or entry["sequence"] != index or entry["previous_hash"] != previous or entry["entry_hash"] != hash_value(core):
return False
previous = entry["entry_hash"]
return True
Expand Down Expand Up @@ -230,7 +254,7 @@ def verify(capsule, policy=None):
"simulation": hash_value(receipt["simulations"]) == receipt["simulation_sha256"],
"decision": hash_value(decisions) == receipt["decision_sha256"],
"output": hash_text(selected["html"]) == receipt["output_sha256"],
"counts": type(selected["allowed"]) is int and type(selected["suppressed"]) is int
"counts": js_integer(selected["allowed"]) and js_integer(selected["suppressed"])
and selected["allowed"] == sum(item["render_allowed"] is True for item in decisions)
and selected["suppressed"] == sum(item["render_allowed"] is not True for item in decisions),
"audit": hash_value(receipt["runtime_audit_stream"]) == receipt["audit_stream_sha256"]
Expand Down
80 changes: 78 additions & 2 deletions scripts/check-evidence-capsule-python.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { executeAccountableIntent, signExecutionReceipt } from "../compiler/accountablePipeline.js";
import { executeAccountableIntent, signExecutionReceipt, verifyExecutionReceipt } from "../compiler/accountablePipeline.js";
import { createAttentionLedger, consumeAttention } from "../runtime/attentionLedger.js";
import { createEvidenceCapsule } from "../runtime/evidenceCapsule.js";
import { canonicalJSONStringify } from "../protocol/canonicalJson.js";

Expand All @@ -18,7 +19,42 @@ function rehash(capsule) {
capsule.digests = Object.fromEntries(["sha256", "sha512"].map(algorithm => [algorithm, crypto.createHash(algorithm).update(bytes).digest("hex")]));
}

function check(name, content, expected, policy = null, reasonIncludes = null) {
function stableStringify(value) {
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
if (value && typeof value === "object") {
return `{${Object.keys(value).sort().filter(key => value[key] !== undefined)
.map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
}
return JSON.stringify(value);
}

function hashValue(value) {
return crypto.createHash("sha256").update(stableStringify(value)).digest("hex");
}

function receiptWithLedger(base, ledger) {
const updated = structuredClone(base);
updated.attention_ledger = ledger;
updated.attention_ledger_sha256 = hashValue(ledger);
const payload = { ...updated };
delete payload.receipt_sha256;
updated.receipt_sha256 = hashValue(payload);
return updated;
}

function resign(receipt, privateKey) {
receipt.attention_ledger_sha256 = hashValue(receipt.attention_ledger);
const payload = { ...receipt };
delete payload.receipt_sha256;
delete payload.signature;
receipt.receipt_sha256 = hashValue(payload);
const { signature } = receipt;
const material = { protocol: "ĀML Execution Receipt Attestation", version: "1.1", algorithm: "Ed25519",
receipt_sha256: receipt.receipt_sha256, signer: signature.signer, signed_at: signature.signed_at };
signature.signature_base64 = crypto.sign(null, Buffer.from(stableStringify(material)), privateKey).toString("base64");
}

function check(name, content, expected, policy = null, reasonIncludes = null, expectedChecks = null) {
const file = path.join(directory, `${name}.json`);
fs.writeFileSync(file, typeof content === "string" ? content : JSON.stringify(content));
const args = [verifier, file];
Expand All @@ -32,6 +68,7 @@ function check(name, content, expected, policy = null, reasonIncludes = null) {
const report = JSON.parse(result.stdout);
if (report.verified !== expected || result.status !== (expected ? 0 : 1) ||
(reasonIncludes && !report.reason?.includes(reasonIncludes)) ||
(expectedChecks && Object.entries(expectedChecks).some(([key, value]) => report.checks?.[key] !== value)) ||
(expected && policy && report.attribution_bound !== true)) {
throw new Error(`${name}: unexpected Python result ${result.stdout} ${result.stderr}`);
}
Expand Down Expand Up @@ -104,6 +141,45 @@ try {
const largeBudgetSignature = signExecutionReceipt(largeBudgetReceipt, privateKey.export({ type: "pkcs8", format: "pem" }),
{ signer: "synthetic", timestamp: "2026-10-10T01:00:00.000Z" });
results.push(check("signed-large-attention-budget", createEvidenceCapsule(largeBudgetSignature), true, signedPolicy));
const roundingLedger = createAttentionLedger(null);
for (const amount of [Number.MAX_SAFE_INTEGER, 1, 1]) consumeAttention(roundingLedger, amount);
if (roundingLedger.consumed !== 2 ** 53) throw new Error("Ledger rounding fixture changed");
const roundingReceipt = receiptWithLedger(receipt, roundingLedger);
if (!verifyExecutionReceipt(roundingReceipt).verified) throw new Error("JavaScript rejected the rounding fixture");
const roundingSignature = signExecutionReceipt(roundingReceipt, privateKey.export({ type: "pkcs8", format: "pem" }),
{ signer: "synthetic", timestamp: "2026-10-10T01:00:00.000Z" });
results.push(check("signed-ledger-binary64-sum", createEvidenceCapsule(roundingSignature), true, signedPolicy));
const roundingSource = JSON.stringify(createEvidenceCapsule(roundingSignature));
const negativeZeroSequence = roundingSource.replace('"sequence":0', '"sequence":-0');
if (negativeZeroSequence === roundingSource ||
!verifyExecutionReceipt(JSON.parse(negativeZeroSequence).receipt).verified) {
throw new Error("Negative-zero sequence fixture changed JavaScript verification");
}
results.push(check("signed-negative-zero-sequence", negativeZeroSequence, true, signedPolicy));

const oneLedger = createAttentionLedger(1);
const oneSignature = signExecutionReceipt(receiptWithLedger(receipt, oneLedger),
privateKey.export({ type: "pkcs8", format: "pem" }),
{ signer: "synthetic", timestamp: "2026-10-10T01:00:00.000Z" });
const forgedBalance = createEvidenceCapsule(oneSignature);
forgedBalance.receipt.attention_ledger.remaining = true;
resign(forgedBalance.receipt, privateKey);
forgedBalance.summary.receipt_sha256 = forgedBalance.receipt.receipt_sha256;
rehash(forgedBalance);
if (verifyExecutionReceipt(forgedBalance.receipt).verified) throw new Error("JavaScript accepted a boolean balance");
results.push(check("signed-ledger-boolean-balance-rejected", forgedBalance, false, signedPolicy,
null, { receipt_hash: true, signature: true, ledger: false, summary: true }));

const zeroSignature = signExecutionReceipt(receiptWithLedger(receipt, createAttentionLedger(0)),
privateKey.export({ type: "pkcs8", format: "pem" }),
{ signer: "synthetic", timestamp: "2026-10-10T01:00:00.000Z" });
const zeroSource = JSON.stringify(createEvidenceCapsule(zeroSignature));
const negativeZero = zeroSource.replace('"remaining":0', '"remaining":-0');
if (negativeZero === zeroSource || verifyExecutionReceipt(JSON.parse(negativeZero).receipt).verified) {
throw new Error("Negative-zero balance fixture does not diverge from JavaScript");
}
results.push(check("signed-ledger-negative-zero-balance-rejected", negativeZero, false, signedPolicy,
null, { receipt_hash: true, signature: true, ledger: false, summary: true }));
for (const [name, key_probe] of [
["array-index-keys", { "10": "ten", "2": "two", "01": "leading", "0": "zero",
"4294967294": "last index", "4294967295": "ordinary key" }],
Expand Down
Loading