@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/timestamp/tsa.ts
CHANGED
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { base64ToUint8Array, uint8ArrayEqual } from "@freedomofpress/crypto-browser";
|
|
17
|
+
import { assertTimestampVerificationData } from "../bundle.js";
|
|
17
18
|
import { CertificateChainVerifier, X509Certificate } from "../x509/index.js";
|
|
18
|
-
import { RawTimestampAuthority } from "../interfaces.js";
|
|
19
|
+
import { parseValidityPeriod, RawTimestampAuthority } from "../interfaces.js";
|
|
19
20
|
import { RFC3161Timestamp } from "../rfc3161/index.js";
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -32,6 +33,20 @@ export async function verifyRFC3161Timestamp(
|
|
|
32
33
|
data: Uint8Array,
|
|
33
34
|
timestampAuthorities: RawTimestampAuthority[]
|
|
34
35
|
): Promise<Date> {
|
|
36
|
+
return (await verifyTimestamp(timestamp, data, timestampAuthorities)).signingTime;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface VerifiedTimestamp {
|
|
40
|
+
signingTime: Date;
|
|
41
|
+
// Empty means unknown, which cannot add independence to named operators.
|
|
42
|
+
operator: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function verifyTimestamp(
|
|
46
|
+
timestamp: RFC3161Timestamp,
|
|
47
|
+
data: Uint8Array,
|
|
48
|
+
timestampAuthorities: RawTimestampAuthority[]
|
|
49
|
+
): Promise<VerifiedTimestamp> {
|
|
35
50
|
const signingTime = timestamp.signingTime;
|
|
36
51
|
|
|
37
52
|
// Filter for CAs which were valid at the time of signing
|
|
@@ -51,18 +66,19 @@ export async function verifyRFC3161Timestamp(
|
|
|
51
66
|
validAuthorities.map(ca => verifyTimestampForCA(timestamp, data, ca))
|
|
52
67
|
);
|
|
53
68
|
|
|
54
|
-
const verified = verificationResults.
|
|
69
|
+
const verified = verificationResults.findIndex(
|
|
55
70
|
result => result.status === "fulfilled"
|
|
56
71
|
);
|
|
57
72
|
|
|
58
|
-
if (
|
|
73
|
+
if (verified < 0) {
|
|
59
74
|
const errors = verificationResults
|
|
60
75
|
.filter(r => r.status === "rejected")
|
|
61
76
|
.map(r => (r as PromiseRejectedResult).reason?.message || 'Unknown error');
|
|
62
77
|
throw new Error(`Timestamp could not be verified against any trusted authority. Errors: ${errors.join(', ')}`);
|
|
63
78
|
}
|
|
64
79
|
|
|
65
|
-
|
|
80
|
+
const ca = validAuthorities[verified];
|
|
81
|
+
return { signingTime, operator: ca.operator || "" };
|
|
66
82
|
}
|
|
67
83
|
|
|
68
84
|
/**
|
|
@@ -75,18 +91,8 @@ function filterCertAuthorities(
|
|
|
75
91
|
validAt: Date
|
|
76
92
|
): RawTimestampAuthority[] {
|
|
77
93
|
return authorities.filter(ca => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const end = ca.validFor.end ? new Date(ca.validFor.end) : null;
|
|
81
|
-
|
|
82
|
-
if (start && validAt < start) {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
if (end && validAt > end) {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return true;
|
|
94
|
+
const { start, end } = parseValidityPeriod(ca.validFor);
|
|
95
|
+
return start <= validAt && validAt <= end;
|
|
90
96
|
});
|
|
91
97
|
}
|
|
92
98
|
|
|
@@ -160,14 +166,15 @@ async function verifyTimestampForCA(
|
|
|
160
166
|
* @param timestampData - The timestamp verification data from the bundle
|
|
161
167
|
* @param signature - The signature being verified
|
|
162
168
|
* @param timestampAuthorities - List of trusted timestamp authorities
|
|
163
|
-
* @returns
|
|
169
|
+
* @returns The verified signing times and their operators (empty if no timestamps present)
|
|
164
170
|
* @throws Error if any timestamp fails verification or duplicates are found
|
|
165
171
|
*/
|
|
166
172
|
export async function verifyBundleTimestamp(
|
|
167
173
|
timestampData: any,
|
|
168
174
|
signature: Uint8Array,
|
|
169
175
|
timestampAuthorities: RawTimestampAuthority[]
|
|
170
|
-
): Promise<
|
|
176
|
+
): Promise<VerifiedTimestamp[]> {
|
|
177
|
+
assertTimestampVerificationData(timestampData);
|
|
171
178
|
if (!timestampData?.rfc3161Timestamps?.length) {
|
|
172
179
|
return [];
|
|
173
180
|
}
|
|
@@ -175,21 +182,15 @@ export async function verifyBundleTimestamp(
|
|
|
175
182
|
// Verify ALL timestamps - each must succeed (matches sigstore-js behavior)
|
|
176
183
|
// Collect verification results for duplicate checking after verification
|
|
177
184
|
// Reference: https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/verifier.ts#L86-L92
|
|
178
|
-
const verifiedResults: Array<
|
|
185
|
+
const verifiedResults: Array<VerifiedTimestamp & { signerSerialNumber: string }> = [];
|
|
179
186
|
|
|
180
187
|
for (const tsData of timestampData.rfc3161Timestamps) {
|
|
181
188
|
// Decode the base64-encoded timestamp
|
|
182
189
|
const timestampBytes = base64ToUint8Array(tsData.signedTimestamp);
|
|
183
190
|
const timestamp = RFC3161Timestamp.parse(timestampBytes);
|
|
184
191
|
|
|
185
|
-
const signingTime = await verifyRFC3161Timestamp(
|
|
186
|
-
timestamp,
|
|
187
|
-
signature,
|
|
188
|
-
timestampAuthorities
|
|
189
|
-
);
|
|
190
|
-
|
|
191
192
|
verifiedResults.push({
|
|
192
|
-
|
|
193
|
+
...(await verifyTimestamp(timestamp, signature, timestampAuthorities)),
|
|
193
194
|
signerSerialNumber: Array.from(timestamp.signerSerialNumber).join(','),
|
|
194
195
|
});
|
|
195
196
|
}
|
|
@@ -207,5 +208,5 @@ export async function verifyBundleTimestamp(
|
|
|
207
208
|
}
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
return verifiedResults
|
|
211
|
-
}
|
|
211
|
+
return verifiedResults;
|
|
212
|
+
}
|
package/src/tlog/body.ts
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* - Direct bundle comparison instead of SignatureContent abstraction
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { base64Decode } from "@freedomofpress/crypto-browser";
|
|
12
|
+
import { base64Decode, base64ToUint8Array, Uint8ArrayToString } from "@freedomofpress/crypto-browser";
|
|
13
13
|
import type { SigstoreBundle, TLogEntry } from "../bundle.js";
|
|
14
|
+
import { X509Certificate } from "../x509/cert.js";
|
|
14
15
|
import { verifyHashedRekordBody } from "./hashedrekord.js";
|
|
15
16
|
import { verifyDSSEBody } from "./dsse.js";
|
|
16
17
|
import { verifyIntotoBody } from "./intoto.js";
|
|
@@ -21,9 +22,11 @@ export interface RekorEntry {
|
|
|
21
22
|
spec: unknown;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
// Checks that the entry body matches the bundle content and was logged under the signing certificate.
|
|
24
26
|
export async function verifyTLogBody(
|
|
25
27
|
entry: TLogEntry,
|
|
26
|
-
bundle: SigstoreBundle
|
|
28
|
+
bundle: SigstoreBundle,
|
|
29
|
+
cert: X509Certificate,
|
|
27
30
|
): Promise<void> {
|
|
28
31
|
const rekorEntry = parseCanonicalBody(entry);
|
|
29
32
|
|
|
@@ -37,16 +40,32 @@ export async function verifyTLogBody(
|
|
|
37
40
|
|
|
38
41
|
switch (rekorEntry.kind) {
|
|
39
42
|
case "hashedrekord":
|
|
40
|
-
return verifyHashedRekordBody(rekorEntry, bundle);
|
|
43
|
+
return verifyHashedRekordBody(rekorEntry, bundle, cert);
|
|
41
44
|
case "dsse":
|
|
42
|
-
return verifyDSSEBody(rekorEntry, bundle);
|
|
45
|
+
return verifyDSSEBody(rekorEntry, bundle, cert);
|
|
43
46
|
case "intoto":
|
|
44
|
-
return verifyIntotoBody(rekorEntry, bundle);
|
|
47
|
+
return verifyIntotoBody(rekorEntry, bundle, cert);
|
|
45
48
|
default:
|
|
46
49
|
throw new Error(`Unsupported TLog entry kind: ${rekorEntry.kind}`);
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
// Compares the certificate recorded in the entry (base64 DER, or base64 PEM for Rekor v1) with the signing certificate.
|
|
54
|
+
export function assertLoggedCertificate(
|
|
55
|
+
cert: X509Certificate,
|
|
56
|
+
encoded: unknown,
|
|
57
|
+
pem: boolean,
|
|
58
|
+
): void {
|
|
59
|
+
if (typeof encoded !== "string" || encoded === "") {
|
|
60
|
+
throw new Error("TLog entry does not record the signing certificate");
|
|
61
|
+
}
|
|
62
|
+
const bytes = base64ToUint8Array(encoded);
|
|
63
|
+
const logged = X509Certificate.parse(pem ? Uint8ArrayToString(bytes) : bytes);
|
|
64
|
+
if (!cert.equals(logged)) {
|
|
65
|
+
throw new Error("Certificate in TLog entry does not match the signing certificate");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
50
69
|
function parseCanonicalBody(entry: TLogEntry): RekorEntry {
|
|
51
70
|
try {
|
|
52
71
|
const decodedBody = base64Decode(entry.canonicalizedBody);
|
package/src/tlog/checkpoint.ts
CHANGED
|
@@ -7,13 +7,6 @@ https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/timestamp/
|
|
|
7
7
|
Key differences:
|
|
8
8
|
- Uses Uint8Array instead of Buffer
|
|
9
9
|
- Uses Web Crypto API instead of Node.js crypto module
|
|
10
|
-
- Manual handling of ED25519 (raw) vs ECDSA (DER) signature formats
|
|
11
|
-
- Inline key import from raw bytes
|
|
12
|
-
|
|
13
|
-
Additional functionality not in reference:
|
|
14
|
-
- filterTLogsByDate() - inlined from sigstore-js/packages/verify/src/trust/filter.ts
|
|
15
|
-
- importTLogKey() - new browser-compatible key import with multiple format support
|
|
16
|
-
- Special handling for Rekor v2 bundles without integratedTime
|
|
17
10
|
|
|
18
11
|
Follows the signed note format specification:
|
|
19
12
|
https://github.com/transparency-dev/formats/blob/main/log/README.md
|
|
@@ -21,14 +14,12 @@ https://github.com/transparency-dev/formats/blob/main/log/README.md
|
|
|
21
14
|
|
|
22
15
|
import {
|
|
23
16
|
base64ToUint8Array,
|
|
24
|
-
importKey,
|
|
25
|
-
KeyTypes,
|
|
26
17
|
stringToUint8Array,
|
|
27
18
|
uint8ArrayEqual,
|
|
28
19
|
verifySignature,
|
|
29
20
|
} from "@freedomofpress/crypto-browser";
|
|
30
21
|
import type { TLogEntry } from "../bundle.js";
|
|
31
|
-
import type {
|
|
22
|
+
import type { RekorKeyInfo } from "../interfaces.js";
|
|
32
23
|
|
|
33
24
|
// Signed note format per https://github.com/transparency-dev/formats
|
|
34
25
|
// Body is separated from signatures by a blank line
|
|
@@ -126,129 +117,38 @@ export class LogCheckpoint {
|
|
|
126
117
|
}
|
|
127
118
|
}
|
|
128
119
|
|
|
129
|
-
// Verifies checkpoint
|
|
120
|
+
// Verifies that the checkpoint is signed by the entry's log and describes the same tree as the inclusion proof.
|
|
130
121
|
export async function verifyCheckpoint(
|
|
131
122
|
entry: TLogEntry,
|
|
132
|
-
|
|
123
|
+
log: RekorKeyInfo
|
|
133
124
|
): Promise<void> {
|
|
134
|
-
|
|
125
|
+
const proof = entry.inclusionProof;
|
|
126
|
+
if (!proof?.checkpoint?.envelope) {
|
|
135
127
|
throw new Error("Missing checkpoint in inclusion proof");
|
|
136
128
|
}
|
|
137
129
|
|
|
138
|
-
|
|
139
|
-
const entryLogId = base64ToUint8Array(entry.logId.keyId);
|
|
140
|
-
const matchingTLogs = tlogs.filter((tlog) => {
|
|
141
|
-
const tlogId = base64ToUint8Array(tlog.logId.keyId);
|
|
142
|
-
return uint8ArrayEqual(tlogId, entryLogId);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Then filter by date if integratedTime is available
|
|
146
|
-
// For Rekor v2 bundles, integratedTime may be null, so we skip date filtering
|
|
147
|
-
const validTLogs = entry.integratedTime
|
|
148
|
-
? filterTLogsByDate(matchingTLogs, new Date(Number(entry.integratedTime) * 1000))
|
|
149
|
-
: matchingTLogs;
|
|
150
|
-
|
|
151
|
-
const inclusionProof = entry.inclusionProof;
|
|
152
|
-
const signedNote = SignedNote.fromString(inclusionProof.checkpoint.envelope);
|
|
130
|
+
const signedNote = SignedNote.fromString(proof.checkpoint.envelope);
|
|
153
131
|
const checkpoint = LogCheckpoint.fromString(signedNote.note);
|
|
154
|
-
|
|
155
|
-
if (!(await verifySignedNote(signedNote, validTLogs))) {
|
|
156
|
-
throw new Error("Invalid checkpoint signature");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const rootHash = base64ToUint8Array(inclusionProof.rootHash);
|
|
160
|
-
if (!uint8ArrayEqual(checkpoint.logHash, rootHash)) {
|
|
161
|
-
throw new Error("Root hash mismatch between checkpoint and inclusion proof");
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Verifies checkpoint signatures using trusted TLog keys
|
|
166
|
-
async function verifySignedNote(
|
|
167
|
-
signedNote: SignedNote,
|
|
168
|
-
tlogs: RawLogs
|
|
169
|
-
): Promise<boolean> {
|
|
170
132
|
const data = stringToUint8Array(signedNote.note);
|
|
171
133
|
|
|
172
|
-
//
|
|
173
|
-
let
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
if (!tlog) {
|
|
183
|
-
// Skip unknown signatures (e.g., from witnesses we don't know about)
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const publicKey = await importTLogKey(tlog);
|
|
188
|
-
|
|
189
|
-
// Both ED25519 and ECDSA verification are now handled by verifySignature
|
|
190
|
-
// ED25519 signatures are raw (64 bytes), ECDSA signatures are DER-encoded
|
|
191
|
-
const verified = await verifySignature(
|
|
192
|
-
publicKey,
|
|
193
|
-
data,
|
|
194
|
-
signature.signature,
|
|
195
|
-
tlog.hashAlgorithm
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
if (verified) {
|
|
199
|
-
hasValidSignature = true;
|
|
134
|
+
// Signatures from other parties (e.g. witnesses) are ignored; the key hint is the first 4 bytes of the log ID.
|
|
135
|
+
let valid = false;
|
|
136
|
+
for (const sig of signedNote.signatures) {
|
|
137
|
+
if (
|
|
138
|
+
uint8ArrayEqual(sig.keyHint, log.logId.subarray(0, 4)) &&
|
|
139
|
+
(await verifySignature(log.publicKey, data, sig.signature, log.hashAlgorithm))
|
|
140
|
+
) {
|
|
141
|
+
valid = true;
|
|
200
142
|
}
|
|
201
143
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// Inlined from sigstore-js/packages/verify/src/trust/filter.ts
|
|
208
|
-
function filterTLogsByDate(tlogs: RawLogs, targetDate: Date): RawLogs {
|
|
209
|
-
return tlogs.filter((tlog) => {
|
|
210
|
-
const start = new Date(tlog.publicKey.validFor.start);
|
|
211
|
-
const end = tlog.publicKey.validFor.end
|
|
212
|
-
? new Date(tlog.publicKey.validFor.end)
|
|
213
|
-
: null;
|
|
214
|
-
|
|
215
|
-
return targetDate >= start && (!end || targetDate <= end);
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Browser-compatible key import supporting multiple formats (not in sigstore-js reference)
|
|
220
|
-
async function importTLogKey(tlog: RawLogs[0]): Promise<CryptoKey> {
|
|
221
|
-
// Parse keyDetails to extract key type and scheme
|
|
222
|
-
// Formats can be:
|
|
223
|
-
// - "PKIX_ECDSA_P256_SHA_256" (production format)
|
|
224
|
-
// - "PKIX_ED25519"
|
|
225
|
-
// - "ecdsa-sha2-nistp256" (SSH/test format)
|
|
226
|
-
const keyDetails = tlog.publicKey.keyDetails;
|
|
227
|
-
let keyType: string;
|
|
228
|
-
let scheme: string;
|
|
229
|
-
|
|
230
|
-
if (keyDetails === "ecdsa-sha2-nistp256") {
|
|
231
|
-
// SSH-style ECDSA P-256 format used in tests
|
|
232
|
-
keyType = KeyTypes.Ecdsa;
|
|
233
|
-
scheme = "P256-SHA256";
|
|
234
|
-
} else if (keyDetails.includes("ECDSA")) {
|
|
235
|
-
keyType = KeyTypes.Ecdsa;
|
|
236
|
-
// Extract the curve and hash, e.g., "P256_SHA_256" from "PKIX_ECDSA_P256_SHA_256"
|
|
237
|
-
scheme = keyDetails.replace("PKIX_ECDSA_", "").replace(/_/g, "-")
|
|
238
|
-
} else if (keyDetails.includes("ED25519")) {
|
|
239
|
-
keyType = KeyTypes.Ed25519;
|
|
240
|
-
scheme = KeyTypes.Ed25519;
|
|
241
|
-
} else if (keyDetails.includes("RSA")) {
|
|
242
|
-
keyType = KeyTypes.RSA;
|
|
243
|
-
// Extract RSA details, e.g., "PSS_SHA_256" from "PKIX_RSA_PSS_SHA_256"
|
|
244
|
-
scheme = keyDetails.replace("PKIX_RSA_", "").replace(/_/g, "-")
|
|
245
|
-
} else {
|
|
246
|
-
throw new Error(`Unsupported key type in keyDetails: ${keyDetails}`);
|
|
144
|
+
if (!valid) {
|
|
145
|
+
throw new Error("Invalid checkpoint signature");
|
|
247
146
|
}
|
|
248
147
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
148
|
+
if (
|
|
149
|
+
checkpoint.logSize !== BigInt(proof.treeSize) ||
|
|
150
|
+
!uint8ArrayEqual(checkpoint.logHash, base64ToUint8Array(proof.rootHash))
|
|
151
|
+
) {
|
|
152
|
+
throw new Error("Checkpoint does not match inclusion proof");
|
|
153
|
+
}
|
|
254
154
|
}
|
package/src/tlog/dsse.ts
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
import { base64ToUint8Array, hexToUint8Array, uint8ArrayEqual } from "@freedomofpress/crypto-browser";
|
|
15
15
|
import { getHashAlgorithm } from "../interfaces.js";
|
|
16
16
|
import type { SigstoreBundle } from "../bundle.js";
|
|
17
|
-
import type {
|
|
17
|
+
import type { X509Certificate } from "../x509/cert.js";
|
|
18
|
+
import { assertLoggedCertificate, type RekorEntry } from "./body.js";
|
|
18
19
|
|
|
19
20
|
interface DSSESpec {
|
|
20
21
|
signatures?: Array<{
|
|
@@ -40,15 +41,16 @@ interface DSSEEntry extends RekorEntry {
|
|
|
40
41
|
|
|
41
42
|
export async function verifyDSSEBody(
|
|
42
43
|
entry: RekorEntry,
|
|
43
|
-
bundle: SigstoreBundle
|
|
44
|
+
bundle: SigstoreBundle,
|
|
45
|
+
cert: X509Certificate,
|
|
44
46
|
): Promise<void> {
|
|
45
47
|
const dsseEntry = entry as DSSEEntry;
|
|
46
48
|
|
|
47
49
|
switch (dsseEntry.apiVersion) {
|
|
48
50
|
case "0.0.1":
|
|
49
|
-
return verifyDSSE001Body(dsseEntry, bundle);
|
|
51
|
+
return verifyDSSE001Body(dsseEntry, bundle, cert);
|
|
50
52
|
case "0.0.2":
|
|
51
|
-
return verifyDSSE002Body(dsseEntry, bundle);
|
|
53
|
+
return verifyDSSE002Body(dsseEntry, bundle, cert);
|
|
52
54
|
default:
|
|
53
55
|
throw new Error(
|
|
54
56
|
`Unsupported dsse version: ${dsseEntry.apiVersion}`
|
|
@@ -58,7 +60,8 @@ export async function verifyDSSEBody(
|
|
|
58
60
|
|
|
59
61
|
async function verifyDSSE001Body(
|
|
60
62
|
entry: DSSEEntry,
|
|
61
|
-
bundle: SigstoreBundle
|
|
63
|
+
bundle: SigstoreBundle,
|
|
64
|
+
cert: X509Certificate,
|
|
62
65
|
): Promise<void> {
|
|
63
66
|
if (!bundle.dsseEnvelope) {
|
|
64
67
|
throw new Error("Bundle missing dsseEnvelope for DSSE entry");
|
|
@@ -67,6 +70,7 @@ async function verifyDSSE001Body(
|
|
|
67
70
|
if (!entry.spec.signatures || entry.spec.signatures.length !== 1) {
|
|
68
71
|
throw new Error("DSSE entry must have exactly one signature");
|
|
69
72
|
}
|
|
73
|
+
assertLoggedCertificate(cert, entry.spec.signatures[0].verifier, true);
|
|
70
74
|
|
|
71
75
|
const tlogSig = entry.spec.signatures[0].signature;
|
|
72
76
|
const tlogSigBytes = base64ToUint8Array(tlogSig);
|
|
@@ -101,7 +105,8 @@ async function verifyDSSE001Body(
|
|
|
101
105
|
// New functionality for DSSE v0.0.2 (not in sigstore-js reference, which only supports v0.0.1)
|
|
102
106
|
async function verifyDSSE002Body(
|
|
103
107
|
entry: DSSEEntry,
|
|
104
|
-
bundle: SigstoreBundle
|
|
108
|
+
bundle: SigstoreBundle,
|
|
109
|
+
cert: X509Certificate,
|
|
105
110
|
): Promise<void> {
|
|
106
111
|
if (!bundle.dsseEnvelope) {
|
|
107
112
|
throw new Error("Bundle missing dsseEnvelope for DSSE v0.0.2 entry");
|
|
@@ -115,6 +120,7 @@ async function verifyDSSE002Body(
|
|
|
115
120
|
if (!spec.signatures || spec.signatures.length !== 1) {
|
|
116
121
|
throw new Error("DSSE v0.0.2 entry must have exactly one signature");
|
|
117
122
|
}
|
|
123
|
+
assertLoggedCertificate(cert, spec.signatures[0].verifier?.x509Certificate?.rawBytes, false);
|
|
118
124
|
|
|
119
125
|
const tlogSig = spec.signatures[0].content;
|
|
120
126
|
const tlogSigBytes = base64ToUint8Array(tlogSig);
|
package/src/tlog/hashedrekord.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
import { base64ToUint8Array, hexToUint8Array, uint8ArrayEqual } from "@freedomofpress/crypto-browser";
|
|
14
14
|
import type { SigstoreBundle } from "../bundle.js";
|
|
15
|
-
import type {
|
|
15
|
+
import type { X509Certificate } from "../x509/cert.js";
|
|
16
|
+
import { assertLoggedCertificate, type RekorEntry } from "./body.js";
|
|
16
17
|
|
|
17
18
|
interface HashedRekordSpec {
|
|
18
19
|
signature: {
|
|
@@ -39,8 +40,8 @@ interface HashedRekordV002Spec {
|
|
|
39
40
|
hashedRekordV002: {
|
|
40
41
|
signature: {
|
|
41
42
|
content: string;
|
|
42
|
-
verifier
|
|
43
|
-
x509Certificate
|
|
43
|
+
verifier?: {
|
|
44
|
+
x509Certificate?: {
|
|
44
45
|
rawBytes: string;
|
|
45
46
|
};
|
|
46
47
|
};
|
|
@@ -54,15 +55,16 @@ interface HashedRekordV002Spec {
|
|
|
54
55
|
|
|
55
56
|
export async function verifyHashedRekordBody(
|
|
56
57
|
entry: RekorEntry,
|
|
57
|
-
bundle: SigstoreBundle
|
|
58
|
+
bundle: SigstoreBundle,
|
|
59
|
+
cert: X509Certificate,
|
|
58
60
|
): Promise<void> {
|
|
59
61
|
const hashedRekordEntry = entry as HashedRekordEntry;
|
|
60
62
|
|
|
61
63
|
switch (hashedRekordEntry.apiVersion) {
|
|
62
64
|
case "0.0.1":
|
|
63
|
-
return verifyHashedRekordV001Body(hashedRekordEntry, bundle);
|
|
65
|
+
return verifyHashedRekordV001Body(hashedRekordEntry, bundle, cert);
|
|
64
66
|
case "0.0.2":
|
|
65
|
-
return verifyHashedRekordV002Body(hashedRekordEntry, bundle);
|
|
67
|
+
return verifyHashedRekordV002Body(hashedRekordEntry, bundle, cert);
|
|
66
68
|
default:
|
|
67
69
|
throw new Error(
|
|
68
70
|
`Unsupported hashedrekord version: ${hashedRekordEntry.apiVersion}`
|
|
@@ -72,12 +74,14 @@ export async function verifyHashedRekordBody(
|
|
|
72
74
|
|
|
73
75
|
function verifyHashedRekordV001Body(
|
|
74
76
|
entry: HashedRekordEntry,
|
|
75
|
-
bundle: SigstoreBundle
|
|
77
|
+
bundle: SigstoreBundle,
|
|
78
|
+
cert: X509Certificate,
|
|
76
79
|
): void {
|
|
77
80
|
const spec = entry.spec as HashedRekordSpec;
|
|
78
81
|
if (!bundle.messageSignature) {
|
|
79
82
|
throw new Error("Bundle missing messageSignature for hashedrekord entry");
|
|
80
83
|
}
|
|
84
|
+
assertLoggedCertificate(cert, spec.signature?.publicKey?.content, true);
|
|
81
85
|
|
|
82
86
|
const tlogSig = spec.signature.content || "";
|
|
83
87
|
const tlogSigBytes = base64ToUint8Array(tlogSig);
|
|
@@ -101,12 +105,14 @@ function verifyHashedRekordV001Body(
|
|
|
101
105
|
// New functionality for HashedRekord v0.0.2 (not in sigstore-js reference, which only supports v0.0.1)
|
|
102
106
|
function verifyHashedRekordV002Body(
|
|
103
107
|
entry: HashedRekordEntry,
|
|
104
|
-
bundle: SigstoreBundle
|
|
108
|
+
bundle: SigstoreBundle,
|
|
109
|
+
cert: X509Certificate,
|
|
105
110
|
): void {
|
|
106
111
|
const spec = (entry.spec as HashedRekordV002Spec).hashedRekordV002;
|
|
107
112
|
if (!bundle.messageSignature) {
|
|
108
113
|
throw new Error("Bundle missing messageSignature for hashedrekord v0.0.2 entry");
|
|
109
114
|
}
|
|
115
|
+
assertLoggedCertificate(cert, spec?.signature?.verifier?.x509Certificate?.rawBytes, false);
|
|
110
116
|
|
|
111
117
|
// NOTE: HashedRekord v0.0.2 uses single base64 encoding (unlike intoto which is double-encoded)
|
|
112
118
|
// Verified against Sigstore conformance test suite bundles
|
package/src/tlog/intoto.ts
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
import { base64Decode, base64ToUint8Array, hexToUint8Array, uint8ArrayEqual } from "@freedomofpress/crypto-browser";
|
|
14
14
|
import { getHashAlgorithm } from "../interfaces.js";
|
|
15
15
|
import type { SigstoreBundle } from "../bundle.js";
|
|
16
|
-
import type {
|
|
16
|
+
import type { X509Certificate } from "../x509/cert.js";
|
|
17
|
+
import { assertLoggedCertificate, type RekorEntry } from "./body.js";
|
|
17
18
|
|
|
18
19
|
interface IntotoEnvelope {
|
|
19
20
|
payload: string;
|
|
@@ -48,7 +49,8 @@ interface IntotoEntry extends RekorEntry {
|
|
|
48
49
|
|
|
49
50
|
export async function verifyIntotoBody(
|
|
50
51
|
entry: RekorEntry,
|
|
51
|
-
bundle: SigstoreBundle
|
|
52
|
+
bundle: SigstoreBundle,
|
|
53
|
+
cert: X509Certificate,
|
|
52
54
|
): Promise<void> {
|
|
53
55
|
const intotoEntry = entry as IntotoEntry;
|
|
54
56
|
|
|
@@ -67,6 +69,7 @@ export async function verifyIntotoBody(
|
|
|
67
69
|
if (!tlogEnvelope.signatures || tlogEnvelope.signatures.length !== 1) {
|
|
68
70
|
throw new Error("Intoto entry must have exactly one signature");
|
|
69
71
|
}
|
|
72
|
+
assertLoggedCertificate(cert, tlogEnvelope.signatures[0].publicKey, true);
|
|
70
73
|
|
|
71
74
|
// The signature in the intoto tlog entry is double-base64-encoded:
|
|
72
75
|
// 1. Raw signature bytes are base64-encoded (normal)
|