@enactprotocol/trust 2.0.0
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 +38 -0
- package/package.json +32 -0
- package/src/hash.ts +120 -0
- package/src/index.ts +28 -0
- package/src/keys.ts +157 -0
- package/src/sigstore/attestation.ts +501 -0
- package/src/sigstore/cosign.ts +564 -0
- package/src/sigstore/index.ts +139 -0
- package/src/sigstore/oauth/client.ts +89 -0
- package/src/sigstore/oauth/index.ts +95 -0
- package/src/sigstore/oauth/server.ts +163 -0
- package/src/sigstore/policy.ts +450 -0
- package/src/sigstore/signing.ts +569 -0
- package/src/sigstore/types.ts +613 -0
- package/src/sigstore/verification.ts +355 -0
- package/src/types.ts +80 -0
- package/tests/hash.test.ts +180 -0
- package/tests/index.test.ts +8 -0
- package/tests/keys.test.ts +147 -0
- package/tests/sigstore/attestation.test.ts +369 -0
- package/tests/sigstore/policy.test.ts +260 -0
- package/tests/sigstore/signing.test.ts +220 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cosign CLI integration for interactive OIDC signing
|
|
3
|
+
*
|
|
4
|
+
* The sigstore-js library is designed for CI environments where OIDC tokens
|
|
5
|
+
* are available via environment variables. For interactive local signing,
|
|
6
|
+
* we shell out to the cosign CLI which handles the browser-based OAuth flow.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync, spawn } from "node:child_process";
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import type { SigstoreBundle } from "./types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if cosign CLI is available
|
|
17
|
+
*/
|
|
18
|
+
export function isCosignAvailable(): boolean {
|
|
19
|
+
try {
|
|
20
|
+
execSync("which cosign", { encoding: "utf-8", stdio: "pipe" });
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get cosign version information
|
|
29
|
+
*/
|
|
30
|
+
export function getCosignVersion(): string | undefined {
|
|
31
|
+
try {
|
|
32
|
+
const output = execSync("cosign version", { encoding: "utf-8", stdio: "pipe" });
|
|
33
|
+
const match = output.match(/GitVersion:\s+v?([\d.]+)/);
|
|
34
|
+
return match?.[1];
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Options for cosign signing
|
|
42
|
+
*/
|
|
43
|
+
export interface CosignSignOptions {
|
|
44
|
+
/** Timeout in milliseconds for the OIDC flow */
|
|
45
|
+
timeout?: number;
|
|
46
|
+
/** Output bundle path (if not provided, a temp file is used) */
|
|
47
|
+
outputPath?: string;
|
|
48
|
+
/** Whether to run in verbose mode */
|
|
49
|
+
verbose?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Result of cosign signing
|
|
54
|
+
*/
|
|
55
|
+
export interface CosignSignResult {
|
|
56
|
+
/** The Sigstore bundle */
|
|
57
|
+
bundle: SigstoreBundle;
|
|
58
|
+
/** Path where the bundle was saved */
|
|
59
|
+
bundlePath: string;
|
|
60
|
+
/** Signer identity (email) extracted from the bundle */
|
|
61
|
+
signerIdentity: string | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Sign a blob (file or buffer) using cosign with interactive OIDC
|
|
66
|
+
*
|
|
67
|
+
* This opens a browser for OAuth authentication with Sigstore's public
|
|
68
|
+
* OIDC provider. The signature, certificate, and Rekor entry are bundled
|
|
69
|
+
* together in the Sigstore bundle format.
|
|
70
|
+
*
|
|
71
|
+
* @param data - The data to sign (Buffer or path to file)
|
|
72
|
+
* @param options - Signing options
|
|
73
|
+
* @returns The signing result with bundle
|
|
74
|
+
*/
|
|
75
|
+
export async function signWithCosign(
|
|
76
|
+
data: Buffer | string,
|
|
77
|
+
options: CosignSignOptions = {}
|
|
78
|
+
): Promise<CosignSignResult> {
|
|
79
|
+
if (!isCosignAvailable()) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
"cosign CLI is not installed. Install it with: brew install cosign\n" +
|
|
82
|
+
"See: https://docs.sigstore.dev/cosign/system_config/installation/"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { timeout = 120000, outputPath, verbose = false } = options;
|
|
87
|
+
|
|
88
|
+
// Create temp directory for working files
|
|
89
|
+
const tempDir = join(tmpdir(), `enact-sign-${Date.now()}`);
|
|
90
|
+
mkdirSync(tempDir, { recursive: true });
|
|
91
|
+
|
|
92
|
+
const blobPath = join(tempDir, "blob");
|
|
93
|
+
const bundlePath = outputPath ?? join(tempDir, "bundle.json");
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Write data to temp file if it's a buffer
|
|
97
|
+
if (Buffer.isBuffer(data)) {
|
|
98
|
+
writeFileSync(blobPath, data);
|
|
99
|
+
} else if (typeof data === "string" && existsSync(data)) {
|
|
100
|
+
// It's a file path, copy to temp location
|
|
101
|
+
const content = readFileSync(data);
|
|
102
|
+
writeFileSync(blobPath, content);
|
|
103
|
+
} else {
|
|
104
|
+
// It's string content
|
|
105
|
+
writeFileSync(blobPath, data);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Run cosign sign-blob with bundle output
|
|
109
|
+
// The --yes flag auto-confirms the OIDC consent prompt
|
|
110
|
+
const args = [
|
|
111
|
+
"sign-blob",
|
|
112
|
+
"--yes", // Auto-confirm OIDC consent
|
|
113
|
+
"--bundle",
|
|
114
|
+
bundlePath,
|
|
115
|
+
"--output-signature",
|
|
116
|
+
"/dev/null", // We only want the bundle
|
|
117
|
+
"--output-certificate",
|
|
118
|
+
"/dev/null", // Bundle includes the cert
|
|
119
|
+
blobPath,
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
if (verbose) {
|
|
123
|
+
console.log(`Running: cosign ${args.join(" ")}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await new Promise<void>((resolve, reject) => {
|
|
127
|
+
const proc = spawn("cosign", args, {
|
|
128
|
+
stdio: verbose ? "inherit" : ["inherit", "pipe", "pipe"],
|
|
129
|
+
timeout,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
let stderr = "";
|
|
133
|
+
|
|
134
|
+
if (!verbose) {
|
|
135
|
+
proc.stderr?.on("data", (data) => {
|
|
136
|
+
stderr += data.toString();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
proc.on("error", (err) => {
|
|
141
|
+
reject(new Error(`Failed to run cosign: ${err.message}`));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
proc.on("close", (code) => {
|
|
145
|
+
if (code === 0) {
|
|
146
|
+
resolve();
|
|
147
|
+
} else {
|
|
148
|
+
// Check for common error patterns
|
|
149
|
+
if (stderr.includes("context deadline exceeded") || stderr.includes("timeout")) {
|
|
150
|
+
reject(
|
|
151
|
+
new Error(
|
|
152
|
+
"OIDC authentication timed out. Please try again and complete the browser flow."
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
} else if (stderr.includes("cancelled")) {
|
|
156
|
+
reject(new Error("Signing was cancelled."));
|
|
157
|
+
} else {
|
|
158
|
+
reject(new Error(`cosign exited with code ${code}: ${stderr || "(no output)"}`));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Read the bundle
|
|
165
|
+
if (!existsSync(bundlePath)) {
|
|
166
|
+
throw new Error("cosign did not produce a bundle file");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const bundleContent = readFileSync(bundlePath, "utf-8");
|
|
170
|
+
const bundle = JSON.parse(bundleContent) as SigstoreBundle;
|
|
171
|
+
|
|
172
|
+
// Extract signer identity from the bundle if possible
|
|
173
|
+
const signerIdentity = extractSignerFromBundle(bundle);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
bundle,
|
|
177
|
+
bundlePath,
|
|
178
|
+
signerIdentity,
|
|
179
|
+
};
|
|
180
|
+
} finally {
|
|
181
|
+
// Clean up temp files (but not the output bundle if specified)
|
|
182
|
+
try {
|
|
183
|
+
if (existsSync(blobPath)) {
|
|
184
|
+
unlinkSync(blobPath);
|
|
185
|
+
}
|
|
186
|
+
if (!outputPath && existsSync(bundlePath)) {
|
|
187
|
+
unlinkSync(bundlePath);
|
|
188
|
+
}
|
|
189
|
+
// Try to remove temp dir
|
|
190
|
+
if (existsSync(tempDir)) {
|
|
191
|
+
const { rmdirSync } = require("node:fs");
|
|
192
|
+
rmdirSync(tempDir, { recursive: true });
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
// Ignore cleanup errors
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Sign an in-toto attestation using cosign
|
|
202
|
+
*
|
|
203
|
+
* For in-toto attestations, we use cosign attest-blob which wraps the
|
|
204
|
+
* attestation in a DSSE envelope.
|
|
205
|
+
*
|
|
206
|
+
* @param attestation - The in-toto statement to sign
|
|
207
|
+
* @param options - Signing options
|
|
208
|
+
* @returns The signing result with bundle
|
|
209
|
+
*/
|
|
210
|
+
export async function attestWithCosign(
|
|
211
|
+
attestation: Record<string, unknown>,
|
|
212
|
+
options: CosignSignOptions = {}
|
|
213
|
+
): Promise<CosignSignResult> {
|
|
214
|
+
if (!isCosignAvailable()) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
"cosign CLI is not installed. Install it with: brew install cosign\n" +
|
|
217
|
+
"See: https://docs.sigstore.dev/cosign/system_config/installation/"
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const { timeout = 120000, outputPath, verbose = false } = options;
|
|
222
|
+
|
|
223
|
+
// Create temp directory for working files
|
|
224
|
+
const tempDir = join(tmpdir(), `enact-attest-${Date.now()}`);
|
|
225
|
+
mkdirSync(tempDir, { recursive: true });
|
|
226
|
+
|
|
227
|
+
const predicatePath = join(tempDir, "predicate.json");
|
|
228
|
+
const bundlePath = outputPath ?? join(tempDir, "bundle.json");
|
|
229
|
+
// cosign attest-blob needs a subject file (the thing being attested)
|
|
230
|
+
// For tool attestations, we'll create a dummy subject file
|
|
231
|
+
const subjectPath = join(tempDir, "subject");
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
// Extract the predicate from the in-toto statement
|
|
235
|
+
// cosign attest-blob takes the predicate separately
|
|
236
|
+
const statement = attestation as {
|
|
237
|
+
_type: string;
|
|
238
|
+
subject: Array<{ name: string; digest: Record<string, string> }>;
|
|
239
|
+
predicateType: string;
|
|
240
|
+
predicate: unknown;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Write the predicate to a file
|
|
244
|
+
writeFileSync(predicatePath, JSON.stringify(statement.predicate, null, 2));
|
|
245
|
+
|
|
246
|
+
// Create a subject file with the expected content
|
|
247
|
+
// The subject should be the content that matches the digest in the statement
|
|
248
|
+
// For now, we'll just create a placeholder and rely on the predicate
|
|
249
|
+
const subjectName = statement.subject?.[0]?.name ?? "tool.yaml";
|
|
250
|
+
writeFileSync(subjectPath, subjectName);
|
|
251
|
+
|
|
252
|
+
// Use cosign attest-blob
|
|
253
|
+
// Note: attest-blob is for custom predicates, which is what we have
|
|
254
|
+
const args = [
|
|
255
|
+
"attest-blob",
|
|
256
|
+
"--yes", // Auto-confirm OIDC consent
|
|
257
|
+
"--bundle",
|
|
258
|
+
bundlePath,
|
|
259
|
+
"--predicate",
|
|
260
|
+
predicatePath,
|
|
261
|
+
"--type",
|
|
262
|
+
statement.predicateType,
|
|
263
|
+
subjectPath,
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
if (verbose) {
|
|
267
|
+
console.log(`Running: cosign ${args.join(" ")}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
await new Promise<void>((resolve, reject) => {
|
|
271
|
+
const proc = spawn("cosign", args, {
|
|
272
|
+
stdio: verbose ? "inherit" : ["inherit", "pipe", "pipe"],
|
|
273
|
+
timeout,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
let stderr = "";
|
|
277
|
+
|
|
278
|
+
if (!verbose) {
|
|
279
|
+
proc.stderr?.on("data", (data) => {
|
|
280
|
+
stderr += data.toString();
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
proc.on("error", (err) => {
|
|
285
|
+
reject(new Error(`Failed to run cosign: ${err.message}`));
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
proc.on("close", (code) => {
|
|
289
|
+
if (code === 0) {
|
|
290
|
+
resolve();
|
|
291
|
+
} else {
|
|
292
|
+
if (stderr.includes("context deadline exceeded") || stderr.includes("timeout")) {
|
|
293
|
+
reject(
|
|
294
|
+
new Error(
|
|
295
|
+
"OIDC authentication timed out. Please try again and complete the browser flow."
|
|
296
|
+
)
|
|
297
|
+
);
|
|
298
|
+
} else if (stderr.includes("cancelled")) {
|
|
299
|
+
reject(new Error("Signing was cancelled."));
|
|
300
|
+
} else {
|
|
301
|
+
reject(new Error(`cosign exited with code ${code}: ${stderr || "(no output)"}`));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Read the bundle
|
|
308
|
+
if (!existsSync(bundlePath)) {
|
|
309
|
+
throw new Error("cosign did not produce a bundle file");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const bundleContent = readFileSync(bundlePath, "utf-8");
|
|
313
|
+
const bundle = JSON.parse(bundleContent) as SigstoreBundle;
|
|
314
|
+
|
|
315
|
+
// Extract signer identity from the bundle
|
|
316
|
+
const signerIdentity = extractSignerFromBundle(bundle);
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
bundle,
|
|
320
|
+
bundlePath,
|
|
321
|
+
signerIdentity,
|
|
322
|
+
};
|
|
323
|
+
} finally {
|
|
324
|
+
// Clean up temp files
|
|
325
|
+
try {
|
|
326
|
+
for (const file of [predicatePath, subjectPath]) {
|
|
327
|
+
if (existsSync(file)) {
|
|
328
|
+
unlinkSync(file);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (!outputPath && existsSync(bundlePath)) {
|
|
332
|
+
unlinkSync(bundlePath);
|
|
333
|
+
}
|
|
334
|
+
if (existsSync(tempDir)) {
|
|
335
|
+
const { rmdirSync } = require("node:fs");
|
|
336
|
+
rmdirSync(tempDir, { recursive: true });
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
// Ignore cleanup errors
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Verify a blob signature using cosign
|
|
346
|
+
*
|
|
347
|
+
* @param data - The data that was signed
|
|
348
|
+
* @param bundle - The Sigstore bundle
|
|
349
|
+
* @param expectedIdentity - Expected signer identity (email)
|
|
350
|
+
* @param expectedIssuer - Expected OIDC issuer
|
|
351
|
+
* @returns Whether verification succeeded
|
|
352
|
+
*/
|
|
353
|
+
export async function verifyWithCosign(
|
|
354
|
+
data: Buffer | string,
|
|
355
|
+
bundle: SigstoreBundle,
|
|
356
|
+
expectedIdentity?: string,
|
|
357
|
+
expectedIssuer?: string
|
|
358
|
+
): Promise<{ verified: boolean; error?: string | undefined; identity?: string | undefined }> {
|
|
359
|
+
if (!isCosignAvailable()) {
|
|
360
|
+
throw new Error("cosign CLI is not installed");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const tempDir = join(tmpdir(), `enact-verify-${Date.now()}`);
|
|
364
|
+
mkdirSync(tempDir, { recursive: true });
|
|
365
|
+
|
|
366
|
+
const blobPath = join(tempDir, "blob");
|
|
367
|
+
const bundlePath = join(tempDir, "bundle.json");
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
// Write data and bundle to temp files
|
|
371
|
+
if (Buffer.isBuffer(data)) {
|
|
372
|
+
writeFileSync(blobPath, data);
|
|
373
|
+
} else {
|
|
374
|
+
writeFileSync(blobPath, data);
|
|
375
|
+
}
|
|
376
|
+
writeFileSync(bundlePath, JSON.stringify(bundle, null, 2));
|
|
377
|
+
|
|
378
|
+
// Build cosign verify-blob command
|
|
379
|
+
const args = ["verify-blob", "--bundle", bundlePath];
|
|
380
|
+
|
|
381
|
+
if (expectedIdentity) {
|
|
382
|
+
args.push("--certificate-identity", expectedIdentity);
|
|
383
|
+
} else {
|
|
384
|
+
// Use regex to match any identity
|
|
385
|
+
args.push("--certificate-identity-regexp", ".*");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (expectedIssuer) {
|
|
389
|
+
args.push("--certificate-oidc-issuer", expectedIssuer);
|
|
390
|
+
} else {
|
|
391
|
+
// Match common Sigstore OIDC issuers
|
|
392
|
+
args.push(
|
|
393
|
+
"--certificate-oidc-issuer-regexp",
|
|
394
|
+
"(https://accounts.google.com|https://github.com/login/oauth|https://oauth2.sigstore.dev/auth)"
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
args.push(blobPath);
|
|
399
|
+
|
|
400
|
+
execSync(`cosign ${args.join(" ")}`, {
|
|
401
|
+
encoding: "utf-8",
|
|
402
|
+
stdio: "pipe",
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const identity = extractSignerFromBundle(bundle);
|
|
406
|
+
return {
|
|
407
|
+
verified: true,
|
|
408
|
+
error: undefined,
|
|
409
|
+
identity,
|
|
410
|
+
};
|
|
411
|
+
} catch (err) {
|
|
412
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
413
|
+
return {
|
|
414
|
+
verified: false,
|
|
415
|
+
error,
|
|
416
|
+
};
|
|
417
|
+
} finally {
|
|
418
|
+
// Clean up
|
|
419
|
+
try {
|
|
420
|
+
for (const file of [blobPath, bundlePath]) {
|
|
421
|
+
if (existsSync(file)) {
|
|
422
|
+
unlinkSync(file);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (existsSync(tempDir)) {
|
|
426
|
+
const { rmdirSync } = require("node:fs");
|
|
427
|
+
rmdirSync(tempDir, { recursive: true });
|
|
428
|
+
}
|
|
429
|
+
} catch {
|
|
430
|
+
// Ignore cleanup errors
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Extract signer identity (email) from a Sigstore bundle
|
|
437
|
+
*
|
|
438
|
+
* The certificate in the bundle contains the signer's email in the
|
|
439
|
+
* Subject Alternative Name (SAN) extension.
|
|
440
|
+
*/
|
|
441
|
+
function extractSignerFromBundle(bundle: SigstoreBundle): string | undefined {
|
|
442
|
+
try {
|
|
443
|
+
// The certificate is in verificationMaterial.certificate.rawBytes (base64)
|
|
444
|
+
const certB64 = (
|
|
445
|
+
bundle as unknown as {
|
|
446
|
+
verificationMaterial?: {
|
|
447
|
+
certificate?: {
|
|
448
|
+
rawBytes?: string;
|
|
449
|
+
};
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
)?.verificationMaterial?.certificate?.rawBytes;
|
|
453
|
+
|
|
454
|
+
if (!certB64) {
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Decode the certificate
|
|
459
|
+
const certDer = Buffer.from(certB64, "base64");
|
|
460
|
+
|
|
461
|
+
// Simple extraction of email from certificate
|
|
462
|
+
// Look for the email pattern in the SAN extension
|
|
463
|
+
// This is a simplified extraction - a proper implementation would parse X.509
|
|
464
|
+
const certStr = certDer.toString("latin1");
|
|
465
|
+
|
|
466
|
+
// Look for email pattern - match word chars, dots, hyphens, plus before @
|
|
467
|
+
// and domain after, but stop at non-word characters
|
|
468
|
+
const emailMatch = certStr.match(/[\w.+-]+@[\w.-]+\.[a-zA-Z]{2,}/);
|
|
469
|
+
return emailMatch?.[0];
|
|
470
|
+
} catch {
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Verify an attestation bundle using cosign
|
|
477
|
+
*
|
|
478
|
+
* @param bundle - The Sigstore bundle containing a DSSE-wrapped attestation
|
|
479
|
+
* @param expectedIdentity - Expected signer identity (email)
|
|
480
|
+
* @param expectedIssuer - Expected OIDC issuer
|
|
481
|
+
* @param predicateType - The attestation predicate type (optional)
|
|
482
|
+
* @returns Verification result
|
|
483
|
+
*/
|
|
484
|
+
export async function verifyAttestationWithCosign(
|
|
485
|
+
bundle: SigstoreBundle,
|
|
486
|
+
expectedIdentity?: string,
|
|
487
|
+
expectedIssuer?: string,
|
|
488
|
+
predicateType?: string
|
|
489
|
+
): Promise<{ verified: boolean; error?: string | undefined; identity?: string | undefined }> {
|
|
490
|
+
if (!isCosignAvailable()) {
|
|
491
|
+
throw new Error("cosign CLI is not installed");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const tempDir = join(tmpdir(), `enact-verify-attest-${Date.now()}`);
|
|
495
|
+
mkdirSync(tempDir, { recursive: true });
|
|
496
|
+
|
|
497
|
+
const bundlePath = join(tempDir, "bundle.json");
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
writeFileSync(bundlePath, JSON.stringify(bundle, null, 2));
|
|
501
|
+
|
|
502
|
+
// Build cosign verify-blob-attestation command
|
|
503
|
+
const args = ["verify-blob-attestation", "--bundle", bundlePath];
|
|
504
|
+
|
|
505
|
+
if (expectedIdentity) {
|
|
506
|
+
args.push("--certificate-identity", expectedIdentity);
|
|
507
|
+
} else {
|
|
508
|
+
args.push("--certificate-identity-regexp", ".*");
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (expectedIssuer) {
|
|
512
|
+
args.push("--certificate-oidc-issuer", expectedIssuer);
|
|
513
|
+
} else {
|
|
514
|
+
// Match common Sigstore OIDC issuers
|
|
515
|
+
args.push("--certificate-oidc-issuer-regexp", ".*");
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (predicateType) {
|
|
519
|
+
args.push("--type", predicateType);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Don't check claims against a subject file
|
|
523
|
+
args.push("--check-claims=false");
|
|
524
|
+
|
|
525
|
+
// Use /dev/null as the "subject" - attestation verification doesn't need it
|
|
526
|
+
args.push("/dev/null");
|
|
527
|
+
|
|
528
|
+
// Use spawnSync to avoid shell escaping issues
|
|
529
|
+
const { spawnSync } = require("node:child_process");
|
|
530
|
+
const result = spawnSync("cosign", args, {
|
|
531
|
+
encoding: "utf-8",
|
|
532
|
+
stdio: "pipe",
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
if (result.status !== 0) {
|
|
536
|
+
throw new Error(result.stderr || result.stdout || `cosign exited with code ${result.status}`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const identity = extractSignerFromBundle(bundle);
|
|
540
|
+
return {
|
|
541
|
+
verified: true,
|
|
542
|
+
error: undefined,
|
|
543
|
+
identity,
|
|
544
|
+
};
|
|
545
|
+
} catch (err) {
|
|
546
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
547
|
+
return {
|
|
548
|
+
verified: false,
|
|
549
|
+
error,
|
|
550
|
+
};
|
|
551
|
+
} finally {
|
|
552
|
+
try {
|
|
553
|
+
if (existsSync(bundlePath)) {
|
|
554
|
+
unlinkSync(bundlePath);
|
|
555
|
+
}
|
|
556
|
+
if (existsSync(tempDir)) {
|
|
557
|
+
const { rmdirSync } = require("node:fs");
|
|
558
|
+
rmdirSync(tempDir, { recursive: true });
|
|
559
|
+
}
|
|
560
|
+
} catch {
|
|
561
|
+
// Ignore cleanup errors
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sigstore integration for Enact
|
|
3
|
+
*
|
|
4
|
+
* This module provides Sigstore-based attestation signing and verification
|
|
5
|
+
* capabilities for the Enact tool ecosystem.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Types
|
|
9
|
+
export type {
|
|
10
|
+
// OIDC types
|
|
11
|
+
OIDCProvider,
|
|
12
|
+
OIDCIdentity,
|
|
13
|
+
OIDCOptions,
|
|
14
|
+
// Certificate types
|
|
15
|
+
FulcioCertificate,
|
|
16
|
+
FulcioCertificateOptions,
|
|
17
|
+
// Rekor types
|
|
18
|
+
RekorEntry,
|
|
19
|
+
RekorInclusionProof,
|
|
20
|
+
RekorEntryOptions,
|
|
21
|
+
// Attestation types
|
|
22
|
+
InTotoStatement,
|
|
23
|
+
InTotoSubject,
|
|
24
|
+
SLSAProvenancePredicate,
|
|
25
|
+
SLSAResourceDescriptor,
|
|
26
|
+
// Bundle types
|
|
27
|
+
SigstoreBundle,
|
|
28
|
+
TransparencyLogEntry,
|
|
29
|
+
// Signing/verification types
|
|
30
|
+
SigningOptions,
|
|
31
|
+
SigningResult,
|
|
32
|
+
VerificationOptions,
|
|
33
|
+
VerificationResult,
|
|
34
|
+
VerificationDetails,
|
|
35
|
+
ExpectedIdentity,
|
|
36
|
+
// Trust types
|
|
37
|
+
TrustRoot,
|
|
38
|
+
CertificateAuthority,
|
|
39
|
+
TransparencyLog,
|
|
40
|
+
TimestampAuthority,
|
|
41
|
+
TrustPolicy,
|
|
42
|
+
TrustedIdentityRule,
|
|
43
|
+
TrustPolicyResult,
|
|
44
|
+
VerifiedAttestation,
|
|
45
|
+
// Enact-specific types
|
|
46
|
+
EnactToolPredicate,
|
|
47
|
+
EnactAttestationBundle,
|
|
48
|
+
} from "./types";
|
|
49
|
+
|
|
50
|
+
// Signing
|
|
51
|
+
export {
|
|
52
|
+
signArtifact,
|
|
53
|
+
signAttestation,
|
|
54
|
+
extractOIDCIdentity,
|
|
55
|
+
extractCertificateFromBundle,
|
|
56
|
+
extractIdentityFromBundle,
|
|
57
|
+
detectOIDCProvider,
|
|
58
|
+
getOIDCTokenFromEnvironment,
|
|
59
|
+
FULCIO_PUBLIC_URL,
|
|
60
|
+
REKOR_PUBLIC_URL,
|
|
61
|
+
TSA_PUBLIC_URL,
|
|
62
|
+
OIDC_ISSUERS,
|
|
63
|
+
} from "./signing";
|
|
64
|
+
|
|
65
|
+
// OAuth Identity Provider (for interactive signing)
|
|
66
|
+
export {
|
|
67
|
+
OAuthIdentityProvider,
|
|
68
|
+
CallbackServer,
|
|
69
|
+
OAuthClient,
|
|
70
|
+
initializeOAuthClient,
|
|
71
|
+
SIGSTORE_OAUTH_ISSUER,
|
|
72
|
+
SIGSTORE_CLIENT_ID,
|
|
73
|
+
} from "./oauth";
|
|
74
|
+
export type {
|
|
75
|
+
OAuthIdentityProviderOptions,
|
|
76
|
+
IdentityProvider,
|
|
77
|
+
} from "./oauth";
|
|
78
|
+
|
|
79
|
+
// Cosign CLI integration (fallback for interactive signing)
|
|
80
|
+
export {
|
|
81
|
+
isCosignAvailable,
|
|
82
|
+
getCosignVersion,
|
|
83
|
+
signWithCosign,
|
|
84
|
+
attestWithCosign,
|
|
85
|
+
verifyWithCosign,
|
|
86
|
+
verifyAttestationWithCosign,
|
|
87
|
+
} from "./cosign";
|
|
88
|
+
export type { CosignSignOptions, CosignSignResult } from "./cosign";
|
|
89
|
+
|
|
90
|
+
// Verification
|
|
91
|
+
export {
|
|
92
|
+
verifyBundle,
|
|
93
|
+
createBundleVerifier,
|
|
94
|
+
isVerified,
|
|
95
|
+
} from "./verification";
|
|
96
|
+
|
|
97
|
+
// Attestation creation
|
|
98
|
+
export {
|
|
99
|
+
createSubjectFromContent,
|
|
100
|
+
createSubjectFromFile,
|
|
101
|
+
createSubjectWithMultipleDigests,
|
|
102
|
+
createStatement,
|
|
103
|
+
createSLSAProvenance,
|
|
104
|
+
createSLSAProvenanceStatement,
|
|
105
|
+
createEnactToolPredicate,
|
|
106
|
+
createEnactToolStatement,
|
|
107
|
+
createEnactAuditPredicate,
|
|
108
|
+
createEnactAuditStatement,
|
|
109
|
+
createResourceDescriptorFromFile,
|
|
110
|
+
createResourceDescriptorFromContent,
|
|
111
|
+
// Constants
|
|
112
|
+
ENACT_BASE_URL,
|
|
113
|
+
INTOTO_STATEMENT_TYPE,
|
|
114
|
+
SLSA_PROVENANCE_TYPE,
|
|
115
|
+
ENACT_TOOL_TYPE,
|
|
116
|
+
ENACT_AUDIT_TYPE,
|
|
117
|
+
ENACT_BUILD_TYPE,
|
|
118
|
+
} from "./attestation";
|
|
119
|
+
|
|
120
|
+
// Trust policy
|
|
121
|
+
export {
|
|
122
|
+
createTrustPolicy,
|
|
123
|
+
createIdentityRule,
|
|
124
|
+
evaluateTrustPolicy,
|
|
125
|
+
isTrusted,
|
|
126
|
+
serializeTrustPolicy,
|
|
127
|
+
deserializeTrustPolicy,
|
|
128
|
+
DEFAULT_TRUST_POLICY,
|
|
129
|
+
PERMISSIVE_POLICY,
|
|
130
|
+
STRICT_POLICY,
|
|
131
|
+
} from "./policy";
|
|
132
|
+
|
|
133
|
+
// Re-export attestation option types
|
|
134
|
+
export type {
|
|
135
|
+
SLSAProvenanceOptions,
|
|
136
|
+
EnactToolAttestationOptions,
|
|
137
|
+
EnactAuditAttestationOptions,
|
|
138
|
+
EnactAuditPredicate,
|
|
139
|
+
} from "./attestation";
|