189 lines
6.0 KiB
TypeScript
189 lines
6.0 KiB
TypeScript
import { assertEquals } from "jsr:@std/assert";
|
|
|
|
import {
|
|
buildMerkleTree,
|
|
leafHash,
|
|
nodeHash,
|
|
verifyInclusionProof,
|
|
} from "./audit_merkle.ts";
|
|
import { encodeHex } from "jsr:@std/encoding/hex";
|
|
|
|
Deno.test("Audit Merkle - leafHash", async () => {
|
|
const entry = new Uint8Array([0x01, 0x02, 0x03]);
|
|
const hash = await leafHash(entry);
|
|
|
|
// SHA-256(0x00 || 0x01, 0x02, 0x03)
|
|
const expectedData = new Uint8Array([0x00, 0x01, 0x02, 0x03]);
|
|
const expectedHash = await crypto.subtle.digest("SHA-256", expectedData);
|
|
|
|
assertEquals(encodeHex(hash), encodeHex(new Uint8Array(expectedHash)));
|
|
});
|
|
|
|
Deno.test("Audit Merkle - nodeHash", async () => {
|
|
const left = new Uint8Array([0x11, 0x22]);
|
|
const right = new Uint8Array([0x33, 0x44]);
|
|
const hash = await nodeHash(left, right);
|
|
|
|
// SHA-256(0x01 || 0x11, 0x22 || 0x33, 0x44)
|
|
const expectedData = new Uint8Array([0x01, 0x11, 0x22, 0x33, 0x44]);
|
|
const expectedHash = await crypto.subtle.digest("SHA-256", expectedData);
|
|
|
|
assertEquals(encodeHex(hash), encodeHex(new Uint8Array(expectedHash)));
|
|
});
|
|
|
|
Deno.test("Audit Merkle - buildMerkleTree (empty)", async () => {
|
|
const root = await buildMerkleTree([]);
|
|
const expectedHash = await crypto.subtle.digest("SHA-256", new Uint8Array(0));
|
|
assertEquals(encodeHex(root), encodeHex(new Uint8Array(expectedHash)));
|
|
});
|
|
|
|
Deno.test("Audit Merkle - buildMerkleTree (1 leaf)", async () => {
|
|
const leaf = await leafHash(new Uint8Array([0x01]));
|
|
const root = await buildMerkleTree([leaf]);
|
|
assertEquals(encodeHex(root), encodeHex(leaf));
|
|
});
|
|
|
|
Deno.test("Audit Merkle - buildMerkleTree (2 leaves)", async () => {
|
|
const leaf1 = await leafHash(new Uint8Array([0x01]));
|
|
const leaf2 = await leafHash(new Uint8Array([0x02]));
|
|
const root = await buildMerkleTree([leaf1, leaf2]);
|
|
|
|
const expectedNode = await nodeHash(leaf1, leaf2);
|
|
assertEquals(encodeHex(root), encodeHex(expectedNode));
|
|
});
|
|
|
|
Deno.test("Audit Merkle - buildMerkleTree (3 leaves)", async () => {
|
|
const leaf1 = await leafHash(new Uint8Array([0x01]));
|
|
const leaf2 = await leafHash(new Uint8Array([0x02]));
|
|
const leaf3 = await leafHash(new Uint8Array([0x03]));
|
|
|
|
const root = await buildMerkleTree([leaf1, leaf2, leaf3]);
|
|
|
|
const leftChild = await nodeHash(leaf1, leaf2);
|
|
const expectedNode = await nodeHash(leftChild, leaf3);
|
|
assertEquals(encodeHex(root), encodeHex(expectedNode));
|
|
});
|
|
|
|
import { auditLog, flush } from "./audit.ts";
|
|
import { sqlWrapper } from "./db.ts";
|
|
|
|
Deno.test("Audit Logger - Micro-batcher Flush", async () => {
|
|
const queries: any[] = [];
|
|
|
|
const originalSql = sqlWrapper.sql;
|
|
// Mock SQL
|
|
const mockSql = ((strings: any, ...values: any[]) => {
|
|
const queryStr = strings.join("?");
|
|
queries.push({ queryStr, values });
|
|
|
|
if (queryStr.includes("SELECT tree_size FROM audit_sths")) {
|
|
return Promise.resolve([{ tree_size: "0" }]);
|
|
}
|
|
|
|
if (queryStr.includes("SELECT leaf_hash FROM audit_records")) {
|
|
return Promise.resolve([
|
|
{ leaf_hash: "010203" }, // dummy hex
|
|
{ leaf_hash: "040506" },
|
|
]);
|
|
}
|
|
|
|
return Promise.resolve([]);
|
|
}) as any;
|
|
|
|
sqlWrapper.sql = mockSql;
|
|
|
|
const { valkey } = await import("./valkey.ts");
|
|
const pubCalls: any[] = [];
|
|
const setCalls: any[] = [];
|
|
|
|
const originalPub = valkey.publish;
|
|
const originalSet = valkey.set;
|
|
|
|
valkey.publish = (async (channel: string, message: string) => {
|
|
pubCalls.push({ channel, message });
|
|
return 1;
|
|
}) as any;
|
|
|
|
valkey.set = (async (key: string, value: string) => {
|
|
setCalls.push({ key, value });
|
|
return "OK";
|
|
}) as any;
|
|
|
|
await flush();
|
|
|
|
// Clean up mocks
|
|
sqlWrapper.sql = originalSql;
|
|
valkey.publish = originalPub;
|
|
valkey.set = originalSet;
|
|
|
|
// The flush should have called Valkey pub/sub and set
|
|
assertEquals(pubCalls.length, 1);
|
|
assertEquals(pubCalls[0].channel, "auth:audit:sth");
|
|
|
|
assertEquals(setCalls.length, 1);
|
|
assertEquals(setCalls[0].key, "auth:audit:latest_sth");
|
|
|
|
const payload = JSON.parse(pubCalls[0].message);
|
|
assertEquals(payload.tree_size, 2);
|
|
assertEquals(typeof payload.root_hash, "string");
|
|
assertEquals(typeof payload.signature, "string");
|
|
assertEquals(typeof payload.created_at, "string");
|
|
|
|
const insertSTHQuery = queries.find((q) =>
|
|
q.queryStr.includes("INSERT INTO audit_sths")
|
|
);
|
|
assertEquals(insertSTHQuery !== undefined, true);
|
|
});
|
|
|
|
Deno.test("Audit Logger - auditLog computes leaf hash synchronously", async () => {
|
|
const queries: any[] = [];
|
|
|
|
const originalSql = sqlWrapper.sql;
|
|
const mockSql = ((strings: any, ...values: any[]) => {
|
|
const queryStr = strings.join("?");
|
|
queries.push({ queryStr, values });
|
|
return Promise.resolve([]);
|
|
}) as any;
|
|
|
|
sqlWrapper.sql = mockSql;
|
|
|
|
auditLog("user1", "test_action", "res1", { test: 123 }, "127.0.0.1");
|
|
|
|
// Wait a small amount for the promise to resolve internally
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
|
|
sqlWrapper.sql = originalSql;
|
|
|
|
assertEquals(queries.length, 1);
|
|
const insertQuery = queries[0];
|
|
assertEquals(
|
|
insertQuery.queryStr.includes("INSERT INTO audit_records"),
|
|
true,
|
|
);
|
|
|
|
// Should have leaf_hash
|
|
assertEquals(insertQuery.queryStr.includes("leaf_hash"), true);
|
|
|
|
// The last value in the values array is the leaf_hash
|
|
const lastValue = insertQuery.values[insertQuery.values.length - 1];
|
|
assertEquals(typeof lastValue, "string");
|
|
assertEquals(lastValue.length, 64); // SHA-256 hex is 64 chars
|
|
});
|
|
|
|
Deno.test("Audit Merkle - verifyInclusionProof", async () => {
|
|
const leaf1 = await leafHash(new Uint8Array([0x01]));
|
|
const leaf2 = await leafHash(new Uint8Array([0x02]));
|
|
const leaf3 = await leafHash(new Uint8Array([0x03]));
|
|
const leaf4 = await leafHash(new Uint8Array([0x04]));
|
|
|
|
const root = await buildMerkleTree([leaf1, leaf2, leaf3, leaf4]);
|
|
|
|
const _node12 = await nodeHash(leaf1, leaf2);
|
|
const node34 = await nodeHash(leaf3, leaf4);
|
|
|
|
// Proof for leaf 1 (index 0): sibling is leaf2, then sibling is node34
|
|
const proof1 = [leaf2, node34];
|
|
const isValid1 = await verifyInclusionProof(leaf1, proof1, 0, 4, root);
|
|
assertEquals(isValid1, true);
|
|
});
|