@freedomofpress/sigstore-browser 0.1.2 → 0.1.4
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/interfaces.d.ts +1 -0
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js +18 -0
- package/dist/interfaces.js.map +1 -1
- package/dist/oid.d.ts +2 -0
- package/dist/oid.d.ts.map +1 -1
- package/dist/oid.js +5 -1
- package/dist/oid.js.map +1 -1
- package/dist/sigstore.d.ts.map +1 -1
- package/dist/sigstore.js +96 -55
- package/dist/sigstore.js.map +1 -1
- package/dist/timestamp/tsa.d.ts +6 -4
- package/dist/timestamp/tsa.d.ts.map +1 -1
- package/dist/timestamp/tsa.js +27 -25
- package/dist/timestamp/tsa.js.map +1 -1
- package/dist/tlog/dsse.js +11 -11
- package/dist/tlog/dsse.js.map +1 -1
- package/dist/tlog/intoto.d.ts.map +1 -1
- package/dist/tlog/intoto.js +7 -4
- package/dist/tlog/intoto.js.map +1 -1
- package/dist/x509/cert.d.ts +11 -3
- package/dist/x509/cert.d.ts.map +1 -1
- package/dist/x509/cert.js +58 -26
- package/dist/x509/cert.js.map +1 -1
- package/dist/x509/sct.d.ts.map +1 -1
- package/dist/x509/sct.js +4 -0
- package/dist/x509/sct.js.map +1 -1
- package/package.json +1 -1
- package/src/index.browser.test.ts +37 -0
- package/src/interfaces.ts +20 -0
- package/src/oid.ts +7 -1
- package/src/sigstore.ts +109 -60
- package/src/timestamp/tsa.ts +37 -30
- package/src/tlog/dsse.ts +11 -11
- package/src/tlog/intoto.ts +8 -4
- package/src/x509/cert.ts +68 -24
- package/src/x509/sct.ts +4 -0
package/src/sigstore.ts
CHANGED
|
@@ -36,6 +36,37 @@ import { verifyTLogBody } from "./tlog/body.js";
|
|
|
36
36
|
import { verifyBundleTimestamp } from "./timestamp/tsa.js";
|
|
37
37
|
import { TrustedRootProvider } from "./trust/tuf.js";
|
|
38
38
|
|
|
39
|
+
const MEDIA_TYPE_BASE = "application/vnd.dev.sigstore.bundle";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract bundle version from mediaType string
|
|
43
|
+
* Reference: https://github.com/sigstore/sigstore-go/blob/main/pkg/bundle/bundle.go#L159-L177
|
|
44
|
+
*/
|
|
45
|
+
function getBundleVersion(mediaType: string): string {
|
|
46
|
+
switch (mediaType) {
|
|
47
|
+
case `${MEDIA_TYPE_BASE}+json;version=0.1`:
|
|
48
|
+
return "0.1";
|
|
49
|
+
case `${MEDIA_TYPE_BASE}+json;version=0.2`:
|
|
50
|
+
return "0.2";
|
|
51
|
+
case `${MEDIA_TYPE_BASE}+json;version=0.3`:
|
|
52
|
+
return "0.3";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// New format: "application/vnd.dev.sigstore.bundle.v0.3+json"
|
|
56
|
+
if (mediaType.startsWith(`${MEDIA_TYPE_BASE}.v`) && mediaType.endsWith("+json")) {
|
|
57
|
+
const version = mediaType
|
|
58
|
+
.replace(`${MEDIA_TYPE_BASE}.v`, "")
|
|
59
|
+
.replace("+json", "");
|
|
60
|
+
// Basic semver validation (major.minor or major.minor.patch)
|
|
61
|
+
if (/^\d+\.\d+(\.\d+)?$/.test(version)) {
|
|
62
|
+
return version;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Default to 0.1 for unknown formats
|
|
67
|
+
return "0.1";
|
|
68
|
+
}
|
|
69
|
+
|
|
39
70
|
export interface SigstoreVerifierOptions {
|
|
40
71
|
tlogThreshold?: number;
|
|
41
72
|
ctlogThreshold?: number;
|
|
@@ -244,8 +275,7 @@ export class SigstoreVerifier {
|
|
|
244
275
|
|
|
245
276
|
// Adapted from https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/key/sct.ts
|
|
246
277
|
// Key differences:
|
|
247
|
-
// - Adds duplicate SCT detection
|
|
248
|
-
// - Adds SCT timestamp validity checking
|
|
278
|
+
// - Adds duplicate SCT detection (not in reference)
|
|
249
279
|
// - Inline CT log filtering by logID and validity period (reference uses filterTLogAuthorities)
|
|
250
280
|
// - Returns array of verified SCT logIDs for threshold checking (matches reference behavior)
|
|
251
281
|
async verifySCT(
|
|
@@ -312,42 +342,39 @@ export class SigstoreVerifier {
|
|
|
312
342
|
preCert.appendUint24(tbs.length);
|
|
313
343
|
preCert.appendView(tbs);
|
|
314
344
|
|
|
315
|
-
//
|
|
345
|
+
// Calculate and return the verification results for each SCT
|
|
346
|
+
// Unlike sigstore-go which counts verified SCTs and checks threshold at the end,
|
|
347
|
+
// sigstore-js throws immediately if any SCT fails verification
|
|
316
348
|
const verifiedSCTs: Uint8Array[] = [];
|
|
317
349
|
|
|
318
350
|
for (const sct of extSCT.signedCertificateTimestamps) {
|
|
319
|
-
// SCT should be within certificate validity period
|
|
320
|
-
if (sct.datetime < cert.notBefore || sct.datetime > cert.notAfter) {
|
|
321
|
-
continue; // Skip invalid SCT timestamp, don't fail yet
|
|
322
|
-
}
|
|
323
|
-
|
|
324
351
|
// Find the CT log that matches this SCT's log ID and is valid for the SCT datetime
|
|
325
352
|
const validCTLogs = ctlogs.filter((log) => {
|
|
326
353
|
// Check if log IDs match
|
|
327
|
-
if (log.logID
|
|
328
|
-
for (let i = 0; i < log.logID.length; i++) {
|
|
329
|
-
if (log.logID[i] !== sct.logID[i]) return false;
|
|
330
|
-
}
|
|
354
|
+
if (!uint8ArrayEqual(log.logID, sct.logID)) return false;
|
|
331
355
|
// Check that the SCT datetime is within the log's validity period
|
|
332
356
|
return log.validFor.start <= sct.datetime && sct.datetime <= log.validFor.end;
|
|
333
357
|
});
|
|
334
358
|
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
359
|
+
// See if the SCT is valid for any of the CT logs
|
|
360
|
+
const verified = await (async () => {
|
|
361
|
+
for (const log of validCTLogs) {
|
|
362
|
+
try {
|
|
363
|
+
if (await sct.verify(preCert.buffer, log.publicKey)) {
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
// Continue trying other logs
|
|
342
368
|
}
|
|
343
|
-
} catch (e) {
|
|
344
|
-
// Continue trying other logs
|
|
345
369
|
}
|
|
346
|
-
|
|
370
|
+
return false;
|
|
371
|
+
})();
|
|
347
372
|
|
|
348
|
-
if (verified) {
|
|
349
|
-
|
|
373
|
+
if (!verified) {
|
|
374
|
+
throw new Error("SCT verification failed");
|
|
350
375
|
}
|
|
376
|
+
|
|
377
|
+
verifiedSCTs.push(sct.logID);
|
|
351
378
|
}
|
|
352
379
|
|
|
353
380
|
return verifiedSCTs;
|
|
@@ -360,9 +387,10 @@ export class SigstoreVerifier {
|
|
|
360
387
|
): Promise<boolean> {
|
|
361
388
|
const entries = bundle.verificationMaterial.tlogEntries;
|
|
362
389
|
|
|
363
|
-
|
|
390
|
+
// Check that bundle has enough tlog entries to meet threshold
|
|
391
|
+
if (entries.length < this.options.tlogThreshold) {
|
|
364
392
|
throw new Error(
|
|
365
|
-
|
|
393
|
+
`Not enough tlog entries: ${entries.length} < ${this.options.tlogThreshold}`,
|
|
366
394
|
);
|
|
367
395
|
}
|
|
368
396
|
|
|
@@ -390,9 +418,8 @@ export class SigstoreVerifier {
|
|
|
390
418
|
const entry = entries[0];
|
|
391
419
|
|
|
392
420
|
// Extract bundle version from mediaType
|
|
393
|
-
//
|
|
394
|
-
const
|
|
395
|
-
const bundleVersion = versionMatch ? versionMatch[1] : "0.1";
|
|
421
|
+
// Reference: https://github.com/sigstore/sigstore-go/blob/main/pkg/bundle/bundle.go#L159-L177
|
|
422
|
+
const bundleVersion = getBundleVersion(bundle.mediaType);
|
|
396
423
|
const isV02OrLater = parseFloat(bundleVersion) >= 0.2;
|
|
397
424
|
|
|
398
425
|
// Bundle v0.2+ requires an inclusion proof
|
|
@@ -512,14 +539,31 @@ export class SigstoreVerifier {
|
|
|
512
539
|
}
|
|
513
540
|
}
|
|
514
541
|
} else if (bodyJson.kind === "dsse") {
|
|
515
|
-
// DSSE
|
|
516
|
-
//
|
|
517
|
-
|
|
542
|
+
// DSSE v0.0.1: certificate is in spec.signatures[0].verifier (base64-encoded PEM)
|
|
543
|
+
// https://github.com/sigstore/sigstore-go/blob/main/pkg/tlog/entry.go#L356-L357
|
|
544
|
+
const verifierContent = bodyJson.spec.signatures?.[0]?.verifier;
|
|
545
|
+
if (verifierContent) {
|
|
546
|
+
const pemString = Uint8ArrayToString(base64ToUint8Array(verifierContent));
|
|
547
|
+
const loggedCert = X509Certificate.parse(pemString);
|
|
548
|
+
if (!cert.equals(loggedCert)) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
"Certificate in DSSE tlog entry does not match the signing certificate.",
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
518
554
|
} else if (bodyJson.kind === "intoto") {
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
|
|
555
|
+
// intoto v0.0.2: certificate is in spec.content.envelope.signatures[0].publicKey (base64-encoded PEM)
|
|
556
|
+
// https://github.com/sigstore/sigstore-go/blob/main/pkg/tlog/entry.go#L360-L361
|
|
557
|
+
const publicKeyContent = bodyJson.spec.content?.envelope?.signatures?.[0]?.publicKey;
|
|
558
|
+
if (publicKeyContent) {
|
|
559
|
+
const pemString = Uint8ArrayToString(base64ToUint8Array(publicKeyContent));
|
|
560
|
+
const loggedCert = X509Certificate.parse(pemString);
|
|
561
|
+
if (!cert.equals(loggedCert)) {
|
|
562
|
+
throw new Error(
|
|
563
|
+
"Certificate in intoto tlog entry does not match the signing certificate.",
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
523
567
|
} else {
|
|
524
568
|
// Unknown entry type - this should not happen with standard Sigstore bundles
|
|
525
569
|
throw new Error(`Unsupported tlog entry kind: ${bodyJson.kind}`);
|
|
@@ -537,15 +581,17 @@ export class SigstoreVerifier {
|
|
|
537
581
|
throw new Error("No transparency log entries found in bundle");
|
|
538
582
|
}
|
|
539
583
|
|
|
540
|
-
|
|
584
|
+
// Verify inclusion proof for ALL entries, not just the first one
|
|
585
|
+
// Reference: https://github.com/sigstore/sigstore-go/blob/main/pkg/verify/tlog.go#L74-L127
|
|
586
|
+
for (const entry of bundle.verificationMaterial.tlogEntries) {
|
|
587
|
+
// Only verify if there's an inclusion proof (v0.3/rekor2 bundles)
|
|
588
|
+
// v0.1 bundles use inclusion promises instead, verified in verifyInclusionPromise
|
|
589
|
+
if (entry.inclusionProof) {
|
|
590
|
+
await verifyMerkleInclusion(entry);
|
|
541
591
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
await verifyMerkleInclusion(entry);
|
|
546
|
-
|
|
547
|
-
if (entry.inclusionProof.checkpoint) {
|
|
548
|
-
await verifyCheckpoint(entry, this.rawRoot.tlogs);
|
|
592
|
+
if (entry.inclusionProof.checkpoint) {
|
|
593
|
+
await verifyCheckpoint(entry, this.rawRoot.tlogs);
|
|
594
|
+
}
|
|
549
595
|
}
|
|
550
596
|
}
|
|
551
597
|
}
|
|
@@ -629,26 +675,29 @@ export class SigstoreVerifier {
|
|
|
629
675
|
// # 5 Rekor inclusion proof (Merkle tree verification)
|
|
630
676
|
await this.verifyInclusionProof(bundle);
|
|
631
677
|
|
|
632
|
-
// # 5.1 Rekor body verification
|
|
633
|
-
|
|
634
|
-
await verifyTLogBody(
|
|
635
|
-
bundle.verificationMaterial.tlogEntries[0],
|
|
636
|
-
bundle
|
|
637
|
-
);
|
|
678
|
+
// # 5.1 Rekor body verification - verify ALL entries (matches sigstore-js verifier.ts:147)
|
|
679
|
+
for (const entry of bundle.verificationMaterial.tlogEntries) {
|
|
680
|
+
await verifyTLogBody(entry, bundle);
|
|
638
681
|
}
|
|
639
682
|
|
|
640
|
-
// # 6 TSA Timestamp Verification
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
683
|
+
// # 6 TSA Timestamp Verification
|
|
684
|
+
// Verify all timestamps and enforce threshold
|
|
685
|
+
// Reference: https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/verifier.ts#L101-L106
|
|
686
|
+
const verifiedTimestamps = await verifyBundleTimestamp(
|
|
687
|
+
bundle.verificationMaterial.timestampVerificationData,
|
|
688
|
+
signature,
|
|
689
|
+
this.rawRoot?.timestampAuthorities || []
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
if (verifiedTimestamps.length < this.options.tsaThreshold) {
|
|
693
|
+
throw new Error(
|
|
694
|
+
`Not enough verified TSA timestamps: ${verifiedTimestamps.length} < ${this.options.tsaThreshold}`
|
|
648
695
|
);
|
|
696
|
+
}
|
|
649
697
|
|
|
650
|
-
|
|
651
|
-
|
|
698
|
+
// If we have verified timestamps, check certificate validity at each timestamp time
|
|
699
|
+
for (const verifiedTimestamp of verifiedTimestamps) {
|
|
700
|
+
if (!signingCert.validForDate(verifiedTimestamp)) {
|
|
652
701
|
throw new Error(
|
|
653
702
|
"Certificate was not valid at the time of timestamping"
|
|
654
703
|
);
|
package/src/timestamp/tsa.ts
CHANGED
|
@@ -152,53 +152,60 @@ async function verifyTimestampForCA(
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
|
-
*
|
|
155
|
+
* Verifies all timestamps in a bundle's timestamp verification data
|
|
156
156
|
*
|
|
157
|
-
*
|
|
157
|
+
* Matches sigstore-js behavior: all timestamps must successfully verify.
|
|
158
|
+
* Reference: https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/verifier.ts#L64-L108
|
|
158
159
|
*
|
|
159
160
|
* @param timestampData - The timestamp verification data from the bundle
|
|
160
161
|
* @param signature - The signature being verified
|
|
161
162
|
* @param timestampAuthorities - List of trusted timestamp authorities
|
|
162
|
-
* @returns
|
|
163
|
+
* @returns Array of verified signing times (empty if no timestamps present)
|
|
164
|
+
* @throws Error if any timestamp fails verification or duplicates are found
|
|
163
165
|
*/
|
|
164
166
|
export async function verifyBundleTimestamp(
|
|
165
167
|
timestampData: any,
|
|
166
168
|
signature: Uint8Array,
|
|
167
169
|
timestampAuthorities: RawTimestampAuthority[]
|
|
168
|
-
): Promise<Date
|
|
170
|
+
): Promise<Date[]> {
|
|
169
171
|
if (!timestampData?.rfc3161Timestamps?.length) {
|
|
170
|
-
return
|
|
172
|
+
return [];
|
|
171
173
|
}
|
|
172
174
|
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
+
// Verify ALL timestamps - each must succeed (matches sigstore-js behavior)
|
|
176
|
+
// Collect verification results for duplicate checking after verification
|
|
177
|
+
// Reference: https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/verifier.ts#L86-L92
|
|
178
|
+
const verifiedResults: Array<{ signingTime: Date; signerSerialNumber: string }> = [];
|
|
179
|
+
|
|
175
180
|
for (const tsData of timestampData.rfc3161Timestamps) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
// Decode the base64-encoded timestamp
|
|
182
|
+
const timestampBytes = base64ToUint8Array(tsData.signedTimestamp);
|
|
183
|
+
const timestamp = RFC3161Timestamp.parse(timestampBytes);
|
|
184
|
+
|
|
185
|
+
const signingTime = await verifyRFC3161Timestamp(
|
|
186
|
+
timestamp,
|
|
187
|
+
signature,
|
|
188
|
+
timestampAuthorities
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
verifiedResults.push({
|
|
192
|
+
signingTime,
|
|
193
|
+
signerSerialNumber: Array.from(timestamp.signerSerialNumber).join(','),
|
|
194
|
+
});
|
|
180
195
|
}
|
|
181
196
|
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
for (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
signature,
|
|
193
|
-
timestampAuthorities
|
|
194
|
-
);
|
|
195
|
-
return signingTime;
|
|
196
|
-
} catch (e) {
|
|
197
|
-
// Continue to next timestamp if this one fails
|
|
198
|
-
errors.push(e instanceof Error ? e.message : String(e));
|
|
199
|
-
continue;
|
|
197
|
+
// Check for duplicate timestamps using deep equality on parsed values
|
|
198
|
+
// sigstore-js uses isDeepStrictEqual on {type, logID, timestamp} objects
|
|
199
|
+
for (let i = 0; i < verifiedResults.length; i++) {
|
|
200
|
+
for (let j = i + 1; j < verifiedResults.length; j++) {
|
|
201
|
+
if (
|
|
202
|
+
verifiedResults[i].signingTime.getTime() === verifiedResults[j].signingTime.getTime() &&
|
|
203
|
+
verifiedResults[i].signerSerialNumber === verifiedResults[j].signerSerialNumber
|
|
204
|
+
) {
|
|
205
|
+
throw new Error("Duplicate TSA timestamp detected");
|
|
206
|
+
}
|
|
200
207
|
}
|
|
201
208
|
}
|
|
202
209
|
|
|
203
|
-
|
|
210
|
+
return verifiedResults.map(r => r.signingTime);
|
|
204
211
|
}
|
package/src/tlog/dsse.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { base64ToUint8Array, hexToUint8Array, uint8ArrayEqual } from "@freedomofpress/crypto-browser";
|
|
15
|
-
import {
|
|
15
|
+
import { getHashAlgorithm } from "../interfaces.js";
|
|
16
16
|
import type { SigstoreBundle } from "../bundle.js";
|
|
17
17
|
import type { RekorEntry } from "./body.js";
|
|
18
18
|
|
|
@@ -81,16 +81,16 @@ async function verifyDSSE001Body(
|
|
|
81
81
|
throw new Error("DSSE signature mismatch between TLog entry and bundle");
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
throw new Error("DSSE entry missing payloadHash");
|
|
84
|
+
if (!entry.spec.payloadHash?.value || !entry.spec.payloadHash?.algorithm) {
|
|
85
|
+
throw new Error("DSSE entry missing payloadHash or algorithm");
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
const
|
|
88
|
+
const hashAlg = getHashAlgorithm(entry.spec.payloadHash.algorithm);
|
|
89
|
+
const tlogHashBytes = hexToUint8Array(entry.spec.payloadHash.value);
|
|
90
90
|
|
|
91
91
|
const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
|
|
92
92
|
const bundleHashBytes = new Uint8Array(
|
|
93
|
-
await crypto.subtle.digest(
|
|
93
|
+
await crypto.subtle.digest(hashAlg, payloadBytes as Uint8Array<ArrayBuffer>)
|
|
94
94
|
);
|
|
95
95
|
|
|
96
96
|
if (!uint8ArrayEqual(tlogHashBytes, bundleHashBytes)) {
|
|
@@ -129,16 +129,16 @@ async function verifyDSSE002Body(
|
|
|
129
129
|
throw new Error("DSSE signature mismatch between TLog entry and bundle (v0.0.2)");
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
throw new Error("DSSE v0.0.2 entry missing payloadHash");
|
|
132
|
+
if (!spec.payloadHash?.digest || !spec.payloadHash?.algorithm) {
|
|
133
|
+
throw new Error("DSSE v0.0.2 entry missing payloadHash or algorithm");
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
const
|
|
136
|
+
const hashAlg = getHashAlgorithm(spec.payloadHash.algorithm);
|
|
137
|
+
const tlogHashBytes = base64ToUint8Array(spec.payloadHash.digest);
|
|
138
138
|
|
|
139
139
|
const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
|
|
140
140
|
const bundleHashBytes = new Uint8Array(
|
|
141
|
-
await crypto.subtle.digest(
|
|
141
|
+
await crypto.subtle.digest(hashAlg, payloadBytes as Uint8Array<ArrayBuffer>)
|
|
142
142
|
);
|
|
143
143
|
|
|
144
144
|
if (!uint8ArrayEqual(tlogHashBytes, bundleHashBytes)) {
|
package/src/tlog/intoto.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { base64Decode, base64ToUint8Array, hexToUint8Array, uint8ArrayEqual } from "@freedomofpress/crypto-browser";
|
|
14
|
-
import {
|
|
14
|
+
import { getHashAlgorithm } from "../interfaces.js";
|
|
15
15
|
import type { SigstoreBundle } from "../bundle.js";
|
|
16
16
|
import type { RekorEntry } from "./body.js";
|
|
17
17
|
|
|
@@ -87,12 +87,16 @@ export async function verifyIntotoBody(
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
if (intotoEntry.spec.content.payloadHash) {
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
if (!intotoEntry.spec.content.payloadHash.algorithm) {
|
|
91
|
+
throw new Error("Intoto entry missing payloadHash algorithm");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hashAlg = getHashAlgorithm(intotoEntry.spec.content.payloadHash.algorithm);
|
|
95
|
+
const tlogHashBytes = hexToUint8Array(intotoEntry.spec.content.payloadHash.value);
|
|
92
96
|
|
|
93
97
|
const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
|
|
94
98
|
const bundleHashBytes = new Uint8Array(
|
|
95
|
-
await crypto.subtle.digest(
|
|
99
|
+
await crypto.subtle.digest(hashAlg, payloadBytes as Uint8Array<ArrayBuffer>)
|
|
96
100
|
);
|
|
97
101
|
|
|
98
102
|
if (!uint8ArrayEqual(tlogHashBytes, bundleHashBytes)) {
|
package/src/x509/cert.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
verifySignature,
|
|
23
23
|
} from "@freedomofpress/crypto-browser";
|
|
24
24
|
import { KeyTypes } from "../interfaces.js";
|
|
25
|
-
import { ECDSA_CURVE_NAMES, ECDSA_SIGNATURE_ALGOS, RSA_SIGNATURE_ALGOS } from "../oid.js";
|
|
25
|
+
import { DEFAULT_HASH_ALGORITHM, ECDSA_CURVE_NAMES, ECDSA_SIGNATURE_ALGOS, OID_RSASSA_PSS, RSA_SIGNATURE_ALGOS, SHA2_HASH_ALGOS } from "../oid.js";
|
|
26
26
|
import {
|
|
27
27
|
X509AuthorityKeyIDExtension,
|
|
28
28
|
X509BasicConstraintsExtension,
|
|
@@ -96,27 +96,33 @@ export class X509Certificate {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
* Import the public key with a specific hash algorithm for RSA keys
|
|
100
|
-
* For ECDSA keys,
|
|
99
|
+
* Import the public key with a specific hash algorithm and signature scheme for RSA keys.
|
|
100
|
+
* For ECDSA keys, both parameters are ignored.
|
|
101
|
+
* @param hashAlg - Hash algorithm (e.g., "sha384") for RSA keys
|
|
102
|
+
* @param usePss - If true, import as RSA-PSS key; if false, import as PKCS#1 v1.5
|
|
101
103
|
*/
|
|
102
|
-
public async getPublicKeyObj(hashAlg?: string): Promise<CryptoKey> {
|
|
104
|
+
public async getPublicKeyObj(hashAlg?: string, usePss?: boolean): Promise<CryptoKey> {
|
|
103
105
|
const publicKey = this.subjectPublicKeyInfoObj.toDER();
|
|
104
106
|
const spki = ASN1Obj.parseBuffer(publicKey);
|
|
105
107
|
const algorithmOID = spki.subs[0].subs[0].toOID();
|
|
106
108
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
109
|
+
// RSA key (OID 1.2.840.113549.1.1.1) or RSA-PSS key (OID 1.2.840.113549.1.1.10)
|
|
110
|
+
const isRsaKey = algorithmOID === "1.2.840.113549.1.1.1";
|
|
111
|
+
const isRsaPssKey = algorithmOID === OID_RSASSA_PSS;
|
|
112
|
+
|
|
113
|
+
if (isRsaPssKey) {
|
|
114
|
+
// WebCrypto doesn't support importing keys with id-RSASSA-PSS OID, only rsaEncryption
|
|
115
|
+
throw new Error(
|
|
116
|
+
"RSA-PSS public keys (id-RSASSA-PSS OID) are not supported by WebCrypto. " +
|
|
117
|
+
"Only certificates with standard RSA keys (rsaEncryption OID) signed using RSA-PSS are supported."
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (isRsaKey) {
|
|
122
|
+
const hash = hashAlg || DEFAULT_HASH_ALGORITHM;
|
|
123
|
+
// crypto-browser extracts hash from scheme and uses PSS unless "PKCS1" is present
|
|
124
|
+
const scheme = usePss ? hash : `PKCS1_${hash}`;
|
|
125
|
+
return importKey(KeyTypes.RSA, scheme, Uint8ArrayToBase64(publicKey));
|
|
120
126
|
} else {
|
|
121
127
|
// ECDSA key - the curve OID is in the second element
|
|
122
128
|
const curveOID = spki.subs[0].subs[1]?.toOID();
|
|
@@ -134,7 +140,41 @@ export class X509Certificate {
|
|
|
134
140
|
|
|
135
141
|
get signatureAlgorithm(): string {
|
|
136
142
|
const oid: string = this.signatureAlgorithmObj.subs[0].toOID();
|
|
137
|
-
return ECDSA_SIGNATURE_ALGOS[oid];
|
|
143
|
+
return ECDSA_SIGNATURE_ALGOS[oid] || RSA_SIGNATURE_ALGOS[oid] || this.parseRsaPssHashAlgorithm();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get signatureAlgorithmOid(): string {
|
|
147
|
+
return this.signatureAlgorithmObj.subs[0].toOID();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parse hash algorithm from RSA-PSS signature algorithm parameters.
|
|
152
|
+
* RSA-PSS parameters are: SEQUENCE { hashAlgorithm, maskGenAlgorithm, saltLength, trailerField }
|
|
153
|
+
*/
|
|
154
|
+
private parseRsaPssHashAlgorithm(): string {
|
|
155
|
+
const sigAlgOid = this.signatureAlgorithmObj.subs[0].toOID();
|
|
156
|
+
if (sigAlgOid !== OID_RSASSA_PSS) {
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// RSA-PSS has parameters in the second element of the signature algorithm sequence
|
|
161
|
+
const params = this.signatureAlgorithmObj.subs[1];
|
|
162
|
+
if (!params || params.subs.length === 0) {
|
|
163
|
+
return DEFAULT_HASH_ALGORITHM;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// First element is hashAlgorithm [0] EXPLICIT
|
|
167
|
+
const hashAlgWrapper = params.subs[0];
|
|
168
|
+
if (hashAlgWrapper && hashAlgWrapper.subs.length > 0) {
|
|
169
|
+
// The wrapper contains a SEQUENCE with the hash algorithm OID
|
|
170
|
+
const hashAlgSeq = hashAlgWrapper.subs[0];
|
|
171
|
+
if (hashAlgSeq && hashAlgSeq.subs.length > 0) {
|
|
172
|
+
const hashOid = hashAlgSeq.subs[0].toOID();
|
|
173
|
+
return SHA2_HASH_ALGOS[hashOid] || DEFAULT_HASH_ALGORITHM;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return DEFAULT_HASH_ALGORITHM;
|
|
138
178
|
}
|
|
139
179
|
|
|
140
180
|
get signatureValue(): Uint8Array {
|
|
@@ -214,15 +254,19 @@ export class X509Certificate {
|
|
|
214
254
|
|
|
215
255
|
public async verify(issuerCertificate?: X509Certificate): Promise<boolean> {
|
|
216
256
|
// Extract the hash algorithm from this certificate's signature algorithm
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
257
|
+
const sigAlgOID = this.signatureAlgorithmOid;
|
|
258
|
+
const isRsaPss = sigAlgOID === OID_RSASSA_PSS;
|
|
259
|
+
|
|
260
|
+
// Get hash algorithm - for RSA-PSS, parse from parameters; otherwise lookup in tables
|
|
261
|
+
const hashAlg = isRsaPss
|
|
262
|
+
? this.parseRsaPssHashAlgorithm()
|
|
263
|
+
: (RSA_SIGNATURE_ALGOS[sigAlgOID] || ECDSA_SIGNATURE_ALGOS[sigAlgOID]);
|
|
220
264
|
|
|
221
265
|
// Use the issuer's public key if provided, otherwise use the subject's (for self-signed certs)
|
|
222
|
-
// Import with the correct hash algorithm for this certificate's signature
|
|
266
|
+
// Import with the correct hash algorithm and key type for this certificate's signature
|
|
223
267
|
const publicKeyObj = issuerCertificate
|
|
224
|
-
? await issuerCertificate.getPublicKeyObj(hashAlg)
|
|
225
|
-
: await this.getPublicKeyObj(hashAlg);
|
|
268
|
+
? await issuerCertificate.getPublicKeyObj(hashAlg, isRsaPss)
|
|
269
|
+
: await this.getPublicKeyObj(hashAlg, isRsaPss);
|
|
226
270
|
|
|
227
271
|
return await verifySignature(
|
|
228
272
|
publicKeyObj,
|
package/src/x509/sct.ts
CHANGED
|
@@ -112,7 +112,11 @@ export class SignedCertificateTimestamp {
|
|
|
112
112
|
const stream = new ByteStream(buf);
|
|
113
113
|
|
|
114
114
|
// Version - enum { v1(0), (255) }
|
|
115
|
+
// RFC 6962 specifies only v1(0) is valid (for now)
|
|
115
116
|
const version = stream.getUint8();
|
|
117
|
+
if (version !== 0) {
|
|
118
|
+
throw new Error(`Unsupported SCT version: ${version} (expected 0 for v1)`);
|
|
119
|
+
}
|
|
116
120
|
|
|
117
121
|
// Log ID - struct { opaque key_id[32]; }
|
|
118
122
|
const logID = stream.getBlock(32);
|