@propxchain/core-client 0.3.0-canary.28 → 0.3.0-canary.29
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.
- package/dist/audit/auditChain.d.ts +20 -0
- package/dist/audit/auditChain.d.ts.map +1 -0
- package/dist/audit/auditChain.js +67 -0
- package/dist/audit/auditChain.js.map +1 -0
- package/dist/audit/index.d.ts +4 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +6 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/sealAuditBundle.d.ts +36 -0
- package/dist/audit/sealAuditBundle.d.ts.map +1 -0
- package/dist/audit/sealAuditBundle.js +58 -0
- package/dist/audit/sealAuditBundle.js.map +1 -0
- package/dist/audit/verifyAuditLog.d.ts +28 -0
- package/dist/audit/verifyAuditLog.d.ts.map +1 -0
- package/dist/audit/verifyAuditLog.js +106 -0
- package/dist/audit/verifyAuditLog.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/src/audit/auditChain.ts +88 -0
- package/src/audit/index.ts +19 -0
- package/src/audit/sealAuditBundle.ts +90 -0
- package/src/audit/verifyAuditLog.ts +168 -0
- package/src/index.ts +13 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Principal } from "@icp-sdk/core/principal";
|
|
2
|
+
/** Structural shape of a decoded ledger_manager AuditEvent (candid). */
|
|
3
|
+
export interface CertifiedAuditEvent {
|
|
4
|
+
eventId: bigint;
|
|
5
|
+
transactionId: string;
|
|
6
|
+
eventType: string;
|
|
7
|
+
timestamp: bigint;
|
|
8
|
+
caller: Principal;
|
|
9
|
+
details: string;
|
|
10
|
+
metadata: [] | [string];
|
|
11
|
+
}
|
|
12
|
+
/** Canonical event encoding — mirrors ledger_manager.encodeAuditEvent. */
|
|
13
|
+
export declare function encodeAuditEvent(e: CertifiedAuditEvent): Uint8Array;
|
|
14
|
+
/**
|
|
15
|
+
* Recompute a transaction's chain head from its events — mirrors
|
|
16
|
+
* ledger_manager.chainHead. Events are sorted by eventId ascending (the
|
|
17
|
+
* canister's canonical order) so the result is independent of array order.
|
|
18
|
+
*/
|
|
19
|
+
export declare function recomputeHeadHash(events: CertifiedAuditEvent[]): Promise<Uint8Array>;
|
|
20
|
+
//# sourceMappingURL=auditChain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auditChain.d.ts","sourceRoot":"","sources":["../../src/audit/auditChain.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACzB;AA0BD,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,mBAAmB,GAAG,UAAU,CAgBnE;AAOD;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,mBAAmB,EAAE,GAC5B,OAAO,CAAC,UAAU,CAAC,CASrB"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Audit hash chain — TypeScript mirror of ledger_manager's derived chain (ADR 0013).
|
|
2
|
+
//
|
|
3
|
+
// This MUST match the Motoko encoding in
|
|
4
|
+
// packages/core/src/ledger_manager/main.mo (encodeAuditEvent / chainHead)
|
|
5
|
+
// byte-for-byte, or verification will reject genuine records. The canonical
|
|
6
|
+
// encoding: each field 4-byte big-endian length-prefixed UTF-8; metadata uses
|
|
7
|
+
// a 1-byte presence tag (0 = null, 1 = present). eventHash_i =
|
|
8
|
+
// SHA-256(prevHash ++ encode(event_i)); genesis prevHash = 32 zero bytes;
|
|
9
|
+
// events ordered by eventId ascending.
|
|
10
|
+
const encoder = new TextEncoder();
|
|
11
|
+
function lenPrefixed(bytes) {
|
|
12
|
+
const n = bytes.length;
|
|
13
|
+
const out = new Uint8Array(4 + n);
|
|
14
|
+
out[0] = (n >>> 24) & 0xff;
|
|
15
|
+
out[1] = (n >>> 16) & 0xff;
|
|
16
|
+
out[2] = (n >>> 8) & 0xff;
|
|
17
|
+
out[3] = n & 0xff;
|
|
18
|
+
out.set(bytes, 4);
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
function concat(chunks) {
|
|
22
|
+
const total = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
23
|
+
const out = new Uint8Array(total);
|
|
24
|
+
let offset = 0;
|
|
25
|
+
for (const c of chunks) {
|
|
26
|
+
out.set(c, offset);
|
|
27
|
+
offset += c.length;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
/** Canonical event encoding — mirrors ledger_manager.encodeAuditEvent. */
|
|
32
|
+
export function encodeAuditEvent(e) {
|
|
33
|
+
const parts = [
|
|
34
|
+
lenPrefixed(encoder.encode(e.eventId.toString())),
|
|
35
|
+
lenPrefixed(encoder.encode(e.transactionId)),
|
|
36
|
+
lenPrefixed(encoder.encode(e.eventType)),
|
|
37
|
+
lenPrefixed(encoder.encode(e.timestamp.toString())),
|
|
38
|
+
lenPrefixed(encoder.encode(e.caller.toText())),
|
|
39
|
+
lenPrefixed(encoder.encode(e.details)),
|
|
40
|
+
];
|
|
41
|
+
if (e.metadata.length === 0) {
|
|
42
|
+
parts.push(new Uint8Array([0]));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
parts.push(new Uint8Array([1]));
|
|
46
|
+
parts.push(lenPrefixed(encoder.encode(e.metadata[0])));
|
|
47
|
+
}
|
|
48
|
+
return concat(parts);
|
|
49
|
+
}
|
|
50
|
+
async function sha256(data) {
|
|
51
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
52
|
+
return new Uint8Array(digest);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Recompute a transaction's chain head from its events — mirrors
|
|
56
|
+
* ledger_manager.chainHead. Events are sorted by eventId ascending (the
|
|
57
|
+
* canister's canonical order) so the result is independent of array order.
|
|
58
|
+
*/
|
|
59
|
+
export async function recomputeHeadHash(events) {
|
|
60
|
+
const ordered = [...events].sort((a, b) => a.eventId < b.eventId ? -1 : a.eventId > b.eventId ? 1 : 0);
|
|
61
|
+
let head = new Uint8Array(32); // genesis: 32 zero bytes
|
|
62
|
+
for (const e of ordered) {
|
|
63
|
+
head = await sha256(concat([head, encodeAuditEvent(e)]));
|
|
64
|
+
}
|
|
65
|
+
return head;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=auditChain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auditChain.js","sourceRoot":"","sources":["../../src/audit/auditChain.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { type CertifiedAuditEvent, encodeAuditEvent, recomputeHeadHash, } from "./auditChain.js";
|
|
2
|
+
export { type CertifiedTransactionLog, type VerifyResult, type VerifyOptions, verifyCertifiedAuditLog, } from "./verifyAuditLog.js";
|
|
3
|
+
export { AUDIT_SEAL_DOC_TYPE, type AuditSealBundle, buildAuditSealBundle, serializeAuditSealBundle, } from "./sealAuditBundle.js";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/audit/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,mBAAmB,EACxB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,mBAAmB,EACnB,KAAK,eAAe,EACpB,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Verifiable audit trail (ADR 0013) — chain recompute, certificate
|
|
2
|
+
// verification, and sealed proof bundles for the per-transaction audit log.
|
|
3
|
+
export { encodeAuditEvent, recomputeHeadHash, } from "./auditChain.js";
|
|
4
|
+
export { verifyCertifiedAuditLog, } from "./verifyAuditLog.js";
|
|
5
|
+
export { AUDIT_SEAL_DOC_TYPE, buildAuditSealBundle, serializeAuditSealBundle, } from "./sealAuditBundle.js";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/audit/index.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,4EAA4E;AAC5E,OAAO,EAEL,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAIL,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,mBAAmB,EAEnB,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type CertifiedTransactionLog } from "./verifyAuditLog.js";
|
|
2
|
+
export declare const AUDIT_SEAL_DOC_TYPE = "audit_seal";
|
|
3
|
+
interface SerializableAuditEvent {
|
|
4
|
+
eventId: string;
|
|
5
|
+
transactionId: string;
|
|
6
|
+
eventType: string;
|
|
7
|
+
timestamp: string;
|
|
8
|
+
caller: string;
|
|
9
|
+
details: string;
|
|
10
|
+
metadata: string | null;
|
|
11
|
+
}
|
|
12
|
+
export interface AuditSealBundle {
|
|
13
|
+
version: 1;
|
|
14
|
+
canisterId: string;
|
|
15
|
+
transactionId: string;
|
|
16
|
+
/** Client capture time (ISO). The authoritative time is inside the certificate. */
|
|
17
|
+
capturedAt: string;
|
|
18
|
+
events: SerializableAuditEvent[];
|
|
19
|
+
headHashHex: string;
|
|
20
|
+
certificateB64: string;
|
|
21
|
+
witnessB64: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build a sealed, serializable proof bundle from a certified log response.
|
|
25
|
+
* Throws if the response carries no certificate (nothing to seal).
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildAuditSealBundle(args: {
|
|
28
|
+
canisterId: string;
|
|
29
|
+
transactionId: string;
|
|
30
|
+
capturedAt: string;
|
|
31
|
+
log: CertifiedTransactionLog;
|
|
32
|
+
}): AuditSealBundle;
|
|
33
|
+
/** Serialize a bundle to bytes for on-chain storage (document_storage). */
|
|
34
|
+
export declare function serializeAuditSealBundle(bundle: AuditSealBundle): Uint8Array;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=sealAuditBundle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sealAuditBundle.d.ts","sourceRoot":"","sources":["../../src/audit/sealAuditBundle.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,KAAK,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAEnE,eAAO,MAAM,mBAAmB,eAAe,CAAC;AAEhD,UAAU,sBAAsB;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,mFAAmF;IACnF,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,sBAAsB,EAAE,CAAC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB;AAaD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,uBAAuB,CAAC;CAC9B,GAAG,eAAe,CAuBlB;AAED,2EAA2E;AAC3E,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,eAAe,GAAG,UAAU,CAE5E"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Sealed audit bundle (ADR 0013, proof model ii).
|
|
2
|
+
//
|
|
3
|
+
// A contemporaneous, self-contained snapshot of a transaction's certified
|
|
4
|
+
// audit log. Built from a getCertifiedTransactionLog response (a query, so the
|
|
5
|
+
// subnet certificate is present) — typically at completion, and regenerable on
|
|
6
|
+
// demand. The bundle verifies offline against the IC root key, even if
|
|
7
|
+
// PropXchain is gone: a forensic verifier re-runs verifyCertifiedAuditLog with
|
|
8
|
+
// a relaxed freshness window.
|
|
9
|
+
//
|
|
10
|
+
// Persistence: the serialized bundle is stored on-chain in document_storage
|
|
11
|
+
// (docType "audit_seal") per ADR 0008. That upload uses the existing document
|
|
12
|
+
// upload path and is the caller's step — buildAuditSealBundle only produces the
|
|
13
|
+
// bytes (no document bodies are embedded; events reference any documents by
|
|
14
|
+
// hash only).
|
|
15
|
+
export const AUDIT_SEAL_DOC_TYPE = "audit_seal";
|
|
16
|
+
function toHex(bytes) {
|
|
17
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
18
|
+
}
|
|
19
|
+
function toB64(bytes) {
|
|
20
|
+
// Browser + Node (>=20) both expose btoa; build a binary string first.
|
|
21
|
+
let binary = "";
|
|
22
|
+
for (const b of bytes)
|
|
23
|
+
binary += String.fromCharCode(b);
|
|
24
|
+
return btoa(binary);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build a sealed, serializable proof bundle from a certified log response.
|
|
28
|
+
* Throws if the response carries no certificate (nothing to seal).
|
|
29
|
+
*/
|
|
30
|
+
export function buildAuditSealBundle(args) {
|
|
31
|
+
const { canisterId, transactionId, capturedAt, log } = args;
|
|
32
|
+
if (log.certificate.length === 0) {
|
|
33
|
+
throw new Error("cannot seal: certified log response has no certificate");
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
version: 1,
|
|
37
|
+
canisterId,
|
|
38
|
+
transactionId,
|
|
39
|
+
capturedAt,
|
|
40
|
+
events: log.events.map((e) => ({
|
|
41
|
+
eventId: e.eventId.toString(),
|
|
42
|
+
transactionId: e.transactionId,
|
|
43
|
+
eventType: e.eventType,
|
|
44
|
+
timestamp: e.timestamp.toString(),
|
|
45
|
+
caller: e.caller.toText(),
|
|
46
|
+
details: e.details,
|
|
47
|
+
metadata: e.metadata.length === 0 ? null : e.metadata[0],
|
|
48
|
+
})),
|
|
49
|
+
headHashHex: toHex(log.headHash),
|
|
50
|
+
certificateB64: toB64(log.certificate[0]),
|
|
51
|
+
witnessB64: toB64(log.witness),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Serialize a bundle to bytes for on-chain storage (document_storage). */
|
|
55
|
+
export function serializeAuditSealBundle(bundle) {
|
|
56
|
+
return new TextEncoder().encode(JSON.stringify(bundle));
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=sealAuditBundle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sealAuditBundle.js","sourceRoot":"","sources":["../../src/audit/sealAuditBundle.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,EAAE;AACF,0EAA0E;AAC1E,+EAA+E;AAC/E,+EAA+E;AAC/E,uEAAuE;AACvE,+EAA+E;AAC/E,8BAA8B;AAC9B,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,gFAAgF;AAChF,4EAA4E;AAC5E,cAAc;AAId,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAAC;AAwBhD,SAAS,KAAK,CAAC,KAAiB;IAC9B,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED,SAAS,KAAK,CAAC,KAAiB;IAC9B,uEAAuE;IACvE,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACxD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAKpC;IACC,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5D,IAAI,GAAG,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO;QACL,OAAO,EAAE,CAAC;QACV,UAAU;QACV,aAAa;QACb,UAAU;QACV,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC7B,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE;YAC7B,aAAa,EAAE,CAAC,CAAC,aAAa;YAC9B,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,QAAQ,EAAE;YACjC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE;YACzB,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;SACzD,CAAC,CAAC;QACH,WAAW,EAAE,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC;QAChC,cAAc,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QACzC,UAAU,EAAE,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;KAC/B,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,wBAAwB,CAAC,MAAuB;IAC9D,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;AAC1D,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type CertifiedAuditEvent } from "./auditChain.js";
|
|
2
|
+
/** Decoded ledger_manager.getCertifiedTransactionLog response. */
|
|
3
|
+
export interface CertifiedTransactionLog {
|
|
4
|
+
events: CertifiedAuditEvent[];
|
|
5
|
+
headHash: Uint8Array;
|
|
6
|
+
certificate: [] | [Uint8Array];
|
|
7
|
+
witness: Uint8Array;
|
|
8
|
+
}
|
|
9
|
+
export interface VerifyResult {
|
|
10
|
+
verified: boolean;
|
|
11
|
+
reason?: string;
|
|
12
|
+
eventCount: number;
|
|
13
|
+
}
|
|
14
|
+
export interface VerifyOptions {
|
|
15
|
+
canisterId: string;
|
|
16
|
+
/** IC root public key — the mainnet constant, or `agent.rootKey` for local. */
|
|
17
|
+
rootKey: Uint8Array;
|
|
18
|
+
transactionId: string;
|
|
19
|
+
log: CertifiedTransactionLog;
|
|
20
|
+
/**
|
|
21
|
+
* Certificate freshness window. Omit for live verification (5 min). For
|
|
22
|
+
* forensic verification of a sealed bundle, pass a large value (e.g.
|
|
23
|
+
* Number.MAX_SAFE_INTEGER) to accept a historical certificate.
|
|
24
|
+
*/
|
|
25
|
+
maxCertificateTimeOffsetMs?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function verifyCertifiedAuditLog(opts: VerifyOptions): Promise<VerifyResult>;
|
|
28
|
+
//# sourceMappingURL=verifyAuditLog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifyAuditLog.d.ts","sourceRoot":"","sources":["../../src/audit/verifyAuditLog.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,KAAK,mBAAmB,EAAqB,MAAM,iBAAiB,CAAC;AAiC9E,kEAAkE;AAClE,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,QAAQ,EAAE,UAAU,CAAC;IACrB,WAAW,EAAE,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;IAC/B,OAAO,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,OAAO,EAAE,UAAU,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,GAAG,EAAE,uBAAuB,CAAC;IAC7B;;;;OAIG;IACH,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAiBD,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,YAAY,CAAC,CAkEvB"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Verifier for the certified per-transaction audit log (ADR 0013).
|
|
2
|
+
//
|
|
3
|
+
// Proves a transaction's audit trail is genuine canister state against the IC
|
|
4
|
+
// root key, trusting no part of PropXchain:
|
|
5
|
+
// 1. verify the subnet certificate + Merkle witness;
|
|
6
|
+
// 2. read the certified headHash for the transaction from the witness tree;
|
|
7
|
+
// 3. confirm the response's stated head matches the certified head;
|
|
8
|
+
// 4. recompute the chain from the events and confirm it matches.
|
|
9
|
+
//
|
|
10
|
+
// Uses the @dfinity/* family (agent, principal, certificate-verification)
|
|
11
|
+
// throughout — that is the lineage certificate-verification depends on, so the
|
|
12
|
+
// HashTree/Principal types line up. The rest of core-client uses @icp-sdk;
|
|
13
|
+
// this leaf module's boundary is plain Uint8Array/string, so the split is
|
|
14
|
+
// contained.
|
|
15
|
+
//
|
|
16
|
+
// Live verification keeps the certificate-freshness window tight. Forensic
|
|
17
|
+
// verification of a sealed historical bundle (ADR 0013, proof model ii)
|
|
18
|
+
// relaxes it by design — the bundle proves a fact as-of the certificate's
|
|
19
|
+
// /time, not a current one.
|
|
20
|
+
import { lookup_path, lookupResultToBuffer } from "@dfinity/agent";
|
|
21
|
+
import { Principal } from "@dfinity/principal";
|
|
22
|
+
import { recomputeHeadHash } from "./auditChain.js";
|
|
23
|
+
let cachedVerify;
|
|
24
|
+
async function loadVerifyCertification() {
|
|
25
|
+
if (cachedVerify)
|
|
26
|
+
return cachedVerify;
|
|
27
|
+
const mod = (await import("@dfinity/certificate-verification"));
|
|
28
|
+
const fn = mod.verifyCertification ?? mod.default?.verifyCertification;
|
|
29
|
+
if (!fn) {
|
|
30
|
+
throw new Error("verifyCertification not found in @dfinity/certificate-verification");
|
|
31
|
+
}
|
|
32
|
+
cachedVerify = fn;
|
|
33
|
+
return fn;
|
|
34
|
+
}
|
|
35
|
+
const FIVE_MINUTES_MS = 5 * 60 * 1000;
|
|
36
|
+
function toArrayBuffer(u8) {
|
|
37
|
+
const copy = new Uint8Array(u8.length);
|
|
38
|
+
copy.set(u8);
|
|
39
|
+
return copy.buffer;
|
|
40
|
+
}
|
|
41
|
+
function equalBytes(a, b) {
|
|
42
|
+
if (a.length !== b.length)
|
|
43
|
+
return false;
|
|
44
|
+
let diff = 0;
|
|
45
|
+
for (let i = 0; i < a.length; i++)
|
|
46
|
+
diff |= a[i] ^ b[i];
|
|
47
|
+
return diff === 0;
|
|
48
|
+
}
|
|
49
|
+
export async function verifyCertifiedAuditLog(opts) {
|
|
50
|
+
const { log, transactionId } = opts;
|
|
51
|
+
const eventCount = log.events.length;
|
|
52
|
+
if (log.certificate.length === 0) {
|
|
53
|
+
return { verified: false, reason: "no certificate in response", eventCount };
|
|
54
|
+
}
|
|
55
|
+
// 1. Verify the subnet certificate + witness against the IC root key.
|
|
56
|
+
let tree;
|
|
57
|
+
try {
|
|
58
|
+
const verifyCertification = await loadVerifyCertification();
|
|
59
|
+
tree = await verifyCertification({
|
|
60
|
+
canisterId: Principal.fromText(opts.canisterId),
|
|
61
|
+
encodedCertificate: toArrayBuffer(log.certificate[0]),
|
|
62
|
+
encodedTree: toArrayBuffer(log.witness),
|
|
63
|
+
rootKey: toArrayBuffer(opts.rootKey),
|
|
64
|
+
maxCertificateTimeOffsetMs: opts.maxCertificateTimeOffsetMs ?? FIVE_MINUTES_MS,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return {
|
|
69
|
+
verified: false,
|
|
70
|
+
reason: `certificate verification failed: ${String(err)}`,
|
|
71
|
+
eventCount,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// 2. Read the certified headHash for this transaction from the witness tree.
|
|
75
|
+
// The path segment is the UTF-8 bytes of the transactionId — exactly the key
|
|
76
|
+
// ledger_manager certified (Text.encodeUtf8(transactionId)).
|
|
77
|
+
const lookup = lookup_path([toArrayBuffer(new TextEncoder().encode(transactionId))], tree);
|
|
78
|
+
const certifiedBuf = lookupResultToBuffer(lookup);
|
|
79
|
+
if (!certifiedBuf) {
|
|
80
|
+
return {
|
|
81
|
+
verified: false,
|
|
82
|
+
reason: "transaction head not present in certified tree",
|
|
83
|
+
eventCount,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const certifiedHead = new Uint8Array(certifiedBuf);
|
|
87
|
+
// 3. The response's stated head must match the certified value.
|
|
88
|
+
if (!equalBytes(certifiedHead, log.headHash)) {
|
|
89
|
+
return {
|
|
90
|
+
verified: false,
|
|
91
|
+
reason: "response headHash does not match certified value",
|
|
92
|
+
eventCount,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// 4. The head recomputed from the events must match the certified head.
|
|
96
|
+
const recomputed = await recomputeHeadHash(log.events);
|
|
97
|
+
if (!equalBytes(recomputed, certifiedHead)) {
|
|
98
|
+
return {
|
|
99
|
+
verified: false,
|
|
100
|
+
reason: "recomputed chain head does not match certified head — events tampered",
|
|
101
|
+
eventCount,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return { verified: true, eventCount };
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=verifyAuditLog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifyAuditLog.js","sourceRoot":"","sources":["../../src/audit/verifyAuditLog.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { PropXchainClient } from "./client.js";
|
|
|
2
2
|
export { MockPropXchainClient, isMockEnabled, MOCK_ENV_FLAG, } from "./mock.js";
|
|
3
3
|
export { CoreClientError, wrapAgentError, type CoreClientErrorCode, } from "./errors.js";
|
|
4
4
|
export { withRetry, type RetryOptions } from "./retry.js";
|
|
5
|
+
export { type CertifiedAuditEvent, encodeAuditEvent, recomputeHeadHash, type CertifiedTransactionLog, type VerifyResult, type VerifyOptions, verifyCertifiedAuditLog, AUDIT_SEAL_DOC_TYPE, type AuditSealBundle, buildAuditSealBundle, serializeAuditSealBundle, } from "./audit/index.js";
|
|
5
6
|
export { createAuditTrailStub, createAgentHarnessStub, type AuditTrailStub, type AgentHarnessStub, type AuditEvent, type AuditFilter, } from "./stubs.js";
|
|
6
7
|
export { MAINNET_CANISTER_IDS, type CanisterIds, type IPropXchainClient, type PropXchainClientOptions, type TransactionManagerService, type DocumentStorageService, type UserManagementService, type LedgerManagerService, type PropertyRegistryService, type DocumentVerificationService, type EmailServiceService, type LandRegistryIntegrationService, } from "./types.js";
|
|
7
8
|
export { Principal, Ed25519KeyIdentity, AuthClient, IdbStorage, Actor, HttpAgent, type Identity, type ActorSubclass, type ActorConfig, type ActorMethod, type AuthClientCreateOptions, type AuthClientSignInOptions, } from "./auth.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,aAAa,GACd,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,eAAe,EACf,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oBAAoB,EACpB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,2BAA2B,EAChC,KAAK,mBAAmB,EACxB,KAAK,8BAA8B,GACpC,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,UAAU,EACV,UAAU,EACV,KAAK,EACL,SAAS,EACT,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,GAC7B,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,UAAU,IAAI,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAC/F,OAAO,EAAE,UAAU,IAAI,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AACzF,OAAO,EAAE,UAAU,IAAI,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AACvF,OAAO,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AACrF,OAAO,EAAE,UAAU,IAAI,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAC3F,OAAO,EAAE,UAAU,IAAI,uBAAuB,EAAE,MAAM,4CAA4C,CAAC;AACnG,OAAO,EAAE,UAAU,IAAI,eAAe,EAAE,MAAM,oCAAoC,CAAC;AACnF,OAAO,EAAE,UAAU,IAAI,0BAA0B,EAAE,MAAM,gDAAgD,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,aAAa,GACd,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,eAAe,EACf,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EACL,KAAK,mBAAmB,EACxB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,uBAAuB,EACvB,mBAAmB,EACnB,KAAK,eAAe,EACpB,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oBAAoB,EACpB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,2BAA2B,EAChC,KAAK,mBAAmB,EACxB,KAAK,8BAA8B,GACpC,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,UAAU,EACV,UAAU,EACV,KAAK,EACL,SAAS,EACT,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,GAC7B,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,UAAU,IAAI,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAC/F,OAAO,EAAE,UAAU,IAAI,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AACzF,OAAO,EAAE,UAAU,IAAI,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AACvF,OAAO,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AACrF,OAAO,EAAE,UAAU,IAAI,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAC3F,OAAO,EAAE,UAAU,IAAI,uBAAuB,EAAE,MAAM,4CAA4C,CAAC;AACnG,OAAO,EAAE,UAAU,IAAI,eAAe,EAAE,MAAM,oCAAoC,CAAC;AACnF,OAAO,EAAE,UAAU,IAAI,0BAA0B,EAAE,MAAM,gDAAgD,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { PropXchainClient } from "./client.js";
|
|
|
2
2
|
export { MockPropXchainClient, isMockEnabled, MOCK_ENV_FLAG, } from "./mock.js";
|
|
3
3
|
export { CoreClientError, wrapAgentError, } from "./errors.js";
|
|
4
4
|
export { withRetry } from "./retry.js";
|
|
5
|
+
export { encodeAuditEvent, recomputeHeadHash, verifyCertifiedAuditLog, AUDIT_SEAL_DOC_TYPE, buildAuditSealBundle, serializeAuditSealBundle, } from "./audit/index.js";
|
|
5
6
|
export { createAuditTrailStub, createAgentHarnessStub, } from "./stubs.js";
|
|
6
7
|
export { MAINNET_CANISTER_IDS, } from "./types.js";
|
|
7
8
|
export { Principal, Ed25519KeyIdentity, AuthClient, IdbStorage, Actor, HttpAgent, } from "./auth.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,aAAa,GACd,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,eAAe,EACf,cAAc,GAEf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,SAAS,EAAqB,MAAM,YAAY,CAAC;AAC1D,OAAO,EACL,oBAAoB,EACpB,sBAAsB,GAKvB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oBAAoB,GAYrB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,UAAU,EACV,UAAU,EACV,KAAK,EACL,SAAS,GAOV,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,UAAU,IAAI,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAC/F,OAAO,EAAE,UAAU,IAAI,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AACzF,OAAO,EAAE,UAAU,IAAI,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AACvF,OAAO,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AACrF,OAAO,EAAE,UAAU,IAAI,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAC3F,OAAO,EAAE,UAAU,IAAI,uBAAuB,EAAE,MAAM,4CAA4C,CAAC;AACnG,OAAO,EAAE,UAAU,IAAI,eAAe,EAAE,MAAM,oCAAoC,CAAC;AACnF,OAAO,EAAE,UAAU,IAAI,0BAA0B,EAAE,MAAM,gDAAgD,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,aAAa,GACd,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,eAAe,EACf,cAAc,GAEf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,SAAS,EAAqB,MAAM,YAAY,CAAC;AAC1D,OAAO,EAEL,gBAAgB,EAChB,iBAAiB,EAIjB,uBAAuB,EACvB,mBAAmB,EAEnB,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,oBAAoB,EACpB,sBAAsB,GAKvB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oBAAoB,GAYrB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,UAAU,EACV,UAAU,EACV,KAAK,EACL,SAAS,GAOV,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,UAAU,IAAI,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAC/F,OAAO,EAAE,UAAU,IAAI,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AACzF,OAAO,EAAE,UAAU,IAAI,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AACvF,OAAO,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AACrF,OAAO,EAAE,UAAU,IAAI,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAC3F,OAAO,EAAE,UAAU,IAAI,uBAAuB,EAAE,MAAM,4CAA4C,CAAC;AACnG,OAAO,EAAE,UAAU,IAAI,eAAe,EAAE,MAAM,oCAAoC,CAAC;AACnF,OAAO,EAAE,UAAU,IAAI,0BAA0B,EAAE,MAAM,gDAAgD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@propxchain/core-client",
|
|
3
|
-
"version": "0.3.0-canary.
|
|
3
|
+
"version": "0.3.0-canary.29",
|
|
4
4
|
"description": "Typed TypeScript client for the PropXchain core canisters on the Internet Computer. Includes a deterministic mock mode for local development and tests.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@dfinity/agent": "^1.4.0",
|
|
39
|
+
"@dfinity/certificate-verification": "^3.1.0",
|
|
40
|
+
"@dfinity/principal": "^1.4.0",
|
|
38
41
|
"@icp-sdk/auth": "^7.0.0",
|
|
39
42
|
"@icp-sdk/core": "^5.4.0"
|
|
40
43
|
},
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Audit hash chain — TypeScript mirror of ledger_manager's derived chain (ADR 0013).
|
|
2
|
+
//
|
|
3
|
+
// This MUST match the Motoko encoding in
|
|
4
|
+
// packages/core/src/ledger_manager/main.mo (encodeAuditEvent / chainHead)
|
|
5
|
+
// byte-for-byte, or verification will reject genuine records. The canonical
|
|
6
|
+
// encoding: each field 4-byte big-endian length-prefixed UTF-8; metadata uses
|
|
7
|
+
// a 1-byte presence tag (0 = null, 1 = present). eventHash_i =
|
|
8
|
+
// SHA-256(prevHash ++ encode(event_i)); genesis prevHash = 32 zero bytes;
|
|
9
|
+
// events ordered by eventId ascending.
|
|
10
|
+
|
|
11
|
+
import { Principal } from "@icp-sdk/core/principal";
|
|
12
|
+
|
|
13
|
+
/** Structural shape of a decoded ledger_manager AuditEvent (candid). */
|
|
14
|
+
export interface CertifiedAuditEvent {
|
|
15
|
+
eventId: bigint;
|
|
16
|
+
transactionId: string;
|
|
17
|
+
eventType: string;
|
|
18
|
+
timestamp: bigint;
|
|
19
|
+
caller: Principal;
|
|
20
|
+
details: string;
|
|
21
|
+
metadata: [] | [string];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const encoder = new TextEncoder();
|
|
25
|
+
|
|
26
|
+
function lenPrefixed(bytes: Uint8Array): Uint8Array {
|
|
27
|
+
const n = bytes.length;
|
|
28
|
+
const out = new Uint8Array(4 + n);
|
|
29
|
+
out[0] = (n >>> 24) & 0xff;
|
|
30
|
+
out[1] = (n >>> 16) & 0xff;
|
|
31
|
+
out[2] = (n >>> 8) & 0xff;
|
|
32
|
+
out[3] = n & 0xff;
|
|
33
|
+
out.set(bytes, 4);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function concat(chunks: Uint8Array[]): Uint8Array {
|
|
38
|
+
const total = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
39
|
+
const out = new Uint8Array(total);
|
|
40
|
+
let offset = 0;
|
|
41
|
+
for (const c of chunks) {
|
|
42
|
+
out.set(c, offset);
|
|
43
|
+
offset += c.length;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Canonical event encoding — mirrors ledger_manager.encodeAuditEvent. */
|
|
49
|
+
export function encodeAuditEvent(e: CertifiedAuditEvent): Uint8Array {
|
|
50
|
+
const parts: Uint8Array[] = [
|
|
51
|
+
lenPrefixed(encoder.encode(e.eventId.toString())),
|
|
52
|
+
lenPrefixed(encoder.encode(e.transactionId)),
|
|
53
|
+
lenPrefixed(encoder.encode(e.eventType)),
|
|
54
|
+
lenPrefixed(encoder.encode(e.timestamp.toString())),
|
|
55
|
+
lenPrefixed(encoder.encode(e.caller.toText())),
|
|
56
|
+
lenPrefixed(encoder.encode(e.details)),
|
|
57
|
+
];
|
|
58
|
+
if (e.metadata.length === 0) {
|
|
59
|
+
parts.push(new Uint8Array([0]));
|
|
60
|
+
} else {
|
|
61
|
+
parts.push(new Uint8Array([1]));
|
|
62
|
+
parts.push(lenPrefixed(encoder.encode(e.metadata[0])));
|
|
63
|
+
}
|
|
64
|
+
return concat(parts);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function sha256(data: Uint8Array): Promise<Uint8Array> {
|
|
68
|
+
const digest = await crypto.subtle.digest("SHA-256", data as unknown as BufferSource);
|
|
69
|
+
return new Uint8Array(digest);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Recompute a transaction's chain head from its events — mirrors
|
|
74
|
+
* ledger_manager.chainHead. Events are sorted by eventId ascending (the
|
|
75
|
+
* canister's canonical order) so the result is independent of array order.
|
|
76
|
+
*/
|
|
77
|
+
export async function recomputeHeadHash(
|
|
78
|
+
events: CertifiedAuditEvent[],
|
|
79
|
+
): Promise<Uint8Array> {
|
|
80
|
+
const ordered = [...events].sort((a, b) =>
|
|
81
|
+
a.eventId < b.eventId ? -1 : a.eventId > b.eventId ? 1 : 0,
|
|
82
|
+
);
|
|
83
|
+
let head: Uint8Array = new Uint8Array(32); // genesis: 32 zero bytes
|
|
84
|
+
for (const e of ordered) {
|
|
85
|
+
head = await sha256(concat([head, encodeAuditEvent(e)]));
|
|
86
|
+
}
|
|
87
|
+
return head;
|
|
88
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Verifiable audit trail (ADR 0013) — chain recompute, certificate
|
|
2
|
+
// verification, and sealed proof bundles for the per-transaction audit log.
|
|
3
|
+
export {
|
|
4
|
+
type CertifiedAuditEvent,
|
|
5
|
+
encodeAuditEvent,
|
|
6
|
+
recomputeHeadHash,
|
|
7
|
+
} from "./auditChain.js";
|
|
8
|
+
export {
|
|
9
|
+
type CertifiedTransactionLog,
|
|
10
|
+
type VerifyResult,
|
|
11
|
+
type VerifyOptions,
|
|
12
|
+
verifyCertifiedAuditLog,
|
|
13
|
+
} from "./verifyAuditLog.js";
|
|
14
|
+
export {
|
|
15
|
+
AUDIT_SEAL_DOC_TYPE,
|
|
16
|
+
type AuditSealBundle,
|
|
17
|
+
buildAuditSealBundle,
|
|
18
|
+
serializeAuditSealBundle,
|
|
19
|
+
} from "./sealAuditBundle.js";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Sealed audit bundle (ADR 0013, proof model ii).
|
|
2
|
+
//
|
|
3
|
+
// A contemporaneous, self-contained snapshot of a transaction's certified
|
|
4
|
+
// audit log. Built from a getCertifiedTransactionLog response (a query, so the
|
|
5
|
+
// subnet certificate is present) — typically at completion, and regenerable on
|
|
6
|
+
// demand. The bundle verifies offline against the IC root key, even if
|
|
7
|
+
// PropXchain is gone: a forensic verifier re-runs verifyCertifiedAuditLog with
|
|
8
|
+
// a relaxed freshness window.
|
|
9
|
+
//
|
|
10
|
+
// Persistence: the serialized bundle is stored on-chain in document_storage
|
|
11
|
+
// (docType "audit_seal") per ADR 0008. That upload uses the existing document
|
|
12
|
+
// upload path and is the caller's step — buildAuditSealBundle only produces the
|
|
13
|
+
// bytes (no document bodies are embedded; events reference any documents by
|
|
14
|
+
// hash only).
|
|
15
|
+
|
|
16
|
+
import { type CertifiedTransactionLog } from "./verifyAuditLog.js";
|
|
17
|
+
|
|
18
|
+
export const AUDIT_SEAL_DOC_TYPE = "audit_seal";
|
|
19
|
+
|
|
20
|
+
interface SerializableAuditEvent {
|
|
21
|
+
eventId: string;
|
|
22
|
+
transactionId: string;
|
|
23
|
+
eventType: string;
|
|
24
|
+
timestamp: string;
|
|
25
|
+
caller: string;
|
|
26
|
+
details: string;
|
|
27
|
+
metadata: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AuditSealBundle {
|
|
31
|
+
version: 1;
|
|
32
|
+
canisterId: string;
|
|
33
|
+
transactionId: string;
|
|
34
|
+
/** Client capture time (ISO). The authoritative time is inside the certificate. */
|
|
35
|
+
capturedAt: string;
|
|
36
|
+
events: SerializableAuditEvent[];
|
|
37
|
+
headHashHex: string;
|
|
38
|
+
certificateB64: string;
|
|
39
|
+
witnessB64: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toHex(bytes: Uint8Array): string {
|
|
43
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toB64(bytes: Uint8Array): string {
|
|
47
|
+
// Browser + Node (>=20) both expose btoa; build a binary string first.
|
|
48
|
+
let binary = "";
|
|
49
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
50
|
+
return btoa(binary);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build a sealed, serializable proof bundle from a certified log response.
|
|
55
|
+
* Throws if the response carries no certificate (nothing to seal).
|
|
56
|
+
*/
|
|
57
|
+
export function buildAuditSealBundle(args: {
|
|
58
|
+
canisterId: string;
|
|
59
|
+
transactionId: string;
|
|
60
|
+
capturedAt: string;
|
|
61
|
+
log: CertifiedTransactionLog;
|
|
62
|
+
}): AuditSealBundle {
|
|
63
|
+
const { canisterId, transactionId, capturedAt, log } = args;
|
|
64
|
+
if (log.certificate.length === 0) {
|
|
65
|
+
throw new Error("cannot seal: certified log response has no certificate");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
version: 1,
|
|
69
|
+
canisterId,
|
|
70
|
+
transactionId,
|
|
71
|
+
capturedAt,
|
|
72
|
+
events: log.events.map((e) => ({
|
|
73
|
+
eventId: e.eventId.toString(),
|
|
74
|
+
transactionId: e.transactionId,
|
|
75
|
+
eventType: e.eventType,
|
|
76
|
+
timestamp: e.timestamp.toString(),
|
|
77
|
+
caller: e.caller.toText(),
|
|
78
|
+
details: e.details,
|
|
79
|
+
metadata: e.metadata.length === 0 ? null : e.metadata[0],
|
|
80
|
+
})),
|
|
81
|
+
headHashHex: toHex(log.headHash),
|
|
82
|
+
certificateB64: toB64(log.certificate[0]),
|
|
83
|
+
witnessB64: toB64(log.witness),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Serialize a bundle to bytes for on-chain storage (document_storage). */
|
|
88
|
+
export function serializeAuditSealBundle(bundle: AuditSealBundle): Uint8Array {
|
|
89
|
+
return new TextEncoder().encode(JSON.stringify(bundle));
|
|
90
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Verifier for the certified per-transaction audit log (ADR 0013).
|
|
2
|
+
//
|
|
3
|
+
// Proves a transaction's audit trail is genuine canister state against the IC
|
|
4
|
+
// root key, trusting no part of PropXchain:
|
|
5
|
+
// 1. verify the subnet certificate + Merkle witness;
|
|
6
|
+
// 2. read the certified headHash for the transaction from the witness tree;
|
|
7
|
+
// 3. confirm the response's stated head matches the certified head;
|
|
8
|
+
// 4. recompute the chain from the events and confirm it matches.
|
|
9
|
+
//
|
|
10
|
+
// Uses the @dfinity/* family (agent, principal, certificate-verification)
|
|
11
|
+
// throughout — that is the lineage certificate-verification depends on, so the
|
|
12
|
+
// HashTree/Principal types line up. The rest of core-client uses @icp-sdk;
|
|
13
|
+
// this leaf module's boundary is plain Uint8Array/string, so the split is
|
|
14
|
+
// contained.
|
|
15
|
+
//
|
|
16
|
+
// Live verification keeps the certificate-freshness window tight. Forensic
|
|
17
|
+
// verification of a sealed historical bundle (ADR 0013, proof model ii)
|
|
18
|
+
// relaxes it by design — the bundle proves a fact as-of the certificate's
|
|
19
|
+
// /time, not a current one.
|
|
20
|
+
|
|
21
|
+
import { lookup_path, lookupResultToBuffer } from "@dfinity/agent";
|
|
22
|
+
import type { HashTree } from "@dfinity/agent";
|
|
23
|
+
import { Principal } from "@dfinity/principal";
|
|
24
|
+
import { type CertifiedAuditEvent, recomputeHeadHash } from "./auditChain.js";
|
|
25
|
+
|
|
26
|
+
// @dfinity/certificate-verification ships an ESM .d.ts (named exports, no
|
|
27
|
+
// default) but a UMD .cjs as `main`. TS sees ESM; Node's loader sees CJS. A
|
|
28
|
+
// static named import typechecks but fails at runtime; a default import does
|
|
29
|
+
// the reverse. Load it dynamically and fall back across both shapes so the
|
|
30
|
+
// verifier works in Node, browser bundlers, and typecheck alike.
|
|
31
|
+
interface VerifyCertificationParams {
|
|
32
|
+
canisterId: Principal;
|
|
33
|
+
encodedCertificate: ArrayBuffer;
|
|
34
|
+
encodedTree: ArrayBuffer;
|
|
35
|
+
rootKey: ArrayBuffer;
|
|
36
|
+
maxCertificateTimeOffsetMs: number;
|
|
37
|
+
}
|
|
38
|
+
type VerifyCertificationFn = (p: VerifyCertificationParams) => Promise<HashTree>;
|
|
39
|
+
|
|
40
|
+
let cachedVerify: VerifyCertificationFn | undefined;
|
|
41
|
+
async function loadVerifyCertification(): Promise<VerifyCertificationFn> {
|
|
42
|
+
if (cachedVerify) return cachedVerify;
|
|
43
|
+
const mod = (await import("@dfinity/certificate-verification")) as unknown as {
|
|
44
|
+
verifyCertification?: VerifyCertificationFn;
|
|
45
|
+
default?: { verifyCertification?: VerifyCertificationFn };
|
|
46
|
+
};
|
|
47
|
+
const fn = mod.verifyCertification ?? mod.default?.verifyCertification;
|
|
48
|
+
if (!fn) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"verifyCertification not found in @dfinity/certificate-verification",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
cachedVerify = fn;
|
|
54
|
+
return fn;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Decoded ledger_manager.getCertifiedTransactionLog response. */
|
|
58
|
+
export interface CertifiedTransactionLog {
|
|
59
|
+
events: CertifiedAuditEvent[];
|
|
60
|
+
headHash: Uint8Array;
|
|
61
|
+
certificate: [] | [Uint8Array];
|
|
62
|
+
witness: Uint8Array;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface VerifyResult {
|
|
66
|
+
verified: boolean;
|
|
67
|
+
reason?: string;
|
|
68
|
+
eventCount: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface VerifyOptions {
|
|
72
|
+
canisterId: string;
|
|
73
|
+
/** IC root public key — the mainnet constant, or `agent.rootKey` for local. */
|
|
74
|
+
rootKey: Uint8Array;
|
|
75
|
+
transactionId: string;
|
|
76
|
+
log: CertifiedTransactionLog;
|
|
77
|
+
/**
|
|
78
|
+
* Certificate freshness window. Omit for live verification (5 min). For
|
|
79
|
+
* forensic verification of a sealed bundle, pass a large value (e.g.
|
|
80
|
+
* Number.MAX_SAFE_INTEGER) to accept a historical certificate.
|
|
81
|
+
*/
|
|
82
|
+
maxCertificateTimeOffsetMs?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const FIVE_MINUTES_MS = 5 * 60 * 1000;
|
|
86
|
+
|
|
87
|
+
function toArrayBuffer(u8: Uint8Array): ArrayBuffer {
|
|
88
|
+
const copy = new Uint8Array(u8.length);
|
|
89
|
+
copy.set(u8);
|
|
90
|
+
return copy.buffer;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function equalBytes(a: Uint8Array, b: Uint8Array): boolean {
|
|
94
|
+
if (a.length !== b.length) return false;
|
|
95
|
+
let diff = 0;
|
|
96
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
97
|
+
return diff === 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function verifyCertifiedAuditLog(
|
|
101
|
+
opts: VerifyOptions,
|
|
102
|
+
): Promise<VerifyResult> {
|
|
103
|
+
const { log, transactionId } = opts;
|
|
104
|
+
const eventCount = log.events.length;
|
|
105
|
+
|
|
106
|
+
if (log.certificate.length === 0) {
|
|
107
|
+
return { verified: false, reason: "no certificate in response", eventCount };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 1. Verify the subnet certificate + witness against the IC root key.
|
|
111
|
+
let tree: HashTree;
|
|
112
|
+
try {
|
|
113
|
+
const verifyCertification = await loadVerifyCertification();
|
|
114
|
+
tree = await verifyCertification({
|
|
115
|
+
canisterId: Principal.fromText(opts.canisterId),
|
|
116
|
+
encodedCertificate: toArrayBuffer(log.certificate[0]),
|
|
117
|
+
encodedTree: toArrayBuffer(log.witness),
|
|
118
|
+
rootKey: toArrayBuffer(opts.rootKey),
|
|
119
|
+
maxCertificateTimeOffsetMs:
|
|
120
|
+
opts.maxCertificateTimeOffsetMs ?? FIVE_MINUTES_MS,
|
|
121
|
+
});
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return {
|
|
124
|
+
verified: false,
|
|
125
|
+
reason: `certificate verification failed: ${String(err)}`,
|
|
126
|
+
eventCount,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 2. Read the certified headHash for this transaction from the witness tree.
|
|
131
|
+
// The path segment is the UTF-8 bytes of the transactionId — exactly the key
|
|
132
|
+
// ledger_manager certified (Text.encodeUtf8(transactionId)).
|
|
133
|
+
const lookup = lookup_path(
|
|
134
|
+
[toArrayBuffer(new TextEncoder().encode(transactionId))],
|
|
135
|
+
tree,
|
|
136
|
+
);
|
|
137
|
+
const certifiedBuf = lookupResultToBuffer(lookup);
|
|
138
|
+
if (!certifiedBuf) {
|
|
139
|
+
return {
|
|
140
|
+
verified: false,
|
|
141
|
+
reason: "transaction head not present in certified tree",
|
|
142
|
+
eventCount,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const certifiedHead = new Uint8Array(certifiedBuf);
|
|
146
|
+
|
|
147
|
+
// 3. The response's stated head must match the certified value.
|
|
148
|
+
if (!equalBytes(certifiedHead, log.headHash)) {
|
|
149
|
+
return {
|
|
150
|
+
verified: false,
|
|
151
|
+
reason: "response headHash does not match certified value",
|
|
152
|
+
eventCount,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 4. The head recomputed from the events must match the certified head.
|
|
157
|
+
const recomputed = await recomputeHeadHash(log.events);
|
|
158
|
+
if (!equalBytes(recomputed, certifiedHead)) {
|
|
159
|
+
return {
|
|
160
|
+
verified: false,
|
|
161
|
+
reason:
|
|
162
|
+
"recomputed chain head does not match certified head — events tampered",
|
|
163
|
+
eventCount,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { verified: true, eventCount };
|
|
168
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,19 @@ export {
|
|
|
10
10
|
type CoreClientErrorCode,
|
|
11
11
|
} from "./errors.js";
|
|
12
12
|
export { withRetry, type RetryOptions } from "./retry.js";
|
|
13
|
+
export {
|
|
14
|
+
type CertifiedAuditEvent,
|
|
15
|
+
encodeAuditEvent,
|
|
16
|
+
recomputeHeadHash,
|
|
17
|
+
type CertifiedTransactionLog,
|
|
18
|
+
type VerifyResult,
|
|
19
|
+
type VerifyOptions,
|
|
20
|
+
verifyCertifiedAuditLog,
|
|
21
|
+
AUDIT_SEAL_DOC_TYPE,
|
|
22
|
+
type AuditSealBundle,
|
|
23
|
+
buildAuditSealBundle,
|
|
24
|
+
serializeAuditSealBundle,
|
|
25
|
+
} from "./audit/index.js";
|
|
13
26
|
export {
|
|
14
27
|
createAuditTrailStub,
|
|
15
28
|
createAgentHarnessStub,
|