@passportsign/core 0.1.0 → 0.2.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.
Files changed (106) hide show
  1. package/dist/badge.d.ts +5 -0
  2. package/dist/badge.d.ts.map +1 -1
  3. package/dist/badge.js +8 -2
  4. package/dist/badge.js.map +1 -1
  5. package/dist/bind.d.ts.map +1 -1
  6. package/dist/bind.js +2 -8
  7. package/dist/bind.js.map +1 -1
  8. package/dist/bundle-fs.d.ts +16 -0
  9. package/dist/bundle-fs.d.ts.map +1 -0
  10. package/dist/bundle-fs.js +31 -0
  11. package/dist/bundle-fs.js.map +1 -0
  12. package/dist/bundle.d.ts +13 -5
  13. package/dist/bundle.d.ts.map +1 -1
  14. package/dist/bundle.js +18 -20
  15. package/dist/bundle.js.map +1 -1
  16. package/dist/canonical.d.ts.map +1 -1
  17. package/dist/canonical.js +3 -4
  18. package/dist/canonical.js.map +1 -1
  19. package/dist/classify.d.ts +68 -0
  20. package/dist/classify.d.ts.map +1 -0
  21. package/dist/classify.js +117 -0
  22. package/dist/classify.js.map +1 -0
  23. package/dist/dsse-common.d.ts +32 -0
  24. package/dist/dsse-common.d.ts.map +1 -0
  25. package/dist/dsse-common.js +26 -0
  26. package/dist/dsse-common.js.map +1 -0
  27. package/dist/dsse-web.d.ts +28 -0
  28. package/dist/dsse-web.d.ts.map +1 -0
  29. package/dist/dsse-web.js +81 -0
  30. package/dist/dsse-web.js.map +1 -0
  31. package/dist/dsse.d.ts +2 -26
  32. package/dist/dsse.d.ts.map +1 -1
  33. package/dist/dsse.js +2 -19
  34. package/dist/dsse.js.map +1 -1
  35. package/dist/encoding.d.ts +20 -0
  36. package/dist/encoding.d.ts.map +1 -0
  37. package/dist/encoding.js +88 -0
  38. package/dist/encoding.js.map +1 -0
  39. package/dist/github.js +2 -2
  40. package/dist/github.js.map +1 -1
  41. package/dist/index.d.ts +9 -3
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +8 -2
  44. package/dist/index.js.map +1 -1
  45. package/dist/log/rekor.d.ts +1 -1
  46. package/dist/log/rekor.d.ts.map +1 -1
  47. package/dist/log/rekor.js +7 -10
  48. package/dist/log/rekor.js.map +1 -1
  49. package/dist/lookup.d.ts +46 -0
  50. package/dist/lookup.d.ts.map +1 -0
  51. package/dist/lookup.js +101 -0
  52. package/dist/lookup.js.map +1 -0
  53. package/dist/merkle.js +3 -3
  54. package/dist/merkle.js.map +1 -1
  55. package/dist/nonce.js +1 -1
  56. package/dist/nonce.js.map +1 -1
  57. package/dist/profile-index.d.ts +64 -0
  58. package/dist/profile-index.d.ts.map +1 -0
  59. package/dist/profile-index.js +161 -0
  60. package/dist/profile-index.js.map +1 -0
  61. package/dist/revoke.d.ts +30 -0
  62. package/dist/revoke.d.ts.map +1 -0
  63. package/dist/revoke.js +42 -0
  64. package/dist/revoke.js.map +1 -0
  65. package/dist/sdk-payload.d.ts.map +1 -1
  66. package/dist/sdk-payload.js +4 -6
  67. package/dist/sdk-payload.js.map +1 -1
  68. package/dist/statement.d.ts +41 -0
  69. package/dist/statement.d.ts.map +1 -1
  70. package/dist/statement.js +43 -0
  71. package/dist/statement.js.map +1 -1
  72. package/dist/submit.d.ts +3 -3
  73. package/dist/submit.d.ts.map +1 -1
  74. package/dist/submit.js +3 -14
  75. package/dist/submit.js.map +1 -1
  76. package/dist/verifier.d.ts.map +1 -1
  77. package/dist/verifier.js +4 -14
  78. package/dist/verifier.js.map +1 -1
  79. package/dist/web.d.ts +35 -0
  80. package/dist/web.d.ts.map +1 -0
  81. package/dist/web.js +35 -0
  82. package/dist/web.js.map +1 -0
  83. package/package.json +6 -2
  84. package/src/badge.ts +124 -113
  85. package/src/bind.ts +128 -137
  86. package/src/bundle-fs.ts +40 -0
  87. package/src/bundle.ts +138 -127
  88. package/src/canonical.ts +33 -33
  89. package/src/classify.ts +165 -0
  90. package/src/dsse-common.ts +45 -0
  91. package/src/dsse-web.ts +97 -0
  92. package/src/dsse.ts +63 -91
  93. package/src/encoding.ts +96 -0
  94. package/src/github.ts +196 -196
  95. package/src/index.ts +59 -2
  96. package/src/log/rekor.ts +330 -334
  97. package/src/lookup.ts +175 -0
  98. package/src/merkle.ts +187 -187
  99. package/src/nonce.ts +53 -53
  100. package/src/profile-index.ts +222 -0
  101. package/src/revoke.ts +67 -0
  102. package/src/sdk-payload.ts +60 -62
  103. package/src/statement.ts +203 -119
  104. package/src/submit.ts +38 -54
  105. package/src/verifier.ts +304 -317
  106. package/src/web.ts +175 -0
