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
4 changes: 3 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ const receipt = executeAccountableIntent(intent, {
```

### `verifyExecutionReceipt(receipt)`
Recomputes receipt integrity and verifies bound runtime artifacts.
Recomputes receipt integrity and verifies bound runtime artifacts. A receipt
missing its audit stream or attention ledger, or carrying a non-array audit,
cannot verify even if its outer hash is recalculated.

### `signExecutionReceipt(receipt, privateKeyPem, options?)`
Adds an Ed25519 attestation to a valid receipt. Rejects private keys of other algorithm types.
Expand Down
10 changes: 5 additions & 5 deletions compiler/accountablePipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,9 @@ export function verifyExecutionReceipt(receipt) {
const outputHashValid = typeof receipt.selected_render?.html === "string" && sha256(receipt.selected_render.html) === receipt.output_sha256;
const selectedRenderCountsValid = decisions !== null && Number.isInteger(receipt.selected_render?.allowed) && Number.isInteger(receipt.selected_render?.suppressed) && receipt.selected_render.allowed === decisions.filter(item => item?.render_allowed).length && receipt.selected_render.suppressed === decisions.filter(item => !item?.render_allowed).length;

let audit = { verified: true };
let auditHashValid = true;
let runtimeAuditFlagValid = true;
let audit = { verified: false };
let auditHashValid = false;
let runtimeAuditFlagValid = false;
if (receipt.runtime_audit_stream) {
try {
audit = verifyAuditStream(receipt.runtime_audit_stream);
Expand All @@ -141,8 +141,8 @@ export function verifyExecutionReceipt(receipt) {
}
}

let ledgerHashValid = true;
let ledgerStructureValid = true;
let ledgerHashValid = false;
let ledgerStructureValid = false;
if (receipt.attention_ledger) {
try {
ledgerHashValid = sha256(receipt.attention_ledger) === receipt.attention_ledger_sha256;
Expand Down
3 changes: 2 additions & 1 deletion docs/ACCOUNTABLE_AI_PIPELINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ The resulting execution receipt binds together:
- counterfactual policy simulations
- selected composed-policy decision
- final browser output
- hash-chained runtime audit stream and cumulative attention ledger
- SHA-256 hashes for each major stage
- optional Ed25519 signature

Expand Down Expand Up @@ -73,7 +74,7 @@ That makes policy differences inspectable instead of silently embedding them in

## Receipt integrity

`receipt_sha256` covers the unsigned execution receipt. A mutation to intent, context, simulations, decisions, AML source, or output causes `verifyExecutionReceipt()` to fail.
`receipt_sha256` covers the unsigned execution receipt. A mutation to intent, context, simulations, decisions, AML source, or output causes `verifyExecutionReceipt()` to fail. Verification also requires the runtime audit stream and attention ledger and checks their hashes and structure. Recalculating an outer receipt hash cannot make missing evidence or a non-array audit pass.

The receipt can additionally be signed with Ed25519, providing a detached identity/attestation layer on top of content integrity. Signing rejects RSA and EC private keys, and verification requires an actual Ed25519 public key and one canonical 64-byte Ed25519 signature encoding. A trusted fingerprint still has to come from outside the receipt.

Expand Down
2 changes: 1 addition & 1 deletion docs/EVIDENCE_CAPSULE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ 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. 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. RSA and EC signatures mislabeled as Ed25519, noncanonical signature base64, 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. Receipts with a missing audit stream, missing ledger, or non-array audit entries must fail even when a test signer recalculates the receipt signature and capsule digests. RSA and EC signatures mislabeled as Ed25519, noncanonical signature base64, 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.

Expand Down
5 changes: 5 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,14 @@ export interface ExecutionReceipt {
context: AmlContext;
intent_sha256: string;
aml_sha256: string;
audit_stream_sha256: string;
attention_ledger_sha256: string;
output_sha256: string;
receipt_sha256: string;
intent: AmlIntent;
runtime_audit_stream: { protocol: "ĀML Runtime Audit Stream"; entries: unknown[]; [key: string]: unknown };
runtime_audit_verified: boolean;
attention_ledger: { protocol: "ĀML Attention Ledger"; entries: unknown[]; [key: string]: unknown };
selected_render: {
policy_id: string;
allowed: number;
Expand Down
1 change: 1 addition & 0 deletions runtime/auditStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function appendAuditEvent(stream, event, options = {}) {

export function verifyAuditStream(stream) {
if (!stream || stream.protocol !== "ĀML Runtime Audit Stream") throw new Error("Invalid ĀML runtime audit stream.");
if (!Array.isArray(stream.entries)) throw new Error("ĀML runtime audit entries must be an array.");
const checks = [];
let previous = null;
for (let i = 0; i < stream.entries.length; i++) {
Expand Down
26 changes: 25 additions & 1 deletion schema/execution-receipt.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/aruintelligence/aml-core/blob/main/schema/execution-receipt.schema.json",
"title": "ĀML Accountable Execution Receipt",
"description": "Machine-readable contract for intent-to-render accountability receipts emitted by the ĀML v1.2 research pipeline.",
"description": "Machine-readable contract for intent-to-render accountability receipts emitted by the ĀML pipeline. Runtime audit and attention evidence must be present for verification.",
"type": "object",
"additionalProperties": true,
"required": [
Expand All @@ -14,11 +14,16 @@
"intent_sha256",
"aml_sha256",
"simulation_sha256",
"audit_stream_sha256",
"attention_ledger_sha256",
"output_sha256",
"decision_sha256",
"intent",
"aml_source",
"simulations",
"runtime_audit_stream",
"runtime_audit_verified",
"attention_ledger",
"selected_render",
"receipt_sha256"
],
Expand All @@ -39,12 +44,31 @@
"intent_sha256": { "$ref": "#/$defs/sha256" },
"aml_sha256": { "$ref": "#/$defs/sha256" },
"simulation_sha256": { "$ref": "#/$defs/sha256" },
"audit_stream_sha256": { "$ref": "#/$defs/sha256" },
"attention_ledger_sha256": { "$ref": "#/$defs/sha256" },
"output_sha256": { "$ref": "#/$defs/sha256" },
"decision_sha256": { "$ref": "#/$defs/sha256" },
"receipt_sha256": { "$ref": "#/$defs/sha256" },
"intent": { "type": "object" },
"aml_source": { "type": "string" },
"simulations": { "type": "object" },
"runtime_audit_stream": {
"type": "object",
"required": ["protocol", "entries"],
"properties": {
"protocol": { "const": "ĀML Runtime Audit Stream" },
"entries": { "type": "array" }
}
},
"runtime_audit_verified": { "type": "boolean" },
"attention_ledger": {
"type": "object",
"required": ["protocol", "entries"],
"properties": {
"protocol": { "const": "ĀML Attention Ledger" },
"entries": { "type": "array" }
}
},
"selected_render": {
"type": "object",
"required": ["policy_id", "allowed", "suppressed", "decisions", "html"],
Expand Down
26 changes: 24 additions & 2 deletions scripts/check-evidence-capsule-python.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ function receiptWithLedger(base, ledger) {
return updated;
}

function resign(receipt, privateKey) {
receipt.attention_ledger_sha256 = hashValue(receipt.attention_ledger);
function signModifiedReceipt(receipt, privateKey) {
const payload = { ...receipt };
delete payload.receipt_sha256;
delete payload.signature;
Expand All @@ -54,6 +53,11 @@ function resign(receipt, privateKey) {
signature.signature_base64 = crypto.sign(null, Buffer.from(stableStringify(material)), privateKey).toString("base64");
}

function resign(receipt, privateKey) {
receipt.attention_ledger_sha256 = hashValue(receipt.attention_ledger);
signModifiedReceipt(receipt, privateKey);
}

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));
Expand Down Expand Up @@ -96,6 +100,24 @@ try {
const signedPolicy = { trusted_receipt_fingerprints: [signed.signature.public_key_sha256] };
results.push(check("signed-external-trust-required", signedCapsule, false));
results.push(check("signed-valid", signedCapsule, true, signedPolicy));
for (const field of ["runtime_audit_stream", "attention_ledger"]) {
const omitted = structuredClone(signedCapsule);
delete omitted.receipt[field];
signModifiedReceipt(omitted.receipt, privateKey);
omitted.summary.receipt_sha256 = omitted.receipt.receipt_sha256;
rehash(omitted);
if (verifyEvidenceCapsule(omitted).verified) throw new Error(`JavaScript accepted a receipt missing ${field}`);
results.push(check(`signed-missing-${field}-rejected`, omitted, false, signedPolicy, field));
}
const nonArrayAudit = structuredClone(signedCapsule);
nonArrayAudit.receipt.runtime_audit_stream.entries = "";
nonArrayAudit.receipt.audit_stream_sha256 = hashValue(nonArrayAudit.receipt.runtime_audit_stream);
signModifiedReceipt(nonArrayAudit.receipt, privateKey);
nonArrayAudit.summary.receipt_sha256 = nonArrayAudit.receipt.receipt_sha256;
rehash(nonArrayAudit);
if (verifyEvidenceCapsule(nonArrayAudit).verified) throw new Error("JavaScript accepted non-array audit entries");
results.push(check("signed-non-array-audit-rejected", nonArrayAudit, false, signedPolicy,
null, { receipt_hash: true, audit: false, signature: true, summary: true }));
const alteredEncoding = structuredClone(signedCapsule);
alteredEncoding.receipt.signature.signature_base64 += "!!!!";
rehash(alteredEncoding);
Expand Down
30 changes: 30 additions & 0 deletions test/accountable-pipeline.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,36 @@ test("receipt verification rejects re-hashed audit verification flag mismatch",
assert.equal(verification.verified, false);
});

test("receipt verification and signing require present audit and attention evidence", () => {
const { privateKey } = crypto.generateKeyPairSync("ed25519");
const pem = privateKey.export({ type: "pkcs8", format: "pem" });
for (const [field, validity] of [
["runtime_audit_stream", "audit_stream_valid"],
["attention_ledger", "attention_ledger_valid"]
]) {
const receipt = freshReceipt();
delete receipt[field];
refreshOuterReceiptHash(receipt);
const verification = verifyExecutionReceipt(receipt);
assert.equal(verification.receipt_hash_valid, true);
assert.equal(verification[validity], false);
assert.equal(verification.verified, false);
assert.throws(() => signExecutionReceipt(receipt, pem), /invalid ĀML execution receipt/);
}
});

test("receipt verification rejects a non-array audit even when its hashes are refreshed", () => {
const receipt = freshReceipt();
receipt.runtime_audit_stream.entries = "";
receipt.audit_stream_sha256 = sha256(receipt.runtime_audit_stream);
refreshOuterReceiptHash(receipt);

const verification = verifyExecutionReceipt(receipt);
assert.equal(verification.receipt_hash_valid, true);
assert.equal(verification.audit_stream_valid, false);
assert.equal(verification.verified, false);
});

test("receipt verification rejects a structurally invalid ledger even when its hashes are refreshed", () => {
const receipt = freshReceipt();
const entry = receipt.attention_ledger.entries[0];
Expand Down
Loading