@sanning/proof 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.
@@ -0,0 +1,124 @@
1
+ // Attested-evidence-export composer (`proof export`, evidence-export.md §5,
2
+ // phase 2). The last-mile issuer/runtime job the kernel does NOT own (§5.1
3
+ // "Out of the kernel"): take a source `sanning.anchor.trace/v1` bundle + a set of
4
+ // operator attestation records + an exporter Ed25519 key, and PRODUCE one signed,
5
+ // offline-verifiable `sanning.evidence.export/v1` artifact — exactly the bytes the
6
+ // kernel's verifyExportBody round-trip-verifies.
7
+ //
8
+ // This is the inverse of evidence.ts's verifyExportBody, and it is deliberately
9
+ // thin: it never reimplements the verdict recompute (it calls the kernel's own
10
+ // recomputeExportVerdict, which reuses verifyEvidenceBundle + verifyAttestation +
11
+ // buildVerdictObject), so the cached kernel_verdict it embeds cannot drift from
12
+ // what the verifier recomputes (§5 step 5). Pure/offline — no network in this
13
+ // slice; a `--sidecar <url>` attestation fetch is a documented follow-up (§5.1),
14
+ // not built here.
15
+ //
16
+ // Trust boundary (P-4, "anyone can export"): the WRAPPER is Ed25519-signed by the
17
+ // exporter's key; the embedded operator attestations keep their own RSA-PSS
18
+ // signatures untouched (the composer never re-signs them — it is not the
19
+ // operator). The only per-record transformation is a boundary transcode of the
20
+ // attestation `signature` to the lowercase hex §2.2 stores; that field is OUTSIDE
21
+ // the signed attestation payload (JCS(payload)), so re-encoding it is safe and
22
+ // does not invalidate the operator signature.
23
+ import { EXPORT_BODY_TYPE, recomputeExportVerdict, } from "./evidence.js";
24
+ import { base64UrlToBytes, bytesToHex, ed25519PublicKey, ed25519Sign, sha256Hex, utf8, } from "./crypto.js";
25
+ import { jcs } from "./verifier.js";
26
+ const GENESIS = "GENESIS";
27
+ const EXPORT_SCHEMA = "sanning.evidence.export/v1";
28
+ // Transcode an attestation `signature` to the lowercase hex §2.2 mandates. The
29
+ // issuer emits either lowercase-hex or unpadded base64url; §2.2 stores hex, so
30
+ // this is the composer's boundary normalization. Heuristic mirrors the repo's
31
+ // existing hex-vs-text discriminator (cli.ts decodeLogValue): an even-length,
32
+ // all-hex-digit string is already hex (lowercased); anything else is decoded as
33
+ // base64url and hex-encoded. A 256-byte RSA signature is 512 hex chars vs ~342
34
+ // base64url chars, so the two never collide in practice. Throws on a value that
35
+ // is neither.
36
+ export function attestationSignatureToHex(signature) {
37
+ if (signature.length > 0 && signature.length % 2 === 0 && /^[0-9a-fA-F]+$/.test(signature)) {
38
+ return signature.toLowerCase();
39
+ }
40
+ return bytesToHex(base64UrlToBytes(signature));
41
+ }
42
+ // Normalize one attestation record for embedding: deep-clone (never mutate the
43
+ // caller's input) and transcode its signature to lowercase hex. Everything else
44
+ // — payload, public_key, signature_alg, checkpoint_tx_id — rides verbatim so the
45
+ // operator's RSA-PSS signature over JCS(payload) remains valid.
46
+ function normalizeAttestation(rec) {
47
+ const cloned = JSON.parse(JSON.stringify(rec));
48
+ if (typeof cloned.signature !== "string") {
49
+ throw new Error("attestation record missing a string `signature`");
50
+ }
51
+ cloned.signature = attestationSignatureToHex(cloned.signature);
52
+ return cloned;
53
+ }
54
+ // Compose an attested evidence export (evidence-export.md §5).
55
+ //
56
+ // 1. Normalize + transcode the attestation records (boundary hex, above).
57
+ // 2. Recompute the §4 kernel_verdict over the source bundle + attestations,
58
+ // reusing the kernel's own verify path (recomputeExportVerdict) so the cached
59
+ // copy byte-agrees with the verifier's later recompute (§5 step 5).
60
+ // 3. Assemble the export body: kernel_verdict + inline source_bundle +
61
+ // source_bundle_hash = SHA-256(JCS(source_bundle)) + attestations[] + schema.
62
+ // 4. Build the sanning.evidence/v1 wrapper (body_type export, Ed25519), strip
63
+ // signature, JCS, Ed25519-sign with the exporter key, attach.
64
+ //
65
+ // Returns the signed artifact + a small compose summary.
66
+ export async function composeExport(sourceBundle, attestations, exporterKey, options = {}) {
67
+ if (sourceBundle === null || typeof sourceBundle !== "object" || Array.isArray(sourceBundle)) {
68
+ throw new Error("composeExport: source bundle must be a JSON object");
69
+ }
70
+ if (!Array.isArray(attestations)) {
71
+ throw new Error("composeExport: attestations must be an array");
72
+ }
73
+ if (typeof exporterKey?.privateKey !== "string" || exporterKey.privateKey.length === 0) {
74
+ throw new Error("composeExport: exporterKey.privateKey (32-byte hex seed) is required");
75
+ }
76
+ const generatedAt = options.generatedAt ?? new Date().toISOString();
77
+ const previousHash = options.previousHash ?? GENESIS;
78
+ const issuerId = options.issuerId ?? "sanning-verify";
79
+ const gateway = options.gateway ?? null;
80
+ // (1) boundary-transcode the attestation signatures to hex.
81
+ const records = attestations.map(normalizeAttestation);
82
+ // (2) recompute the §4 verdict via the KERNEL's own verify path (no reimpl).
83
+ const composed = await recomputeExportVerdict(sourceBundle, records, generatedAt);
84
+ // (3) assemble the export body. source_bundle_hash is SHA-256(JCS(source)) —
85
+ // the linkage commitment the wrapper signature transitively covers (§2.4).
86
+ const sourceBundleHash = await sha256Hex(utf8(jcs(sourceBundle)));
87
+ const body = {
88
+ kernel_verdict: composed.verdict,
89
+ source_bundle: sourceBundle,
90
+ source_bundle_hash: sourceBundleHash,
91
+ attestations: records,
92
+ export_schema: EXPORT_SCHEMA,
93
+ };
94
+ // (4) build + sign the sanning.evidence/v1 wrapper. The wrapper `verdict` is the
95
+ // coarse family rollup DERIVED from body.kernel_verdict (display context, never
96
+ // trusted — the verifier recomputes). body_hash = SHA-256(JCS(body)).
97
+ const publicKey = exporterKey.publicKey ?? (await ed25519PublicKey(exporterKey.privateKey));
98
+ const wrapperPre = {
99
+ spec_version: "sanning.evidence/v1",
100
+ body_type: EXPORT_BODY_TYPE,
101
+ issuer: { kind: "issuer", issuer_id: issuerId },
102
+ generated_at: generatedAt,
103
+ gateway,
104
+ verdict: {
105
+ status: composed.verdict.status,
106
+ ...(composed.verdict.summary !== undefined ? { summary: composed.verdict.summary } : {}),
107
+ ...(composed.verdict.counts !== undefined ? { counts: composed.verdict.counts } : {}),
108
+ as_of: generatedAt,
109
+ },
110
+ body,
111
+ body_hash: await sha256Hex(utf8(jcs(body))),
112
+ previous_hash: previousHash,
113
+ signature_alg: "ed25519",
114
+ public_key: publicKey,
115
+ };
116
+ const signature = await ed25519Sign(utf8(jcs(wrapperPre)), exporterKey.privateKey);
117
+ const bundle = { ...wrapperPre, signature };
118
+ return {
119
+ bundle,
120
+ status: composed.status,
121
+ sourceBundleHash,
122
+ boundAttestations: composed.attestations.filter((a) => a.ok).length,
123
+ };
124
+ }
@@ -0,0 +1,16 @@
1
+ export declare function bytesToHex(bytes: Uint8Array): string;
2
+ export declare function hexToBytes(hex: string): Uint8Array;
3
+ export declare function utf8(s: string): Uint8Array;
4
+ export declare function sha256Bytes(bytes: Uint8Array): Promise<Uint8Array>;
5
+ export declare function sha256Hex(bytes: Uint8Array): Promise<string>;
6
+ export declare function ed25519Verify(signatureHex: string, message: Uint8Array, publicKeyHex: string): Promise<boolean>;
7
+ export declare function ed25519Sign(message: Uint8Array, privateKeyHex: string): Promise<string>;
8
+ export declare function ed25519PublicKey(privateKeyHex: string): Promise<string>;
9
+ export declare function verifyRsaPssSha256(payloadBytes: Uint8Array, signatureHex: string, publicKey: {
10
+ kty: "RSA";
11
+ n: string;
12
+ e: string;
13
+ }): Promise<boolean>;
14
+ export declare function deriveOperatorAddress(nBase64url: string): Promise<string>;
15
+ export declare function base64UrlToBytes(b64url: string): Uint8Array;
16
+ export declare function bytesToBase64Url(bytes: Uint8Array): string;
package/dist/crypto.js ADDED
@@ -0,0 +1,215 @@
1
+ // Low-level cryptographic primitives for the verifier. Deliberately thin: the
2
+ // only third-party crypto dependency is @noble/ed25519 (single-file, audited,
3
+ // zero-dependency). SHA-256 and SHA-512 come from WebCrypto — no extra hashing
4
+ // library to trust.
5
+ import * as ed from "@noble/ed25519";
6
+ // @noble/ed25519 needs SHA-512 to verify. Wire it to WebCrypto rather than
7
+ // pulling in @noble/hashes. globalThis.crypto.subtle exists in every modern
8
+ // browser and in Node >= 19, so the same code path runs in the app and in tests.
9
+ ed.etc.sha512Async = async (...msgs) => new Uint8Array(await crypto.subtle.digest("SHA-512", asBufferSource(ed.etc.concatBytes(...msgs))));
10
+ // WebCrypto's digest() wants a BufferSource backed by a (non-shared) ArrayBuffer.
11
+ // Our byte arrays always are; this keeps TS 5.7's stricter Uint8Array<ArrayBufferLike>
12
+ // typing satisfied without a runtime copy.
13
+ function asBufferSource(bytes) {
14
+ return bytes;
15
+ }
16
+ export function bytesToHex(bytes) {
17
+ let out = "";
18
+ for (const b of bytes)
19
+ out += b.toString(16).padStart(2, "0");
20
+ return out;
21
+ }
22
+ export function hexToBytes(hex) {
23
+ if (hex.length % 2 !== 0)
24
+ throw new Error("hexToBytes: odd-length string");
25
+ // Full validation: parseInt() partially parses ("1g" -> 1), which would let
26
+ // malformed hex through. A strict charset check closes that.
27
+ if (!/^[0-9a-fA-F]*$/.test(hex))
28
+ throw new Error("hexToBytes: non-hex characters");
29
+ const out = new Uint8Array(hex.length / 2);
30
+ for (let i = 0; i < out.length; i++) {
31
+ out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
32
+ }
33
+ return out;
34
+ }
35
+ export function utf8(s) {
36
+ return new TextEncoder().encode(s);
37
+ }
38
+ export async function sha256Bytes(bytes) {
39
+ return new Uint8Array(await crypto.subtle.digest("SHA-256", asBufferSource(bytes)));
40
+ }
41
+ export async function sha256Hex(bytes) {
42
+ return bytesToHex(await sha256Bytes(bytes));
43
+ }
44
+ // Verify an Ed25519 signature. Inputs are hex (signature, public key) as they
45
+ // appear in the envelope; message is raw bytes. Never throws — a malformed
46
+ // signature or key is "not verified," not an exception, so a hostile envelope
47
+ // can't crash the checker.
48
+ export async function ed25519Verify(signatureHex, message, publicKeyHex) {
49
+ try {
50
+ // zip215:false → strict RFC-8032 verification, matching the Go agent's
51
+ // crypto/ed25519 exactly (noble defaults to the more lenient zip215:true).
52
+ return await ed.verifyAsync(hexToBytes(signatureHex), message, hexToBytes(publicKeyHex), {
53
+ zip215: false,
54
+ });
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ // Ed25519 SIGN the raw `message` bytes with a 32-byte private-key seed (lowercase
61
+ // hex). Returns the signature as lowercase hex. The signing counterpart to
62
+ // ed25519Verify — the ONE signing primitive the composer (`proof export`,
63
+ // evidence-export.md §5) needs to Ed25519-sign the export wrapper with the
64
+ // exporter's key ("anyone can export", P-4). The @noble sha512Async hook wired at
65
+ // module load serves signing and verification alike, so this stays WebCrypto-only.
66
+ export async function ed25519Sign(message, privateKeyHex) {
67
+ return bytesToHex(await ed.signAsync(message, hexToBytes(privateKeyHex)));
68
+ }
69
+ // Derive the Ed25519 public key (lowercase hex) from a 32-byte private-key seed
70
+ // (lowercase hex) — the wrapper's embedded `public_key` when composing an export.
71
+ export async function ed25519PublicKey(privateKeyHex) {
72
+ return bytesToHex(await ed.getPublicKeyAsync(hexToBytes(privateKeyHex)));
73
+ }
74
+ // ---------------------------------------------------------------------------
75
+ // RSA-PSS operator-attestation crypto (sanning.evidence.export/v1)
76
+ //
77
+ // The attested-evidence-export body embeds operator attestations signed with
78
+ // RSA-PSS-SHA-256 over the JCS-canonical attestation payload; the operator key
79
+ // is the operator's Arweave RSA wallet. These two pure functions are the only
80
+ // new primitive that body needs and the only RSA in the kernel: verify one
81
+ // attestation signature, and derive the operator's Arweave address from the
82
+ // embedded modulus (the self-describing key→wallet binding).
83
+ //
84
+ // RSA-PSS parameters are PINNED for cross-kernel byte-agreement between the
85
+ // issuer, the TS kernel, and the Python kernel: hash = SHA-256; MGF1 with
86
+ // SHA-256; salt length = 32 (the SHA-256 digest length, i.e. OpenSSL's
87
+ // RSA_PSS_SALTLEN_DIGEST); padding = PSS. saltLength=32 is load-bearing — the
88
+ // shipped issuer's former RSA_PSS_SALTLEN_AUTO (which resolves to the maximum,
89
+ // key-size-dependent salt on signing) is NOT verifiable by WebCrypto, which
90
+ // does not auto-detect salt on verify. The pin is what makes these round-trip.
91
+ const RSA_PSS_SALT_LENGTH = 32;
92
+ // The ONLY RSA public exponent a legitimate operator (Arweave wallet) key uses.
93
+ // The operator-address binding SHA-256(n) commits to the modulus ALONE and
94
+ // assumes this exponent; a verifier MUST reject any other. Without it, e=1 makes
95
+ // RSA verification the identity (s^1 mod n = s), letting an attacker forge a
96
+ // valid-looking attestation from a victim operator's PUBLIC modulus with NO
97
+ // private key. See the exponent guard in verifyRsaPssSha256.
98
+ const RSA_PUBLIC_EXPONENT = 65537n;
99
+ // Verify an RSA-PSS-SHA-256 signature over `payloadBytes` (the raw JCS bytes of
100
+ // the attestation payload) with the JWK RSA public key `{kty:"RSA", n, e}`
101
+ // (`n`/`e` base64url) and a lowercase-hex `signatureHex`.
102
+ //
103
+ // Malformed-vs-failed follows the kernel's split: a malformed key (JWK that
104
+ // won't import) or malformed signature hex is a caller/input error and THROWS a
105
+ // prefixed Error — the same "malformed" signal hexToBytes raises and the CLI
106
+ // buckets as exit-2. A well-formed key + signature that simply does not verify
107
+ // (wrong signer, tampered payload, wrong salt length) returns `false` and never
108
+ // throws, so a hostile attestation can't crash the checker (same contract as
109
+ // ed25519Verify above).
110
+ export async function verifyRsaPssSha256(payloadBytes, signatureHex, publicKey) {
111
+ let signature;
112
+ try {
113
+ signature = hexToBytes(signatureHex);
114
+ }
115
+ catch (e) {
116
+ throw new Error(`verifyRsaPssSha256: malformed signature hex: ${stringifyErr(e)}`);
117
+ }
118
+ // Enforce the RSA public exponent (see RSA_PUBLIC_EXPONENT). A structurally
119
+ // valid key with a non-65537 exponent is not a legitimate operator key, so
120
+ // its signature does not validly verify — return false (a FAILED attestation,
121
+ // exit 1), never accept it. A malformed `e` falls through to importKey below,
122
+ // which throws the malformed-key error (exit 2). This is the guard that closes
123
+ // the e=1 identity-forgery: e=1 imports fine in WebCrypto and would otherwise
124
+ // "verify" an attacker-built PSS encoded message with no private key.
125
+ // Decode `e`. A non-base64url `e` is malformed — importKey raises it below.
126
+ let eBytes = null;
127
+ try {
128
+ eBytes = base64UrlToBytes(publicKey.e);
129
+ }
130
+ catch {
131
+ eBytes = null;
132
+ }
133
+ // An empty `e` is malformed too: WebCrypto imports it as e=0 (which would
134
+ // return false), while the Python kernel's key import raises. Throw here so
135
+ // an empty exponent is malformed/exit-2 in BOTH kernels, not a divergence.
136
+ if (eBytes !== null && eBytes.length === 0) {
137
+ throw new Error("verifyRsaPssSha256: malformed RSA public key: empty exponent");
138
+ }
139
+ const exponent = eBytes === null ? null : bytesToBigInt(eBytes);
140
+ if (exponent !== null && exponent !== RSA_PUBLIC_EXPONENT) {
141
+ return false;
142
+ }
143
+ let key;
144
+ try {
145
+ key = await crypto.subtle.importKey("jwk", { kty: publicKey.kty, n: publicKey.n, e: publicKey.e, ext: true }, { name: "RSA-PSS", hash: "SHA-256" }, false, ["verify"]);
146
+ }
147
+ catch (e) {
148
+ throw new Error(`verifyRsaPssSha256: malformed RSA public key: ${stringifyErr(e)}`);
149
+ }
150
+ try {
151
+ return await crypto.subtle.verify({ name: "RSA-PSS", saltLength: RSA_PSS_SALT_LENGTH }, key, asBufferSource(signature), asBufferSource(payloadBytes));
152
+ }
153
+ catch {
154
+ // Some engines throw (rather than return false) on a wrong-length RSA
155
+ // signature. That is still just "not verified," not a malformed-input
156
+ // condition — the signature parsed as hex fine; it's the wrong signature.
157
+ return false;
158
+ }
159
+ }
160
+ // Derive an operator's Arweave wallet address from the base64url JWK modulus
161
+ // `n`: address = base64url(SHA-256(rawModulusBytes)), where rawModulusBytes is
162
+ // the decoded base64url `n`. This is the same owner→address derivation
163
+ // arweave-js uses (SHA-256 over the modulus octets, base64url without padding),
164
+ // so it binds an embedded RSA key to a specific wallet with no roster lookup.
165
+ //
166
+ // Async because the kernel takes SHA-256 from WebCrypto (crypto.subtle.digest),
167
+ // which is Promise-based — the module is deliberately WebCrypto-only with no
168
+ // second hashing library and no node:crypto, so it stays browser-portable.
169
+ // Throws a prefixed Error on a malformed (non-base64url) `n`.
170
+ export async function deriveOperatorAddress(nBase64url) {
171
+ const modulus = base64UrlToBytes(nBase64url);
172
+ const digest = await sha256Bytes(modulus);
173
+ return bytesToBase64Url(digest);
174
+ }
175
+ // base64url (RFC 4648 §5, unpadded) codecs. Kept WebCrypto/browser-portable —
176
+ // atob/btoa exist in browsers and in Node >= 16 — so the kernel stays free of
177
+ // node:Buffer. Arweave and JWK both use unpadded base64url. Exported because the
178
+ // composer transcodes an issuer-emitted base64url attestation signature to the
179
+ // lowercase hex §2.2 stores (evidence-export.md §2.2 — the composer's boundary job).
180
+ export function base64UrlToBytes(b64url) {
181
+ if (!/^[A-Za-z0-9_-]*$/.test(b64url)) {
182
+ throw new Error("base64url: non-base64url characters");
183
+ }
184
+ let b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
185
+ const rem = b64.length % 4;
186
+ if (rem === 1)
187
+ throw new Error("base64url: invalid length");
188
+ if (rem === 2)
189
+ b64 += "==";
190
+ else if (rem === 3)
191
+ b64 += "=";
192
+ const bin = atob(b64);
193
+ const out = new Uint8Array(bin.length);
194
+ for (let i = 0; i < bin.length; i++)
195
+ out[i] = bin.charCodeAt(i);
196
+ return out;
197
+ }
198
+ export function bytesToBase64Url(bytes) {
199
+ let bin = "";
200
+ for (const b of bytes)
201
+ bin += String.fromCharCode(b);
202
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
203
+ }
204
+ // Big-endian unsigned integer from bytes — used only for the RSA public-exponent
205
+ // guard, so alternate encodings of 65537 (e.g. with leading zero octets) still
206
+ // compare equal.
207
+ function bytesToBigInt(bytes) {
208
+ let v = 0n;
209
+ for (const b of bytes)
210
+ v = (v << 8n) | BigInt(b);
211
+ return v;
212
+ }
213
+ function stringifyErr(e) {
214
+ return e instanceof Error ? e.message : String(e);
215
+ }
@@ -0,0 +1,214 @@
1
+ import type { Envelope } from "./types.js";
2
+ export declare const ANCHOR_TRACE_BODY_TYPE = "sanning.anchor.trace/v1";
3
+ export declare const EXPORT_BODY_TYPE = "sanning.evidence.export/v1";
4
+ export interface EvidenceIssuer {
5
+ kind: string;
6
+ tenant_id?: string;
7
+ agent_id?: string;
8
+ producer_id?: string;
9
+ [k: string]: unknown;
10
+ }
11
+ export interface EvidenceVerdict {
12
+ status: string;
13
+ summary?: string;
14
+ counts?: Record<string, number>;
15
+ as_of?: string;
16
+ [k: string]: unknown;
17
+ }
18
+ export interface TraceCheckpoint {
19
+ tx_id: string;
20
+ envelope: Envelope;
21
+ record_bytes: string;
22
+ merkle_root: string;
23
+ }
24
+ export interface TraceInclusion {
25
+ leaf_hash: string;
26
+ leaf_index: number;
27
+ leaf_count: number;
28
+ audit_path: string[];
29
+ checkpoint_tx_id: string;
30
+ }
31
+ export interface TraceEvent {
32
+ envelope: Envelope;
33
+ record_bytes?: string;
34
+ content?: string;
35
+ inclusion: TraceInclusion;
36
+ }
37
+ export interface AnchorTraceBody {
38
+ checkpoints: TraceCheckpoint[];
39
+ events: TraceEvent[];
40
+ }
41
+ export interface EvidenceBundle {
42
+ spec_version: string;
43
+ body_type: string;
44
+ issuer: EvidenceIssuer;
45
+ generated_at: string;
46
+ gateway?: string | null;
47
+ verdict: EvidenceVerdict;
48
+ body?: unknown;
49
+ body_ref?: string;
50
+ body_hash: string;
51
+ previous_hash?: string;
52
+ signature_alg: string;
53
+ public_key: string;
54
+ signature: string;
55
+ [k: string]: unknown;
56
+ }
57
+ export type EvidenceStatus = "verified" | "partial" | "failed" | "malformed";
58
+ export type OnChainOutcome = "confirm" | "mismatch" | "unreachable";
59
+ export interface PerGatewayOutcome {
60
+ gateway: string;
61
+ outcome: OnChainOutcome;
62
+ }
63
+ export interface OnChainResult {
64
+ rollup: OnChainOutcome;
65
+ onChainOk: boolean | null;
66
+ perGateway: PerGatewayOutcome[];
67
+ }
68
+ export interface CheckpointResult {
69
+ txId: string;
70
+ envelopeOk: boolean;
71
+ merkleRootOk: boolean;
72
+ onChainOk: boolean | null;
73
+ onChain: OnChainResult | null;
74
+ ok: boolean;
75
+ errors: string[];
76
+ }
77
+ export interface EventResult {
78
+ eventId: string;
79
+ envelopeOk: boolean;
80
+ payloadBindingOk: boolean | null;
81
+ inclusionOk: boolean;
82
+ checkpointBound: boolean;
83
+ contentOk: boolean | null;
84
+ ok: boolean;
85
+ errors: string[];
86
+ }
87
+ export interface EvidenceBundleResult {
88
+ status: EvidenceStatus;
89
+ specVersionOk: boolean;
90
+ bodyHashOk: boolean;
91
+ signatureOk: boolean;
92
+ bodyType: string | null;
93
+ onChainChecked: boolean;
94
+ checkpoints: CheckpointResult[];
95
+ events: EventResult[];
96
+ assertedStatus: string | null;
97
+ export?: ExportResult;
98
+ errors: string[];
99
+ }
100
+ export interface AttestationRecord {
101
+ checkpoint_tx_id: string;
102
+ payload: AttestationPayload;
103
+ signature_alg: string;
104
+ public_key: {
105
+ kty: "RSA";
106
+ n: string;
107
+ e: string;
108
+ };
109
+ signature: string;
110
+ [k: string]: unknown;
111
+ }
112
+ export interface AttestationPayload {
113
+ tx_id: string;
114
+ data_hash: string;
115
+ operator: string;
116
+ subject_ref?: {
117
+ hash: string;
118
+ type: string;
119
+ };
120
+ level?: number;
121
+ gateway?: string;
122
+ [k: string]: unknown;
123
+ }
124
+ export interface ExportBody {
125
+ kernel_verdict: VerdictObject;
126
+ source_bundle?: EvidenceBundle;
127
+ source_bundle_ref?: string;
128
+ source_bundle_hash: string;
129
+ attestations: AttestationRecord[];
130
+ export_schema?: string;
131
+ [k: string]: unknown;
132
+ }
133
+ export interface VerdictEvent {
134
+ event_id: string;
135
+ signature_ok: boolean;
136
+ payload_bound: boolean | null;
137
+ inclusion_ok: boolean;
138
+ content_ok: boolean | null;
139
+ status: string;
140
+ }
141
+ export interface VerdictOnChainGateway {
142
+ gateway: string;
143
+ outcome: OnChainOutcome;
144
+ block_height?: number;
145
+ }
146
+ export interface VerdictOnChain {
147
+ rollup: OnChainOutcome;
148
+ on_chain_ok: boolean | null;
149
+ per_gateway: VerdictOnChainGateway[];
150
+ }
151
+ export interface VerdictAttestation {
152
+ operator: string;
153
+ gateway?: string;
154
+ signature_ok: boolean;
155
+ operator_address_bound: boolean;
156
+ data_hash_bound: boolean;
157
+ level?: number;
158
+ subject_ref_ok: boolean | null;
159
+ }
160
+ export interface VerdictCheckpoint {
161
+ checkpoint_tx_id: string;
162
+ merkle_root_ok: boolean;
163
+ on_chain: VerdictOnChain | null;
164
+ attestations: VerdictAttestation[];
165
+ }
166
+ export interface VerdictObject {
167
+ schema_version: string;
168
+ status: string;
169
+ summary?: string;
170
+ counts?: {
171
+ verified: number;
172
+ failed: number;
173
+ undetermined: number;
174
+ };
175
+ as_of?: string;
176
+ events: VerdictEvent[];
177
+ checkpoints: VerdictCheckpoint[];
178
+ custody_chain: boolean | null;
179
+ [k: string]: unknown;
180
+ }
181
+ export interface AttestationResult {
182
+ checkpointTxId: string;
183
+ operator: string;
184
+ gateway: string | null;
185
+ signatureOk: boolean;
186
+ operatorAddressBound: boolean;
187
+ dataHashBound: boolean;
188
+ checkpointResolved: boolean;
189
+ level: number | null;
190
+ subjectRefOk: boolean | null;
191
+ ok: boolean;
192
+ errors: string[];
193
+ }
194
+ export interface ExportResult {
195
+ sourceLinkageOk: boolean;
196
+ sourceStatus: EvidenceStatus;
197
+ verdictAgreementOk: boolean;
198
+ attestations: AttestationResult[];
199
+ verdict: VerdictObject;
200
+ status: EvidenceStatus;
201
+ }
202
+ export interface VerifyEvidenceOptions {
203
+ gateways?: string[];
204
+ content?: Record<string, Uint8Array | string>;
205
+ fetchImpl?: typeof fetch;
206
+ }
207
+ export declare function verifyEvidenceBundle(bundle: unknown, options?: VerifyEvidenceOptions): Promise<EvidenceBundleResult>;
208
+ export interface ComposeVerdictResult {
209
+ verdict: VerdictObject;
210
+ sourceStatus: EvidenceStatus;
211
+ status: EvidenceStatus;
212
+ attestations: AttestationResult[];
213
+ }
214
+ export declare function recomputeExportVerdict(sourceBundle: EvidenceBundle, attestationRecords: AttestationRecord[], generatedAt: string, options?: VerifyEvidenceOptions): Promise<ComposeVerdictResult>;