@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.
- package/README.md +20 -16
- package/dist/bundle.d.ts +3 -2
- package/dist/bundle.js +66 -1
- package/dist/bundle.js.map +1 -1
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js.map +1 -1
- package/dist/dsse.d.ts +0 -1
- package/dist/dsse.js.map +1 -1
- package/dist/errors.d.ts +0 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +9 -10
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +18 -4
- package/dist/interfaces.js +15 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/oid.d.ts +0 -1
- package/dist/oid.js.map +1 -1
- package/dist/policy.d.ts +3 -4
- package/dist/policy.js +5 -5
- package/dist/policy.js.map +1 -1
- package/dist/rfc3161/error.d.ts +0 -1
- package/dist/rfc3161/error.js.map +1 -1
- package/dist/rfc3161/index.d.ts +0 -1
- package/dist/rfc3161/index.js.map +1 -1
- package/dist/rfc3161/timestamp.d.ts +0 -1
- package/dist/rfc3161/timestamp.js.map +1 -1
- package/dist/rfc3161/tstinfo.d.ts +0 -1
- package/dist/rfc3161/tstinfo.js.map +1 -1
- package/dist/sigstore.d.ts +10 -20
- package/dist/sigstore.js +167 -400
- package/dist/sigstore.js.map +1 -1
- package/dist/timestamp/tsa.d.ts +6 -3
- package/dist/timestamp/tsa.js +16 -19
- package/dist/timestamp/tsa.js.map +1 -1
- package/dist/tlog/body.d.ts +3 -2
- package/dist/tlog/body.js +19 -6
- package/dist/tlog/body.js.map +1 -1
- package/dist/tlog/checkpoint.d.ts +2 -3
- package/dist/tlog/checkpoint.js +17 -95
- package/dist/tlog/checkpoint.js.map +1 -1
- package/dist/tlog/dsse.d.ts +3 -3
- package/dist/tlog/dsse.js +11 -5
- package/dist/tlog/dsse.js.map +1 -1
- package/dist/tlog/hashedrekord.d.ts +3 -3
- package/dist/tlog/hashedrekord.js +8 -5
- package/dist/tlog/hashedrekord.js.map +1 -1
- package/dist/tlog/intoto.d.ts +3 -3
- package/dist/tlog/intoto.js +3 -1
- package/dist/tlog/intoto.js.map +1 -1
- package/dist/tlog/merkle.d.ts +0 -1
- package/dist/tlog/merkle.js.map +1 -1
- package/dist/trust/tuf-root.d.ts +0 -1
- package/dist/trust/tuf-root.js.map +1 -1
- package/dist/trust/tuf.d.ts +0 -1
- package/dist/trust/tuf.js.map +1 -1
- package/dist/x509/cert.d.ts +0 -1
- package/dist/x509/cert.js +1 -1
- package/dist/x509/cert.js.map +1 -1
- package/dist/x509/chain.d.ts +0 -1
- package/dist/x509/chain.js +2 -2
- package/dist/x509/chain.js.map +1 -1
- package/dist/x509/ext.d.ts +0 -1
- package/dist/x509/ext.js.map +1 -1
- package/dist/x509/index.d.ts +1 -2
- package/dist/x509/index.js +1 -1
- package/dist/x509/index.js.map +1 -1
- package/dist/x509/sct.d.ts +0 -1
- package/dist/x509/sct.js.map +1 -1
- package/package.json +15 -8
- package/.github/workflows/conformance.yml +0 -29
- package/.github/workflows/publish.yml +0 -47
- package/dist/bundle.d.ts.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/dsse.d.ts.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/interfaces.d.ts.map +0 -1
- package/dist/oid.d.ts.map +0 -1
- package/dist/policy.d.ts.map +0 -1
- package/dist/rfc3161/error.d.ts.map +0 -1
- package/dist/rfc3161/index.d.ts.map +0 -1
- package/dist/rfc3161/timestamp.d.ts.map +0 -1
- package/dist/rfc3161/tstinfo.d.ts.map +0 -1
- package/dist/sigstore.d.ts.map +0 -1
- package/dist/timestamp/tsa.d.ts.map +0 -1
- package/dist/tlog/body.d.ts.map +0 -1
- package/dist/tlog/checkpoint.d.ts.map +0 -1
- package/dist/tlog/dsse.d.ts.map +0 -1
- package/dist/tlog/hashedrekord.d.ts.map +0 -1
- package/dist/tlog/intoto.d.ts.map +0 -1
- package/dist/tlog/merkle.d.ts.map +0 -1
- package/dist/trust/tuf-root.d.ts.map +0 -1
- package/dist/trust/tuf.d.ts.map +0 -1
- package/dist/x509/cert.d.ts.map +0 -1
- package/dist/x509/chain.d.ts.map +0 -1
- package/dist/x509/ext.d.ts.map +0 -1
- package/dist/x509/index.d.ts.map +0 -1
- package/dist/x509/sct.d.ts.map +0 -1
- package/eslint.config.mjs +0 -31
- package/src/bundle.ts +0 -99
- package/src/cli.ts +0 -290
- package/src/dsse.ts +0 -49
- package/src/errors.ts +0 -82
- package/src/index.browser.test.ts +0 -290
- package/src/index.ts +0 -93
- package/src/interfaces.ts +0 -123
- package/src/oid.ts +0 -34
- package/src/policy.ts +0 -476
- package/src/rfc3161/error.ts +0 -16
- package/src/rfc3161/index.ts +0 -17
- package/src/rfc3161/timestamp.ts +0 -268
- package/src/rfc3161/tstinfo.ts +0 -78
- package/src/sigstore.ts +0 -948
- package/src/timestamp/tsa.ts +0 -211
- package/src/tlog/body.ts +0 -65
- package/src/tlog/checkpoint.ts +0 -254
- package/src/tlog/dsse.ts +0 -147
- package/src/tlog/hashedrekord.ts +0 -130
- package/src/tlog/intoto.ts +0 -106
- package/src/tlog/merkle.ts +0 -152
- package/src/trust/README.md +0 -38
- package/src/trust/tuf-root.ts +0 -3
- package/src/trust/tuf.ts +0 -233
- package/src/x509/cert.ts +0 -569
- package/src/x509/chain.ts +0 -212
- package/src/x509/ext.ts +0 -356
- package/src/x509/index.ts +0 -19
- package/src/x509/sct.ts +0 -156
- package/tsconfig.json +0 -18
- package/vite.config.playwright.ts +0 -14
- package/vite.config.ts +0 -18
package/src/cli.ts
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { createHash, webcrypto } from "node:crypto";
|
|
3
|
-
import { access, readFile } from "node:fs/promises";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
base64ToUint8Array,
|
|
8
|
-
hexToUint8Array,
|
|
9
|
-
Uint8ArrayToHex,
|
|
10
|
-
Uint8ArrayToString,
|
|
11
|
-
} from "@freedomofpress/crypto-browser";
|
|
12
|
-
import { SigstoreBundle } from "./bundle.js";
|
|
13
|
-
import { TrustedRoot } from "./interfaces.js";
|
|
14
|
-
import { SigstoreVerifier } from "./sigstore.js";
|
|
15
|
-
import { TrustedRootProvider } from "./trust/tuf.js";
|
|
16
|
-
|
|
17
|
-
// Ensure the global Web Crypto implementation is available when running under Node.js
|
|
18
|
-
if (typeof globalThis.crypto === "undefined") {
|
|
19
|
-
globalThis.crypto = webcrypto as unknown as Crypto;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface CLIOptions {
|
|
23
|
-
bundlePath: string;
|
|
24
|
-
certificateIdentity: string;
|
|
25
|
-
certificateOidcIssuer: string;
|
|
26
|
-
trustedRootPath: string | undefined;
|
|
27
|
-
artifactInput: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
type ArtifactInput =
|
|
31
|
-
| { type: "file"; path: string; data: Uint8Array; digestHex: string }
|
|
32
|
-
| { type: "digest"; digestHex: string };
|
|
33
|
-
|
|
34
|
-
const USAGE = `Usage: sigstore-browser verify-bundle [--staging] --bundle FILE --certificate-identity IDENTITY --certificate-oidc-issuer URL [--trusted-root FILE] FILE_OR_DIGEST`;
|
|
35
|
-
|
|
36
|
-
function printUsage(): void {
|
|
37
|
-
console.error(USAGE);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function parseArgs(argv: string[]): CLIOptions {
|
|
41
|
-
const options: Partial<CLIOptions> = {};
|
|
42
|
-
let command: string | undefined;
|
|
43
|
-
let artifactInput: string | undefined;
|
|
44
|
-
|
|
45
|
-
for (let i = 0; i < argv.length; i++) {
|
|
46
|
-
const current = argv[i];
|
|
47
|
-
|
|
48
|
-
if (current === "--help" || current === "-h") {
|
|
49
|
-
printUsage();
|
|
50
|
-
process.exit(0);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (current.startsWith("--")) {
|
|
54
|
-
const [name, providedValue] = current.split("=", 2);
|
|
55
|
-
|
|
56
|
-
switch (name) {
|
|
57
|
-
case "--bundle": {
|
|
58
|
-
const value = providedValue ?? argv[++i];
|
|
59
|
-
if (!value) {
|
|
60
|
-
throw new Error("Missing value for --bundle");
|
|
61
|
-
}
|
|
62
|
-
options.bundlePath = value;
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
case "--certificate-identity": {
|
|
66
|
-
const value = providedValue ?? argv[++i];
|
|
67
|
-
if (!value) {
|
|
68
|
-
throw new Error("Missing value for --certificate-identity");
|
|
69
|
-
}
|
|
70
|
-
options.certificateIdentity = value;
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
73
|
-
case "--certificate-oidc-issuer": {
|
|
74
|
-
const value = providedValue ?? argv[++i];
|
|
75
|
-
if (!value) {
|
|
76
|
-
throw new Error("Missing value for --certificate-oidc-issuer");
|
|
77
|
-
}
|
|
78
|
-
options.certificateOidcIssuer = value;
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
case "--trusted-root": {
|
|
82
|
-
const value = providedValue ?? argv[++i];
|
|
83
|
-
if (!value) {
|
|
84
|
-
throw new Error("Missing value for --trusted-root");
|
|
85
|
-
}
|
|
86
|
-
options.trustedRootPath = value;
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
default:
|
|
90
|
-
throw new Error(`Unknown option: ${name}`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (!command) {
|
|
97
|
-
command = current;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!artifactInput) {
|
|
102
|
-
artifactInput = current;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
throw new Error(`Unexpected argument: ${current}`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (command !== "verify-bundle") {
|
|
110
|
-
throw new Error("No command provided. Expected 'verify-bundle'.");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (!artifactInput) {
|
|
114
|
-
throw new Error("Missing FILE_OR_DIGEST argument.");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (!options.bundlePath) {
|
|
118
|
-
throw new Error("Missing required option --bundle");
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (!options.certificateIdentity) {
|
|
122
|
-
throw new Error("Missing required option --certificate-identity");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (!options.certificateOidcIssuer) {
|
|
126
|
-
throw new Error("Missing required option --certificate-oidc-issuer");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
bundlePath: options.bundlePath,
|
|
131
|
-
certificateIdentity: options.certificateIdentity,
|
|
132
|
-
certificateOidcIssuer: options.certificateOidcIssuer,
|
|
133
|
-
trustedRootPath: options.trustedRootPath,
|
|
134
|
-
artifactInput,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function fileExists(filePath: string): Promise<boolean> {
|
|
139
|
-
try {
|
|
140
|
-
await access(filePath);
|
|
141
|
-
return true;
|
|
142
|
-
} catch {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function resolveArtifact(input: string): Promise<ArtifactInput> {
|
|
148
|
-
const digestPrefix = "sha256:";
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
input.startsWith(digestPrefix) &&
|
|
152
|
-
input.length === digestPrefix.length + 64 &&
|
|
153
|
-
/^[0-9a-fA-F]+$/.test(input.slice(digestPrefix.length)) &&
|
|
154
|
-
!(await fileExists(input))
|
|
155
|
-
) {
|
|
156
|
-
return {
|
|
157
|
-
type: "digest",
|
|
158
|
-
digestHex: input.slice(digestPrefix.length).toLowerCase(),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const resolvedPath = path.resolve(input);
|
|
163
|
-
const artifactBuffer = await readFile(resolvedPath);
|
|
164
|
-
const digestHex = createHash("sha256").update(artifactBuffer).digest("hex");
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
type: "file",
|
|
168
|
-
path: resolvedPath,
|
|
169
|
-
data: new Uint8Array(artifactBuffer),
|
|
170
|
-
digestHex,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async function loadTrustedRoot(
|
|
175
|
-
pathInput: string | undefined,
|
|
176
|
-
): Promise<TrustedRoot> {
|
|
177
|
-
if (!pathInput) {
|
|
178
|
-
// Use TUF to fetch the latest trusted root (matches sigstore-js behavior)
|
|
179
|
-
const tufProvider = new TrustedRootProvider({
|
|
180
|
-
metadataUrl: "https://tuf-repo-cdn.sigstore.dev/",
|
|
181
|
-
targetBaseUrl: "https://tuf-repo-cdn.sigstore.dev/targets/",
|
|
182
|
-
});
|
|
183
|
-
return await tufProvider.getTrustedRoot();
|
|
184
|
-
}
|
|
185
|
-
const resolvedPath = path.resolve(pathInput);
|
|
186
|
-
const raw = await readFile(resolvedPath, "utf8");
|
|
187
|
-
return JSON.parse(raw) as TrustedRoot;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async function loadBundle(bundlePath: string): Promise<SigstoreBundle> {
|
|
191
|
-
const resolved = path.resolve(bundlePath);
|
|
192
|
-
const raw = await readFile(resolved, "utf8");
|
|
193
|
-
return JSON.parse(raw) as SigstoreBundle;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function verifyBundle(options: CLIOptions): Promise<void> {
|
|
197
|
-
const [trustedRoot, bundle, artifact] = await Promise.all([
|
|
198
|
-
loadTrustedRoot(options.trustedRootPath),
|
|
199
|
-
loadBundle(options.bundlePath),
|
|
200
|
-
resolveArtifact(options.artifactInput),
|
|
201
|
-
]);
|
|
202
|
-
|
|
203
|
-
const verifier = new SigstoreVerifier();
|
|
204
|
-
await verifier.loadSigstoreRoot(trustedRoot);
|
|
205
|
-
|
|
206
|
-
// Extract and verify the artifact digest from the bundle
|
|
207
|
-
let bundleDigestHex: string;
|
|
208
|
-
let bundleDigestBytes: Uint8Array;
|
|
209
|
-
|
|
210
|
-
if (bundle.messageSignature) {
|
|
211
|
-
// Regular bundle: digest is in messageSignature
|
|
212
|
-
if (bundle.messageSignature.messageDigest.algorithm !== "SHA2_256") {
|
|
213
|
-
throw new Error(
|
|
214
|
-
`Unsupported message digest algorithm: ${bundle.messageSignature.messageDigest.algorithm}`,
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
bundleDigestBytes = base64ToUint8Array(
|
|
219
|
-
bundle.messageSignature.messageDigest.digest,
|
|
220
|
-
);
|
|
221
|
-
bundleDigestHex = Uint8ArrayToHex(bundleDigestBytes).toLowerCase();
|
|
222
|
-
} else if (bundle.dsseEnvelope) {
|
|
223
|
-
// DSSE bundle: digest is in the in-toto statement payload
|
|
224
|
-
const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
|
|
225
|
-
const payload = JSON.parse(Uint8ArrayToString(payloadBytes));
|
|
226
|
-
|
|
227
|
-
if (!payload.subject || payload.subject.length === 0) {
|
|
228
|
-
throw new Error("DSSE payload has no subject");
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Find matching subject by comparing artifact digest with all subjects
|
|
232
|
-
let matchedSubject = null;
|
|
233
|
-
for (const subject of payload.subject) {
|
|
234
|
-
const subjectDigest = subject.digest?.["sha256"];
|
|
235
|
-
if (subjectDigest && artifact.digestHex === subjectDigest.toLowerCase()) {
|
|
236
|
-
matchedSubject = subject;
|
|
237
|
-
break;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (!matchedSubject) {
|
|
242
|
-
throw new Error(
|
|
243
|
-
`Artifact digest ${artifact.digestHex} does not match any subject in DSSE payload`
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
bundleDigestHex = matchedSubject.digest["sha256"].toLowerCase();
|
|
248
|
-
|
|
249
|
-
// Convert hex digest to bytes for verification target
|
|
250
|
-
bundleDigestBytes = hexToUint8Array(bundleDigestHex);
|
|
251
|
-
} else {
|
|
252
|
-
throw new Error("Bundle does not contain a message signature or DSSE envelope");
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// For non-DSSE bundles, verify the digest matches
|
|
256
|
-
if (bundle.messageSignature && artifact.digestHex !== bundleDigestHex) {
|
|
257
|
-
throw new Error(
|
|
258
|
-
"Artifact digest does not match the digest embedded in the bundle.",
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const verificationTarget =
|
|
263
|
-
artifact.type === "file" ? artifact.data : bundleDigestBytes;
|
|
264
|
-
const isDigestOnly = artifact.type === "digest";
|
|
265
|
-
|
|
266
|
-
await verifier.verifyArtifact(
|
|
267
|
-
options.certificateIdentity,
|
|
268
|
-
options.certificateOidcIssuer,
|
|
269
|
-
bundle,
|
|
270
|
-
verificationTarget,
|
|
271
|
-
isDigestOnly,
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async function main(): Promise<void> {
|
|
276
|
-
try {
|
|
277
|
-
const options = parseArgs(process.argv.slice(2));
|
|
278
|
-
await verifyBundle(options);
|
|
279
|
-
console.log("Bundle verification succeeded.");
|
|
280
|
-
} catch (error) {
|
|
281
|
-
if (error instanceof Error) {
|
|
282
|
-
console.error(error.message);
|
|
283
|
-
} else {
|
|
284
|
-
console.error(error);
|
|
285
|
-
}
|
|
286
|
-
process.exitCode = 1;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
await main();
|
package/src/dsse.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright 2023 The Sigstore Authors.
|
|
3
|
-
|
|
4
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
you may not use this file except in compliance with the License.
|
|
6
|
-
You may obtain a copy of the License at
|
|
7
|
-
|
|
8
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
|
|
10
|
-
Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
See the License for the specific language governing permissions and
|
|
14
|
-
limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
const PAE_PREFIX = "DSSEv1";
|
|
17
|
-
|
|
18
|
-
// DSSE Pre-Authentication Encoding
|
|
19
|
-
export function preAuthEncoding(
|
|
20
|
-
payloadType: string,
|
|
21
|
-
payload: Uint8Array,
|
|
22
|
-
): Uint8Array {
|
|
23
|
-
const prefix = [
|
|
24
|
-
PAE_PREFIX,
|
|
25
|
-
payloadType.length,
|
|
26
|
-
payloadType,
|
|
27
|
-
payload.length,
|
|
28
|
-
"",
|
|
29
|
-
].join(" ");
|
|
30
|
-
|
|
31
|
-
// DSSE spec requires ASCII encoding for the prefix
|
|
32
|
-
// TextEncoder will throw if the string contains non-ASCII characters
|
|
33
|
-
const encoder = new TextEncoder();
|
|
34
|
-
const prefixBuffer = encoder.encode(prefix);
|
|
35
|
-
|
|
36
|
-
// Verify all characters are ASCII (< 128)
|
|
37
|
-
for (let i = 0; i < prefixBuffer.length; i++) {
|
|
38
|
-
if (prefixBuffer[i] > 127) {
|
|
39
|
-
throw new Error(`Invalid non-ASCII character in PAE prefix at position ${i}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Concatenate prefix and payload
|
|
44
|
-
const combinedArray = new Uint8Array(prefixBuffer.length + payload.length);
|
|
45
|
-
|
|
46
|
-
combinedArray.set(prefixBuffer, 0);
|
|
47
|
-
combinedArray.set(payload, prefixBuffer.length);
|
|
48
|
-
return combinedArray;
|
|
49
|
-
}
|
package/src/errors.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright 2023 The Sigstore Authors.
|
|
3
|
-
|
|
4
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
you may not use this file except in compliance with the License.
|
|
6
|
-
You may obtain a copy of the License at
|
|
7
|
-
|
|
8
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
|
|
10
|
-
Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
See the License for the specific language governing permissions and
|
|
14
|
-
limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Base class for all verification errors
|
|
19
|
-
* Provides structured error handling with error codes
|
|
20
|
-
*/
|
|
21
|
-
export class VerificationError extends Error {
|
|
22
|
-
constructor(public code: string, message: string) {
|
|
23
|
-
super(message);
|
|
24
|
-
this.name = 'VerificationError';
|
|
25
|
-
Object.setPrototypeOf(this, VerificationError.prototype);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Error thrown when timestamp verification fails
|
|
31
|
-
*/
|
|
32
|
-
export class TimestampError extends VerificationError {
|
|
33
|
-
constructor(message: string) {
|
|
34
|
-
super('TIMESTAMP_ERROR', message);
|
|
35
|
-
this.name = 'TimestampError';
|
|
36
|
-
Object.setPrototypeOf(this, TimestampError.prototype);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Error thrown when certificate verification fails
|
|
42
|
-
*/
|
|
43
|
-
export class CertificateError extends VerificationError {
|
|
44
|
-
constructor(message: string) {
|
|
45
|
-
super('CERTIFICATE_ERROR', message);
|
|
46
|
-
this.name = 'CertificateError';
|
|
47
|
-
Object.setPrototypeOf(this, CertificateError.prototype);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Error thrown when transparency log verification fails
|
|
53
|
-
*/
|
|
54
|
-
export class TLogError extends VerificationError {
|
|
55
|
-
constructor(message: string) {
|
|
56
|
-
super('TLOG_ERROR', message);
|
|
57
|
-
this.name = 'TLogError';
|
|
58
|
-
Object.setPrototypeOf(this, TLogError.prototype);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Error thrown when signature verification fails
|
|
64
|
-
*/
|
|
65
|
-
export class SignatureError extends VerificationError {
|
|
66
|
-
constructor(message: string) {
|
|
67
|
-
super('SIGNATURE_ERROR', message);
|
|
68
|
-
this.name = 'SignatureError';
|
|
69
|
-
Object.setPrototypeOf(this, SignatureError.prototype);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Error thrown when policy verification fails
|
|
75
|
-
*/
|
|
76
|
-
export class PolicyError extends VerificationError {
|
|
77
|
-
constructor(message: string) {
|
|
78
|
-
super('POLICY_ERROR', message);
|
|
79
|
-
this.name = 'PolicyError';
|
|
80
|
-
Object.setPrototypeOf(this, PolicyError.prototype);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { SigstoreVerifier, assertRekorV2Timestamp } from "./sigstore.js";
|
|
3
|
-
import { X509Certificate } from "./x509/cert.js";
|
|
4
|
-
|
|
5
|
-
describe("Sigstore Browser Integration Tests", () => {
|
|
6
|
-
it("should initialize SigstoreVerifier in browser", () => {
|
|
7
|
-
const verifier = new SigstoreVerifier();
|
|
8
|
-
expect(verifier).toBeDefined();
|
|
9
|
-
expect(verifier).toBeInstanceOf(SigstoreVerifier);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("should have crypto.subtle available for ECDSA P-256 verification", async () => {
|
|
13
|
-
expect(globalThis.crypto).toBeDefined();
|
|
14
|
-
expect(globalThis.crypto.subtle).toBeDefined();
|
|
15
|
-
|
|
16
|
-
// Test ECDSA P-256 which is what Sigstore uses
|
|
17
|
-
const keyPair = await crypto.subtle.generateKey(
|
|
18
|
-
{
|
|
19
|
-
name: "ECDSA",
|
|
20
|
-
namedCurve: "P-256",
|
|
21
|
-
},
|
|
22
|
-
true,
|
|
23
|
-
["sign", "verify"]
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
const data = new TextEncoder().encode("test");
|
|
27
|
-
const signature = await crypto.subtle.sign(
|
|
28
|
-
{ name: "ECDSA", hash: "SHA-256" },
|
|
29
|
-
keyPair.privateKey,
|
|
30
|
-
data
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
const valid = await crypto.subtle.verify(
|
|
34
|
-
{ name: "ECDSA", hash: "SHA-256" },
|
|
35
|
-
keyPair.publicKey,
|
|
36
|
-
signature,
|
|
37
|
-
data
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
expect(valid).toBe(true);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("should have crypto.subtle available for ECDSA P-384 verification", async () => {
|
|
44
|
-
// Test ECDSA P-384 which is also used by Sigstore
|
|
45
|
-
const keyPair = await crypto.subtle.generateKey(
|
|
46
|
-
{
|
|
47
|
-
name: "ECDSA",
|
|
48
|
-
namedCurve: "P-384",
|
|
49
|
-
},
|
|
50
|
-
true,
|
|
51
|
-
["sign", "verify"]
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const data = new TextEncoder().encode("test");
|
|
55
|
-
const signature = await crypto.subtle.sign(
|
|
56
|
-
{ name: "ECDSA", hash: "SHA-384" },
|
|
57
|
-
keyPair.privateKey,
|
|
58
|
-
data
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
const valid = await crypto.subtle.verify(
|
|
62
|
-
{ name: "ECDSA", hash: "SHA-384" },
|
|
63
|
-
keyPair.publicKey,
|
|
64
|
-
signature,
|
|
65
|
-
data
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
expect(valid).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("should support SHA-256 hashing in browser", async () => {
|
|
72
|
-
const data = new TextEncoder().encode("test data");
|
|
73
|
-
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
74
|
-
|
|
75
|
-
expect(hash).toBeInstanceOf(ArrayBuffer);
|
|
76
|
-
expect(hash.byteLength).toBe(32);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("should support SHA-384 hashing in browser", async () => {
|
|
80
|
-
const data = new TextEncoder().encode("test data");
|
|
81
|
-
const hash = await crypto.subtle.digest("SHA-384", data);
|
|
82
|
-
|
|
83
|
-
expect(hash).toBeInstanceOf(ArrayBuffer);
|
|
84
|
-
expect(hash.byteLength).toBe(48);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("should support SHA-512 hashing in browser", async () => {
|
|
88
|
-
const data = new TextEncoder().encode("test data");
|
|
89
|
-
const hash = await crypto.subtle.digest("SHA-512", data);
|
|
90
|
-
|
|
91
|
-
expect(hash).toBeInstanceOf(ArrayBuffer);
|
|
92
|
-
expect(hash.byteLength).toBe(64);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("should support X.509 certificate key import (SPKI format)", async () => {
|
|
96
|
-
const keyPair = await crypto.subtle.generateKey(
|
|
97
|
-
{
|
|
98
|
-
name: "ECDSA",
|
|
99
|
-
namedCurve: "P-256",
|
|
100
|
-
},
|
|
101
|
-
true,
|
|
102
|
-
["sign", "verify"]
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
const exported = await crypto.subtle.exportKey("spki", keyPair.publicKey);
|
|
106
|
-
const imported = await crypto.subtle.importKey(
|
|
107
|
-
"spki",
|
|
108
|
-
exported,
|
|
109
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
110
|
-
true,
|
|
111
|
-
["verify"]
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
expect(imported).toBeDefined();
|
|
115
|
-
expect(imported.type).toBe("public");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("should support RSA-PSS for signature verification", async () => {
|
|
119
|
-
const keyPair = await crypto.subtle.generateKey(
|
|
120
|
-
{
|
|
121
|
-
name: "RSA-PSS",
|
|
122
|
-
modulusLength: 2048,
|
|
123
|
-
publicExponent: new Uint8Array([1, 0, 1]),
|
|
124
|
-
hash: "SHA-256",
|
|
125
|
-
},
|
|
126
|
-
true,
|
|
127
|
-
["sign", "verify"]
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const data = new TextEncoder().encode("test");
|
|
131
|
-
const signature = await crypto.subtle.sign(
|
|
132
|
-
{
|
|
133
|
-
name: "RSA-PSS",
|
|
134
|
-
saltLength: 32,
|
|
135
|
-
},
|
|
136
|
-
keyPair.privateKey,
|
|
137
|
-
data
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
const valid = await crypto.subtle.verify(
|
|
141
|
-
{
|
|
142
|
-
name: "RSA-PSS",
|
|
143
|
-
saltLength: 32,
|
|
144
|
-
},
|
|
145
|
-
keyPair.publicKey,
|
|
146
|
-
signature,
|
|
147
|
-
data
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
expect(valid).toBe(true);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("should verify RSA-PSS signed X.509 certificate", async () => {
|
|
154
|
-
// Self-signed certificate with standard RSA key but RSA-PSS signature (SHA-256, salt=32)
|
|
155
|
-
// WebCrypto requires rsaEncryption OID for the public key, not id-RSASSA-PSS
|
|
156
|
-
const rsaPssCertPem = `-----BEGIN CERTIFICATE-----
|
|
157
|
-
MIIDhTCCAjmgAwIBAgIUYzfguLRx71Cbn2Ty7xCt3GH+N6AwQQYJKoZIhvcNAQEK
|
|
158
|
-
MDSgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEF
|
|
159
|
-
AKIDAgEgMB4xHDAaBgNVBAMME1Rlc3QgUlNBLVBTUyBTaWduZWQwHhcNMjUxMTMw
|
|
160
|
-
MDMwMDU2WhcNMjYxMTMwMDMwMDU2WjAeMRwwGgYDVQQDDBNUZXN0IFJTQS1QU1Mg
|
|
161
|
-
U2lnbmVkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA29B/88QFBILg
|
|
162
|
-
YCWp1pF3xBj7FVhBUCv6JwyZAouYWUxgGmpFg6RLgbaWk59oDtEFQ5xRGSDXLuMW
|
|
163
|
-
GL5wZVCjSnmYcBsBDROO+HfLKwDRcHw09bJY979PDHPy5EPZale/JsdxE+fepUw1
|
|
164
|
-
XQDvlK5Dyp6WKl9UBjTtl6+LHKV/KTo+SiaQP2vsbZvTIt3s8MHybV/ixYXlSPQt
|
|
165
|
-
Kju50REAaqSSYqxNzsq8Z2sjJbFBcatt5C1JhfoSyIHz5D0slriv8835DYhk0u4n
|
|
166
|
-
bvNzXhWeF7htHTiPWCYwaTr5cgjyhEWg3SNP+Tu9dxCYVXm7/jWTHRdPqUPQqFsq
|
|
167
|
-
wXqsrzSP+wIDAQABo1MwUTAdBgNVHQ4EFgQU+UcBxKusOtY4lSjqyFuxQfN5zdIw
|
|
168
|
-
HwYDVR0jBBgwFoAU+UcBxKusOtY4lSjqyFuxQfN5zdIwDwYDVR0TAQH/BAUwAwEB
|
|
169
|
-
/zBBBgkqhkiG9w0BAQowNKAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZIhvcNAQEI
|
|
170
|
-
MA0GCWCGSAFlAwQCAQUAogMCASADggEBAFIn3VpfydVvpI82NwfeGxWlhZhgD2dX
|
|
171
|
-
gJUhj6pcXqwnlCzpyRI4e3uNO1h9nr9UrogI0AV2IDhVdUYxz3F2/680JtxXpB1x
|
|
172
|
-
J02BSAYllvXgf9efc20lFzbPDzvMzKnwx3S3proewbJ4uVPHzvKubWsx8MwEuI0X
|
|
173
|
-
8xkI1qMIXgevTCt+LGhp+0C/u8WsW3or5ZI7GnZNowPJEAtZnfO+VORB7FvbzEGr
|
|
174
|
-
ewS+2T7Qz4oXaQMidPOjr1Q8WKqaKO4yCtC8cz4qVWi3lNqAcAGtonQMXUiflEWV
|
|
175
|
-
9kqKQx+LJYJqxKkBtDRnRLnVNn4+inotrgnV126LsCe3uLaGwJIFy1U=
|
|
176
|
-
-----END CERTIFICATE-----`;
|
|
177
|
-
|
|
178
|
-
const cert = X509Certificate.parse(rsaPssCertPem);
|
|
179
|
-
|
|
180
|
-
// Verify it's an RSA-PSS certificate
|
|
181
|
-
expect(cert.signatureAlgorithmOid).toBe("1.2.840.113549.1.1.10");
|
|
182
|
-
expect(cert.signatureAlgorithm).toBe("sha256");
|
|
183
|
-
|
|
184
|
-
// Verify the self-signed certificate (signature verification)
|
|
185
|
-
const valid = await cert.verify();
|
|
186
|
-
expect(valid).toBe(true);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("should support TextEncoder/TextDecoder for UTF-8", () => {
|
|
190
|
-
const encoder = new TextEncoder();
|
|
191
|
-
const decoder = new TextDecoder();
|
|
192
|
-
|
|
193
|
-
const text = "Hello Sigstore 🔐";
|
|
194
|
-
const encoded = encoder.encode(text);
|
|
195
|
-
const decoded = decoder.decode(encoded);
|
|
196
|
-
|
|
197
|
-
expect(decoded).toBe(text);
|
|
198
|
-
expect(encoded).toBeInstanceOf(Uint8Array);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("should support base64 encoding/decoding (atob/btoa)", () => {
|
|
202
|
-
const text = "Hello Sigstore";
|
|
203
|
-
const base64 = btoa(text);
|
|
204
|
-
const decoded = atob(base64);
|
|
205
|
-
|
|
206
|
-
expect(decoded).toBe(text);
|
|
207
|
-
expect(base64).toBe("SGVsbG8gU2lnc3RvcmU=");
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("should support Uint8Array operations", () => {
|
|
211
|
-
const arr1 = new Uint8Array([1, 2, 3, 4, 5]);
|
|
212
|
-
const arr2 = new Uint8Array([6, 7, 8, 9, 10]);
|
|
213
|
-
|
|
214
|
-
const combined = new Uint8Array(arr1.length + arr2.length);
|
|
215
|
-
combined.set(arr1, 0);
|
|
216
|
-
combined.set(arr2, arr1.length);
|
|
217
|
-
|
|
218
|
-
expect(combined.length).toBe(10);
|
|
219
|
-
expect(combined[0]).toBe(1);
|
|
220
|
-
expect(combined[5]).toBe(6);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("should support ArrayBuffer operations", () => {
|
|
224
|
-
const buffer = new ArrayBuffer(32);
|
|
225
|
-
const view = new Uint8Array(buffer);
|
|
226
|
-
|
|
227
|
-
view[0] = 255;
|
|
228
|
-
view[31] = 128;
|
|
229
|
-
|
|
230
|
-
expect(buffer.byteLength).toBe(32);
|
|
231
|
-
expect(view[0]).toBe(255);
|
|
232
|
-
expect(view[31]).toBe(128);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("should support DataView for byte manipulation", () => {
|
|
236
|
-
const buffer = new ArrayBuffer(8);
|
|
237
|
-
const view = new DataView(buffer);
|
|
238
|
-
|
|
239
|
-
view.setUint32(0, 0x12345678, false); // big-endian
|
|
240
|
-
view.setUint32(4, 0x9abcdef0, false);
|
|
241
|
-
|
|
242
|
-
expect(view.getUint32(0, false)).toBe(0x12345678);
|
|
243
|
-
expect(view.getUint8(0)).toBe(0x12);
|
|
244
|
-
expect(view.getUint8(1)).toBe(0x34);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("should support JSON parsing of complex structures", () => {
|
|
248
|
-
const json = '{"mediaType":"application/vnd.dev.sigstore.bundle+json;version=0.3","verificationMaterial":{"tlogEntries":[{"logIndex":"123"}]}}';
|
|
249
|
-
const parsed = JSON.parse(json);
|
|
250
|
-
|
|
251
|
-
expect(parsed.mediaType).toBeDefined();
|
|
252
|
-
expect(parsed.verificationMaterial).toBeDefined();
|
|
253
|
-
expect(parsed.verificationMaterial.tlogEntries).toBeInstanceOf(Array);
|
|
254
|
-
expect(parsed.verificationMaterial.tlogEntries[0].logIndex).toBe("123");
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
describe("Rekor v2 observer-timestamp enforcement", () => {
|
|
259
|
-
// Regression test: a Rekor-v2-shaped bundle (no integratedTime in the tlog
|
|
260
|
-
// entry) must carry a signed RFC3161 timestamp. A previous guard only checked
|
|
261
|
-
// that `timestampVerificationData` was present, which an empty object ({})
|
|
262
|
-
// satisfies in JavaScript — letting a crafted bundle pass verification with
|
|
263
|
-
// zero verified timestamps and the signing certificate's validity window left
|
|
264
|
-
// completely unanchored in time.
|
|
265
|
-
|
|
266
|
-
it("rejects a missing timestampVerificationData", () => {
|
|
267
|
-
expect(() => assertRekorV2Timestamp(undefined)).toThrow(
|
|
268
|
-
"Rekor v2 bundles require a timestamp for verification.",
|
|
269
|
-
);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it("rejects an empty timestampVerificationData object ({})", () => {
|
|
273
|
-
// This is the exact bypass the fix closes: {} is truthy.
|
|
274
|
-
expect(() => assertRekorV2Timestamp({})).toThrow(
|
|
275
|
-
"Rekor v2 bundles require a timestamp for verification.",
|
|
276
|
-
);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it("rejects timestampVerificationData with an empty rfc3161Timestamps array", () => {
|
|
280
|
-
expect(() =>
|
|
281
|
-
assertRekorV2Timestamp({ rfc3161Timestamps: [] }),
|
|
282
|
-
).toThrow("Rekor v2 bundles require a timestamp for verification.");
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it("accepts timestampVerificationData with at least one RFC3161 timestamp", () => {
|
|
286
|
-
expect(() =>
|
|
287
|
-
assertRekorV2Timestamp({ rfc3161Timestamps: [{ signedTimestamp: "…" }] }),
|
|
288
|
-
).not.toThrow();
|
|
289
|
-
});
|
|
290
|
-
});
|