@freedomofpress/sigstore-browser 0.1.14 → 0.1.16

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 (132) hide show
  1. package/README.md +20 -16
  2. package/dist/bundle.d.ts +3 -2
  3. package/dist/bundle.js +66 -1
  4. package/dist/bundle.js.map +1 -1
  5. package/dist/cli.d.ts +0 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/dsse.d.ts +0 -1
  8. package/dist/dsse.js.map +1 -1
  9. package/dist/errors.d.ts +0 -1
  10. package/dist/errors.js.map +1 -1
  11. package/dist/index.d.ts +9 -10
  12. package/dist/index.js +5 -5
  13. package/dist/index.js.map +1 -1
  14. package/dist/interfaces.d.ts +18 -4
  15. package/dist/interfaces.js +15 -1
  16. package/dist/interfaces.js.map +1 -1
  17. package/dist/oid.d.ts +0 -1
  18. package/dist/oid.js.map +1 -1
  19. package/dist/policy.d.ts +3 -4
  20. package/dist/policy.js +5 -5
  21. package/dist/policy.js.map +1 -1
  22. package/dist/rfc3161/error.d.ts +0 -1
  23. package/dist/rfc3161/error.js.map +1 -1
  24. package/dist/rfc3161/index.d.ts +0 -1
  25. package/dist/rfc3161/index.js.map +1 -1
  26. package/dist/rfc3161/timestamp.d.ts +0 -1
  27. package/dist/rfc3161/timestamp.js.map +1 -1
  28. package/dist/rfc3161/tstinfo.d.ts +0 -1
  29. package/dist/rfc3161/tstinfo.js.map +1 -1
  30. package/dist/sigstore.d.ts +10 -20
  31. package/dist/sigstore.js +167 -400
  32. package/dist/sigstore.js.map +1 -1
  33. package/dist/timestamp/tsa.d.ts +6 -3
  34. package/dist/timestamp/tsa.js +16 -19
  35. package/dist/timestamp/tsa.js.map +1 -1
  36. package/dist/tlog/body.d.ts +3 -2
  37. package/dist/tlog/body.js +19 -6
  38. package/dist/tlog/body.js.map +1 -1
  39. package/dist/tlog/checkpoint.d.ts +2 -3
  40. package/dist/tlog/checkpoint.js +17 -95
  41. package/dist/tlog/checkpoint.js.map +1 -1
  42. package/dist/tlog/dsse.d.ts +3 -3
  43. package/dist/tlog/dsse.js +11 -5
  44. package/dist/tlog/dsse.js.map +1 -1
  45. package/dist/tlog/hashedrekord.d.ts +3 -3
  46. package/dist/tlog/hashedrekord.js +8 -5
  47. package/dist/tlog/hashedrekord.js.map +1 -1
  48. package/dist/tlog/intoto.d.ts +3 -3
  49. package/dist/tlog/intoto.js +3 -1
  50. package/dist/tlog/intoto.js.map +1 -1
  51. package/dist/tlog/merkle.d.ts +0 -1
  52. package/dist/tlog/merkle.js.map +1 -1
  53. package/dist/trust/tuf-root.d.ts +0 -1
  54. package/dist/trust/tuf-root.js.map +1 -1
  55. package/dist/trust/tuf.d.ts +0 -1
  56. package/dist/trust/tuf.js.map +1 -1
  57. package/dist/x509/cert.d.ts +0 -1
  58. package/dist/x509/cert.js +1 -1
  59. package/dist/x509/cert.js.map +1 -1
  60. package/dist/x509/chain.d.ts +0 -1
  61. package/dist/x509/chain.js +2 -2
  62. package/dist/x509/chain.js.map +1 -1
  63. package/dist/x509/ext.d.ts +0 -1
  64. package/dist/x509/ext.js.map +1 -1
  65. package/dist/x509/index.d.ts +1 -2
  66. package/dist/x509/index.js +1 -1
  67. package/dist/x509/index.js.map +1 -1
  68. package/dist/x509/sct.d.ts +0 -1
  69. package/dist/x509/sct.js.map +1 -1
  70. package/package.json +15 -8
  71. package/.github/workflows/conformance.yml +0 -29
  72. package/.github/workflows/publish.yml +0 -47
  73. package/dist/bundle.d.ts.map +0 -1
  74. package/dist/cli.d.ts.map +0 -1
  75. package/dist/dsse.d.ts.map +0 -1
  76. package/dist/errors.d.ts.map +0 -1
  77. package/dist/index.d.ts.map +0 -1
  78. package/dist/interfaces.d.ts.map +0 -1
  79. package/dist/oid.d.ts.map +0 -1
  80. package/dist/policy.d.ts.map +0 -1
  81. package/dist/rfc3161/error.d.ts.map +0 -1
  82. package/dist/rfc3161/index.d.ts.map +0 -1
  83. package/dist/rfc3161/timestamp.d.ts.map +0 -1
  84. package/dist/rfc3161/tstinfo.d.ts.map +0 -1
  85. package/dist/sigstore.d.ts.map +0 -1
  86. package/dist/timestamp/tsa.d.ts.map +0 -1
  87. package/dist/tlog/body.d.ts.map +0 -1
  88. package/dist/tlog/checkpoint.d.ts.map +0 -1
  89. package/dist/tlog/dsse.d.ts.map +0 -1
  90. package/dist/tlog/hashedrekord.d.ts.map +0 -1
  91. package/dist/tlog/intoto.d.ts.map +0 -1
  92. package/dist/tlog/merkle.d.ts.map +0 -1
  93. package/dist/trust/tuf-root.d.ts.map +0 -1
  94. package/dist/trust/tuf.d.ts.map +0 -1
  95. package/dist/x509/cert.d.ts.map +0 -1
  96. package/dist/x509/chain.d.ts.map +0 -1
  97. package/dist/x509/ext.d.ts.map +0 -1
  98. package/dist/x509/index.d.ts.map +0 -1
  99. package/dist/x509/sct.d.ts.map +0 -1
  100. package/eslint.config.mjs +0 -31
  101. package/src/bundle.ts +0 -99
  102. package/src/cli.ts +0 -290
  103. package/src/dsse.ts +0 -49
  104. package/src/errors.ts +0 -82
  105. package/src/index.browser.test.ts +0 -290
  106. package/src/index.ts +0 -93
  107. package/src/interfaces.ts +0 -123
  108. package/src/oid.ts +0 -34
  109. package/src/policy.ts +0 -476
  110. package/src/rfc3161/error.ts +0 -16
  111. package/src/rfc3161/index.ts +0 -17
  112. package/src/rfc3161/timestamp.ts +0 -268
  113. package/src/rfc3161/tstinfo.ts +0 -78
  114. package/src/sigstore.ts +0 -948
  115. package/src/timestamp/tsa.ts +0 -211
  116. package/src/tlog/body.ts +0 -65
  117. package/src/tlog/checkpoint.ts +0 -254
  118. package/src/tlog/dsse.ts +0 -147
  119. package/src/tlog/hashedrekord.ts +0 -130
  120. package/src/tlog/intoto.ts +0 -106
  121. package/src/tlog/merkle.ts +0 -152
  122. package/src/trust/README.md +0 -38
  123. package/src/trust/tuf-root.ts +0 -3
  124. package/src/trust/tuf.ts +0 -233
  125. package/src/x509/cert.ts +0 -569
  126. package/src/x509/chain.ts +0 -212
  127. package/src/x509/ext.ts +0 -356
  128. package/src/x509/index.ts +0 -19
  129. package/src/x509/sct.ts +0 -156
  130. package/tsconfig.json +0 -18
  131. package/vite.config.playwright.ts +0 -14
  132. package/vite.config.ts +0 -18
