@zanii/blackbox 0.2.0 → 0.4.0
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/README.md +26 -1
- package/dist/a2a/index.d.ts +77 -0
- package/dist/a2a/index.js +305 -0
- package/dist/agents/index.d.ts +8 -0
- package/dist/analysis/accuracy.d.ts +24 -0
- package/dist/analysis/accuracy.js +45 -0
- package/dist/analysis/credential.d.ts +101 -0
- package/dist/analysis/credential.js +142 -0
- package/dist/analysis/faults.js +115 -0
- package/dist/analysis/grounding.d.ts +122 -0
- package/dist/analysis/grounding.js +445 -0
- package/dist/analysis/hallucination.d.ts +32 -0
- package/dist/analysis/hallucination.js +357 -0
- package/dist/analysis/index.d.ts +23 -0
- package/dist/analysis/index.js +93 -0
- package/dist/analysis/memory.d.ts +8 -0
- package/dist/analysis/memory.js +35 -8
- package/dist/analysis/reference.d.ts +49 -0
- package/dist/analysis/reference.js +164 -0
- package/dist/analysis/taxonomy.d.ts +18 -0
- package/dist/analysis/taxonomy.js +66 -0
- package/dist/approvals/index.d.ts +23 -0
- package/dist/approvals/index.js +48 -0
- package/dist/archive/index.d.ts +39 -0
- package/dist/archive/index.js +96 -0
- package/dist/archive/parquet.d.ts +2 -0
- package/dist/archive/parquet.js +185 -0
- package/dist/badge/index.d.ts +16 -0
- package/dist/badge/index.js +48 -0
- package/dist/bom/index.d.ts +14 -0
- package/dist/bom/index.js +152 -0
- package/dist/cli.js +114 -10
- package/dist/compliance/art12.d.ts +35 -0
- package/dist/compliance/art12.js +190 -0
- package/dist/compliance/index.d.ts +36 -2
- package/dist/compliance/index.js +78 -11
- package/dist/compliance/zanii.d.ts +29 -0
- package/dist/compliance/zanii.js +84 -0
- package/dist/constitution/index.d.ts +57 -0
- package/dist/constitution/index.js +131 -0
- package/dist/cv/index.d.ts +39 -0
- package/dist/cv/index.js +108 -0
- package/dist/disclosure/index.d.ts +31 -0
- package/dist/disclosure/index.js +113 -0
- package/dist/encryption/index.d.ts +9 -0
- package/dist/encryption/index.js +31 -0
- package/dist/evidence/index.d.ts +60 -0
- package/dist/evidence/index.js +151 -0
- package/dist/federation/index.d.ts +35 -0
- package/dist/federation/index.js +102 -0
- package/dist/finance/index.d.ts +126 -0
- package/dist/finance/index.js +320 -0
- package/dist/fleet/index.js +9 -0
- package/dist/gov/index.d.ts +108 -0
- package/dist/gov/index.js +225 -0
- package/dist/health/index.d.ts +120 -0
- package/dist/health/index.js +233 -0
- package/dist/index.d.ts +34 -5
- package/dist/index.js +34 -5
- package/dist/memory/index.d.ts +36 -0
- package/dist/memory/index.js +85 -0
- package/dist/occurrence/index.d.ts +11 -0
- package/dist/occurrence/index.js +18 -0
- package/dist/ocsf/index.d.ts +1 -1
- package/dist/ocsf/index.js +36 -3
- package/dist/otlp/index.d.ts +8 -1
- package/dist/otlp/index.js +258 -1
- package/dist/packs/index.js +44 -4
- package/dist/policy/delta.js +7 -1
- package/dist/policy/index.d.ts +40 -6
- package/dist/policy/index.js +186 -8
- package/dist/policy/zanii.d.ts +31 -0
- package/dist/policy/zanii.js +87 -0
- package/dist/pq/index.d.ts +23 -0
- package/dist/pq/index.js +104 -0
- package/dist/search/index.d.ts +23 -0
- package/dist/search/index.js +69 -0
- package/dist/session/index.d.ts +107 -1
- package/dist/session/index.js +189 -11
- package/dist/sla/index.d.ts +61 -0
- package/dist/sla/index.js +197 -0
- package/dist/succession/index.d.ts +50 -0
- package/dist/succession/index.js +123 -0
- package/dist/timestamp/index.d.ts +24 -0
- package/dist/timestamp/index.js +274 -0
- package/dist/tokens/index.d.ts +6 -0
- package/dist/tokens/index.js +46 -0
- package/dist/transparency/index.d.ts +188 -0
- package/dist/transparency/index.js +712 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walls/index.d.ts +31 -0
- package/dist/walls/index.js +119 -0
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type Obj = Record<string, unknown>;
|
|
2
|
+
/** Zanii's agentcv from its parts. */
|
|
3
|
+
export declare function buildCv(o: {
|
|
4
|
+
did: string;
|
|
5
|
+
summary: Obj;
|
|
6
|
+
generatedAt: string;
|
|
7
|
+
owner?: string | null;
|
|
8
|
+
work?: Obj[];
|
|
9
|
+
authority?: Obj[];
|
|
10
|
+
benchmarks?: Obj[];
|
|
11
|
+
skills?: string[];
|
|
12
|
+
}): Obj;
|
|
13
|
+
export declare const cvHash: (cv: Obj) => string;
|
|
14
|
+
/** Zanii's offline check: structure, the subject's signature when there is one, and warnings. */
|
|
15
|
+
export declare function verifyCv(cv: unknown): {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
reasons: string[];
|
|
18
|
+
signed: boolean | null;
|
|
19
|
+
warnings: string[];
|
|
20
|
+
};
|
|
21
|
+
type Summary = {
|
|
22
|
+
session_id: string;
|
|
23
|
+
label: string | null;
|
|
24
|
+
started_at: string;
|
|
25
|
+
last_event_at: string;
|
|
26
|
+
closed: boolean;
|
|
27
|
+
outcome: string | null;
|
|
28
|
+
tools: Record<string, number>;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The agent behind a label, as Zanii's agentcv, from session summaries (spec/fleet.md §1) and the
|
|
32
|
+
* anchor receipt of each anchored session: summary.receipts counts the anchored sessions, and each
|
|
33
|
+
* work entry (the newest 20) cites its anchor receipt.
|
|
34
|
+
*/
|
|
35
|
+
export declare function agentCv(label: string, summaries: readonly Summary[], o: {
|
|
36
|
+
anchored: Readonly<Record<string, string>>;
|
|
37
|
+
generatedAt: string;
|
|
38
|
+
}): Obj;
|
|
39
|
+
export {};
|
package/dist/cv/index.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Agent CVs (spec/fleet.md §7): Zanii's agentcv, a portable view over what the record already
|
|
2
|
+
// holds, every work entry backed by a ledger ref (@zanii/cv), built here from the gateway's own
|
|
3
|
+
// logbook. Pure; mirrors sdks/python/src/zanii_blackbox/cv.py; pinned by spec/vectors/cv.json.
|
|
4
|
+
import { canonicalBytes, jcsHash, publicKeyFromDid } from "@zanii/core";
|
|
5
|
+
import { ed25519Verify } from "../transparency/index.js";
|
|
6
|
+
const WORK_SHOWN = 20;
|
|
7
|
+
const validTs = (ts) => typeof ts === "string" && !Number.isNaN(Date.parse(ts));
|
|
8
|
+
/** Zanii's agentcv from its parts. */
|
|
9
|
+
export function buildCv(o) {
|
|
10
|
+
if (!o.did)
|
|
11
|
+
throw new Error("did is required");
|
|
12
|
+
if (!o.summary || Object.keys(o.summary).length === 0)
|
|
13
|
+
throw new Error("summary is required (the ledger aggregate — the un-inflatable part)");
|
|
14
|
+
if (!validTs(o.generatedAt))
|
|
15
|
+
throw new Error("generated_at must be an ISO timestamp");
|
|
16
|
+
return {
|
|
17
|
+
v: 1,
|
|
18
|
+
type: "agentcv",
|
|
19
|
+
did: o.did,
|
|
20
|
+
owner: o.owner ?? null,
|
|
21
|
+
summary: o.summary,
|
|
22
|
+
work: o.work ?? [],
|
|
23
|
+
authority: o.authority ?? [],
|
|
24
|
+
benchmarks: o.benchmarks ?? [],
|
|
25
|
+
skills: o.skills ?? [],
|
|
26
|
+
generated_at: o.generatedAt,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export const cvHash = (cv) => jcsHash(cv);
|
|
30
|
+
const skillBacked = (skill, scopes) => [...scopes].some((sc) => typeof sc === "string" &&
|
|
31
|
+
(sc === skill || sc === "*" || (sc.endsWith(".*") && skill.startsWith(sc.slice(0, -1)))));
|
|
32
|
+
/** Zanii's offline check: structure, the subject's signature when there is one, and warnings. */
|
|
33
|
+
export function verifyCv(cv) {
|
|
34
|
+
const c = (cv ?? {});
|
|
35
|
+
const reasons = [];
|
|
36
|
+
const warnings = [];
|
|
37
|
+
if (c.v !== 1 || c.type !== "agentcv")
|
|
38
|
+
reasons.push("not an agentcv (v1)");
|
|
39
|
+
if (!c.did)
|
|
40
|
+
reasons.push("missing did");
|
|
41
|
+
const summary = c.summary;
|
|
42
|
+
if (!summary || typeof summary !== "object" || !Number.isSafeInteger(summary.receipts))
|
|
43
|
+
reasons.push("missing/invalid summary (the ledger aggregate)");
|
|
44
|
+
if (!validTs(c.generated_at))
|
|
45
|
+
reasons.push("missing/invalid generated_at");
|
|
46
|
+
let signed = null;
|
|
47
|
+
if (typeof c.sig === "string") {
|
|
48
|
+
const pub = publicKeyFromDid(String(c.did));
|
|
49
|
+
const m = /^ed25519:([0-9a-f]{128})$/.exec(c.sig);
|
|
50
|
+
if (!pub || !m) {
|
|
51
|
+
signed = false;
|
|
52
|
+
reasons.push("malformed signature or subject key");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const { sig: _sig, ...unsigned } = c;
|
|
56
|
+
signed = ed25519Verify(pub, canonicalBytes(unsigned), Buffer.from(m[1], "hex"));
|
|
57
|
+
if (!signed)
|
|
58
|
+
reasons.push("signature does not verify against the subject did (CV was altered or signed by someone else)");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (reasons.length === 0 && summary) {
|
|
62
|
+
const scopes = new Set((c.authority ?? []).map((a) => a.scope));
|
|
63
|
+
for (const s of c.skills ?? [])
|
|
64
|
+
if (!skillBacked(s, scopes))
|
|
65
|
+
warnings.push(`skill "${s}" is claimed but not backed by any delegated authority scope`);
|
|
66
|
+
const work = c.work ?? [];
|
|
67
|
+
if (work.length > summary.receipts)
|
|
68
|
+
warnings.push(`work lists ${work.length} entries but the ledger only counts ${summary.receipts} receipts — the curated list cannot exceed the real total`);
|
|
69
|
+
if (summary.revoked)
|
|
70
|
+
warnings.push("the subject's authority is REVOKED on the ledger");
|
|
71
|
+
}
|
|
72
|
+
return { ok: reasons.length === 0, reasons, signed, warnings };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The agent behind a label, as Zanii's agentcv, from session summaries (spec/fleet.md §1) and the
|
|
76
|
+
* anchor receipt of each anchored session: summary.receipts counts the anchored sessions, and each
|
|
77
|
+
* work entry (the newest 20) cites its anchor receipt.
|
|
78
|
+
*/
|
|
79
|
+
export function agentCv(label, summaries, o) {
|
|
80
|
+
const mine = summaries.filter((s) => s.label === label);
|
|
81
|
+
if (mine.length === 0)
|
|
82
|
+
throw new Error(`no sessions for ${label}`);
|
|
83
|
+
const tools = new Set(mine.flatMap((s) => Object.keys(s.tools ?? {})));
|
|
84
|
+
const closed = mine.filter((s) => s.closed);
|
|
85
|
+
const onLedger = mine.filter((s) => s.session_id in o.anchored);
|
|
86
|
+
const newest = [...onLedger].sort((a, b) => a.last_event_at < b.last_event_at ? 1 : a.last_event_at > b.last_event_at ? -1 : 0);
|
|
87
|
+
const max = (xs) => xs.reduce((a, b) => (b > a ? b : a));
|
|
88
|
+
const min = (xs) => xs.reduce((a, b) => (b < a ? b : a));
|
|
89
|
+
return buildCv({
|
|
90
|
+
did: `urn:blackbox:agent:${label}`,
|
|
91
|
+
summary: {
|
|
92
|
+
receipts: onLedger.length,
|
|
93
|
+
distinct_counterparties: tools.size,
|
|
94
|
+
first_seen: min(mine.map((s) => s.started_at)),
|
|
95
|
+
last_seen: max(mine.map((s) => s.last_event_at)),
|
|
96
|
+
revoked: false,
|
|
97
|
+
fully_anchored: closed.length > 0 && closed.every((s) => s.session_id in o.anchored),
|
|
98
|
+
audit_bundle: null,
|
|
99
|
+
},
|
|
100
|
+
generatedAt: o.generatedAt,
|
|
101
|
+
work: newest.slice(0, WORK_SHOWN).map((s) => ({
|
|
102
|
+
session_id: s.session_id,
|
|
103
|
+
outcome: s.outcome,
|
|
104
|
+
at: s.last_event_at,
|
|
105
|
+
ref: o.anchored[s.session_id],
|
|
106
|
+
})),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface DisclosureEnvelope {
|
|
2
|
+
_zr: "sd-v1";
|
|
3
|
+
root: string;
|
|
4
|
+
keys: string[];
|
|
5
|
+
}
|
|
6
|
+
export interface Disclosure {
|
|
7
|
+
_zr: "sd-disclosure-v1";
|
|
8
|
+
key: string;
|
|
9
|
+
value: unknown;
|
|
10
|
+
salt: string;
|
|
11
|
+
index: number;
|
|
12
|
+
size: number;
|
|
13
|
+
proof: string[];
|
|
14
|
+
root: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The scalar leaves of `args` (an empty object or array is a leaf too), as [JSON Pointer, value],
|
|
18
|
+
* sorted by pointer; null when `args` isn't an object, or is too big or too deep to commit to.
|
|
19
|
+
*/
|
|
20
|
+
export declare function argumentLeaves(args: unknown): Array<[string, unknown]> | null;
|
|
21
|
+
/** `meta.disclosure` for a call with these arguments and body hash; null when there's none. */
|
|
22
|
+
export declare function disclosureEnvelope(args: unknown, bodyHash: string, key: Uint8Array): DisclosureEnvelope | null;
|
|
23
|
+
/** One disclosure per pointer. Throws on a pointer that isn't a leaf of the arguments. */
|
|
24
|
+
export declare function discloseFields(args: unknown, bodyHash: string, key: Uint8Array, pointers: readonly string[]): Disclosure[];
|
|
25
|
+
/** Zanii's check: the root and size match, keys[index] is the key, and the proof verifies. */
|
|
26
|
+
export declare function verifyDisclosure(d: unknown, envelope: unknown): boolean;
|
|
27
|
+
/** Checks disclosures against the envelope in a stored event line's `meta.disclosure`. */
|
|
28
|
+
export declare function verifyEventDisclosure(line: string, disclosures: readonly unknown[]): {
|
|
29
|
+
ok: boolean;
|
|
30
|
+
fields: Record<string, unknown>;
|
|
31
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Selective disclosure of a tool call's arguments (spec/disclosure.md), in Zanii's Selective
|
|
2
|
+
// Disclosure v1 format (@zanii/redact / zanii.redact, byte for byte). Pure; mirrors
|
|
3
|
+
// sdks/python/src/zanii_blackbox/disclosure.py; pinned by spec/vectors/disclosure.json.
|
|
4
|
+
import { createHmac } from "node:crypto";
|
|
5
|
+
import { canonicalBytes, inclusionProof, leafHash, merkleRoot, verifyInclusion } from "@zanii/core";
|
|
6
|
+
const MAX_LEAVES = 256;
|
|
7
|
+
const MAX_DEPTH = 8;
|
|
8
|
+
const hex = (b) => Buffer.from(b).toString("hex");
|
|
9
|
+
const escapeKey = (k) => k.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
10
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
11
|
+
/**
|
|
12
|
+
* The scalar leaves of `args` (an empty object or array is a leaf too), as [JSON Pointer, value],
|
|
13
|
+
* sorted by pointer; null when `args` isn't an object, or is too big or too deep to commit to.
|
|
14
|
+
*/
|
|
15
|
+
export function argumentLeaves(args) {
|
|
16
|
+
if (!isObj(args))
|
|
17
|
+
return null;
|
|
18
|
+
const out = [];
|
|
19
|
+
let tooDeep = false;
|
|
20
|
+
const walk = (v, ptr, depth) => {
|
|
21
|
+
if (depth > MAX_DEPTH)
|
|
22
|
+
tooDeep = true;
|
|
23
|
+
if (tooDeep || out.length > MAX_LEAVES)
|
|
24
|
+
return;
|
|
25
|
+
if (isObj(v) && Object.keys(v).length)
|
|
26
|
+
for (const [k, x] of Object.entries(v))
|
|
27
|
+
walk(x, `${ptr}/${escapeKey(k)}`, depth + 1);
|
|
28
|
+
else if (Array.isArray(v) && v.length)
|
|
29
|
+
for (const [i, x] of v.entries())
|
|
30
|
+
walk(x, `${ptr}/${i}`, depth + 1);
|
|
31
|
+
else
|
|
32
|
+
out.push([ptr, v]);
|
|
33
|
+
};
|
|
34
|
+
walk(args, "", 0);
|
|
35
|
+
if (tooDeep || out.length > MAX_LEAVES || out.length === 0)
|
|
36
|
+
return null;
|
|
37
|
+
return out.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
38
|
+
}
|
|
39
|
+
const salt = (key, bodyHash, pointer) => createHmac("sha256", key).update(`${bodyHash}\n${pointer}`).digest("hex").slice(0, 32);
|
|
40
|
+
const leaf = (pointer, value, s) => leafHash(canonicalBytes(["zr-field-v1", pointer, value, s]));
|
|
41
|
+
function tree(args, bodyHash, key) {
|
|
42
|
+
const leaves = argumentLeaves(args);
|
|
43
|
+
if (!leaves)
|
|
44
|
+
return null;
|
|
45
|
+
const salts = leaves.map(([p]) => salt(key, bodyHash, p));
|
|
46
|
+
const hashes = leaves.map(([p, v], i) => leaf(p, v, salts[i]));
|
|
47
|
+
return { leaves, salts, hashes, root: `sha256:${hex(merkleRoot(hashes))}` };
|
|
48
|
+
}
|
|
49
|
+
/** `meta.disclosure` for a call with these arguments and body hash; null when there's none. */
|
|
50
|
+
export function disclosureEnvelope(args, bodyHash, key) {
|
|
51
|
+
const t = tree(args, bodyHash, key);
|
|
52
|
+
return t ? { _zr: "sd-v1", root: t.root, keys: t.leaves.map(([p]) => p) } : null;
|
|
53
|
+
}
|
|
54
|
+
/** One disclosure per pointer. Throws on a pointer that isn't a leaf of the arguments. */
|
|
55
|
+
export function discloseFields(args, bodyHash, key, pointers) {
|
|
56
|
+
const t = tree(args, bodyHash, key);
|
|
57
|
+
if (!t)
|
|
58
|
+
throw new Error("these arguments have no commitment");
|
|
59
|
+
return pointers.map((p) => {
|
|
60
|
+
const index = t.leaves.findIndex(([k]) => k === p);
|
|
61
|
+
if (index < 0)
|
|
62
|
+
throw new Error(`not a field of this call: ${p}`);
|
|
63
|
+
return {
|
|
64
|
+
_zr: "sd-disclosure-v1",
|
|
65
|
+
key: p,
|
|
66
|
+
value: t.leaves[index][1],
|
|
67
|
+
salt: t.salts[index],
|
|
68
|
+
index,
|
|
69
|
+
size: t.leaves.length,
|
|
70
|
+
proof: inclusionProof(index, t.hashes).map(hex),
|
|
71
|
+
root: t.root,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Zanii's check: the root and size match, keys[index] is the key, and the proof verifies. */
|
|
76
|
+
export function verifyDisclosure(d, envelope) {
|
|
77
|
+
const x = d;
|
|
78
|
+
const e = envelope;
|
|
79
|
+
if (!x || !e || x.root !== e.root || !Array.isArray(e.keys) || typeof e.root !== "string")
|
|
80
|
+
return false;
|
|
81
|
+
const { index, size, key, salt: s, proof } = x;
|
|
82
|
+
if (size !== e.keys.length || typeof index !== "number" || !Number.isInteger(index))
|
|
83
|
+
return false;
|
|
84
|
+
if (index < 0 || index >= e.keys.length || e.keys[index] !== key)
|
|
85
|
+
return false;
|
|
86
|
+
if (typeof key !== "string" || typeof s !== "string" || !Array.isArray(proof))
|
|
87
|
+
return false;
|
|
88
|
+
try {
|
|
89
|
+
return verifyInclusion(leaf(key, x.value, s), index, size, proof.map((p) => new Uint8Array(Buffer.from(String(p), "hex"))), new Uint8Array(Buffer.from(e.root.replace(/^sha256:/, ""), "hex")));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Checks disclosures against the envelope in a stored event line's `meta.disclosure`. */
|
|
96
|
+
export function verifyEventDisclosure(line, disclosures) {
|
|
97
|
+
let envelope;
|
|
98
|
+
try {
|
|
99
|
+
envelope = JSON.parse(line).meta?.disclosure;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return { ok: false, fields: {} };
|
|
103
|
+
}
|
|
104
|
+
const fields = {};
|
|
105
|
+
let ok = envelope !== undefined && disclosures.length > 0;
|
|
106
|
+
for (const d of disclosures) {
|
|
107
|
+
if (verifyDisclosure(d, envelope))
|
|
108
|
+
fields[d.key] = d.value;
|
|
109
|
+
else
|
|
110
|
+
ok = false;
|
|
111
|
+
}
|
|
112
|
+
return { ok, fields };
|
|
113
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface SealedSegment {
|
|
2
|
+
hash: string;
|
|
3
|
+
/** base64: the stored bytes (nonce ‖ ciphertext ‖ tag), or the plain bytes when `key` is null. */
|
|
4
|
+
sealed: string;
|
|
5
|
+
/** base64: the segment's own key, or null for a segment stored before encryption. */
|
|
6
|
+
key: string | null;
|
|
7
|
+
}
|
|
8
|
+
/** The body, when every segment opens, hashes to its own hash, and they join to `bodyHash`; else null. */
|
|
9
|
+
export declare function openSegments(segments: readonly SealedSegment[], bodyHash: string): Uint8Array | null;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The auditor's side of spec/encryption.md §4: open one body from its sealed segments and their
|
|
2
|
+
// keys, and check it against the hash in the chain. Pure; mirrors
|
|
3
|
+
// sdks/python/src/zanii_blackbox/encryption.py; pinned by spec/vectors/body-encryption.json.
|
|
4
|
+
import { createDecipheriv, createHash } from "node:crypto";
|
|
5
|
+
const sha256 = (b) => `sha256:${createHash("sha256").update(b).digest("hex")}`;
|
|
6
|
+
function open(key, hash, sealed) {
|
|
7
|
+
if (key.length !== 32 || sealed.length < 28)
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
const d = createDecipheriv("aes-256-gcm", key, sealed.subarray(0, 12));
|
|
11
|
+
d.setAAD(Buffer.from(hash, "utf8"));
|
|
12
|
+
d.setAuthTag(sealed.subarray(sealed.length - 16));
|
|
13
|
+
return Buffer.concat([d.update(sealed.subarray(12, sealed.length - 16)), d.final()]);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** The body, when every segment opens, hashes to its own hash, and they join to `bodyHash`; else null. */
|
|
20
|
+
export function openSegments(segments, bodyHash) {
|
|
21
|
+
const parts = [];
|
|
22
|
+
for (const s of segments) {
|
|
23
|
+
const bytes = Buffer.from(s.sealed, "base64");
|
|
24
|
+
const plain = s.key === null ? bytes : open(Buffer.from(s.key, "base64"), s.hash, bytes);
|
|
25
|
+
if (!plain || sha256(plain) !== s.hash)
|
|
26
|
+
return null;
|
|
27
|
+
parts.push(plain);
|
|
28
|
+
}
|
|
29
|
+
const body = Buffer.concat(parts);
|
|
30
|
+
return sha256(body) === bodyHash ? new Uint8Array(body) : null;
|
|
31
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Bundle, BundleReport } from "../verify/index.ts";
|
|
2
|
+
type Text = {
|
|
3
|
+
en: string;
|
|
4
|
+
ar: string;
|
|
5
|
+
};
|
|
6
|
+
export interface EvidencePackOptions {
|
|
7
|
+
generated_at: string;
|
|
8
|
+
bundle_sha256: string;
|
|
9
|
+
proofs: "checked" | "not_checked";
|
|
10
|
+
}
|
|
11
|
+
/** spec/investigations.md §7. */
|
|
12
|
+
export declare function evidencePack(bundle: Bundle, report: BundleReport, opts: EvidencePackOptions): {
|
|
13
|
+
v: 1;
|
|
14
|
+
kind: "blackbox-evidence-pack";
|
|
15
|
+
session_id: string;
|
|
16
|
+
label: string | null;
|
|
17
|
+
agent: {} | null;
|
|
18
|
+
anchored_by: string | null;
|
|
19
|
+
ledger_url: string | null;
|
|
20
|
+
record: {
|
|
21
|
+
events: number;
|
|
22
|
+
head: string | null;
|
|
23
|
+
first_at: string | null;
|
|
24
|
+
last_at: string | null;
|
|
25
|
+
closed: boolean;
|
|
26
|
+
};
|
|
27
|
+
anchors: {
|
|
28
|
+
count: number;
|
|
29
|
+
last: {
|
|
30
|
+
count: number;
|
|
31
|
+
receipt_hash: string;
|
|
32
|
+
anchored_at: string;
|
|
33
|
+
} | null;
|
|
34
|
+
};
|
|
35
|
+
transparency: {
|
|
36
|
+
statement: boolean;
|
|
37
|
+
receipt: boolean;
|
|
38
|
+
checkpoint: boolean;
|
|
39
|
+
timestamp: boolean;
|
|
40
|
+
} | null;
|
|
41
|
+
verified: boolean;
|
|
42
|
+
verification: {
|
|
43
|
+
chain: string;
|
|
44
|
+
identity: string;
|
|
45
|
+
proofs: string;
|
|
46
|
+
};
|
|
47
|
+
legal_basis: Text;
|
|
48
|
+
steps: {
|
|
49
|
+
en: string;
|
|
50
|
+
ar: string;
|
|
51
|
+
n: number;
|
|
52
|
+
}[];
|
|
53
|
+
reproduce: string[];
|
|
54
|
+
generated_at: string;
|
|
55
|
+
bundle_sha256: string;
|
|
56
|
+
};
|
|
57
|
+
export type EvidencePack = ReturnType<typeof evidencePack>;
|
|
58
|
+
/** The Markdown copy (spec/investigations.md §7): `en`, `ar` or `both`. */
|
|
59
|
+
export declare function renderEvidencePack(pack: EvidencePack, lang?: "en" | "ar" | "both"): string;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// The court evidence pack (spec/investigations.md §7): why a court, or an expert it appoints, can
|
|
2
|
+
// rely on a record, and how to check it without trusting the operator. Arabic and English. Pure;
|
|
3
|
+
// mirrors sdks/python/src/zanii_blackbox/evidence.py; pinned by spec/vectors/evidence-pack.json.
|
|
4
|
+
import { hashLine } from "../verify/envelope.js";
|
|
5
|
+
const LEGAL_BASIS = {
|
|
6
|
+
en: "Under UAE Federal Decree-Law No. 46 of 2021 on Electronic Transactions and Trust Services, an electronic record and an electronic signature have legal effect and may be admitted as evidence. This pack lets a court, or an expert it appoints, check the record's integrity and origin independently, without relying on the operator. It maps the evidence; it is not legal advice.",
|
|
7
|
+
ar: "بموجب المرسوم بقانون اتحادي رقم 46 لسنة 2021 في شأن المعاملات الإلكترونية وخدمات الثقة، يكون للسجل الإلكتروني والتوقيع الإلكتروني أثر قانوني ويجوز قبولهما دليلًا. تتيح هذه الحزمة للمحكمة، أو لخبير تعيّنه، التحقق المستقل من سلامة السجل ومصدره دون الاعتماد على المشغّل. هي بيان للأدلة، وليست استشارة قانونية.",
|
|
8
|
+
};
|
|
9
|
+
const STEPS = [
|
|
10
|
+
{
|
|
11
|
+
en: "The hash chain: each event's hash covers its content and the previous event's hash (SHA-256 over RFC 8785 JSON). Recomputing them gives the last hash shown above.",
|
|
12
|
+
ar: "سلسلة التجزئة: تجزئة كل حدث تشمل محتواه وتجزئة الحدث السابق (SHA-256 على JSON وفق RFC 8785). إعادة حسابها تعطي آخر تجزئة المذكورة أعلاه.",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
en: "The bodies: each body's SHA-256 matches the hash its event recorded. An erased body is gone, but its hash stays, so the chain still checks.",
|
|
16
|
+
ar: "المحتوى: تطابق تجزئة SHA-256 لكل محتوى التجزئةَ التي سجّلها حدثه. المحتوى الممحوّ لم يعد موجودًا، لكن تجزئته باقية، فتبقى السلسلة قابلة للتحقق.",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
en: "The anchors: each ledger receipt commits to the session, its number of events and its last hash, and is signed by the gateway's DID under a delegation from its owner.",
|
|
20
|
+
ar: "التثبيت: كل إيصال في دفتر الأستاذ يلتزم بالجلسة وعدد أحداثها وآخر تجزئة لها، وموقّع بهوية البوابة (DID) بتفويض من مالكها.",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
en: "The ledger: each receipt is in the ledger's Merkle log (an inclusion proof) under a signed tree head, and the log only ever grew.",
|
|
24
|
+
ar: "دفتر الأستاذ: كل إيصال مُدرَج في سجل ميركل الخاص بدفتر الأستاذ (إثبات إدراج) تحت رأس شجرة موقّع، والسجل لم يزد إلا نموًّا.",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
en: "The gateway's own log, when present: the session's signed statement, its inclusion in the gateway's transparency log, the witnesses' cosignatures and the RFC 3161 time-stamp.",
|
|
28
|
+
ar: "سجل البوابة نفسها، إن وُجد: البيان الموقّع للجلسة، وإدراجه في سجل الشفافية للبوابة، وتواقيع الشهود المشتركة، والختم الزمني وفق RFC 3161.",
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
const REPRODUCE = [
|
|
32
|
+
"npx @zanii/blackbox verify bundle.json --proofs",
|
|
33
|
+
"pip install zanii-blackbox && blackbox verify bundle.json --proofs",
|
|
34
|
+
];
|
|
35
|
+
const parse = (line) => {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(line);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/** spec/investigations.md §7. */
|
|
44
|
+
export function evidencePack(bundle, report, opts) {
|
|
45
|
+
const lines = bundle.lines;
|
|
46
|
+
const first = lines.length ? parse(lines[0]) : {};
|
|
47
|
+
const last = lines.length ? parse(lines.at(-1)) : {};
|
|
48
|
+
const open = first.kind === "session.open" ? first : {};
|
|
49
|
+
const newest = bundle.anchors.at(-1);
|
|
50
|
+
const t = bundle.transparency;
|
|
51
|
+
const proofs = report.proofs === null || opts.proofs === "not_checked"
|
|
52
|
+
? "not_checked"
|
|
53
|
+
: report.proofs.every((p) => p.ok)
|
|
54
|
+
? "checked"
|
|
55
|
+
: "failed";
|
|
56
|
+
return {
|
|
57
|
+
v: 1,
|
|
58
|
+
kind: "blackbox-evidence-pack",
|
|
59
|
+
session_id: bundle.session.session_id,
|
|
60
|
+
label: typeof bundle.session.label === "string" ? bundle.session.label : null,
|
|
61
|
+
agent: open.meta?.agent ?? null,
|
|
62
|
+
anchored_by: bundle.anchored_by,
|
|
63
|
+
ledger_url: bundle.ledger_url,
|
|
64
|
+
record: {
|
|
65
|
+
events: lines.length,
|
|
66
|
+
head: lines.length ? hashLine(lines.at(-1)) : null,
|
|
67
|
+
first_at: typeof first.ts === "string" ? first.ts : null,
|
|
68
|
+
last_at: typeof last.ts === "string" ? last.ts : null,
|
|
69
|
+
closed: last.kind === "session.close",
|
|
70
|
+
},
|
|
71
|
+
anchors: {
|
|
72
|
+
count: bundle.anchors.length,
|
|
73
|
+
last: newest
|
|
74
|
+
? {
|
|
75
|
+
count: newest.count,
|
|
76
|
+
receipt_hash: newest.receipt_hash,
|
|
77
|
+
anchored_at: newest.anchored_at,
|
|
78
|
+
}
|
|
79
|
+
: null,
|
|
80
|
+
},
|
|
81
|
+
transparency: t
|
|
82
|
+
? {
|
|
83
|
+
statement: t.statement !== undefined,
|
|
84
|
+
receipt: t.receipt !== undefined,
|
|
85
|
+
checkpoint: t.checkpoint !== undefined,
|
|
86
|
+
timestamp: t.timestamp !== undefined,
|
|
87
|
+
}
|
|
88
|
+
: null,
|
|
89
|
+
verified: report.ok,
|
|
90
|
+
verification: {
|
|
91
|
+
chain: report.chain.ok ? "ok" : report.chain.code,
|
|
92
|
+
identity: report.identity.ok ? "ok" : "mismatch",
|
|
93
|
+
proofs,
|
|
94
|
+
},
|
|
95
|
+
legal_basis: LEGAL_BASIS,
|
|
96
|
+
steps: STEPS.map((s, i) => ({ n: i + 1, ...s })),
|
|
97
|
+
reproduce: REPRODUCE,
|
|
98
|
+
generated_at: opts.generated_at,
|
|
99
|
+
bundle_sha256: opts.bundle_sha256,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// U+2067 … U+2069 isolate the Arabic paragraphs, so they read right to left in any viewer.
|
|
103
|
+
const rtl = (s) => `${s}`;
|
|
104
|
+
/** The Markdown copy (spec/investigations.md §7): `en`, `ar` or `both`. */
|
|
105
|
+
export function renderEvidencePack(pack, lang = "both") {
|
|
106
|
+
const show = (b) => lang === "en" ? b.en : lang === "ar" ? rtl(b.ar) : `${b.en}\n\n> ${rtl(b.ar)}`;
|
|
107
|
+
const label = (en, ar) => lang === "en" ? en : lang === "ar" ? rtl(ar) : `${en} / ${rtl(ar)}`;
|
|
108
|
+
const r = pack.record;
|
|
109
|
+
const verdict = pack.verified
|
|
110
|
+
? label("VERIFIED", "تم التحقق")
|
|
111
|
+
: label("FAILED VERIFICATION", "فشل التحقق");
|
|
112
|
+
const rows = [
|
|
113
|
+
[label("Session", "الجلسة"), `\`${pack.session_id}\``],
|
|
114
|
+
[label("Label", "التسمية"), pack.label ? pack.label.replaceAll("|", "\\|") : "—"],
|
|
115
|
+
[label("Events", "الأحداث"), String(r.events)],
|
|
116
|
+
[label("Last hash", "آخر تجزئة"), r.head ? `\`${r.head}\`` : "—"],
|
|
117
|
+
[label("Period", "المدة"), `${r.first_at ?? "—"} → ${r.last_at ?? "—"}`],
|
|
118
|
+
[label("Closed", "مغلقة"), r.closed ? label("yes", "نعم") : label("no", "لا")],
|
|
119
|
+
[label("Anchored by", "ثبّتها"), pack.anchored_by ? `\`${pack.anchored_by}\`` : "—"],
|
|
120
|
+
[label("Anchors", "التثبيتات"), String(pack.anchors.count)],
|
|
121
|
+
[label("Chain", "السلسلة"), pack.verification.chain],
|
|
122
|
+
[label("Identity", "الهوية"), pack.verification.identity],
|
|
123
|
+
[label("Ledger proofs", "إثباتات دفتر الأستاذ"), pack.verification.proofs],
|
|
124
|
+
[label("Bundle SHA-256", "تجزئة الحزمة"), `\`${pack.bundle_sha256}\``],
|
|
125
|
+
[label("Generated", "أُنشئت"), pack.generated_at],
|
|
126
|
+
];
|
|
127
|
+
return [
|
|
128
|
+
`# ${label("Evidence pack", "حزمة الأدلة")}`,
|
|
129
|
+
"",
|
|
130
|
+
`**${verdict}**`,
|
|
131
|
+
"",
|
|
132
|
+
`| | |`,
|
|
133
|
+
`|---|---|`,
|
|
134
|
+
...rows.map(([k, v]) => `| ${k} | ${v} |`),
|
|
135
|
+
"",
|
|
136
|
+
`## ${label("Legal basis", "الأساس القانوني")}`,
|
|
137
|
+
"",
|
|
138
|
+
show(pack.legal_basis),
|
|
139
|
+
"",
|
|
140
|
+
`## ${label("How to check it", "خطوات التحقق")}`,
|
|
141
|
+
"",
|
|
142
|
+
...pack.steps.map((s) => `${s.n}. ${show(s)}`),
|
|
143
|
+
"",
|
|
144
|
+
`## ${label("Reproduce offline", "إعادة التحقق دون اتصال")}`,
|
|
145
|
+
"",
|
|
146
|
+
"```sh",
|
|
147
|
+
...pack.reproduce,
|
|
148
|
+
"```",
|
|
149
|
+
"",
|
|
150
|
+
].join("\n");
|
|
151
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type SignedTreeHead } from "@zanii/core";
|
|
2
|
+
/** Zanii's witness co-signature over a signed tree head the caller has checked (signed, append-only). */
|
|
3
|
+
export declare function cosignLedgerSth(sth: SignedTreeHead, o: {
|
|
4
|
+
did: string;
|
|
5
|
+
privateKey: Uint8Array;
|
|
6
|
+
}): {
|
|
7
|
+
v: number;
|
|
8
|
+
witnessed_log_id: string;
|
|
9
|
+
cosignature: SignedTreeHead;
|
|
10
|
+
};
|
|
11
|
+
/** Zanii's check: a valid co-signature over the same (size, root) of a valid tree head. */
|
|
12
|
+
export declare function verifyLedgerCosignature(c: unknown, sth: SignedTreeHead): boolean;
|
|
13
|
+
/** Zanii's equivocation proof: one log key, one size, two roots. Throws unless it really is one. */
|
|
14
|
+
export declare function buildEquivocationProof(a: SignedTreeHead, b: SignedTreeHead): {
|
|
15
|
+
v: number;
|
|
16
|
+
type: string;
|
|
17
|
+
log_id: string;
|
|
18
|
+
size: number;
|
|
19
|
+
sths: SignedTreeHead[];
|
|
20
|
+
};
|
|
21
|
+
/** Re-checks a proof from scratch: both signatures, same log, same size, different roots. */
|
|
22
|
+
export declare function verifyEquivocationProof(proof: unknown): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Zanii's gossip comparison over views already fetched: views of other logs and bad signatures
|
|
25
|
+
* are set aside, and two valid views at one size with different roots are an equivocation proof.
|
|
26
|
+
*/
|
|
27
|
+
export declare function compareViews(logId: string, views: ReadonlyArray<{
|
|
28
|
+
source: string;
|
|
29
|
+
sth?: unknown;
|
|
30
|
+
error?: string;
|
|
31
|
+
}>): {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
reasons: string[];
|
|
34
|
+
equivocation?: ReturnType<typeof buildEquivocationProof>;
|
|
35
|
+
};
|