package/src/verifier.ts CHANGED
@@ -1,317 +1,304 @@
1
- /**
2
- * Bundle verifier — the trust anchor for everyone who is not us.
3
- *
4
- * Day 6 scope: structural integrity (statement hash matches Rekor's
5
- * recorded payloadHash), Merkle inclusion proof against the captured
6
- * root, and log-root consistency between the captured root and the
7
- * current witnessed root.
8
- *
9
- * Day 7 scope (deferred): SDK proof verification. Requires a
10
- * bundle-schema extension to carry SDK inputs (proofs array,
11
- * originalQuery, queryResult). For now `sdk_proof` reports
12
- * `'pending_day_7'`.
13
- */
14
-
15
- import { createHash } from 'node:crypto';
16
-
17
- import { type PassportsignBundle, validateBundle } from './bundle.js';
18
- import { type RekorClient } from './log/rekor.js';
19
- import { hashLeaf, verifyConsistency, verifyInclusion } from './merkle.js';
20
- import { unpackSdkPayload } from './sdk-payload.js';
21
-
22
- export type CheckResult = 'pass' | 'fail' | 'skipped';
23
-
24
- export interface BundleVerifyResult {
25
- /**
26
- * Statement bytes in the bundle hash to the `payloadHash` Rekor recorded
27
- * for the entry.
28
- */
29
- hash_match: CheckResult;
30
- /**
31
- * The captured inclusion proof verifies the Rekor entry's leaf hash
32
- * against the captured root.
33
- */
34
- inclusion_proof: CheckResult;
35
- /**
36
- * The captured root is a prefix of the current witnessed root (the log
37
- * has not been rewritten in a way that orphans our entry). Skipped when
38
- * no rekor client is provided.
39
- */
40
- root_consistency: CheckResult;
41
- /**
42
- * SDK proof verification. `'skipped'` when no SDK verifier is injected.
43
- * `'pass'` when the SDK validates the proofs AND the returned
44
- * uniqueIdentifier matches the statement's predicate.
45
- */
46
- sdk_proof: CheckResult;
47
- /**
48
- * `'pass'` only when every enabled check passes; `'fail'` if any check
49
- * fails; `'pending'` when one or more checks are `'skipped'`.
50
- */
51
- overall: 'pass' | 'fail' | 'pending';
52
- errors: string[];
53
- }
54
-
55
- export interface SdkVerifyInput {
56
- proofs: unknown[];
57
- originalQuery: unknown;
58
- queryResult: unknown;
59
- scope?: string;
60
- devMode?: boolean;
61
- }
62
-
63
- export interface SdkVerifyResult {
64
- verified: boolean;
65
- uniqueIdentifier: string | undefined;
66
- }
67
-
68
- export interface SdkVerifier {
69
- verify(input: SdkVerifyInput): Promise<SdkVerifyResult>;
70
- }
71
-
72
- export interface VerifyBundleDeps {
73
- /** Inject a Rekor client to enable hash_match / inclusion / consistency checks. */
74
- rekor?: RekorClient;
75
- /** Inject a zkPassport SDK verifier to enable the sdk_proof check. */
76
- sdkVerifier?: SdkVerifier;
77
- }
78
-
79
- function hexToBytes(hex: string): Uint8Array {
80
- const out = new Uint8Array(hex.length / 2);
81
- for (let i = 0; i < out.length; i++) {
82
- out[i] = parseInt(hex.substr(i * 2, 2), 16);
83
- }
84
- return out;
85
- }
86
-
87
- function sha256Hex(bytes: Uint8Array): string {
88
- return createHash('sha256').update(bytes).digest('hex');
89
- }
90
-
91
- interface ParsedEntryBody {
92
- payloadHashHex: string;
93
- }
94
-
95
- function parseEntryBody(bodyBase64: string): ParsedEntryBody {
96
- const bytes = Buffer.from(bodyBase64, 'base64').toString('utf8');
97
- const body = JSON.parse(bytes) as Record<string, unknown>;
98
- const spec = body['spec'] as Record<string, unknown> | undefined;
99
- const content = spec?.['content'] as Record<string, unknown> | undefined;
100
- const payloadHash = content?.['payloadHash'] as Record<string, unknown> | undefined;
101
- const value = payloadHash?.['value'];
102
- if (typeof value !== 'string') {
103
- throw new Error('Rekor entry body missing spec.content.payloadHash.value');
104
- }
105
- return { payloadHashHex: value };
106
- }
107
-
108
- /**
109
- * Verify a passportsign bundle. Online checks (hash_match, inclusion_proof,
110
- * root_consistency) require a {@link RekorClient}; without one they are
111
- * marked `'skipped'`. SDK proof verification is Day 7 work and currently
112
- * always returns `'pending_day_7'`.
113
- */
114
- export async function verifyBundle(
115
- bundle: PassportsignBundle,
116
- deps: VerifyBundleDeps = {},
117
- ): Promise<BundleVerifyResult> {
118
- validateBundle(bundle);
119
-
120
- const result: BundleVerifyResult = {
121
- hash_match: 'skipped',
122
- inclusion_proof: 'skipped',
123
- root_consistency: 'skipped',
124
- sdk_proof: 'skipped',
125
- overall: 'pending',
126
- errors: [],
127
- };
128
-
129
- // SDK proof verification (independent of rekor) — runs first because
130
- // it can be done purely from the bundle.
131
- if (deps.sdkVerifier) {
132
- result.sdk_proof = await runSdkVerification(bundle, deps.sdkVerifier, result.errors);
133
- }
134
-
135
- if (!deps.rekor) {
136
- result.overall = computeOverall(result);
137
- return result;
138
- }
139
-
140
- // 1. Fetch the entry from Rekor (any operator's Rekor mirror would do).
141
- let entry;
142
- try {
143
- entry = await deps.rekor.getEntry(bundle.rekor.log_entry_hash);
144
- } catch (err) {
145
- result.errors.push(
146
- `failed to fetch Rekor entry: ${err instanceof Error ? err.message : String(err)}`,
147
- );
148
- result.overall = 'fail';
149
- result.hash_match = 'fail';
150
- result.inclusion_proof = 'fail';
151
- result.root_consistency = 'fail';
152
- return result;
153
- }
154
-
155
- // 2. hash_match: bundle.statement bytes' sha256 must equal entry.body's payloadHash.
156
- const statementBytes = hexToBytes(bundle.statement);
157
- const expectedPayloadHash = sha256Hex(statementBytes);
158
- let entryPayloadHash: string;
159
- try {
160
- entryPayloadHash = parseEntryBody(entry.body).payloadHashHex;
161
- } catch (err) {
162
- result.errors.push(
163
- `failed to parse Rekor entry body: ${err instanceof Error ? err.message : String(err)}`,
164
- );
165
- result.hash_match = 'fail';
166
- result.inclusion_proof = 'fail';
167
- result.root_consistency = 'fail';
168
- result.overall = 'fail';
169
- return result;
170
- }
171
- result.hash_match = expectedPayloadHash === entryPayloadHash ? 'pass' : 'fail';
172
- if (result.hash_match === 'fail') {
173
- result.errors.push(
174
- `payloadHash mismatch: bundle says ${expectedPayloadHash}, Rekor entry has ${entryPayloadHash}`,
175
- );
176
- }
177
-
178
- // 3. inclusion_proof: leaf hash = sha256(0x00 || decoded-body-bytes); verify against captured root.
179
- const bodyBytes = new Uint8Array(Buffer.from(entry.body, 'base64'));
180
- const leaf = hashLeaf(bodyBytes);
181
- const captured = bundle.rekor.inclusion_proof as {
182
- hashes: string[];
183
- logIndex: number;
184
- treeSize: number;
185
- rootHash: string;
186
- };
187
- const proofHashes = captured.hashes.map(hexToBytes);
188
- const rootBytes = hexToBytes(captured.rootHash);
189
- result.inclusion_proof = verifyInclusion(
190
- leaf,
191
- captured.logIndex,
192
- captured.treeSize,
193
- proofHashes,
194
- rootBytes,
195
- )
196
- ? 'pass'
197
- : 'fail';
198
- if (result.inclusion_proof === 'fail') {
199
- result.errors.push('inclusion proof does not verify against captured root');
200
- }
201
-
202
- // 4. root_consistency: captured root must be a prefix of current witnessed root.
203
- try {
204
- const logInfo = await deps.rekor.getLogInfo();
205
- if (logInfo.treeSize < captured.treeSize) {
206
- result.errors.push(
207
- `current tree size ${logInfo.treeSize} is smaller than captured ${captured.treeSize} — log may have been rewound`,
208
- );
209
- result.root_consistency = 'fail';
210
- } else if (logInfo.treeSize === captured.treeSize) {
211
- // Same tree, just compare roots
212
- result.root_consistency =
213
- logInfo.rootHash === captured.rootHash ? 'pass' : 'fail';
214
- if (result.root_consistency === 'fail') {
215
- result.errors.push(
216
- `root mismatch at same treeSize: captured ${captured.rootHash}, current ${logInfo.rootHash}`,
217
- );
218
- }
219
- } else {
220
- const proof = await deps.rekor.getConsistencyProof(
221
- captured.treeSize,
222
- logInfo.treeSize,
223
- );
224
- const proofBytes = proof.hashes.map(hexToBytes);
225
- result.root_consistency = verifyConsistency(
226
- captured.treeSize,
227
- logInfo.treeSize,
228
- rootBytes,
229
- hexToBytes(logInfo.rootHash),
230
- proofBytes,
231
- )
232
- ? 'pass'
233
- : 'fail';
234
- if (result.root_consistency === 'fail') {
235
- result.errors.push(
236
- 'consistency proof does not verify — captured root is not an ancestor of current root',
237
- );
238
- }
239
- }
240
- } catch (err) {
241
- result.errors.push(
242
- `consistency check error: ${err instanceof Error ? err.message : String(err)}`,
243
- );
244
- result.root_consistency = 'fail';
245
- }
246
-
247
- result.overall = computeOverall(result);
248
- return result;
249
- }
250
-
251
- async function runSdkVerification(
252
- bundle: PassportsignBundle,
253
- sdkVerifier: SdkVerifier,
254
- errors: string[],
255
- ): Promise<CheckResult> {
256
- let payload;
257
- try {
258
- payload = unpackSdkPayload(bundle.proof_blob);
259
- } catch (err) {
260
- errors.push(
261
- `failed to unpack SDK payload from bundle.proof_blob: ${
262
- err instanceof Error ? err.message : String(err)
263
- }`,
264
- );
265
- return 'fail';
266
- }
267
-
268
- let sdkResult: SdkVerifyResult;
269
- try {
270
- sdkResult = await sdkVerifier.verify({
271
- proofs: payload.proofs,
272
- originalQuery: payload.original_query,
273
- queryResult: payload.query_result,
274
- devMode: payload.dev_mode,
275
- });
276
- } catch (err) {
277
- errors.push(`SDK verify threw: ${err instanceof Error ? err.message : String(err)}`);
278
- return 'fail';
279
- }
280
-
281
- if (sdkResult.verified !== true) {
282
- errors.push(`SDK reported verified=${String(sdkResult.verified)}`);
283
- return 'fail';
284
- }
285
-
286
- // The returned uniqueIdentifier must match the statement's predicate.
287
- let statementUniqueId: string | undefined;
288
- try {
289
- const statementBytes = Buffer.from(bundle.statement, 'hex').toString('utf8');
290
- const parsed = JSON.parse(statementBytes) as {
291
- predicate?: { unique_identifier?: string };
292
- };
293
- statementUniqueId = parsed.predicate?.unique_identifier;
294
- } catch {
295
- statementUniqueId = undefined;
296
- }
297
-
298
- if (!statementUniqueId) {
299
- errors.push('could not extract unique_identifier from bundle.statement');
300
- return 'fail';
301
- }
302
- if (sdkResult.uniqueIdentifier !== statementUniqueId) {
303
- errors.push(
304
- `SDK uniqueIdentifier ${String(sdkResult.uniqueIdentifier)} does not match statement.predicate.unique_identifier ${statementUniqueId}`,
305
- );
306
- return 'fail';
307
- }
308
-
309
- return 'pass';
310
- }
311
-
312
- function computeOverall(r: BundleVerifyResult): 'pass' | 'fail' | 'pending' {
313
- const all = [r.hash_match, r.inclusion_proof, r.root_consistency, r.sdk_proof];
314
- if (all.some((s) => s === 'fail')) return 'fail';
315
- if (all.every((s) => s === 'pass')) return 'pass';
316
- return 'pending';
317
- }
1
+ /**
2
+ * Bundle verifier — the trust anchor for everyone who is not us.
3
+ *
4
+ * Day 6 scope: structural integrity (statement hash matches Rekor's
5
+ * recorded payloadHash), Merkle inclusion proof against the captured
6
+ * root, and log-root consistency between the captured root and the
7
+ * current witnessed root.
8
+ *
9
+ * Day 7 scope (deferred): SDK proof verification. Requires a
10
+ * bundle-schema extension to carry SDK inputs (proofs array,
11
+ * originalQuery, queryResult). For now `sdk_proof` reports
12
+ * `'pending_day_7'`.
13
+ */
14
+
15
+ import { type PassportsignBundle, validateBundle } from './bundle.js';
16
+ import { base64ToBytes, bytesToUtf8, hexToBytes, sha256Hex } from './encoding.js';
17
+ import { type RekorClient } from './log/rekor.js';
18
+ import { hashLeaf, verifyConsistency, verifyInclusion } from './merkle.js';
19
+ import { unpackSdkPayload } from './sdk-payload.js';
20
+
21
+ export type CheckResult = 'pass' | 'fail' | 'skipped';
22
+
23
+ export interface BundleVerifyResult {
24
+ /**
25
+ * Statement bytes in the bundle hash to the `payloadHash` Rekor recorded
26
+ * for the entry.
27
+ */
28
+ hash_match: CheckResult;
29
+ /**
30
+ * The captured inclusion proof verifies the Rekor entry's leaf hash
31
+ * against the captured root.
32
+ */
33
+ inclusion_proof: CheckResult;
34
+ /**
35
+ * The captured root is a prefix of the current witnessed root (the log
36
+ * has not been rewritten in a way that orphans our entry). Skipped when
37
+ * no rekor client is provided.
38
+ */
39
+ root_consistency: CheckResult;
40
+ /**
41
+ * SDK proof verification. `'skipped'` when no SDK verifier is injected.
42
+ * `'pass'` when the SDK validates the proofs AND the returned
43
+ * uniqueIdentifier matches the statement's predicate.
44
+ */
45
+ sdk_proof: CheckResult;
46
+ /**
47
+ * `'pass'` only when every enabled check passes; `'fail'` if any check
48
+ * fails; `'pending'` when one or more checks are `'skipped'`.
49
+ */
50
+ overall: 'pass' | 'fail' | 'pending';
51
+ errors: string[];
52
+ }
53
+
54
+ export interface SdkVerifyInput {
55
+ proofs: unknown[];
56
+ originalQuery: unknown;
57
+ queryResult: unknown;
58
+ scope?: string;
59
+ devMode?: boolean;
60
+ }
61
+
62
+ export interface SdkVerifyResult {
63
+ verified: boolean;
64
+ uniqueIdentifier: string | undefined;
65
+ }
66
+
67
+ export interface SdkVerifier {
68
+ verify(input: SdkVerifyInput): Promise<SdkVerifyResult>;
69
+ }
70
+
71
+ export interface VerifyBundleDeps {
72
+ /** Inject a Rekor client to enable hash_match / inclusion / consistency checks. */
73
+ rekor?: RekorClient;
74
+ /** Inject a zkPassport SDK verifier to enable the sdk_proof check. */
75
+ sdkVerifier?: SdkVerifier;
76
+ }
77
+
78
+ interface ParsedEntryBody {
79
+ payloadHashHex: string;
80
+ }
81
+
82
+ function parseEntryBody(bodyBase64: string): ParsedEntryBody {
83
+ const bytes = bytesToUtf8(base64ToBytes(bodyBase64));
84
+ const body = JSON.parse(bytes) as Record<string, unknown>;
85
+ const spec = body['spec'] as Record<string, unknown> | undefined;
86
+ const content = spec?.['content'] as Record<string, unknown> | undefined;
87
+ const payloadHash = content?.['payloadHash'] as Record<string, unknown> | undefined;
88
+ const value = payloadHash?.['value'];
89
+ if (typeof value !== 'string') {
90
+ throw new Error('Rekor entry body missing spec.content.payloadHash.value');
91
+ }
92
+ return { payloadHashHex: value };
93
+ }
94
+
95
+ /**
96
+ * Verify a passportsign bundle. Online checks (hash_match, inclusion_proof,
97
+ * root_consistency) require a {@link RekorClient}; without one they are
98
+ * marked `'skipped'`. SDK proof verification is Day 7 work and currently
99
+ * always returns `'pending_day_7'`.
100
+ */
101
+ export async function verifyBundle(
102
+ bundle: PassportsignBundle,
103
+ deps: VerifyBundleDeps = {},
104
+ ): Promise<BundleVerifyResult> {
105
+ validateBundle(bundle);
106
+
107
+ const result: BundleVerifyResult = {
108
+ hash_match: 'skipped',
109
+ inclusion_proof: 'skipped',
110
+ root_consistency: 'skipped',
111
+ sdk_proof: 'skipped',
112
+ overall: 'pending',
113
+ errors: [],
114
+ };
115
+
116
+ // SDK proof verification (independent of rekor) — runs first because
117
+ // it can be done purely from the bundle.
118
+ if (deps.sdkVerifier) {
119
+ result.sdk_proof = await runSdkVerification(bundle, deps.sdkVerifier, result.errors);
120
+ }
121
+
122
+ if (!deps.rekor) {
123
+ result.overall = computeOverall(result);
124
+ return result;
125
+ }
126
+
127
+ // 1. Fetch the entry from Rekor (any operator's Rekor mirror would do).
128
+ let entry;
129
+ try {
130
+ entry = await deps.rekor.getEntry(bundle.rekor.log_entry_hash);
131
+ } catch (err) {
132
+ result.errors.push(
133
+ `failed to fetch Rekor entry: ${err instanceof Error ? err.message : String(err)}`,
134
+ );
135
+ result.overall = 'fail';
136
+ result.hash_match = 'fail';
137
+ result.inclusion_proof = 'fail';
138
+ result.root_consistency = 'fail';
139
+ return result;
140
+ }
141
+
142
+ // 2. hash_match: bundle.statement bytes' sha256 must equal entry.body's payloadHash.
143
+ const statementBytes = hexToBytes(bundle.statement);
144
+ const expectedPayloadHash = sha256Hex(statementBytes);
145
+ let entryPayloadHash: string;
146
+ try {
147
+ entryPayloadHash = parseEntryBody(entry.body).payloadHashHex;
148
+ } catch (err) {
149
+ result.errors.push(
150
+ `failed to parse Rekor entry body: ${err instanceof Error ? err.message : String(err)}`,
151
+ );
152
+ result.hash_match = 'fail';
153
+ result.inclusion_proof = 'fail';
154
+ result.root_consistency = 'fail';
155
+ result.overall = 'fail';
156
+ return result;
157
+ }
158
+ result.hash_match = expectedPayloadHash === entryPayloadHash ? 'pass' : 'fail';
159
+ if (result.hash_match === 'fail') {
160
+ result.errors.push(
161
+ `payloadHash mismatch: bundle says ${expectedPayloadHash}, Rekor entry has ${entryPayloadHash}`,
162
+ );
163
+ }
164
+
165
+ // 3. inclusion_proof: leaf hash = sha256(0x00 || decoded-body-bytes); verify against captured root.
166
+ const bodyBytes = base64ToBytes(entry.body);
167
+ const leaf = hashLeaf(bodyBytes);
168
+ const captured = bundle.rekor.inclusion_proof as {
169
+ hashes: string[];
170
+ logIndex: number;
171
+ treeSize: number;
172
+ rootHash: string;
173
+ };
174
+ const proofHashes = captured.hashes.map(hexToBytes);
175
+ const rootBytes = hexToBytes(captured.rootHash);
176
+ result.inclusion_proof = verifyInclusion(
177
+ leaf,
178
+ captured.logIndex,
179
+ captured.treeSize,
180
+ proofHashes,
181
+ rootBytes,
182
+ )
183
+ ? 'pass'
184
+ : 'fail';
185
+ if (result.inclusion_proof === 'fail') {
186
+ result.errors.push('inclusion proof does not verify against captured root');
187
+ }
188
+
189
+ // 4. root_consistency: captured root must be a prefix of current witnessed root.
190
+ try {
191
+ const logInfo = await deps.rekor.getLogInfo();
192
+ if (logInfo.treeSize < captured.treeSize) {
193
+ result.errors.push(
194
+ `current tree size ${logInfo.treeSize} is smaller than captured ${captured.treeSize} — log may have been rewound`,
195
+ );
196
+ result.root_consistency = 'fail';
197
+ } else if (logInfo.treeSize === captured.treeSize) {
198
+ // Same tree, just compare roots
199
+ result.root_consistency =
200
+ logInfo.rootHash === captured.rootHash ? 'pass' : 'fail';
201
+ if (result.root_consistency === 'fail') {
202
+ result.errors.push(
203
+ `root mismatch at same treeSize: captured ${captured.rootHash}, current ${logInfo.rootHash}`,
204
+ );
205
+ }
206
+ } else {
207
+ const proof = await deps.rekor.getConsistencyProof(
208
+ captured.treeSize,
209
+ logInfo.treeSize,
210
+ );
211
+ const proofBytes = proof.hashes.map(hexToBytes);
212
+ result.root_consistency = verifyConsistency(
213
+ captured.treeSize,
214
+ logInfo.treeSize,
215
+ rootBytes,
216
+ hexToBytes(logInfo.rootHash),
217
+ proofBytes,
218
+ )
219
+ ? 'pass'
220
+ : 'fail';
221
+ if (result.root_consistency === 'fail') {
222
+ result.errors.push(
223
+ 'consistency proof does not verify — captured root is not an ancestor of current root',
224
+ );
225
+ }
226
+ }
227
+ } catch (err) {
228
+ result.errors.push(
229
+ `consistency check error: ${err instanceof Error ? err.message : String(err)}`,
230
+ );
231
+ result.root_consistency = 'fail';
232
+ }
233
+
234
+ result.overall = computeOverall(result);
235
+ return result;
236
+ }
237
+
238
+ async function runSdkVerification(
239
+ bundle: PassportsignBundle,
240
+ sdkVerifier: SdkVerifier,
241
+ errors: string[],
242
+ ): Promise<CheckResult> {
243
+ let payload;
244
+ try {
245
+ payload = unpackSdkPayload(bundle.proof_blob);
246
+ } catch (err) {
247
+ errors.push(
248
+ `failed to unpack SDK payload from bundle.proof_blob: ${
249
+ err instanceof Error ? err.message : String(err)
250
+ }`,
251
+ );
252
+ return 'fail';
253
+ }
254
+
255
+ let sdkResult: SdkVerifyResult;
256
+ try {
257
+ sdkResult = await sdkVerifier.verify({
258
+ proofs: payload.proofs,
259
+ originalQuery: payload.original_query,
260
+ queryResult: payload.query_result,
261
+ devMode: payload.dev_mode,
262
+ });
263
+ } catch (err) {
264
+ errors.push(`SDK verify threw: ${err instanceof Error ? err.message : String(err)}`);
265
+ return 'fail';
266
+ }
267
+
268
+ if (sdkResult.verified !== true) {
269
+ errors.push(`SDK reported verified=${String(sdkResult.verified)}`);
270
+ return 'fail';
271
+ }
272
+
273
+ // The returned uniqueIdentifier must match the statement's predicate.
274
+ let statementUniqueId: string | undefined;
275
+ try {
276
+ const statementBytes = bytesToUtf8(hexToBytes(bundle.statement));
277
+ const parsed = JSON.parse(statementBytes) as {
278
+ predicate?: { unique_identifier?: string };
279
+ };
280
+ statementUniqueId = parsed.predicate?.unique_identifier;
281
+ } catch {
282
+ statementUniqueId = undefined;
283
+ }
284
+
285
+ if (!statementUniqueId) {
286
+ errors.push('could not extract unique_identifier from bundle.statement');
287
+ return 'fail';
288
+ }
289
+ if (sdkResult.uniqueIdentifier !== statementUniqueId) {
290
+ errors.push(
291
+ `SDK uniqueIdentifier ${String(sdkResult.uniqueIdentifier)} does not match statement.predicate.unique_identifier ${statementUniqueId}`,
292
+ );
293
+ return 'fail';
294
+ }
295
+
296
+ return 'pass';
297
+ }
298
+
299
+ function computeOverall(r: BundleVerifyResult): 'pass' | 'fail' | 'pending' {
300
+ const all = [r.hash_match, r.inclusion_proof, r.root_consistency, r.sdk_proof];
301
+ if (all.some((s) => s === 'fail')) return 'fail';
302
+ if (all.every((s) => s === 'pass')) return 'pass';
303
+ return 'pending';
304
+ }