package/src/sigstore.ts DELETED
@@ -1,948 +0,0 @@
1
- import {
2
- base64ToUint8Array,
3
- ByteStream,
4
- canonicalize,
5
- importKey,
6
- stringToUint8Array,
7
- uint8ArrayEqual,
8
- Uint8ArrayToHex,
9
- Uint8ArrayToString,
10
- verifySignature,
11
- verifySignatureOverDigest,
12
- } from "@freedomofpress/crypto-browser";
13
- import { HashAlgorithms } from "./interfaces.js";
14
- import {
15
- CertificateChainVerifier,
16
- EXTENSION_OID_SCT,
17
- X509Certificate,
18
- X509SCTExtension,
19
- } from "./x509/index.js";
20
- import { SigstoreBundle } from "./bundle.js";
21
- import { preAuthEncoding } from "./dsse.js";
22
- import {
23
- CertAuthority,
24
- CTLog,
25
- RawCAs,
26
- RawLogs,
27
- RawTimestampAuthorities,
28
- RekorKeyInfo,
29
- Sigstore,
30
- SigstoreRoots,
31
- TrustedRoot,
32
- } from "./interfaces.js";
33
- import { verifyMerkleInclusion } from "./tlog/merkle.js";
34
- import { verifyCheckpoint } from "./tlog/checkpoint.js";
35
- import { verifyTLogBody } from "./tlog/body.js";
36
- import { verifyBundleTimestamp } from "./timestamp/tsa.js";
37
- import { TrustedRootProvider } from "./trust/tuf.js";
38
- import type { VerificationPolicy } from "./policy.js";
39
- import { AnyOf, AllOf, OIDCIssuer, OIDCIssuerV2, Identity } from "./policy.js";
40
-
41
- const MEDIA_TYPE_BASE = "application/vnd.dev.sigstore.bundle";
42
-
43
- /**
44
- * Extract bundle version from mediaType string
45
- * Reference: https://github.com/sigstore/sigstore-go/blob/main/pkg/bundle/bundle.go#L159-L177
46
- */
47
- function getBundleVersion(mediaType: string): string {
48
- switch (mediaType) {
49
- case `${MEDIA_TYPE_BASE}+json;version=0.1`:
50
- return "0.1";
51
- case `${MEDIA_TYPE_BASE}+json;version=0.2`:
52
- return "0.2";
53
- case `${MEDIA_TYPE_BASE}+json;version=0.3`:
54
- return "0.3";
55
- }
56
-
57
- // New format: "application/vnd.dev.sigstore.bundle.v0.3+json"
58
- if (mediaType.startsWith(`${MEDIA_TYPE_BASE}.v`) && mediaType.endsWith("+json")) {
59
- const version = mediaType
60
- .replace(`${MEDIA_TYPE_BASE}.v`, "")
61
- .replace("+json", "");
62
- // Basic semver validation (major.minor or major.minor.patch)
63
- if (/^\d+\.\d+(\.\d+)?$/.test(version)) {
64
- return version;
65
- }
66
- }
67
-
68
- // Default to 0.1 for unknown formats
69
- return "0.1";
70
- }
71
-
72
- /**
73
- * Rekor v2 entries omit `integratedTime` and MUST carry a signed RFC3161
74
- * timestamp instead. Check for actual content, not just presence: an empty
75
- * `timestampVerificationData: {}` is truthy, so `if (!timestampVerificationData)`
76
- * is bypassable and leaves the cert validity window unanchored in time.
77
- */
78
- export function assertRekorV2Timestamp(
79
- timestampData?: { rfc3161Timestamps?: readonly unknown[] },
80
- ): void {
81
- if (!timestampData?.rfc3161Timestamps?.length) {
82
- throw new Error("Rekor v2 bundles require a timestamp for verification.");
83
- }
84
- }
85
-
86
- export interface SigstoreVerifierOptions {
87
- tlogThreshold?: number;
88
- ctlogThreshold?: number;
89
- tsaThreshold?: number;
90
- }
91
-
92
- export class SigstoreVerifier {
93
- private root: Sigstore | undefined;
94
- private rawRoot: TrustedRoot | undefined;
95
- private options: Required<SigstoreVerifierOptions>;
96
-
97
- constructor(options: SigstoreVerifierOptions = {}) {
98
- this.root = undefined;
99
- this.rawRoot = undefined;
100
- this.options = {
101
- tlogThreshold: options.tlogThreshold ?? 1,
102
- ctlogThreshold: options.ctlogThreshold ?? 1,
103
- tsaThreshold: options.tsaThreshold ?? 0,
104
- };
105
- }
106
-
107
- async loadLog(frozenTimestamp: Date, logs: RawLogs): Promise<RekorKeyInfo | undefined> {
108
- // Load the first Rekor transparency log key that's valid at the frozen timestamp
109
- // and store its log ID for verification. This matches sigstore-go's approach of
110
- // looking up the verifier by log ID (verify/tlog.go:80-83).
111
-
112
- for (const log of logs) {
113
- // if start date is not in the future, and if an end doesn't exist or is in the future
114
- if (
115
- frozenTimestamp > new Date(log.publicKey.validFor.start) &&
116
- (!log.publicKey.validFor.end ||
117
- new Date(log.publicKey.validFor.end) > frozenTimestamp)
118
- ) {
119
- return {
120
- publicKey: await importKey(
121
- log.publicKey.keyDetails,
122
- log.publicKey.keyDetails,
123
- log.publicKey.rawBytes,
124
- ),
125
- logId: base64ToUint8Array(log.logId.keyId),
126
- };
127
- }
128
- }
129
-
130
- // Return undefined instead of throwing - some bundles don't need Rekor keys
131
- // (e.g., v0.3 bundles with inclusion proofs)
132
- return undefined;
133
- }
134
-
135
- async loadCTLogs(frozenTimestamp: Date, ctlogs: RawLogs): Promise<CTLog[]> {
136
- const result: CTLog[] = [];
137
-
138
- for (const log of ctlogs) {
139
- const start = new Date(log.publicKey.validFor.start);
140
- const end = log.publicKey.validFor.end
141
- ? new Date(log.publicKey.validFor.end)
142
- : new Date('9999-12-31'); // No expiry means valid forever
143
-
144
- // Include logs that are valid (started before frozen timestamp)
145
- // We keep all logs, even expired ones, for historical verification
146
- if (start <= frozenTimestamp) {
147
- const publicKey = await importKey(
148
- log.publicKey.keyDetails,
149
- log.publicKey.keyDetails,
150
- log.publicKey.rawBytes,
151
- );
152
-
153
- result.push({
154
- logID: base64ToUint8Array(log.logId.keyId),
155
- publicKey,
156
- validFor: { start, end },
157
- });
158
- }
159
- }
160
-
161
- if (result.length === 0) {
162
- throw new Error("Could not find any valid CT logs in sigstore root.");
163
- }
164
-
165
- return result;
166
- }
167
-
168
- // Adapted from https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/key/certificate.ts#L22-L53
169
- // Verifies that the leaf certificate chains to a trusted CA and is valid at the given timestamp.
170
- // Differences from sigstore-js:
171
- // - This is async (uses await) because our CertificateChainVerifier.verify() is async
172
- // - sigstore-js filters CAs using filterCertAuthorities() before calling this function,
173
- // we do the timestamp filtering inline within this function
174
- async verifyCertificateChain(
175
- timestamp: Date,
176
- leaf: X509Certificate,
177
- certificateAuthorities: CertAuthority[]
178
- ): Promise<X509Certificate[]> {
179
- let lastError: any;
180
-
181
- for (const ca of certificateAuthorities) {
182
- // Check if this CA is valid for the given timestamp
183
- if (timestamp < ca.validFor.start || timestamp > ca.validFor.end) {
184
- continue;
185
- }
186
-
187
- try {
188
- const verifier = new CertificateChainVerifier({
189
- trustedCerts: ca.certChain,
190
- untrustedCert: leaf,
191
- timestamp,
192
- });
193
- return await verifier.verify();
194
- } catch (err) {
195
- lastError = err;
196
- }
197
- }
198
-
199
- throw new Error(`Failed to verify certificate chain: ${lastError?.message || 'No valid CAs found'}`);
200
- }
201
-
202
- // Load timestamp authorities that are valid at the frozen timestamp.
203
- // Unlike sigstore-js which doesn't pre-load TSAs (it passes raw TSA data to timestamp verification),
204
- // we parse and filter them at initialization time for consistency with how we handle CAs and other roots.
205
- loadTSA(
206
- frozenTimestamp: Date,
207
- tsas?: RawTimestampAuthorities,
208
- ): CertAuthority[] {
209
- if (!tsas || tsas.length === 0) {
210
- return [];
211
- }
212
-
213
- const result: CertAuthority[] = [];
214
-
215
- for (const tsa of tsas) {
216
- const start = new Date(tsa.validFor.start);
217
- const end = tsa.validFor.end ? new Date(tsa.validFor.end) : new Date(8640000000000000);
218
-
219
- if (frozenTimestamp > start && frozenTimestamp < end) {
220
- const certChain = tsa.certChain.certificates.map(cert =>
221
- X509Certificate.parse(base64ToUint8Array(cert.rawBytes))
222
- );
223
-
224
- if (certChain.length > 0) {
225
- result.push({
226
- certChain,
227
- validFor: { start, end },
228
- });
229
- }
230
- }
231
- }
232
-
233
- return result;
234
- }
235
-
236
- // Load certificate authorities (Fulcio CAs) that are valid at the frozen timestamp.
237
- // Similar to sigstore-js's filterCertAuthorities() in trust/filter.ts, but we also
238
- // parse the certificates at load time whereas sigstore-js keeps them in the trust material
239
- // and parses them during verification. This pre-loading approach is consistent with our
240
- // architecture of loading all trusted roots at initialization.
241
- loadCA(frozenTimestamp: Date, cas: RawCAs): CertAuthority[] {
242
- const result: CertAuthority[] = [];
243
-
244
- for (const ca of cas) {
245
- const start = new Date(ca.validFor.start);
246
- const end = ca.validFor.end ? new Date(ca.validFor.end) : new Date(8640000000000000);
247
-
248
- if (frozenTimestamp > start && frozenTimestamp < end) {
249
- const certChain = ca.certChain.certificates.map(cert =>
250
- X509Certificate.parse(base64ToUint8Array(cert.rawBytes))
251
- );
252
-
253
- if (certChain.length > 0) {
254
- result.push({
255
- certChain,
256
- validFor: { start, end },
257
- });
258
- }
259
- }
260
- }
261
-
262
- return result;
263
- }
264
-
265
- async loadSigstoreRoot(rawRoot: TrustedRoot) {
266
- const frozenTimestamp = new Date();
267
-
268
- this.rawRoot = rawRoot;
269
- this.root = {
270
- rekor: await this.loadLog(frozenTimestamp, rawRoot[SigstoreRoots.tlogs]),
271
- ctlogs: await this.loadCTLogs(frozenTimestamp, rawRoot[SigstoreRoots.ctlogs]),
272
- certificateAuthorities: this.loadCA(
273
- frozenTimestamp,
274
- rawRoot[SigstoreRoots.certificateAuthorities],
275
- ),
276
- timestampAuthorities: this.loadTSA(frozenTimestamp, rawRoot.timestampAuthorities),
277
- };
278
- }
279
-
280
- /**
281
- * Load Sigstore trusted root via TUF
282
- * Uses The Update Framework for secure, verified updates of trusted root metadata
283
- *
284
- * @param tufProvider Optional TrustedRootProvider instance. If not provided, uses default Sigstore TUF repository
285
- */
286
- async loadSigstoreRootWithTUF(tufProvider?: TrustedRootProvider): Promise<void> {
287
- const provider = tufProvider || new TrustedRootProvider();
288
- const trustedRoot = await provider.getTrustedRoot();
289
- await this.loadSigstoreRoot(trustedRoot);
290
- }
291
-
292
- // Adapted from https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/key/sct.ts
293
- // Key differences:
294
- // - Adds duplicate SCT detection (not in reference)
295
- // - Inline CT log filtering by logID and validity period (reference uses filterTLogAuthorities)
296
- // - Returns array of verified SCT logIDs for threshold checking (matches reference behavior)
297
- async verifySCT(
298
- cert: X509Certificate,
299
- issuer: X509Certificate,
300
- ctlogs: CTLog[],
301
- ): Promise<Uint8Array[]> {
302
- let extSCT: X509SCTExtension | undefined;
303
-
304
- // Verifying the SCT requires that we remove the SCT extension and
305
- // re-encode the TBS structure to DER -- this value is part of the data
306
- // over which the signature is calculated. Since this is a destructive action
307
- // we create a copy of the certificate so we can remove the SCT extension
308
- // without affecting the original certificate.
309
- const clone = cert.clone();
310
-
311
- // Intentionally not using the findExtension method here because we want to
312
- // remove the the SCT extension from the certificate before calculating the
313
- // PreCert structure
314
- for (let i = 0; i < clone.extensions.length; i++) {
315
- const ext = clone.extensions[i];
316
-
317
- if (ext.subs[0].toOID() === EXTENSION_OID_SCT) {
318
- extSCT = new X509SCTExtension(ext);
319
-
320
- // Remove the extension from the certificate
321
- clone.extensions.splice(i, 1);
322
- break;
323
- }
324
- }
325
-
326
- // No SCT extension found - fail verification
327
- if (!extSCT) {
328
- throw new Error("Certificate is missing required SCT extension");
329
- }
330
-
331
- // Found an SCT extension but it has no SCTs - fail verification
332
- if (extSCT.signedCertificateTimestamps.length === 0) {
333
- throw new Error("SCT extension is present but contains no SCTs");
334
- }
335
-
336
- // Check for duplicate SCTs (same log ID)
337
- const seenLogIds = new Set<string>();
338
- for (const sct of extSCT.signedCertificateTimestamps) {
339
- const logIdHex = Uint8ArrayToHex(sct.logID);
340
- if (seenLogIds.has(logIdHex)) {
341
- throw new Error(`Duplicate SCT found for log ID: ${logIdHex}`);
342
- }
343
- seenLogIds.add(logIdHex);
344
- }
345
-
346
- // Construct the PreCert structure
347
- // https://www.rfc-editor.org/rfc/rfc6962#section-3.2
348
- const preCert = new ByteStream();
349
-
350
- // Calculate hash of the issuer's public key
351
- const issuerId = new Uint8Array(
352
- await crypto.subtle.digest(HashAlgorithms.SHA256, issuer.publicKey as BufferSource),
353
- );
354
- preCert.appendView(issuerId);
355
-
356
- // Re-encodes the certificate to DER after removing the SCT extension
357
- const tbs = clone.tbsCertificate.toDER();
358
- preCert.appendUint24(tbs.length);
359
- preCert.appendView(tbs);
360
-
361
- // Calculate and return the verification results for each SCT
362
- // Unlike sigstore-go which counts verified SCTs and checks threshold at the end,
363
- // sigstore-js throws immediately if any SCT fails verification
364
- const verifiedSCTs: Uint8Array[] = [];
365
-
366
- for (const sct of extSCT.signedCertificateTimestamps) {
367
- // Find the CT log that matches this SCT's log ID and is valid for the SCT datetime
368
- const validCTLogs = ctlogs.filter((log) => {
369
- // Check if log IDs match
370
- if (!uint8ArrayEqual(log.logID, sct.logID)) return false;
371
- // Check that the SCT datetime is within the log's validity period
372
- return log.validFor.start <= sct.datetime && sct.datetime <= log.validFor.end;
373
- });
374
-
375
- // See if the SCT is valid for any of the CT logs
376
- const verified = await (async () => {
377
- for (const log of validCTLogs) {
378
- try {
379
- if (await sct.verify(preCert.buffer, log.publicKey)) {
380
- return true;
381
- }
382
- } catch {
383
- // Continue trying other logs
384
- }
385
- }
386
- return false;
387
- })();
388
-
389
- if (!verified) {
390
- throw new Error("SCT verification failed");
391
- }
392
-
393
- verifiedSCTs.push(sct.logID);
394
- }
395
-
396
- return verifiedSCTs;
397
- }
398
-
399
- async verifyInclusionPromise(
400
- cert: X509Certificate,
401
- bundle: SigstoreBundle,
402
- rekor: RekorKeyInfo | undefined,
403
- ): Promise<boolean> {
404
- const entries = bundle.verificationMaterial.tlogEntries;
405
-
406
- // Check that bundle has enough tlog entries to meet threshold
407
- if (entries.length < this.options.tlogThreshold) {
408
- throw new Error(
409
- `Not enough tlog entries: ${entries.length} < ${this.options.tlogThreshold}`,
410
- );
411
- }
412
-
413
- // Prevent DoS via excessive entries and threshold bypass via duplicates
414
- // Matches sigstore-go limits (verify/tlog.go:46-57)
415
- const MAX_TLOG_ENTRIES = 32;
416
- if (entries.length > MAX_TLOG_ENTRIES) {
417
- throw new Error(
418
- `Too many tlog entries: ${entries.length} > ${MAX_TLOG_ENTRIES}`,
419
- );
420
- }
421
-
422
- for (let i = 0; i < entries.length; i++) {
423
- for (let j = i + 1; j < entries.length; j++) {
424
- const iLogId = Uint8ArrayToHex(base64ToUint8Array(entries[i].logId.keyId));
425
- const jLogId = Uint8ArrayToHex(base64ToUint8Array(entries[j].logId.keyId));
426
- if (iLogId === jLogId && entries[i].logIndex === entries[j].logIndex) {
427
- throw new Error(
428
- `Duplicate tlog entry found: logID=${iLogId}, logIndex=${entries[i].logIndex}`,
429
- );
430
- }
431
- }
432
- }
433
-
434
- const entry = entries[0];
435
-
436
- // Extract bundle version from mediaType
437
- // Reference: https://github.com/sigstore/sigstore-go/blob/main/pkg/bundle/bundle.go#L159-L177
438
- const bundleVersion = getBundleVersion(bundle.mediaType);
439
- const isV02OrLater = parseFloat(bundleVersion) >= 0.2;
440
-
441
- // Bundle v0.2+ requires an inclusion proof
442
- if (isV02OrLater && !entry.inclusionProof) {
443
- throw new Error(
444
- "Bundle v0.2+ requires an inclusion proof.",
445
- );
446
- }
447
-
448
- // For rekor2/v0.3 bundles with inclusion proofs, the inclusion promise is optional
449
- if (!entry.inclusionPromise?.signedEntryTimestamp) {
450
- // If there's no inclusion promise, there must be an inclusion proof
451
- if (!entry.inclusionProof) {
452
- throw new Error(
453
- "Bundle must have either an inclusion promise or an inclusion proof.",
454
- );
455
- }
456
- } else {
457
- // Verify the inclusion promise signature if present
458
- // For v0.3 bundles that have both inclusion promise and proof,
459
- // we can skip the promise verification if we don't have a Rekor key
460
- // and there's a valid inclusion proof
461
- if (!rekor && entry.inclusionProof) {
462
- // Skip promise verification if we have an inclusion proof
463
- // The inclusion proof will be verified later
464
- } else {
465
- if (!rekor) {
466
- throw new Error("Rekor public key not found in trusted root");
467
- }
468
-
469
- // Verify the log ID matches (matches sigstore-go verify/tlog.go:80-83)
470
- const entryLogId = base64ToUint8Array(entry.logId.keyId);
471
- if (!uint8ArrayEqual(rekor.logId, entryLogId)) {
472
- throw new Error(
473
- `Rekor log ID mismatch: bundle uses ${Uint8ArrayToHex(entryLogId)} but loaded key is for ${Uint8ArrayToHex(rekor.logId)}`
474
- );
475
- }
476
-
477
- const signature = base64ToUint8Array(
478
- entry.inclusionPromise.signedEntryTimestamp,
479
- );
480
-
481
- const keyId = Uint8ArrayToHex(entryLogId);
482
- const integratedTime = Number(entry.integratedTime);
483
-
484
- const signed = stringToUint8Array(
485
- canonicalize({
486
- body: entry.canonicalizedBody,
487
- integratedTime: integratedTime,
488
- logIndex: Number(entry.logIndex),
489
- logID: keyId,
490
- }),
491
- );
492
-
493
- if (!(await verifySignature(rekor.publicKey, signed, signature))) {
494
- throw new Error(
495
- "Failed to verify the inclusion promise in the provided bundle.",
496
- );
497
- }
498
- }
499
- }
500
-
501
- // Validate integrated time and logged certificate
502
- // Note: Rekor v2 bundles don't have integrated time in the tlog entry
503
- if (entry.integratedTime) {
504
- const integratedTime = Number(entry.integratedTime);
505
- const integratedDate = new Date(integratedTime * 1000);
506
-
507
- if (!cert.validForDate(integratedDate)) {
508
- throw new Error(
509
- "Artifact signing was logged outside of the certificate validity.",
510
- );
511
- }
512
- } else {
513
- // Rekor v2 bundles (no integratedTime) require a signed RFC3161 timestamp.
514
- // An empty timestampVerificationData object ({}) must NOT satisfy this.
515
- assertRekorV2Timestamp(
516
- bundle.verificationMaterial.timestampVerificationData,
517
- );
518
- }
519
-
520
- // Verify that the certificate in the log matches the signing certificate
521
- // The format depends on the entry type (hashedrekord vs dsse) and version
522
- const bodyJson = JSON.parse(Uint8ArrayToString(base64ToUint8Array(entry.canonicalizedBody)));
523
-
524
- if (bodyJson.kind === "hashedrekord") {
525
- let loggedCertContent: string | undefined;
526
-
527
- // Check for hashedRekordV002 structure (Rekor v2)
528
- if (bodyJson.spec.hashedRekordV002) {
529
- const verifier = bodyJson.spec.hashedRekordV002.signature.verifier;
530
- if (verifier?.x509Certificate) {
531
- loggedCertContent = verifier.x509Certificate.rawBytes;
532
- }
533
- }
534
- // Check for older hashedrekord structure
535
- else if (bodyJson.spec.signature?.publicKey) {
536
- loggedCertContent = bodyJson.spec.signature.publicKey.content;
537
- }
538
-
539
- if (loggedCertContent) {
540
- // For hashedrekord v0.0.1, publicKey.content is base64-encoded PEM
541
- // For hashedRekordV002, x509Certificate.rawBytes is base64-encoded DER
542
- let loggedCert: X509Certificate;
543
- if (bodyJson.spec.hashedRekordV002) {
544
- loggedCert = X509Certificate.parse(base64ToUint8Array(loggedCertContent));
545
- } else {
546
- const pemString = Uint8ArrayToString(base64ToUint8Array(loggedCertContent));
547
- loggedCert = X509Certificate.parse(pemString);
548
- }
549
-
550
- if (!cert.equals(loggedCert)) {
551
- throw new Error(
552
- "Certificate in Rekor log does not match the signing certificate.",
553
- );
554
- }
555
- }
556
- } else if (bodyJson.kind === "dsse") {
557
- // DSSE v0.0.1: certificate is in spec.signatures[0].verifier (base64-encoded PEM)
558
- // https://github.com/sigstore/sigstore-go/blob/main/pkg/tlog/entry.go#L356-L357
559
- const verifierContent = bodyJson.spec.signatures?.[0]?.verifier;
560
- if (verifierContent) {
561
- const pemString = Uint8ArrayToString(base64ToUint8Array(verifierContent));
562
- const loggedCert = X509Certificate.parse(pemString);
563
- if (!cert.equals(loggedCert)) {
564
- throw new Error(
565
- "Certificate in DSSE tlog entry does not match the signing certificate.",
566
- );
567
- }
568
- }
569
- } else if (bodyJson.kind === "intoto") {
570
- // intoto v0.0.2: certificate is in spec.content.envelope.signatures[0].publicKey (base64-encoded PEM)
571
- // https://github.com/sigstore/sigstore-go/blob/main/pkg/tlog/entry.go#L360-L361
572
- const publicKeyContent = bodyJson.spec.content?.envelope?.signatures?.[0]?.publicKey;
573
- if (publicKeyContent) {
574
- const pemString = Uint8ArrayToString(base64ToUint8Array(publicKeyContent));
575
- const loggedCert = X509Certificate.parse(pemString);
576
- if (!cert.equals(loggedCert)) {
577
- throw new Error(
578
- "Certificate in intoto tlog entry does not match the signing certificate.",
579
- );
580
- }
581
- }
582
- } else {
583
- // Unknown entry type - this should not happen with standard Sigstore bundles
584
- throw new Error(`Unsupported tlog entry kind: ${bodyJson.kind}`);
585
- }
586
-
587
- return true;
588
- }
589
-
590
- async verifyInclusionProof(bundle: SigstoreBundle): Promise<void> {
591
- if (!this.rawRoot) {
592
- throw new Error("Sigstore root is undefined");
593
- }
594
-
595
- if (bundle.verificationMaterial.tlogEntries.length < 1) {
596
- throw new Error("No transparency log entries found in bundle");
597
- }
598
-
599
- // Verify inclusion proof for ALL entries, not just the first one
600
- // Reference: https://github.com/sigstore/sigstore-go/blob/main/pkg/verify/tlog.go#L74-L127
601
- for (const entry of bundle.verificationMaterial.tlogEntries) {
602
- // Only verify if there's an inclusion proof (v0.3/rekor2 bundles)
603
- // v0.1 bundles use inclusion promises instead, verified in verifyInclusionPromise
604
- if (entry.inclusionProof) {
605
- await verifyMerkleInclusion(entry);
606
-
607
- if (entry.inclusionProof.checkpoint) {
608
- await verifyCheckpoint(entry, this.rawRoot.tlogs);
609
- }
610
- }
611
- }
612
- }
613
-
614
- public async verifyArtifactPolicy(
615
- policy: VerificationPolicy,
616
- bundle: SigstoreBundle,
617
- data: Uint8Array,
618
- isDigestOnly: boolean = false,
619
- ): Promise<boolean> {
620
- // Quick checks first: does the signing certificate have the correct identity?
621
-
622
- if (!this.root) {
623
- throw new Error("Sigstore root is undefined");
624
- }
625
-
626
- const cert = bundle.verificationMaterial.certificate ||
627
- bundle.verificationMaterial.x509CertificateChain?.certificates[0];
628
-
629
- if (!cert) {
630
- throw new Error("No certificate found in bundle");
631
- }
632
-
633
- const signingCert = X509Certificate.parse(base64ToUint8Array(cert.rawBytes));
634
-
635
- // Handle both regular bundles (messageSignature) and DSSE bundles (dsseEnvelope)
636
- let signature: Uint8Array;
637
- if (bundle.messageSignature) {
638
- signature = base64ToUint8Array(bundle.messageSignature.signature);
639
- } else if (bundle.dsseEnvelope) {
640
- if (!bundle.dsseEnvelope.signatures || bundle.dsseEnvelope.signatures.length === 0) {
641
- throw new Error("DSSE envelope has no signatures");
642
- }
643
- signature = base64ToUint8Array(bundle.dsseEnvelope.signatures[0].sig);
644
- } else {
645
- throw new Error("Bundle does not contain a message signature or DSSE envelope");
646
- }
647
- // # 1 Basic stuff: ceritificate policy validation
648
- policy.verify(signingCert);
649
-
650
- // # 2 Certificate validity - verify chain to trusted CA
651
- // Similar to sigstore-js key/index.ts:59-64 which calls verifyCertificateChain()
652
- // Returns the verified certificate path [leaf, intermediate(s), root]
653
- const certPath = await this.verifyCertificateChain(
654
- signingCert.notBefore,
655
- signingCert,
656
- this.root.certificateAuthorities
657
- );
658
-
659
- // # 3 To verify the SCT we need to build a preCert (because the cert was logged without the SCT)
660
- // https://github.com/sigstore/sigstore-js/packages/verify/src/key/sct.ts#L45
661
- // Similar to sigstore-js key/index.ts:67 which uses path[0] (leaf) and path[1] (issuer)
662
- // for SCT verification. Handle edge case where path has only one cert (self-signed root).
663
- const issuerCert = certPath.length > 1 ? certPath[1] : certPath[0];
664
- const verifiedSCTs = await this.verifySCT(signingCert, issuerCert, this.root.ctlogs);
665
- if (verifiedSCTs.length < this.options.ctlogThreshold) {
666
- throw new Error(
667
- `Not enough valid SCTs: found ${verifiedSCTs.length}, required ${this.options.ctlogThreshold}`
668
- );
669
- }
670
-
671
- // # 4 Rekor inclusion promise
672
- if (
673
- !(await this.verifyInclusionPromise(signingCert, bundle, this.root.rekor))
674
- ) {
675
- throw new Error("Inclusion promise validation failed.");
676
- }
677
-
678
- // # 5 Rekor inclusion proof (Merkle tree verification)
679
- await this.verifyInclusionProof(bundle);
680
-
681
- // # 5.1 Rekor body verification - verify ALL entries (matches sigstore-js verifier.ts:147)
682
- for (const entry of bundle.verificationMaterial.tlogEntries) {
683
- await verifyTLogBody(entry, bundle);
684
- }
685
-
686
- // # 6 TSA Timestamp Verification
687
- // Verify all timestamps and enforce threshold
688
- // Reference: https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/verifier.ts#L101-L106
689
- const verifiedTimestamps = await verifyBundleTimestamp(
690
- bundle.verificationMaterial.timestampVerificationData,
691
- signature,
692
- this.rawRoot?.timestampAuthorities || []
693
- );
694
-
695
- if (verifiedTimestamps.length < this.options.tsaThreshold) {
696
- throw new Error(
697
- `Not enough verified TSA timestamps: ${verifiedTimestamps.length} < ${this.options.tsaThreshold}`
698
- );
699
- }
700
-
701
- // If we have verified timestamps, check certificate validity at each timestamp time
702
- for (const verifiedTimestamp of verifiedTimestamps) {
703
- if (!signingCert.validForDate(verifiedTimestamp)) {
704
- throw new Error(
705
- "Certificate was not valid at the time of timestamping"
706
- );
707
- }
708
- }
709
-
710
- // # 7 Revocation *skipping* not really a thing (unsurprisingly)
711
-
712
- // # 8 verify the signed data
713
- if (bundle.dsseEnvelope) {
714
- // For DSSE bundles, verify the signature over the PAE
715
- const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
716
- const payload = JSON.parse(Uint8ArrayToString(payloadBytes));
717
-
718
- // Verify the artifact digest matches a subject in the in-toto statement
719
- if (!payload.subject || payload.subject.length === 0) {
720
- throw new Error("DSSE payload has no subject");
721
- }
722
-
723
- // Compute or extract the artifact digest
724
- let artifactDigest: string;
725
- if (isDigestOnly) {
726
- // data is already the digest bytes
727
- artifactDigest = Uint8ArrayToHex(data);
728
- } else {
729
- // data is the file content, compute the digest
730
- artifactDigest = Uint8ArrayToHex(
731
- new Uint8Array(await crypto.subtle.digest(HashAlgorithms.SHA256, data as BufferSource))
732
- );
733
- }
734
-
735
- // Find matching subject by scanning all subjects (not just [0])
736
- let matchedSubject = null;
737
- for (const subject of payload.subject) {
738
- const subjectDigest = subject.digest?.["sha256"];
739
- if (subjectDigest && artifactDigest === subjectDigest.toLowerCase()) {
740
- matchedSubject = subject;
741
- break;
742
- }
743
- }
744
-
745
- if (!matchedSubject) {
746
- throw new Error(
747
- `Artifact digest ${artifactDigest} does not match any subject in DSSE payload`
748
- );
749
- }
750
-
751
- // Create PAE (Pre-Authentication Encoding) for signature verification
752
- const pae = preAuthEncoding(bundle.dsseEnvelope.payloadType, payloadBytes);
753
-
754
- const publicKey = await signingCert.publicKeyObj;
755
- const verified = await verifySignature(publicKey, pae, signature);
756
- if (!verified) {
757
- throw new Error("DSSE signature verification failed");
758
- }
759
- } else {
760
- const publicKey = await signingCert.publicKeyObj;
761
-
762
- if (isDigestOnly) {
763
- // For hashedrekord bundles, verify signature over the digest directly.
764
- // Uses the same elliptic.js workaround as sigstore-js conformance CLI.
765
- const verified = await verifySignatureOverDigest(publicKey, data, signature);
766
- if (!verified) {
767
- throw new Error("Error verifying signature over digest");
768
- }
769
- } else {
770
- // For regular bundles, verify the signature over the artifact data
771
- const verified = await verifySignature(publicKey, data, signature);
772
- if (!verified) {
773
- const keyAlg = publicKey.algorithm.name || 'unknown';
774
- throw new Error(`Error verifying artifact signature. Key algorithm: ${keyAlg}, Data length: ${data.length}, Signature length: ${signature.length}`);
775
- }
776
- }
777
- }
778
-
779
- return true;
780
- }
781
-
782
- public async verifyArtifact(
783
- identity: string,
784
- issuer: string,
785
- bundle: SigstoreBundle,
786
- data: Uint8Array,
787
- isDigestOnly: boolean = false,
788
- ): Promise<boolean> {
789
-
790
- const policy = new AllOf([
791
- new Identity({ identity }),
792
- new AnyOf([
793
- new OIDCIssuerV2(issuer),
794
- new OIDCIssuer(issuer),
795
- ]),
796
- ]);
797
-
798
- return this.verifyArtifactPolicy(
799
- policy,
800
- bundle,
801
- data,
802
- isDigestOnly,
803
- );
804
- }
805
-
806
-
807
-
808
- /**
809
- * Verify a DSSE bundle using a verification policy.
810
- * This matches sigstore-python's verify_dsse API.
811
- *
812
- * Reference: https://github.com/sigstore/sigstore-python/blob/main/sigstore/verify/verifier.py#L388
813
- *
814
- * Unlike verify_artifact which verifies an artifact against a bundle,
815
- * this method verifies the DSSE envelope itself and returns the payload.
816
- * The caller is responsible for checking that the payload matches their
817
- * expected artifact (e.g., by checking subjects in an in-toto statement).
818
- *
819
- * @param bundle - The Sigstore bundle containing the DSSE envelope
820
- * @param policy - A verification policy to apply to the signing certificate
821
- * @returns The payload type and payload bytes from the verified envelope
822
- */
823
- public async verifyDsse(
824
- bundle: SigstoreBundle,
825
- policy: VerificationPolicy,
826
- ): Promise<{ payloadType: string; payload: Uint8Array }> {
827
- if (!this.root) {
828
- throw new Error("Sigstore root is undefined");
829
- }
830
-
831
- if (!bundle.dsseEnvelope) {
832
- throw new Error("Bundle does not contain a DSSE envelope");
833
- }
834
-
835
- // Extract certificate from bundle
836
- const cert = bundle.verificationMaterial.certificate ||
837
- bundle.verificationMaterial.x509CertificateChain?.certificates[0];
838
-
839
- if (!cert) {
840
- throw new Error("No certificate found in bundle");
841
- }
842
-
843
- const signingCert = X509Certificate.parse(base64ToUint8Array(cert.rawBytes));
844
-
845
- // (1) Verify certificate chain to trusted CA
846
- const certPath = await this.verifyCertificateChain(
847
- signingCert.notBefore,
848
- signingCert,
849
- this.root.certificateAuthorities
850
- );
851
-
852
- // (2) Verify SCTs
853
- const issuerCert = certPath.length > 1 ? certPath[1] : certPath[0];
854
- const verifiedSCTs = await this.verifySCT(signingCert, issuerCert, this.root.ctlogs);
855
- if (verifiedSCTs.length < this.options.ctlogThreshold) {
856
- throw new Error(
857
- `Not enough valid SCTs: found ${verifiedSCTs.length}, required ${this.options.ctlogThreshold}`
858
- );
859
- }
860
-
861
- // (3) Verify the signing certificate against the policy
862
- policy.verify(signingCert);
863
-
864
- // (4) Verify Rekor inclusion promise
865
- if (!(await this.verifyInclusionPromise(signingCert, bundle, this.root.rekor))) {
866
- throw new Error("Inclusion promise validation failed");
867
- }
868
-
869
- // (5) Verify Rekor inclusion proof (Merkle tree verification)
870
- await this.verifyInclusionProof(bundle);
871
-
872
- // Validate envelope has exactly one signature (matches sigstore-python dsse._verify)
873
- if (!bundle.dsseEnvelope.signatures || bundle.dsseEnvelope.signatures.length !== 1) {
874
- throw new Error(
875
- `DSSE envelope must have exactly 1 signature, got ${bundle.dsseEnvelope.signatures?.length ?? 0}`
876
- );
877
- }
878
-
879
- // (6) TSA Timestamp verification
880
- const signature = base64ToUint8Array(bundle.dsseEnvelope.signatures[0].sig);
881
- const verifiedTimestamps = await verifyBundleTimestamp(
882
- bundle.verificationMaterial.timestampVerificationData,
883
- signature,
884
- this.rawRoot?.timestampAuthorities || []
885
- );
886
-
887
- if (verifiedTimestamps.length < this.options.tsaThreshold) {
888
- throw new Error(
889
- `Not enough verified TSA timestamps: ${verifiedTimestamps.length} < ${this.options.tsaThreshold}`
890
- );
891
- }
892
-
893
- for (const verifiedTimestamp of verifiedTimestamps) {
894
- if (!signingCert.validForDate(verifiedTimestamp)) {
895
- throw new Error("Certificate was not valid at the time of timestamping");
896
- }
897
- }
898
-
899
- // Every tlog entry must be anchored in time by at least one observer
900
- // timestamp, matching the observer-timestamp enforcement in the upstream
901
- // sigstore-js and sigstore-go clients. A v1 entry carries integratedTime
902
- // (the signing certificate must be valid at that instant); a v2 entry omits
903
- // it and must instead carry a signed RFC3161 timestamp. Without either, the
904
- // certificate's validity window is never anchored — a crafted bundle that
905
- // strips integratedTime + the inclusion promise and supplies an empty
906
- // timestampVerificationData ({}) would otherwise pass. This mirrors the
907
- // guard in verify(), which callers verifying via verifyDsse() would miss.
908
- for (const entry of bundle.verificationMaterial.tlogEntries) {
909
- if (entry.integratedTime) {
910
- const integratedDate = new Date(Number(entry.integratedTime) * 1000);
911
- if (!signingCert.validForDate(integratedDate)) {
912
- throw new Error(
913
- "Artifact signing was logged outside of the certificate validity.",
914
- );
915
- }
916
- } else {
917
- assertRekorV2Timestamp(
918
- bundle.verificationMaterial.timestampVerificationData,
919
- );
920
- }
921
- }
922
-
923
- // (7) Verify the DSSE envelope signature
924
- const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
925
- const pae = preAuthEncoding(bundle.dsseEnvelope.payloadType, payloadBytes);
926
-
927
- const publicKey = await signingCert.publicKeyObj;
928
- const verified = await verifySignature(publicKey, pae, signature);
929
- if (!verified) {
930
- throw new Error("DSSE signature verification failed");
931
- }
932
-
933
- // (8) Verify the consistency of the log entry's body against the bundle materials
934
- // The entry MUST be of type "dsse" for DSSE verification
935
- for (const entry of bundle.verificationMaterial.tlogEntries) {
936
- if (entry.kindVersion.kind !== "dsse") {
937
- throw new Error(`Expected entry type dsse, got ${entry.kindVersion.kind}`);
938
- }
939
- await verifyTLogBody(entry, bundle);
940
- }
941
-
942
- // Return the verified payload
943
- return {
944
- payloadType: bundle.dsseEnvelope.payloadType,
945
- payload: payloadBytes,
946
- };
947
- }
948
- }