burnledger 0.9.1 → 0.10.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/dist/cjs/audit-pack.d.ts +201 -0
- package/dist/cjs/audit-pack.d.ts.map +1 -0
- package/dist/cjs/audit-pack.js +867 -0
- package/dist/cjs/audit-pack.js.map +1 -0
- package/dist/cjs/client.d.ts +41 -1
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/client.js +58 -0
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/index.d.ts +46 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +69 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/models.d.ts +28 -2
- package/dist/cjs/models.d.ts.map +1 -1
- package/dist/cjs/models.js +19 -1
- package/dist/cjs/models.js.map +1 -1
- package/dist/cjs/run-record.d.ts +130 -0
- package/dist/cjs/run-record.d.ts.map +1 -0
- package/dist/cjs/run-record.js +272 -0
- package/dist/cjs/run-record.js.map +1 -0
- package/dist/cjs/verify.d.ts +58 -0
- package/dist/cjs/verify.d.ts.map +1 -1
- package/dist/cjs/verify.js +207 -5
- package/dist/cjs/verify.js.map +1 -1
- package/dist/esm/audit-pack.d.ts +201 -0
- package/dist/esm/audit-pack.d.ts.map +1 -0
- package/dist/esm/audit-pack.js +858 -0
- package/dist/esm/audit-pack.js.map +1 -0
- package/dist/esm/cli.d.ts +33 -0
- package/dist/esm/cli.d.ts.map +1 -1
- package/dist/esm/cli.js +225 -5
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/client.d.ts +41 -1
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +59 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +46 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +52 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/models.d.ts +28 -2
- package/dist/esm/models.d.ts.map +1 -1
- package/dist/esm/models.js +18 -1
- package/dist/esm/models.js.map +1 -1
- package/dist/esm/run-record.d.ts +130 -0
- package/dist/esm/run-record.d.ts.map +1 -0
- package/dist/esm/run-record.js +262 -0
- package/dist/esm/run-record.js.map +1 -0
- package/dist/esm/verify.d.ts +58 -0
- package/dist/esm/verify.d.ts.map +1 -1
- package/dist/esm/verify.js +200 -8
- package/dist/esm/verify.js.map +1 -1
- package/package.json +1 -1
- package/src/audit-pack.ts +1067 -0
- package/src/cli.ts +234 -5
- package/src/client.ts +82 -1
- package/src/index.ts +117 -1
- package/src/models.ts +47 -3
- package/src/run-record.ts +371 -0
- package/src/verify.ts +228 -8
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/** The run record (docs/run-record.md, ADR-031 option C).
|
|
2
|
+
*
|
|
3
|
+
* A port of core/run_record.go, verdict for verdict. A run is one bulk
|
|
4
|
+
* deletion cycle; the run record is the enclave's signed word on it — what the
|
|
5
|
+
* customer declared the input list to be, what the enclave actually issued
|
|
6
|
+
* under the run, and when it opened and closed. Held beside every record of
|
|
7
|
+
* the run (an audit pack filtered by run_id, every page of it), it lets a
|
|
8
|
+
* reader establish that the records are exactly what was issued, how many
|
|
9
|
+
* listed subjects have no record, and whether the run closed in time.
|
|
10
|
+
*
|
|
11
|
+
* What it does not establish is that the list was the regulator's list: the
|
|
12
|
+
* enclave never sees the list, and list_digest and list_count are the
|
|
13
|
+
* customer's assertion, signed and dated here rather than implicit.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { CryptoOps } from "./crypto.js";
|
|
17
|
+
import { VerificationError } from "./errors.js";
|
|
18
|
+
import {
|
|
19
|
+
buildLogLeafPayload,
|
|
20
|
+
bytesToHex,
|
|
21
|
+
canonicalJson,
|
|
22
|
+
decodeFixed,
|
|
23
|
+
evaluateKey,
|
|
24
|
+
formatTimestamp,
|
|
25
|
+
formatUuid,
|
|
26
|
+
hashLeaf,
|
|
27
|
+
keyIsUsable,
|
|
28
|
+
merkleRoot,
|
|
29
|
+
parseUuid,
|
|
30
|
+
signatureCoversRunId,
|
|
31
|
+
transparencyVerdictName,
|
|
32
|
+
verifyTransparencyHead,
|
|
33
|
+
verifyTransparencyInclusion,
|
|
34
|
+
type PublicKeyInfo,
|
|
35
|
+
} from "./verify.js";
|
|
36
|
+
|
|
37
|
+
/** The domain-separation tag of a run record. */
|
|
38
|
+
export const PAYLOAD_TYPE_RUN_RECORD = "burnledger.run_record.v1";
|
|
39
|
+
|
|
40
|
+
/** What a run was: a regulator's bulk cycle or a customer's own sweep. Carried
|
|
41
|
+
* signed, so an auditor reads what the customer claimed the run to be rather
|
|
42
|
+
* than inferring it from a date. */
|
|
43
|
+
export type RunKind = "drop_cycle" | "bulk_erasure";
|
|
44
|
+
|
|
45
|
+
const RUN_KINDS: ReadonlySet<string> = new Set<RunKind>(["drop_cycle", "bulk_erasure"]);
|
|
46
|
+
|
|
47
|
+
export const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
48
|
+
|
|
49
|
+
/** Mirrors core.RunRecord, as GET /v1/runs/{id} serves it under `record` and
|
|
50
|
+
* an audit pack carries it in its header. UUIDs are canonical lowercase,
|
|
51
|
+
* timestamps whole-second UTC, exactly as the payload signs them. */
|
|
52
|
+
export interface RunRecord {
|
|
53
|
+
runId: string;
|
|
54
|
+
runKind: string;
|
|
55
|
+
/** Signed, so a run record cannot be presented as another team's. */
|
|
56
|
+
teamId: string;
|
|
57
|
+
/** The customer's assertion about the input list: SHA-256 over its canonical
|
|
58
|
+
* form (one normalized identifier per line, sorted, LF-terminated), and how
|
|
59
|
+
* many entries it had. 32 bytes. */
|
|
60
|
+
listDigest: Uint8Array;
|
|
61
|
+
listCount: number;
|
|
62
|
+
/** Merkle root and count over the DISTINCT subject hashes the enclave issued
|
|
63
|
+
* records for under this run, leaves sorted. Computed by the enclave, never
|
|
64
|
+
* asserted. 32 bytes. */
|
|
65
|
+
subjectsRoot: Uint8Array;
|
|
66
|
+
subjectsCount: number;
|
|
67
|
+
/** Merkle root and count over the certificate ids issued under this run,
|
|
68
|
+
* leaves sorted. A run with no records has zero roots and zero counts. */
|
|
69
|
+
recordsRoot: Uint8Array;
|
|
70
|
+
recordsCount: number;
|
|
71
|
+
/** The systems every record in the run covered. */
|
|
72
|
+
systemIds: string[];
|
|
73
|
+
openedAt: string;
|
|
74
|
+
closedAt: string;
|
|
75
|
+
/** The customer-declared deadline, so "on time" is checkable against closedAt. */
|
|
76
|
+
notAfter: string;
|
|
77
|
+
issuer: { keyId: string; enclavePcr0: string };
|
|
78
|
+
/** 64 bytes. */
|
|
79
|
+
signature: Uint8Array;
|
|
80
|
+
transparencyStatus: string;
|
|
81
|
+
/** The record's own inclusion proof once it is in the log, in the shape a
|
|
82
|
+
* certificate's takes. Outside the signature, as on a certificate. */
|
|
83
|
+
transparency?: Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** What a verifier learns about a run record's signature (core.RunRecordResult). */
|
|
87
|
+
export type RunRecordResult =
|
|
88
|
+
| "VALID"
|
|
89
|
+
| "MALFORMED_RUN_RECORD"
|
|
90
|
+
| "UNKNOWN_KEY"
|
|
91
|
+
| "KEY_UNUSABLE"
|
|
92
|
+
| "INVALID_RUN_RECORD_SIGNATURE";
|
|
93
|
+
|
|
94
|
+
/** Mirrors core.RunCheck: what a reader learns by holding a run record beside
|
|
95
|
+
* the records issued under it. */
|
|
96
|
+
export interface RunCheck {
|
|
97
|
+
/** The certificate ids reproduce records_root and records_count. */
|
|
98
|
+
recordsMatch: boolean;
|
|
99
|
+
/** The subject hashes reproduce subjects_root and subjects_count. */
|
|
100
|
+
subjectsMatch: boolean;
|
|
101
|
+
/** listCount minus subjectsCount: how many listed subjects have no record in
|
|
102
|
+
* the run. Zero is the number an auditor wants. */
|
|
103
|
+
listShortfall: number;
|
|
104
|
+
/** closedAt <= notAfter. */
|
|
105
|
+
closedInTime: boolean;
|
|
106
|
+
/** Records whose own signed run_id (format 10.0 and later) names a different
|
|
107
|
+
* run, or no run, while the pack presents them as this run's. A record
|
|
108
|
+
* without the field (older formats) is neither counted nor refused: it says
|
|
109
|
+
* nothing about its run, and the roots are what bind it. */
|
|
110
|
+
mislabelled: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** What checkRunAgainstRecords needs from one record of the run. */
|
|
114
|
+
export interface RunCheckRecord {
|
|
115
|
+
certificateId: string;
|
|
116
|
+
/** subject.identifier_hash, 32 bytes. */
|
|
117
|
+
subjectHash: Uint8Array;
|
|
118
|
+
/** The record's own signed run_id, when it carries one. */
|
|
119
|
+
runId?: string;
|
|
120
|
+
formatVersion?: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type Doc = Record<string, unknown>;
|
|
124
|
+
|
|
125
|
+
function isObject(v: unknown): v is Doc {
|
|
126
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A UUID field as Go's uuid.UUID unmarshals it: null or absent is the nil
|
|
130
|
+
* UUID (encoding/json leaves the zero value), a string in any form uuid.Parse
|
|
131
|
+
* reads is canonical, anything else fails the document. */
|
|
132
|
+
function uuidOrNil(v: unknown, path: string): string {
|
|
133
|
+
if (v === null || v === undefined) return NIL_UUID;
|
|
134
|
+
if (typeof v !== "string") throw new VerificationError(`${path} is not a string`);
|
|
135
|
+
try {
|
|
136
|
+
return formatUuid(parseUuid(v));
|
|
137
|
+
} catch (e) {
|
|
138
|
+
if (e instanceof TypeError) throw new VerificationError(`${path} is not a UUID: ${JSON.stringify(v)}`);
|
|
139
|
+
throw e;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** A fixed byte array as Go's [N]byte unmarshals it: null or absent is all
|
|
144
|
+
* zero, anything else must be exactly N bytes. */
|
|
145
|
+
function bytesOrZero(v: unknown, path: string, length: number): Uint8Array {
|
|
146
|
+
if (v === null || v === undefined) return new Uint8Array(length);
|
|
147
|
+
return decodeFixed(v, path, length);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A count as Go's uint64 unmarshals it: null or absent is 0, and a fraction
|
|
151
|
+
* or a negative fails the document. */
|
|
152
|
+
function countOrZero(v: unknown, path: string): number {
|
|
153
|
+
if (v === null || v === undefined) return 0;
|
|
154
|
+
if (typeof v !== "number" || !Number.isSafeInteger(v) || v < 0) {
|
|
155
|
+
throw new VerificationError(`${path} is not a non-negative integer: ${JSON.stringify(v)}`);
|
|
156
|
+
}
|
|
157
|
+
return v;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function stringOrEmpty(v: unknown, path: string): string {
|
|
161
|
+
if (v === null || v === undefined) return "";
|
|
162
|
+
if (typeof v !== "string") throw new VerificationError(`${path} is not a string`);
|
|
163
|
+
return v;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Read a run record as served, refusing what Go's ParseRunRecordDocument and
|
|
167
|
+
* the unmarshal behind it refuse. Throws a VerificationError naming the field. */
|
|
168
|
+
export function parseRunRecordDocument(doc: unknown): RunRecord {
|
|
169
|
+
if (!isObject(doc)) throw new VerificationError("run record: not a JSON object");
|
|
170
|
+
const rawIssuer = doc.issuer;
|
|
171
|
+
if (rawIssuer !== null && rawIssuer !== undefined && !isObject(rawIssuer)) {
|
|
172
|
+
throw new VerificationError("issuer is not an object");
|
|
173
|
+
}
|
|
174
|
+
const issuer: Doc = isObject(rawIssuer) ? rawIssuer : {};
|
|
175
|
+
const rawSystems = doc.system_ids;
|
|
176
|
+
let systemIds: string[] = [];
|
|
177
|
+
if (rawSystems !== null && rawSystems !== undefined) {
|
|
178
|
+
if (!Array.isArray(rawSystems)) throw new VerificationError("system_ids is not an array");
|
|
179
|
+
systemIds = rawSystems.map((v, i) => uuidOrNil(v, `system_ids[${i}]`));
|
|
180
|
+
}
|
|
181
|
+
const record: RunRecord = {
|
|
182
|
+
runId: uuidOrNil(doc.run_id, "run_id"),
|
|
183
|
+
runKind: stringOrEmpty(doc.run_kind, "run_kind"),
|
|
184
|
+
teamId: uuidOrNil(doc.team_id, "team_id"),
|
|
185
|
+
listDigest: bytesOrZero(doc.list_digest, "list_digest", 32),
|
|
186
|
+
listCount: countOrZero(doc.list_count, "list_count"),
|
|
187
|
+
subjectsRoot: bytesOrZero(doc.subjects_root, "subjects_root", 32),
|
|
188
|
+
subjectsCount: countOrZero(doc.subjects_count, "subjects_count"),
|
|
189
|
+
recordsRoot: bytesOrZero(doc.records_root, "records_root", 32),
|
|
190
|
+
recordsCount: countOrZero(doc.records_count, "records_count"),
|
|
191
|
+
systemIds,
|
|
192
|
+
openedAt: formatTimestamp(doc.opened_at, "opened_at"),
|
|
193
|
+
closedAt: formatTimestamp(doc.closed_at, "closed_at"),
|
|
194
|
+
notAfter: formatTimestamp(doc.not_after, "not_after"),
|
|
195
|
+
issuer: {
|
|
196
|
+
keyId: stringOrEmpty(issuer.key_id, "issuer.key_id"),
|
|
197
|
+
enclavePcr0: stringOrEmpty(issuer.enclave_pcr0, "issuer.enclave_pcr0"),
|
|
198
|
+
},
|
|
199
|
+
signature: bytesOrZero(doc.signature, "signature", 64),
|
|
200
|
+
transparencyStatus: stringOrEmpty(doc.transparency_status, "transparency_status"),
|
|
201
|
+
};
|
|
202
|
+
const transparency = doc.transparency;
|
|
203
|
+
if (transparency !== null && transparency !== undefined) {
|
|
204
|
+
if (!isObject(transparency)) throw new VerificationError("transparency is not an object");
|
|
205
|
+
record.transparency = transparency;
|
|
206
|
+
}
|
|
207
|
+
return record;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The canonical signing payload. Every signed field is present; the system
|
|
211
|
+
* ids are sorted so two records over the same set sign the same bytes. Mirrors
|
|
212
|
+
* core.BuildRunRecordPayload, including its refusal of an unknown run_kind. */
|
|
213
|
+
export function buildRunRecordPayload(r: RunRecord): Uint8Array {
|
|
214
|
+
if (!RUN_KINDS.has(r.runKind)) {
|
|
215
|
+
throw new VerificationError(`run record: unknown run_kind ${r.runKind}`);
|
|
216
|
+
}
|
|
217
|
+
return canonicalJson({
|
|
218
|
+
payload_type: PAYLOAD_TYPE_RUN_RECORD,
|
|
219
|
+
closed_at: formatTimestamp(r.closedAt, "closed_at"),
|
|
220
|
+
enclave_pcr0: r.issuer.enclavePcr0,
|
|
221
|
+
key_id: r.issuer.keyId,
|
|
222
|
+
list_count: r.listCount,
|
|
223
|
+
list_digest: bytesToHex(r.listDigest),
|
|
224
|
+
not_after: formatTimestamp(r.notAfter, "not_after"),
|
|
225
|
+
opened_at: formatTimestamp(r.openedAt, "opened_at"),
|
|
226
|
+
records_count: r.recordsCount,
|
|
227
|
+
records_root: bytesToHex(r.recordsRoot),
|
|
228
|
+
run_id: r.runId,
|
|
229
|
+
run_kind: r.runKind,
|
|
230
|
+
subjects_count: r.subjectsCount,
|
|
231
|
+
subjects_root: bytesToHex(r.subjectsRoot),
|
|
232
|
+
system_ids: [...r.systemIds].sort(),
|
|
233
|
+
team_id: r.teamId,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function epochSeconds(rfc3339: string, field: string): number {
|
|
238
|
+
return Math.floor(Date.parse(formatTimestamp(rfc3339, field)) / 1000);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Checks the record's signature against the reader's keys, in the order every
|
|
242
|
+
* verifier uses: the shape, the key's authority at the record's own close
|
|
243
|
+
* instant, then the signature. Mirrors core.VerifyRunRecord. */
|
|
244
|
+
export async function verifyRunRecord(
|
|
245
|
+
crypto: CryptoOps,
|
|
246
|
+
r: RunRecord,
|
|
247
|
+
keys: Map<string, PublicKeyInfo>,
|
|
248
|
+
): Promise<RunRecordResult> {
|
|
249
|
+
const closedAt = epochSeconds(r.closedAt, "closed_at");
|
|
250
|
+
if (!RUN_KINDS.has(r.runKind) || r.runId === NIL_UUID || closedAt < epochSeconds(r.openedAt, "opened_at")) {
|
|
251
|
+
return "MALFORMED_RUN_RECORD";
|
|
252
|
+
}
|
|
253
|
+
const key = keys.get(r.issuer.keyId);
|
|
254
|
+
if (key === undefined) return "UNKNOWN_KEY";
|
|
255
|
+
if (!keyIsUsable(evaluateKey(key, closedAt))) return "KEY_UNUSABLE";
|
|
256
|
+
const payload = buildRunRecordPayload(r);
|
|
257
|
+
if (!(await crypto.ed25519Verify(key.keyBytes, payload, r.signature))) return "INVALID_RUN_RECORD_SIGNATURE";
|
|
258
|
+
return "VALID";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** What the run record's log leaf commits to: the SHA-256 of its canonical
|
|
262
|
+
* payload. A certificate leaf hashes the whole document less its transparency
|
|
263
|
+
* block; a run record's payload already is the whole signed document. */
|
|
264
|
+
export function runRecordHash(crypto: CryptoOps, r: RunRecord): Promise<Uint8Array> {
|
|
265
|
+
return crypto.sha256(buildRunRecordPayload(r));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Checks the record's own transparency proof the way a certificate's is
|
|
269
|
+
* checked, under core.TransparencyResult's names: NOT_INCLUDED when it carries
|
|
270
|
+
* none, INVALID_INCLUSION_PROOF for a leaf that is not a RUN_RECORD leaf,
|
|
271
|
+
* UNREADABLE_DOCUMENT where the leaf cannot be built. Mirrors
|
|
272
|
+
* core.VerifyRunRecordInclusion. */
|
|
273
|
+
export async function verifyRunRecordInclusion(
|
|
274
|
+
crypto: CryptoOps,
|
|
275
|
+
r: RunRecord,
|
|
276
|
+
key: PublicKeyInfo,
|
|
277
|
+
): Promise<string> {
|
|
278
|
+
const transparency = r.transparency;
|
|
279
|
+
if (transparency === undefined) return "NOT_INCLUDED";
|
|
280
|
+
if (transparency.entry_type !== "RUN_RECORD") return "INVALID_INCLUSION_PROOF";
|
|
281
|
+
let leaf: Uint8Array;
|
|
282
|
+
try {
|
|
283
|
+
const payload = buildLogLeafPayload("RUN_RECORD", r.runId, await runRecordHash(crypto, r), transparency.appended_at);
|
|
284
|
+
leaf = await hashLeaf(crypto, payload);
|
|
285
|
+
} catch (e) {
|
|
286
|
+
if (e instanceof VerificationError) return "UNREADABLE_DOCUMENT";
|
|
287
|
+
throw e;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
await verifyTransparencyHead(crypto, transparency, key, r.issuer.keyId);
|
|
291
|
+
await verifyTransparencyInclusion(crypto, transparency, leaf);
|
|
292
|
+
} catch (e) {
|
|
293
|
+
if (e instanceof VerificationError) return transparencyVerdictName(e);
|
|
294
|
+
throw e;
|
|
295
|
+
}
|
|
296
|
+
return "VALID";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function compareBytes(a: Uint8Array, b: Uint8Array): number {
|
|
300
|
+
const n = Math.min(a.length, b.length);
|
|
301
|
+
for (let i = 0; i < n; i++) {
|
|
302
|
+
if (a[i] !== b[i]) return a[i]! - b[i]!;
|
|
303
|
+
}
|
|
304
|
+
return a.length - b.length;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** The root over a leaf set the way a run record commits to one: leaves
|
|
308
|
+
* sorted bytewise, and 32 zero bytes for the empty set. */
|
|
309
|
+
async function sortedRoot(crypto: CryptoOps, leaves: Uint8Array[]): Promise<Uint8Array> {
|
|
310
|
+
if (leaves.length === 0) return new Uint8Array(32);
|
|
311
|
+
return merkleRoot(crypto, [...leaves].sort(compareBytes));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** What a run record commits to, from what was issued under it. Subjects are
|
|
315
|
+
* de-duplicated — a subject certified twice in one run is one subject — and
|
|
316
|
+
* both leaf sets are sorted, so the roots do not depend on issuance order and a
|
|
317
|
+
* verifier can reproduce them from a pack in any order. A subject leaf is its
|
|
318
|
+
* 32-byte hash and a record leaf the certificate id's 16 bytes, both through
|
|
319
|
+
* the RFC 6962 leaf hash. Mirrors core.RunRoots. */
|
|
320
|
+
export async function runRoots(
|
|
321
|
+
crypto: CryptoOps,
|
|
322
|
+
subjects: Uint8Array[],
|
|
323
|
+
certificateIds: string[],
|
|
324
|
+
): Promise<{ subjectsRoot: Uint8Array; subjectsCount: number; recordsRoot: Uint8Array; recordsCount: number }> {
|
|
325
|
+
const seen = new Set<string>();
|
|
326
|
+
const subjectLeaves: Uint8Array[] = [];
|
|
327
|
+
for (const s of subjects) {
|
|
328
|
+
const key = bytesToHex(s);
|
|
329
|
+
if (seen.has(key)) continue;
|
|
330
|
+
seen.add(key);
|
|
331
|
+
subjectLeaves.push(await hashLeaf(crypto, s));
|
|
332
|
+
}
|
|
333
|
+
const recordLeaves: Uint8Array[] = [];
|
|
334
|
+
for (const id of certificateIds) recordLeaves.push(await hashLeaf(crypto, parseUuid(id)));
|
|
335
|
+
return {
|
|
336
|
+
subjectsRoot: await sortedRoot(crypto, subjectLeaves),
|
|
337
|
+
subjectsCount: subjectLeaves.length,
|
|
338
|
+
recordsRoot: await sortedRoot(crypto, recordLeaves),
|
|
339
|
+
recordsCount: recordLeaves.length,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
344
|
+
return compareBytes(a, b) === 0;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Reproduces the run record's roots from the records a reader holds and
|
|
348
|
+
* compares them. The reader must hold every record of the run — a complete
|
|
349
|
+
* chain of pack pages filtered by the run — or the roots cannot match, which
|
|
350
|
+
* is the honest answer for a partial set. Mirrors core.CheckRunAgainstRecords. */
|
|
351
|
+
export async function checkRunAgainstRecords(
|
|
352
|
+
crypto: CryptoOps,
|
|
353
|
+
r: RunRecord,
|
|
354
|
+
records: RunCheckRecord[],
|
|
355
|
+
): Promise<RunCheck> {
|
|
356
|
+
let mislabelled = 0;
|
|
357
|
+
for (const rec of records) {
|
|
358
|
+
if (rec.runId !== undefined && rec.runId !== r.runId) mislabelled++;
|
|
359
|
+
if (rec.runId === undefined && rec.formatVersion !== undefined && signatureCoversRunId(rec.formatVersion)) {
|
|
360
|
+
mislabelled++;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const roots = await runRoots(crypto, records.map((rec) => rec.subjectHash), records.map((rec) => rec.certificateId));
|
|
364
|
+
return {
|
|
365
|
+
recordsMatch: bytesEqual(roots.recordsRoot, r.recordsRoot) && roots.recordsCount === r.recordsCount,
|
|
366
|
+
subjectsMatch: bytesEqual(roots.subjectsRoot, r.subjectsRoot) && roots.subjectsCount === r.subjectsCount,
|
|
367
|
+
listShortfall: r.listCount - r.subjectsCount,
|
|
368
|
+
closedInTime: epochSeconds(r.closedAt, "closed_at") <= epochSeconds(r.notAfter, "not_after"),
|
|
369
|
+
mislabelled,
|
|
370
|
+
};
|
|
371
|
+
}
|