Authensor's audit trail uses cryptographic receipt chains to create tamper-evident logs. This article explains the technical details: how receipts are constructed, how the chain is formed, and how verification works.
Each receipt is a structured record:
interface Receipt {
id: string; // Unique receipt identifier
timestamp: string; // ISO 8601 timestamp
tool: string; // Tool name
args: unknown; // Tool arguments (serialized)
action: string; // Policy decision: allow, block, escalate
reason: string; // Why the decision was made
threats: Threat[]; // Aegis scan results
principal: Principal; // User and agent identity
hash: string; // SHA-256 hash of this receipt
previousHash: string; // Hash of the previous receipt
}
The hash is computed over a canonical serialization of the receipt data (excluding the hash and previousHash fields themselves):
function computeHash(receipt: Receipt): string {
const data = canonicalize({
id: receipt.id,
timestamp: receipt.timestamp,
tool: receipt.tool,
args: receipt.args,
action: receipt.action,
reason: receipt.reason,
threats: receipt.threats,
principal: receipt.principal,
previousHash: receipt.previousHash,
});
return 'sha256:' + sha256(data);
}
The canonicalize function produces a deterministic JSON representation (sorted keys, consistent formatting). This ensures that the same data always produces the same hash, regardless of key ordering.
When a new receipt is created:
previousHash to the hash of the most recent receipt (or null for the first receipt)hash fieldfunction createReceipt(data: ReceiptData, chain: Receipt[]): Receipt {
const previous = chain.length > 0 ? chain[chain.length - 1] : null;
const receipt: Receipt = {
...data,
id: generateId(),
timestamp: new Date().toISOString(),
previousHash: previous ? previous.hash : null,
hash: '', // Placeholder
};
receipt.hash = computeHash(receipt);
return receipt;
}
To verify the chain, walk through the receipts in order:
function verifyChain(receipts: Receipt[]): VerificationResult {
let expectedPreviousHash: string | null = null;
for (let i = 0; i < receipts.length; i++) {
const receipt = receipts[i];
// Check that previousHash matches
if (receipt.previousHash !== expectedPreviousHash) {
return {
valid: false,
breakAt: i,
reason: 'Previous hash mismatch',
};
}
// Recompute and check the hash
const computed = computeHash(receipt);
if (computed !== receipt.hash) {
return {
valid: false,
breakAt: i,
reason: 'Hash mismatch (data was modified)',
};
}
expectedPreviousHash = receipt.hash;
}
return { valid: true };
}
Modifying a receipt: Changing any field (tool name, args, decision) changes the hash. The stored hash no longer matches the computed hash.
Deleting a receipt: The receipt after the deleted one has a previousHash that no longer matches any existing receipt's hash.
Inserting a receipt: The inserted receipt's hash does not match the previousHash of the next receipt.
Reordering receipts: The previousHash chain breaks at every reordered boundary.
The chain proves that data has not been modified since the hash was computed. It does not prove that the data was correct when originally recorded. If the system records a wrong decision, the chain faithfully preserves that wrong decision.
The chain also does not prevent deletion of the entire chain. To protect against wholesale deletion, replicate the chain to immutable storage or a separate system.
Explore more guides on AI agent safety, prompt injection, and building secure systems.
View All Guides