@freedomofpress/sigstore-browser 0.1.13 → 0.1.15
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/.github/workflows/conformance.yml +1 -1
- package/.github/workflows/publish.yml +1 -1
- package/README.md +20 -16
- package/dist/bundle.d.ts +2 -0
- package/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js +66 -1
- package/dist/bundle.js.map +1 -1
- package/dist/interfaces.d.ts +17 -2
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js +14 -0
- package/dist/interfaces.js.map +1 -1
- package/dist/policy.d.ts +3 -3
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +4 -4
- package/dist/policy.js.map +1 -1
- package/dist/sigstore.d.ts +8 -8
- package/dist/sigstore.d.ts.map +1 -1
- package/dist/sigstore.js +159 -362
- package/dist/sigstore.js.map +1 -1
- package/dist/timestamp/tsa.d.ts +6 -2
- package/dist/timestamp/tsa.d.ts.map +1 -1
- package/dist/timestamp/tsa.js +15 -18
- package/dist/timestamp/tsa.js.map +1 -1
- package/dist/tlog/body.d.ts +3 -1
- package/dist/tlog/body.d.ts.map +1 -1
- package/dist/tlog/body.js +18 -5
- package/dist/tlog/body.js.map +1 -1
- package/dist/tlog/checkpoint.d.ts +2 -2
- package/dist/tlog/checkpoint.d.ts.map +1 -1
- package/dist/tlog/checkpoint.js +17 -95
- package/dist/tlog/checkpoint.js.map +1 -1
- package/dist/tlog/dsse.d.ts +3 -2
- package/dist/tlog/dsse.d.ts.map +1 -1
- package/dist/tlog/dsse.js +8 -5
- package/dist/tlog/dsse.js.map +1 -1
- package/dist/tlog/hashedrekord.d.ts +3 -2
- package/dist/tlog/hashedrekord.d.ts.map +1 -1
- package/dist/tlog/hashedrekord.js +8 -5
- package/dist/tlog/hashedrekord.js.map +1 -1
- package/dist/tlog/intoto.d.ts +3 -2
- package/dist/tlog/intoto.d.ts.map +1 -1
- package/dist/tlog/intoto.js +3 -1
- package/dist/tlog/intoto.js.map +1 -1
- package/package.json +2 -2
- package/src/bundle.ts +87 -0
- package/src/ed25519.browser.test.ts +76 -0
- package/src/index.browser.test.ts +46 -1
- package/src/interfaces.ts +29 -2
- package/src/policy.ts +5 -5
- package/src/security.browser.test.ts +249 -0
- package/src/sigstore.ts +191 -497
- package/src/timestamp/tsa.ts +29 -28
- package/src/tlog/body.ts +24 -5
- package/src/tlog/checkpoint.ts +22 -122
- package/src/tlog/dsse.ts +12 -6
- package/src/tlog/hashedrekord.ts +14 -8
- package/src/tlog/intoto.ts +5 -2
- package/src/verify.browser.test.ts +281 -0
- package/test/fixtures/rekor2.ts +14 -0
- package/test/fixtures/sigstore.ts +13 -0
package/src/sigstore.ts
CHANGED
|
@@ -17,14 +17,14 @@ import {
|
|
|
17
17
|
X509Certificate,
|
|
18
18
|
X509SCTExtension,
|
|
19
19
|
} from "./x509/index.js";
|
|
20
|
-
import { SigstoreBundle } from "./bundle.js";
|
|
20
|
+
import { assertBundle, SigstoreBundle, TLogEntry } from "./bundle.js";
|
|
21
21
|
import { preAuthEncoding } from "./dsse.js";
|
|
22
22
|
import {
|
|
23
23
|
CertAuthority,
|
|
24
24
|
CTLog,
|
|
25
|
+
parseValidityPeriod,
|
|
25
26
|
RawCAs,
|
|
26
27
|
RawLogs,
|
|
27
|
-
RawTimestampAuthorities,
|
|
28
28
|
RekorKeyInfo,
|
|
29
29
|
Sigstore,
|
|
30
30
|
SigstoreRoots,
|
|
@@ -39,6 +39,15 @@ import type { VerificationPolicy } from "./policy.js";
|
|
|
39
39
|
import { AnyOf, AllOf, OIDCIssuer, OIDCIssuerV2, Identity } from "./policy.js";
|
|
40
40
|
|
|
41
41
|
const MEDIA_TYPE_BASE = "application/vnd.dev.sigstore.bundle";
|
|
42
|
+
// Upper bounds on in-toto subjects and digests, matching sigstore-go.
|
|
43
|
+
const MAX_SUBJECTS = 1024;
|
|
44
|
+
const MAX_SUBJECT_DIGESTS = 32;
|
|
45
|
+
|
|
46
|
+
// Unknown operators can satisfy threshold one, but add no independence to named operators.
|
|
47
|
+
function countOperators(operators: Set<string>): number {
|
|
48
|
+
if (operators.size === 0) return 0;
|
|
49
|
+
return Math.max(1, operators.size - (operators.has("") ? 1 : 0));
|
|
50
|
+
}
|
|
42
51
|
|
|
43
52
|
/**
|
|
44
53
|
* Extract bundle version from mediaType string
|
|
@@ -59,14 +68,13 @@ function getBundleVersion(mediaType: string): string {
|
|
|
59
68
|
const version = mediaType
|
|
60
69
|
.replace(`${MEDIA_TYPE_BASE}.v`, "")
|
|
61
70
|
.replace("+json", "");
|
|
62
|
-
//
|
|
63
|
-
if (
|
|
71
|
+
// Only the versions this verifier implements are accepted.
|
|
72
|
+
if (/^0\.[123]$/.test(version)) {
|
|
64
73
|
return version;
|
|
65
74
|
}
|
|
66
75
|
}
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
return "0.1";
|
|
77
|
+
throw new Error(`Unsupported bundle media type: ${mediaType}`);
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
export interface SigstoreVerifierOptions {
|
|
@@ -88,67 +96,48 @@ export class SigstoreVerifier {
|
|
|
88
96
|
ctlogThreshold: options.ctlogThreshold ?? 1,
|
|
89
97
|
tsaThreshold: options.tsaThreshold ?? 0,
|
|
90
98
|
};
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// Load the first Rekor transparency log key that's valid at the frozen timestamp
|
|
95
|
-
// and store its log ID for verification. This matches sigstore-go's approach of
|
|
96
|
-
// looking up the verifier by log ID (verify/tlog.go:80-83).
|
|
97
|
-
|
|
98
|
-
for (const log of logs) {
|
|
99
|
-
// if start date is not in the future, and if an end doesn't exist or is in the future
|
|
100
|
-
if (
|
|
101
|
-
frozenTimestamp > new Date(log.publicKey.validFor.start) &&
|
|
102
|
-
(!log.publicKey.validFor.end ||
|
|
103
|
-
new Date(log.publicKey.validFor.end) > frozenTimestamp)
|
|
104
|
-
) {
|
|
105
|
-
return {
|
|
106
|
-
publicKey: await importKey(
|
|
107
|
-
log.publicKey.keyDetails,
|
|
108
|
-
log.publicKey.keyDetails,
|
|
109
|
-
log.publicKey.rawBytes,
|
|
110
|
-
),
|
|
111
|
-
logId: base64ToUint8Array(log.logId.keyId),
|
|
112
|
-
};
|
|
99
|
+
for (const [name, threshold] of Object.entries(this.options)) {
|
|
100
|
+
if (!Number.isSafeInteger(threshold) || threshold < 0) {
|
|
101
|
+
throw new Error(`${name} must be a non-negative safe integer`);
|
|
113
102
|
}
|
|
114
103
|
}
|
|
115
|
-
|
|
116
|
-
// Return undefined instead of throwing - some bundles don't need Rekor keys
|
|
117
|
-
// (e.g., v0.3 bundles with inclusion proofs)
|
|
118
|
-
return undefined;
|
|
119
104
|
}
|
|
120
105
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const end = log.publicKey.validFor.end
|
|
127
|
-
? new Date(log.publicKey.validFor.end)
|
|
128
|
-
: new Date('9999-12-31'); // No expiry means valid forever
|
|
129
|
-
|
|
130
|
-
// Include logs that are valid (started before frozen timestamp)
|
|
131
|
-
// We keep all logs, even expired ones, for historical verification
|
|
132
|
-
if (start <= frozenTimestamp) {
|
|
133
|
-
const publicKey = await importKey(
|
|
106
|
+
// Loads every Rekor key; entries select theirs by log ID and SETs are checked against its validity window.
|
|
107
|
+
async loadLog(logs: RawLogs): Promise<RekorKeyInfo[]> {
|
|
108
|
+
return Promise.all(
|
|
109
|
+
logs.map(async (log) => ({
|
|
110
|
+
publicKey: await importKey(
|
|
134
111
|
log.publicKey.keyDetails,
|
|
135
112
|
log.publicKey.keyDetails,
|
|
136
113
|
log.publicKey.rawBytes,
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
114
|
+
),
|
|
115
|
+
logId: base64ToUint8Array(log.logId.keyId),
|
|
116
|
+
hashAlgorithm: log.hashAlgorithm,
|
|
117
|
+
// Empty means unknown; countOperators never adds it to the named operator count.
|
|
118
|
+
operator: log.operator || "",
|
|
119
|
+
validFor: parseValidityPeriod(log.publicKey.validFor),
|
|
120
|
+
})),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
146
123
|
|
|
147
|
-
|
|
148
|
-
|
|
124
|
+
// Loads every CT log; SCTs select their log by ID and are checked against its validity window.
|
|
125
|
+
async loadCTLogs(ctlogs: RawLogs): Promise<CTLog[]> {
|
|
126
|
+
if (ctlogs.length === 0) {
|
|
127
|
+
throw new Error("Could not find any CT logs in sigstore root.");
|
|
149
128
|
}
|
|
150
|
-
|
|
151
|
-
|
|
129
|
+
return Promise.all(
|
|
130
|
+
ctlogs.map(async (log) => ({
|
|
131
|
+
logID: base64ToUint8Array(log.logId.keyId),
|
|
132
|
+
publicKey: await importKey(
|
|
133
|
+
log.publicKey.keyDetails,
|
|
134
|
+
log.publicKey.keyDetails,
|
|
135
|
+
log.publicKey.rawBytes,
|
|
136
|
+
),
|
|
137
|
+
operator: log.operator || "",
|
|
138
|
+
validFor: parseValidityPeriod(log.publicKey.validFor),
|
|
139
|
+
})),
|
|
140
|
+
);
|
|
152
141
|
}
|
|
153
142
|
|
|
154
143
|
// Adapted from https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/key/certificate.ts#L22-L53
|
|
@@ -185,82 +174,29 @@ export class SigstoreVerifier {
|
|
|
185
174
|
throw new Error(`Failed to verify certificate chain: ${lastError?.message || 'No valid CAs found'}`);
|
|
186
175
|
}
|
|
187
176
|
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const result: CertAuthority[] = [];
|
|
200
|
-
|
|
201
|
-
for (const tsa of tsas) {
|
|
202
|
-
const start = new Date(tsa.validFor.start);
|
|
203
|
-
const end = tsa.validFor.end ? new Date(tsa.validFor.end) : new Date(8640000000000000);
|
|
204
|
-
|
|
205
|
-
if (frozenTimestamp > start && frozenTimestamp < end) {
|
|
206
|
-
const certChain = tsa.certChain.certificates.map(cert =>
|
|
207
|
-
X509Certificate.parse(base64ToUint8Array(cert.rawBytes))
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
if (certChain.length > 0) {
|
|
211
|
-
result.push({
|
|
212
|
-
certChain,
|
|
213
|
-
validFor: { start, end },
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return result;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Load certificate authorities (Fulcio CAs) that are valid at the frozen timestamp.
|
|
223
|
-
// Similar to sigstore-js's filterCertAuthorities() in trust/filter.ts, but we also
|
|
224
|
-
// parse the certificates at load time whereas sigstore-js keeps them in the trust material
|
|
225
|
-
// and parses them during verification. This pre-loading approach is consistent with our
|
|
226
|
-
// architecture of loading all trusted roots at initialization.
|
|
227
|
-
loadCA(frozenTimestamp: Date, cas: RawCAs): CertAuthority[] {
|
|
228
|
-
const result: CertAuthority[] = [];
|
|
229
|
-
|
|
230
|
-
for (const ca of cas) {
|
|
231
|
-
const start = new Date(ca.validFor.start);
|
|
232
|
-
const end = ca.validFor.end ? new Date(ca.validFor.end) : new Date(8640000000000000);
|
|
233
|
-
|
|
234
|
-
if (frozenTimestamp > start && frozenTimestamp < end) {
|
|
235
|
-
const certChain = ca.certChain.certificates.map(cert =>
|
|
236
|
-
X509Certificate.parse(base64ToUint8Array(cert.rawBytes))
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
if (certChain.length > 0) {
|
|
240
|
-
result.push({
|
|
241
|
-
certChain,
|
|
242
|
-
validFor: { start, end },
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return result;
|
|
177
|
+
// Loads every Fulcio CA; verifyCertificateChain() selects CAs by the observer timestamp.
|
|
178
|
+
loadCA(cas: RawCAs): CertAuthority[] {
|
|
179
|
+
return cas
|
|
180
|
+
.filter((ca) => ca.certChain.certificates.length > 0)
|
|
181
|
+
.map((ca) => ({
|
|
182
|
+
certChain: ca.certChain.certificates.map((cert) =>
|
|
183
|
+
X509Certificate.parse(base64ToUint8Array(cert.rawBytes)),
|
|
184
|
+
),
|
|
185
|
+
validFor: parseValidityPeriod(ca.validFor),
|
|
186
|
+
}));
|
|
249
187
|
}
|
|
250
188
|
|
|
251
189
|
async loadSigstoreRoot(rawRoot: TrustedRoot) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
rekor: await this.loadLog(
|
|
257
|
-
ctlogs: await this.loadCTLogs(
|
|
258
|
-
certificateAuthorities: this.loadCA(
|
|
259
|
-
frozenTimestamp,
|
|
260
|
-
rawRoot[SigstoreRoots.certificateAuthorities],
|
|
261
|
-
),
|
|
262
|
-
timestampAuthorities: this.loadTSA(frozenTimestamp, rawRoot.timestampAuthorities),
|
|
190
|
+
rawRoot.timestampAuthorities?.forEach((authority) =>
|
|
191
|
+
parseValidityPeriod(authority.validFor),
|
|
192
|
+
);
|
|
193
|
+
const root = {
|
|
194
|
+
rekor: await this.loadLog(rawRoot[SigstoreRoots.tlogs]),
|
|
195
|
+
ctlogs: await this.loadCTLogs(rawRoot[SigstoreRoots.ctlogs]),
|
|
196
|
+
certificateAuthorities: this.loadCA(rawRoot[SigstoreRoots.certificateAuthorities]),
|
|
263
197
|
};
|
|
198
|
+
this.rawRoot = rawRoot;
|
|
199
|
+
this.root = root;
|
|
264
200
|
}
|
|
265
201
|
|
|
266
202
|
/**
|
|
@@ -279,12 +215,12 @@ export class SigstoreVerifier {
|
|
|
279
215
|
// Key differences:
|
|
280
216
|
// - Adds duplicate SCT detection (not in reference)
|
|
281
217
|
// - Inline CT log filtering by logID and validity period (reference uses filterTLogAuthorities)
|
|
282
|
-
// - Returns
|
|
218
|
+
// - Returns the distinct operators of the logs whose SCTs verified, for threshold checking
|
|
283
219
|
async verifySCT(
|
|
284
220
|
cert: X509Certificate,
|
|
285
221
|
issuer: X509Certificate,
|
|
286
222
|
ctlogs: CTLog[],
|
|
287
|
-
): Promise<
|
|
223
|
+
): Promise<Set<string>> {
|
|
288
224
|
let extSCT: X509SCTExtension | undefined;
|
|
289
225
|
|
|
290
226
|
// Verifying the SCT requires that we remove the SCT extension and
|
|
@@ -347,7 +283,7 @@ export class SigstoreVerifier {
|
|
|
347
283
|
// Calculate and return the verification results for each SCT
|
|
348
284
|
// Unlike sigstore-go which counts verified SCTs and checks threshold at the end,
|
|
349
285
|
// sigstore-js throws immediately if any SCT fails verification
|
|
350
|
-
const
|
|
286
|
+
const operators = new Set<string>();
|
|
351
287
|
|
|
352
288
|
for (const sct of extSCT.signedCertificateTimestamps) {
|
|
353
289
|
// Find the CT log that matches this SCT's log ID and is valid for the SCT datetime
|
|
@@ -363,350 +299,169 @@ export class SigstoreVerifier {
|
|
|
363
299
|
for (const log of validCTLogs) {
|
|
364
300
|
try {
|
|
365
301
|
if (await sct.verify(preCert.buffer, log.publicKey)) {
|
|
366
|
-
return
|
|
302
|
+
return log;
|
|
367
303
|
}
|
|
368
304
|
} catch {
|
|
369
305
|
// Continue trying other logs
|
|
370
306
|
}
|
|
371
307
|
}
|
|
372
|
-
return
|
|
308
|
+
return undefined;
|
|
373
309
|
})();
|
|
374
310
|
|
|
375
311
|
if (!verified) {
|
|
376
312
|
throw new Error("SCT verification failed");
|
|
377
313
|
}
|
|
378
314
|
|
|
379
|
-
|
|
315
|
+
operators.add(verified.operator);
|
|
380
316
|
}
|
|
381
317
|
|
|
382
|
-
return
|
|
318
|
+
return operators;
|
|
383
319
|
}
|
|
384
320
|
|
|
385
|
-
|
|
321
|
+
// Verifies the signed entry timestamp and returns the integrated time it binds.
|
|
322
|
+
private async verifySET(entry: TLogEntry, promise: string, log: RekorKeyInfo): Promise<Date> {
|
|
323
|
+
const integratedTime = Number(entry.integratedTime);
|
|
324
|
+
const integratedDate = new Date(integratedTime * 1000);
|
|
325
|
+
if (integratedDate < log.validFor.start || integratedDate > log.validFor.end) {
|
|
326
|
+
throw new Error("Rekor key was not valid at the integrated time.");
|
|
327
|
+
}
|
|
328
|
+
const signed = stringToUint8Array(
|
|
329
|
+
canonicalize({
|
|
330
|
+
body: entry.canonicalizedBody,
|
|
331
|
+
integratedTime,
|
|
332
|
+
logIndex: Number(entry.logIndex),
|
|
333
|
+
logID: Uint8ArrayToHex(log.logId),
|
|
334
|
+
}),
|
|
335
|
+
);
|
|
336
|
+
if (!(await verifySignature(log.publicKey, signed, base64ToUint8Array(promise), log.hashAlgorithm))) {
|
|
337
|
+
throw new Error("Failed to verify the inclusion promise in the provided bundle.");
|
|
338
|
+
}
|
|
339
|
+
return integratedDate;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Fully verifies every entry from a trusted log and returns the SET-bound integrated times.
|
|
343
|
+
// Entries from unknown logs are ignored; the threshold counts distinct log operators that verified.
|
|
344
|
+
private async verifyTlogEntries(
|
|
386
345
|
cert: X509Certificate,
|
|
387
346
|
bundle: SigstoreBundle,
|
|
388
|
-
rekor: RekorKeyInfo
|
|
389
|
-
): Promise<
|
|
347
|
+
rekor: RekorKeyInfo[],
|
|
348
|
+
): Promise<Date[]> {
|
|
390
349
|
const entries = bundle.verificationMaterial.tlogEntries;
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
`Too many tlog entries: ${entries.length} > ${MAX_TLOG_ENTRIES}`,
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
for (let i = 0; i < entries.length; i++) {
|
|
409
|
-
for (let j = i + 1; j < entries.length; j++) {
|
|
410
|
-
const iLogId = Uint8ArrayToHex(base64ToUint8Array(entries[i].logId.keyId));
|
|
411
|
-
const jLogId = Uint8ArrayToHex(base64ToUint8Array(entries[j].logId.keyId));
|
|
412
|
-
if (iLogId === jLogId && entries[i].logIndex === entries[j].logIndex) {
|
|
413
|
-
throw new Error(
|
|
414
|
-
`Duplicate tlog entry found: logID=${iLogId}, logIndex=${entries[i].logIndex}`,
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const entry = entries[0];
|
|
421
|
-
|
|
422
|
-
// Extract bundle version from mediaType
|
|
423
|
-
// Reference: https://github.com/sigstore/sigstore-go/blob/main/pkg/bundle/bundle.go#L159-L177
|
|
424
|
-
const bundleVersion = getBundleVersion(bundle.mediaType);
|
|
425
|
-
const isV02OrLater = parseFloat(bundleVersion) >= 0.2;
|
|
426
|
-
|
|
427
|
-
// Bundle v0.2+ requires an inclusion proof
|
|
428
|
-
if (isV02OrLater && !entry.inclusionProof) {
|
|
429
|
-
throw new Error(
|
|
430
|
-
"Bundle v0.2+ requires an inclusion proof.",
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// For rekor2/v0.3 bundles with inclusion proofs, the inclusion promise is optional
|
|
435
|
-
if (!entry.inclusionPromise?.signedEntryTimestamp) {
|
|
436
|
-
// If there's no inclusion promise, there must be an inclusion proof
|
|
437
|
-
if (!entry.inclusionProof) {
|
|
438
|
-
throw new Error(
|
|
439
|
-
"Bundle must have either an inclusion promise or an inclusion proof.",
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
} else {
|
|
443
|
-
// Verify the inclusion promise signature if present
|
|
444
|
-
// For v0.3 bundles that have both inclusion promise and proof,
|
|
445
|
-
// we can skip the promise verification if we don't have a Rekor key
|
|
446
|
-
// and there's a valid inclusion proof
|
|
447
|
-
if (!rekor && entry.inclusionProof) {
|
|
448
|
-
// Skip promise verification if we have an inclusion proof
|
|
449
|
-
// The inclusion proof will be verified later
|
|
450
|
-
} else {
|
|
451
|
-
if (!rekor) {
|
|
452
|
-
throw new Error("Rekor public key not found in trusted root");
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Verify the log ID matches (matches sigstore-go verify/tlog.go:80-83)
|
|
456
|
-
const entryLogId = base64ToUint8Array(entry.logId.keyId);
|
|
457
|
-
if (!uint8ArrayEqual(rekor.logId, entryLogId)) {
|
|
458
|
-
throw new Error(
|
|
459
|
-
`Rekor log ID mismatch: bundle uses ${Uint8ArrayToHex(entryLogId)} but loaded key is for ${Uint8ArrayToHex(rekor.logId)}`
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const signature = base64ToUint8Array(
|
|
464
|
-
entry.inclusionPromise.signedEntryTimestamp,
|
|
465
|
-
);
|
|
466
|
-
|
|
467
|
-
const keyId = Uint8ArrayToHex(entryLogId);
|
|
468
|
-
const integratedTime = Number(entry.integratedTime);
|
|
469
|
-
|
|
470
|
-
const signed = stringToUint8Array(
|
|
471
|
-
canonicalize({
|
|
472
|
-
body: entry.canonicalizedBody,
|
|
473
|
-
integratedTime: integratedTime,
|
|
474
|
-
logIndex: Number(entry.logIndex),
|
|
475
|
-
logID: keyId,
|
|
476
|
-
}),
|
|
477
|
-
);
|
|
478
|
-
|
|
479
|
-
if (!(await verifySignature(rekor.publicKey, signed, signature))) {
|
|
480
|
-
throw new Error(
|
|
481
|
-
"Failed to verify the inclusion promise in the provided bundle.",
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Validate integrated time and logged certificate
|
|
488
|
-
// Note: Rekor v2 bundles don't have integrated time in the tlog entry
|
|
489
|
-
if (entry.integratedTime) {
|
|
490
|
-
const integratedTime = Number(entry.integratedTime);
|
|
491
|
-
const integratedDate = new Date(integratedTime * 1000);
|
|
492
|
-
|
|
493
|
-
if (!cert.validForDate(integratedDate)) {
|
|
494
|
-
throw new Error(
|
|
495
|
-
"Artifact signing was logged outside of the certificate validity.",
|
|
496
|
-
);
|
|
350
|
+
// Only v0.1 bundles may rely on an inclusion promise alone.
|
|
351
|
+
const requireProof = getBundleVersion(bundle.mediaType) !== "0.1";
|
|
352
|
+
const operators = new Set<string>();
|
|
353
|
+
const integratedTimes: Date[] = [];
|
|
354
|
+
|
|
355
|
+
for (const entry of entries) {
|
|
356
|
+
const logId = base64ToUint8Array(entry.logId.keyId);
|
|
357
|
+
const log = rekor.find((l) => uint8ArrayEqual(l.logId, logId));
|
|
358
|
+
if (!log) continue;
|
|
359
|
+
|
|
360
|
+
const promise = entry.inclusionPromise?.signedEntryTimestamp;
|
|
361
|
+
if (!entry.inclusionProof && (requireProof || !promise)) {
|
|
362
|
+
throw new Error("Transparency log entry requires an inclusion proof.");
|
|
497
363
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if (!bundle.verificationMaterial.timestampVerificationData) {
|
|
501
|
-
throw new Error(
|
|
502
|
-
"Rekor v2 bundles require a timestamp for verification.",
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Verify that the certificate in the log matches the signing certificate
|
|
508
|
-
// The format depends on the entry type (hashedrekord vs dsse) and version
|
|
509
|
-
const bodyJson = JSON.parse(Uint8ArrayToString(base64ToUint8Array(entry.canonicalizedBody)));
|
|
510
|
-
|
|
511
|
-
if (bodyJson.kind === "hashedrekord") {
|
|
512
|
-
let loggedCertContent: string | undefined;
|
|
513
|
-
|
|
514
|
-
// Check for hashedRekordV002 structure (Rekor v2)
|
|
515
|
-
if (bodyJson.spec.hashedRekordV002) {
|
|
516
|
-
const verifier = bodyJson.spec.hashedRekordV002.signature.verifier;
|
|
517
|
-
if (verifier?.x509Certificate) {
|
|
518
|
-
loggedCertContent = verifier.x509Certificate.rawBytes;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
// Check for older hashedrekord structure
|
|
522
|
-
else if (bodyJson.spec.signature?.publicKey) {
|
|
523
|
-
loggedCertContent = bodyJson.spec.signature.publicKey.content;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
if (loggedCertContent) {
|
|
527
|
-
// For hashedrekord v0.0.1, publicKey.content is base64-encoded PEM
|
|
528
|
-
// For hashedRekordV002, x509Certificate.rawBytes is base64-encoded DER
|
|
529
|
-
let loggedCert: X509Certificate;
|
|
530
|
-
if (bodyJson.spec.hashedRekordV002) {
|
|
531
|
-
loggedCert = X509Certificate.parse(base64ToUint8Array(loggedCertContent));
|
|
532
|
-
} else {
|
|
533
|
-
const pemString = Uint8ArrayToString(base64ToUint8Array(loggedCertContent));
|
|
534
|
-
loggedCert = X509Certificate.parse(pemString);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (!cert.equals(loggedCert)) {
|
|
538
|
-
throw new Error(
|
|
539
|
-
"Certificate in Rekor log does not match the signing certificate.",
|
|
540
|
-
);
|
|
541
|
-
}
|
|
364
|
+
if (promise) {
|
|
365
|
+
integratedTimes.push(await this.verifySET(entry, promise, log));
|
|
542
366
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const verifierContent = bodyJson.spec.signatures?.[0]?.verifier;
|
|
547
|
-
if (verifierContent) {
|
|
548
|
-
const pemString = Uint8ArrayToString(base64ToUint8Array(verifierContent));
|
|
549
|
-
const loggedCert = X509Certificate.parse(pemString);
|
|
550
|
-
if (!cert.equals(loggedCert)) {
|
|
551
|
-
throw new Error(
|
|
552
|
-
"Certificate in DSSE tlog entry does not match the signing certificate.",
|
|
553
|
-
);
|
|
554
|
-
}
|
|
367
|
+
if (entry.inclusionProof) {
|
|
368
|
+
await verifyMerkleInclusion(entry);
|
|
369
|
+
await verifyCheckpoint(entry, log);
|
|
555
370
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
// https://github.com/sigstore/sigstore-go/blob/main/pkg/tlog/entry.go#L360-L361
|
|
559
|
-
const publicKeyContent = bodyJson.spec.content?.envelope?.signatures?.[0]?.publicKey;
|
|
560
|
-
if (publicKeyContent) {
|
|
561
|
-
const pemString = Uint8ArrayToString(base64ToUint8Array(publicKeyContent));
|
|
562
|
-
const loggedCert = X509Certificate.parse(pemString);
|
|
563
|
-
if (!cert.equals(loggedCert)) {
|
|
564
|
-
throw new Error(
|
|
565
|
-
"Certificate in intoto tlog entry does not match the signing certificate.",
|
|
566
|
-
);
|
|
567
|
-
}
|
|
371
|
+
if (entry.integratedTime && !cert.validForDate(new Date(Number(entry.integratedTime) * 1000))) {
|
|
372
|
+
throw new Error("Artifact signing was logged outside of the certificate validity.");
|
|
568
373
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
throw new Error(`Unsupported tlog entry kind: ${bodyJson.kind}`);
|
|
374
|
+
await verifyTLogBody(entry, bundle, cert);
|
|
375
|
+
operators.add(log.operator);
|
|
572
376
|
}
|
|
573
377
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
async verifyInclusionProof(bundle: SigstoreBundle): Promise<void> {
|
|
578
|
-
if (!this.rawRoot) {
|
|
579
|
-
throw new Error("Sigstore root is undefined");
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
if (bundle.verificationMaterial.tlogEntries.length < 1) {
|
|
583
|
-
throw new Error("No transparency log entries found in bundle");
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// Verify inclusion proof for ALL entries, not just the first one
|
|
587
|
-
// Reference: https://github.com/sigstore/sigstore-go/blob/main/pkg/verify/tlog.go#L74-L127
|
|
588
|
-
for (const entry of bundle.verificationMaterial.tlogEntries) {
|
|
589
|
-
// Only verify if there's an inclusion proof (v0.3/rekor2 bundles)
|
|
590
|
-
// v0.1 bundles use inclusion promises instead, verified in verifyInclusionPromise
|
|
591
|
-
if (entry.inclusionProof) {
|
|
592
|
-
await verifyMerkleInclusion(entry);
|
|
593
|
-
|
|
594
|
-
if (entry.inclusionProof.checkpoint) {
|
|
595
|
-
await verifyCheckpoint(entry, this.rawRoot.tlogs);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
378
|
+
const verifiedOperators = countOperators(operators);
|
|
379
|
+
if (verifiedOperators < this.options.tlogThreshold) {
|
|
380
|
+
throw new Error(`Not enough verified transparency logs: ${verifiedOperators} < ${this.options.tlogThreshold}`);
|
|
598
381
|
}
|
|
382
|
+
return integratedTimes;
|
|
599
383
|
}
|
|
600
384
|
|
|
601
|
-
|
|
602
|
-
|
|
385
|
+
// Shared checks: policy, transparency log, timestamps, chain at every observer time, SCTs.
|
|
386
|
+
// Returns the signing certificate and the bundle's signature.
|
|
387
|
+
private async verifyBundle(
|
|
603
388
|
bundle: SigstoreBundle,
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
if (!this.root) {
|
|
389
|
+
policy: VerificationPolicy,
|
|
390
|
+
): Promise<{ signingCert: X509Certificate; signature: Uint8Array }> {
|
|
391
|
+
assertBundle(bundle);
|
|
392
|
+
if (!this.root || !this.rawRoot) {
|
|
610
393
|
throw new Error("Sigstore root is undefined");
|
|
611
394
|
}
|
|
612
395
|
|
|
613
396
|
const cert = bundle.verificationMaterial.certificate ||
|
|
614
397
|
bundle.verificationMaterial.x509CertificateChain?.certificates[0];
|
|
615
|
-
|
|
616
398
|
if (!cert) {
|
|
617
399
|
throw new Error("No certificate found in bundle");
|
|
618
400
|
}
|
|
619
|
-
|
|
620
401
|
const signingCert = X509Certificate.parse(base64ToUint8Array(cert.rawBytes));
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
let signature: Uint8Array;
|
|
624
|
-
if (bundle.messageSignature) {
|
|
625
|
-
signature = base64ToUint8Array(bundle.messageSignature.signature);
|
|
626
|
-
} else if (bundle.dsseEnvelope) {
|
|
627
|
-
if (!bundle.dsseEnvelope.signatures || bundle.dsseEnvelope.signatures.length === 0) {
|
|
628
|
-
throw new Error("DSSE envelope has no signatures");
|
|
629
|
-
}
|
|
630
|
-
signature = base64ToUint8Array(bundle.dsseEnvelope.signatures[0].sig);
|
|
631
|
-
} else {
|
|
632
|
-
throw new Error("Bundle does not contain a message signature or DSSE envelope");
|
|
633
|
-
}
|
|
634
|
-
// # 1 Basic stuff: ceritificate policy validation
|
|
635
|
-
policy.verify(signingCert);
|
|
636
|
-
|
|
637
|
-
// # 2 Certificate validity - verify chain to trusted CA
|
|
638
|
-
// Similar to sigstore-js key/index.ts:59-64 which calls verifyCertificateChain()
|
|
639
|
-
// Returns the verified certificate path [leaf, intermediate(s), root]
|
|
640
|
-
const certPath = await this.verifyCertificateChain(
|
|
641
|
-
signingCert.notBefore,
|
|
642
|
-
signingCert,
|
|
643
|
-
this.root.certificateAuthorities
|
|
402
|
+
const signature = base64ToUint8Array(
|
|
403
|
+
bundle.messageSignature ? bundle.messageSignature.signature : bundle.dsseEnvelope.signatures[0].sig,
|
|
644
404
|
);
|
|
645
405
|
|
|
646
|
-
|
|
647
|
-
// https://github.com/sigstore/sigstore-js/packages/verify/src/key/sct.ts#L45
|
|
648
|
-
// Similar to sigstore-js key/index.ts:67 which uses path[0] (leaf) and path[1] (issuer)
|
|
649
|
-
// for SCT verification. Handle edge case where path has only one cert (self-signed root).
|
|
650
|
-
const issuerCert = certPath.length > 1 ? certPath[1] : certPath[0];
|
|
651
|
-
const verifiedSCTs = await this.verifySCT(signingCert, issuerCert, this.root.ctlogs);
|
|
652
|
-
if (verifiedSCTs.length < this.options.ctlogThreshold) {
|
|
653
|
-
throw new Error(
|
|
654
|
-
`Not enough valid SCTs: found ${verifiedSCTs.length}, required ${this.options.ctlogThreshold}`
|
|
655
|
-
);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// # 4 Rekor inclusion promise
|
|
659
|
-
if (
|
|
660
|
-
!(await this.verifyInclusionPromise(signingCert, bundle, this.root.rekor))
|
|
661
|
-
) {
|
|
662
|
-
throw new Error("Inclusion promise validation failed.");
|
|
663
|
-
}
|
|
406
|
+
await policy.verify(signingCert);
|
|
664
407
|
|
|
665
|
-
//
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
for (const entry of bundle.verificationMaterial.tlogEntries) {
|
|
670
|
-
await verifyTLogBody(entry, bundle);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// # 6 TSA Timestamp Verification
|
|
674
|
-
// Verify all timestamps and enforce threshold
|
|
675
|
-
// Reference: https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/verifier.ts#L101-L106
|
|
676
|
-
const verifiedTimestamps = await verifyBundleTimestamp(
|
|
408
|
+
// Observer timestamps come from SET-bound integrated times and verified RFC 3161 timestamps.
|
|
409
|
+
// Rekor v2 entries carry no integrated time, so they need a TSA timestamp to be anchored at all.
|
|
410
|
+
const integratedTimes = await this.verifyTlogEntries(signingCert, bundle, this.root.rekor);
|
|
411
|
+
const timestamps = await verifyBundleTimestamp(
|
|
677
412
|
bundle.verificationMaterial.timestampVerificationData,
|
|
678
413
|
signature,
|
|
679
|
-
this.rawRoot
|
|
414
|
+
this.rawRoot.timestampAuthorities || [],
|
|
680
415
|
);
|
|
416
|
+
const tsaOperators = countOperators(new Set(timestamps.map((t) => t.operator)));
|
|
417
|
+
if (tsaOperators < this.options.tsaThreshold) {
|
|
418
|
+
throw new Error(`Not enough verified TSA operators: ${tsaOperators} < ${this.options.tsaThreshold}`);
|
|
419
|
+
}
|
|
420
|
+
const observerTimes = [...integratedTimes, ...timestamps.map((t) => t.signingTime)];
|
|
421
|
+
if (observerTimes.length === 0) {
|
|
422
|
+
throw new Error("No verified observer timestamp anchors the signature in time.");
|
|
423
|
+
}
|
|
681
424
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
);
|
|
425
|
+
// The CA window and the whole chain are checked at every observer time, never at the leaf's own notBefore.
|
|
426
|
+
let certPath: X509Certificate[] = [];
|
|
427
|
+
for (const ts of observerTimes) {
|
|
428
|
+
certPath = await this.verifyCertificateChain(ts, signingCert, this.root.certificateAuthorities);
|
|
429
|
+
}
|
|
430
|
+
const issuerCert = certPath.length > 1 ? certPath[1] : certPath[0];
|
|
431
|
+
const ctOperators = countOperators(await this.verifySCT(signingCert, issuerCert, this.root.ctlogs));
|
|
432
|
+
if (ctOperators < this.options.ctlogThreshold) {
|
|
433
|
+
throw new Error(`Not enough verified CT log operators: ${ctOperators} < ${this.options.ctlogThreshold}`);
|
|
686
434
|
}
|
|
687
435
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
436
|
+
return { signingCert, signature };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
public async verifyArtifactPolicy(
|
|
440
|
+
policy: VerificationPolicy,
|
|
441
|
+
bundle: SigstoreBundle,
|
|
442
|
+
data: Uint8Array,
|
|
443
|
+
isDigestOnly: boolean = false,
|
|
444
|
+
): Promise<boolean> {
|
|
445
|
+
if (isDigestOnly && data.byteLength !== 32) {
|
|
446
|
+
throw new Error("SHA-256 digest must be exactly 32 bytes");
|
|
695
447
|
}
|
|
448
|
+
const { signingCert, signature } = await this.verifyBundle(bundle, policy);
|
|
696
449
|
|
|
697
450
|
// # 7 Revocation *skipping* not really a thing (unsurprisingly)
|
|
698
451
|
|
|
699
452
|
// # 8 verify the signed data
|
|
700
453
|
if (bundle.dsseEnvelope) {
|
|
701
|
-
//
|
|
454
|
+
// Only in-toto statements are understood; other payload types must not be read as one.
|
|
455
|
+
if (bundle.dsseEnvelope.payloadType !== "application/vnd.in-toto+json") {
|
|
456
|
+
throw new Error(`Unsupported DSSE payload type: ${bundle.dsseEnvelope.payloadType}`);
|
|
457
|
+
}
|
|
702
458
|
const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
|
|
703
459
|
const payload = JSON.parse(Uint8ArrayToString(payloadBytes));
|
|
704
460
|
|
|
705
461
|
// Verify the artifact digest matches a subject in the in-toto statement
|
|
706
|
-
if (!payload
|
|
707
|
-
throw new Error(
|
|
462
|
+
if (!Array.isArray(payload?.subject) || payload.subject.length === 0 || payload.subject.length > MAX_SUBJECTS) {
|
|
463
|
+
throw new Error(`DSSE payload must have between 1 and ${MAX_SUBJECTS} subjects`);
|
|
708
464
|
}
|
|
709
|
-
|
|
710
465
|
// Compute or extract the artifact digest
|
|
711
466
|
let artifactDigest: string;
|
|
712
467
|
if (isDigestOnly) {
|
|
@@ -719,14 +474,14 @@ export class SigstoreVerifier {
|
|
|
719
474
|
);
|
|
720
475
|
}
|
|
721
476
|
|
|
722
|
-
//
|
|
723
|
-
let matchedSubject =
|
|
477
|
+
// Every subject must carry a bounded digest map; any one of them may match the artifact.
|
|
478
|
+
let matchedSubject = false;
|
|
724
479
|
for (const subject of payload.subject) {
|
|
725
|
-
const
|
|
726
|
-
if (
|
|
727
|
-
|
|
728
|
-
break;
|
|
480
|
+
const digests = subject?.digest;
|
|
481
|
+
if (!digests || typeof digests !== "object" || Array.isArray(digests) || Object.keys(digests).length > MAX_SUBJECT_DIGESTS) {
|
|
482
|
+
throw new Error(`Invalid DSSE subject digest map (maximum ${MAX_SUBJECT_DIGESTS} digests)`);
|
|
729
483
|
}
|
|
484
|
+
matchedSubject ||= typeof digests.sha256 === "string" && artifactDigest === digests.sha256.toLowerCase();
|
|
730
485
|
}
|
|
731
486
|
|
|
732
487
|
if (!matchedSubject) {
|
|
@@ -744,6 +499,14 @@ export class SigstoreVerifier {
|
|
|
744
499
|
throw new Error("DSSE signature verification failed");
|
|
745
500
|
}
|
|
746
501
|
} else {
|
|
502
|
+
// The bundle's message digest must be the digest of the artifact being verified.
|
|
503
|
+
const { messageDigest } = bundle.messageSignature;
|
|
504
|
+
const digest = isDigestOnly
|
|
505
|
+
? data
|
|
506
|
+
: new Uint8Array(await crypto.subtle.digest(HashAlgorithms.SHA256, data as BufferSource));
|
|
507
|
+
if (messageDigest.algorithm !== "SHA2_256" || !uint8ArrayEqual(digest, base64ToUint8Array(messageDigest.digest))) {
|
|
508
|
+
throw new Error("Artifact digest does not match the bundle message digest");
|
|
509
|
+
}
|
|
747
510
|
const publicKey = await signingCert.publicKeyObj;
|
|
748
511
|
|
|
749
512
|
if (isDigestOnly) {
|
|
@@ -811,78 +574,11 @@ public async verifyArtifact(
|
|
|
811
574
|
bundle: SigstoreBundle,
|
|
812
575
|
policy: VerificationPolicy,
|
|
813
576
|
): Promise<{ payloadType: string; payload: Uint8Array }> {
|
|
814
|
-
|
|
815
|
-
throw new Error("Sigstore root is undefined");
|
|
816
|
-
}
|
|
817
|
-
|
|
577
|
+
const { signingCert, signature } = await this.verifyBundle(bundle, policy);
|
|
818
578
|
if (!bundle.dsseEnvelope) {
|
|
819
579
|
throw new Error("Bundle does not contain a DSSE envelope");
|
|
820
580
|
}
|
|
821
581
|
|
|
822
|
-
// Extract certificate from bundle
|
|
823
|
-
const cert = bundle.verificationMaterial.certificate ||
|
|
824
|
-
bundle.verificationMaterial.x509CertificateChain?.certificates[0];
|
|
825
|
-
|
|
826
|
-
if (!cert) {
|
|
827
|
-
throw new Error("No certificate found in bundle");
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
const signingCert = X509Certificate.parse(base64ToUint8Array(cert.rawBytes));
|
|
831
|
-
|
|
832
|
-
// (1) Verify certificate chain to trusted CA
|
|
833
|
-
const certPath = await this.verifyCertificateChain(
|
|
834
|
-
signingCert.notBefore,
|
|
835
|
-
signingCert,
|
|
836
|
-
this.root.certificateAuthorities
|
|
837
|
-
);
|
|
838
|
-
|
|
839
|
-
// (2) Verify SCTs
|
|
840
|
-
const issuerCert = certPath.length > 1 ? certPath[1] : certPath[0];
|
|
841
|
-
const verifiedSCTs = await this.verifySCT(signingCert, issuerCert, this.root.ctlogs);
|
|
842
|
-
if (verifiedSCTs.length < this.options.ctlogThreshold) {
|
|
843
|
-
throw new Error(
|
|
844
|
-
`Not enough valid SCTs: found ${verifiedSCTs.length}, required ${this.options.ctlogThreshold}`
|
|
845
|
-
);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// (3) Verify the signing certificate against the policy
|
|
849
|
-
policy.verify(signingCert);
|
|
850
|
-
|
|
851
|
-
// (4) Verify Rekor inclusion promise
|
|
852
|
-
if (!(await this.verifyInclusionPromise(signingCert, bundle, this.root.rekor))) {
|
|
853
|
-
throw new Error("Inclusion promise validation failed");
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// (5) Verify Rekor inclusion proof (Merkle tree verification)
|
|
857
|
-
await this.verifyInclusionProof(bundle);
|
|
858
|
-
|
|
859
|
-
// Validate envelope has exactly one signature (matches sigstore-python dsse._verify)
|
|
860
|
-
if (!bundle.dsseEnvelope.signatures || bundle.dsseEnvelope.signatures.length !== 1) {
|
|
861
|
-
throw new Error(
|
|
862
|
-
`DSSE envelope must have exactly 1 signature, got ${bundle.dsseEnvelope.signatures?.length ?? 0}`
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// (6) TSA Timestamp verification
|
|
867
|
-
const signature = base64ToUint8Array(bundle.dsseEnvelope.signatures[0].sig);
|
|
868
|
-
const verifiedTimestamps = await verifyBundleTimestamp(
|
|
869
|
-
bundle.verificationMaterial.timestampVerificationData,
|
|
870
|
-
signature,
|
|
871
|
-
this.rawRoot?.timestampAuthorities || []
|
|
872
|
-
);
|
|
873
|
-
|
|
874
|
-
if (verifiedTimestamps.length < this.options.tsaThreshold) {
|
|
875
|
-
throw new Error(
|
|
876
|
-
`Not enough verified TSA timestamps: ${verifiedTimestamps.length} < ${this.options.tsaThreshold}`
|
|
877
|
-
);
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
for (const verifiedTimestamp of verifiedTimestamps) {
|
|
881
|
-
if (!signingCert.validForDate(verifiedTimestamp)) {
|
|
882
|
-
throw new Error("Certificate was not valid at the time of timestamping");
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
|
|
886
582
|
// (7) Verify the DSSE envelope signature
|
|
887
583
|
const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
|
|
888
584
|
const pae = preAuthEncoding(bundle.dsseEnvelope.payloadType, payloadBytes);
|
|
@@ -893,13 +589,11 @@ public async verifyArtifact(
|
|
|
893
589
|
throw new Error("DSSE signature verification failed");
|
|
894
590
|
}
|
|
895
591
|
|
|
896
|
-
// (8)
|
|
897
|
-
// The entry MUST be of type "dsse" for DSSE verification
|
|
592
|
+
// (8) Every entry MUST be of type "dsse" for DSSE verification
|
|
898
593
|
for (const entry of bundle.verificationMaterial.tlogEntries) {
|
|
899
594
|
if (entry.kindVersion.kind !== "dsse") {
|
|
900
595
|
throw new Error(`Expected entry type dsse, got ${entry.kindVersion.kind}`);
|
|
901
596
|
}
|
|
902
|
-
await verifyTLogBody(entry, bundle);
|
|
903
597
|
}
|
|
904
598
|
|
|
905
599
|
// Return the verified payload
|