@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.
@@ -0,0 +1,501 @@
1
+ /**
2
+ * Attestation generation module
3
+ *
4
+ * This module provides functions for creating in-toto attestations and SLSA provenance
5
+ * statements that can be signed using Sigstore.
6
+ */
7
+
8
+ import { hashContent, hashFile } from "../hash";
9
+ import type {
10
+ EnactToolPredicate,
11
+ InTotoStatement,
12
+ InTotoSubject,
13
+ SLSAProvenancePredicate,
14
+ SLSAResourceDescriptor,
15
+ } from "./types";
16
+
17
+ // ============================================================================
18
+ // Constants
19
+ // ============================================================================
20
+
21
+ /**
22
+ * The primary Enact website/registry URL
23
+ * Used for attestation types, tool URLs, and documentation references
24
+ */
25
+ export const ENACT_BASE_URL = "https://enact.tools";
26
+
27
+ /** in-toto statement type */
28
+ export const INTOTO_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
29
+
30
+ /** SLSA Provenance predicate type v1.0 */
31
+ export const SLSA_PROVENANCE_TYPE = "https://slsa.dev/provenance/v1";
32
+
33
+ /** Enact tool attestation predicate type */
34
+ export const ENACT_TOOL_TYPE = `${ENACT_BASE_URL}/attestation/tool/v1`;
35
+
36
+ /** Enact audit attestation predicate type */
37
+ export const ENACT_AUDIT_TYPE = `${ENACT_BASE_URL}/attestation/audit/v1`;
38
+
39
+ /** Enact build type for SLSA provenance */
40
+ export const ENACT_BUILD_TYPE = `${ENACT_BASE_URL}/build/v1`;
41
+
42
+ // ============================================================================
43
+ // Subject Creation
44
+ // ============================================================================
45
+
46
+ /**
47
+ * Create an in-toto subject from content
48
+ *
49
+ * @param name - The subject name (e.g., file path or artifact identifier)
50
+ * @param content - The content to hash
51
+ * @returns The in-toto subject with sha256 digest
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * const subject = createSubjectFromContent("tool.yaml", yamlContent);
56
+ * // { name: "tool.yaml", digest: { sha256: "abc123..." } }
57
+ * ```
58
+ */
59
+ export function createSubjectFromContent(name: string, content: string | Buffer): InTotoSubject {
60
+ const hash = hashContent(content, "sha256");
61
+ return {
62
+ name,
63
+ digest: {
64
+ sha256: hash.digest,
65
+ },
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Create an in-toto subject from a file
71
+ *
72
+ * @param name - The subject name (can differ from file path)
73
+ * @param filePath - Path to the file to hash
74
+ * @returns Promise resolving to the in-toto subject
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * const subject = await createSubjectFromFile("my-tool@1.0.0", "/path/to/tool.yaml");
79
+ * ```
80
+ */
81
+ export async function createSubjectFromFile(
82
+ name: string,
83
+ filePath: string
84
+ ): Promise<InTotoSubject> {
85
+ const hash = await hashFile(filePath, { algorithm: "sha256" });
86
+ return {
87
+ name,
88
+ digest: {
89
+ sha256: hash.digest,
90
+ },
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Create an in-toto subject with multiple digest algorithms
96
+ *
97
+ * @param name - The subject name
98
+ * @param content - The content to hash
99
+ * @returns Subject with both sha256 and sha512 digests
100
+ */
101
+ export function createSubjectWithMultipleDigests(
102
+ name: string,
103
+ content: string | Buffer
104
+ ): InTotoSubject {
105
+ const sha256 = hashContent(content, "sha256");
106
+ const sha512 = hashContent(content, "sha512");
107
+
108
+ return {
109
+ name,
110
+ digest: {
111
+ sha256: sha256.digest,
112
+ sha512: sha512.digest,
113
+ },
114
+ };
115
+ }
116
+
117
+ // ============================================================================
118
+ // Statement Creation
119
+ // ============================================================================
120
+
121
+ /**
122
+ * Create a generic in-toto statement
123
+ *
124
+ * @param subjects - The subjects (artifacts) covered by this attestation
125
+ * @param predicateType - The predicate type URI
126
+ * @param predicate - The predicate content
127
+ * @returns The in-toto statement
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * const statement = createStatement(
132
+ * [subject],
133
+ * "https://example.com/predicate/v1",
134
+ * { customField: "value" }
135
+ * );
136
+ * ```
137
+ */
138
+ export function createStatement<T>(
139
+ subjects: InTotoSubject[],
140
+ predicateType: string,
141
+ predicate: T
142
+ ): InTotoStatement<T> {
143
+ return {
144
+ _type: INTOTO_STATEMENT_TYPE,
145
+ subject: subjects,
146
+ predicateType,
147
+ predicate,
148
+ };
149
+ }
150
+
151
+ // ============================================================================
152
+ // SLSA Provenance
153
+ // ============================================================================
154
+
155
+ /**
156
+ * Options for creating SLSA provenance
157
+ */
158
+ export interface SLSAProvenanceOptions {
159
+ /** Build type URI (e.g., "https://enact.tools/build/v1") */
160
+ buildType: string;
161
+ /** Builder ID (e.g., "https://github.com/enact-dev/enact-cli") */
162
+ builderId: string;
163
+ /** External parameters (inputs to the build) */
164
+ externalParameters?: Record<string, unknown>;
165
+ /** Internal parameters (builder-controlled) */
166
+ internalParameters?: Record<string, unknown>;
167
+ /** Source dependencies */
168
+ resolvedDependencies?: SLSAResourceDescriptor[];
169
+ /** Build invocation ID */
170
+ invocationId?: string;
171
+ /** Build start time */
172
+ startedOn?: Date;
173
+ /** Build finish time */
174
+ finishedOn?: Date;
175
+ }
176
+
177
+ /**
178
+ * Create a SLSA provenance predicate
179
+ *
180
+ * @param options - Provenance options
181
+ * @returns The SLSA provenance predicate
182
+ *
183
+ * @example
184
+ * ```ts
185
+ * const provenance = createSLSAProvenance({
186
+ * buildType: "https://enact.tools/build/v1",
187
+ * builderId: "https://github.com/enact-dev/enact-cli@v2.0.0",
188
+ * externalParameters: {
189
+ * manifestPath: "tool.yaml"
190
+ * }
191
+ * });
192
+ * ```
193
+ */
194
+ export function createSLSAProvenance(options: SLSAProvenanceOptions): SLSAProvenancePredicate {
195
+ const provenance: SLSAProvenancePredicate = {
196
+ buildDefinition: {
197
+ buildType: options.buildType,
198
+ externalParameters: options.externalParameters || {},
199
+ },
200
+ runDetails: {
201
+ builder: {
202
+ id: options.builderId,
203
+ },
204
+ },
205
+ };
206
+
207
+ // Add optional fields
208
+ if (options.internalParameters) {
209
+ provenance.buildDefinition.internalParameters = options.internalParameters;
210
+ }
211
+
212
+ if (options.resolvedDependencies) {
213
+ provenance.buildDefinition.resolvedDependencies = options.resolvedDependencies;
214
+ }
215
+
216
+ // Add metadata if any timestamps are provided
217
+ if (options.invocationId || options.startedOn || options.finishedOn) {
218
+ provenance.runDetails.metadata = {};
219
+
220
+ if (options.invocationId) {
221
+ provenance.runDetails.metadata.invocationId = options.invocationId;
222
+ }
223
+
224
+ if (options.startedOn) {
225
+ provenance.runDetails.metadata.startedOn = options.startedOn.toISOString();
226
+ }
227
+
228
+ if (options.finishedOn) {
229
+ provenance.runDetails.metadata.finishedOn = options.finishedOn.toISOString();
230
+ }
231
+ }
232
+
233
+ return provenance;
234
+ }
235
+
236
+ /**
237
+ * Create a SLSA provenance statement for an artifact
238
+ *
239
+ * @param subjects - The artifacts to attest
240
+ * @param options - Provenance options
241
+ * @returns The complete in-toto statement with SLSA provenance
242
+ */
243
+ export function createSLSAProvenanceStatement(
244
+ subjects: InTotoSubject[],
245
+ options: SLSAProvenanceOptions
246
+ ): InTotoStatement<SLSAProvenancePredicate> {
247
+ const provenance = createSLSAProvenance(options);
248
+ return createStatement(subjects, SLSA_PROVENANCE_TYPE, provenance);
249
+ }
250
+
251
+ // ============================================================================
252
+ // Enact Tool Attestation
253
+ // ============================================================================
254
+
255
+ /**
256
+ * Options for creating an Enact tool attestation
257
+ */
258
+ export interface EnactToolAttestationOptions {
259
+ /** Tool name */
260
+ name: string;
261
+ /** Tool version */
262
+ version: string;
263
+ /** Tool publisher identity (email or URI) */
264
+ publisher: string;
265
+ /** Tool description */
266
+ description?: string;
267
+ /** Tool repository URL */
268
+ repository?: string;
269
+ /** Build timestamp */
270
+ buildTimestamp?: Date;
271
+ /** Build environment info */
272
+ buildEnvironment?: Record<string, string>;
273
+ /** Source commit SHA */
274
+ sourceCommit?: string;
275
+ /** Bundle hash (for remote signing) */
276
+ bundleHash?: string;
277
+ }
278
+
279
+ /**
280
+ * Create an Enact tool attestation predicate
281
+ *
282
+ * @param options - Tool attestation options
283
+ * @returns The Enact tool predicate
284
+ *
285
+ * @example
286
+ * ```ts
287
+ * const toolPredicate = createEnactToolPredicate({
288
+ * name: "my-tool",
289
+ * version: "1.0.0",
290
+ * publisher: "user@example.com",
291
+ * description: "A useful tool"
292
+ * });
293
+ * ```
294
+ */
295
+ export function createEnactToolPredicate(options: EnactToolAttestationOptions): EnactToolPredicate {
296
+ const predicate: EnactToolPredicate = {
297
+ type: ENACT_TOOL_TYPE,
298
+ tool: {
299
+ name: options.name,
300
+ version: options.version,
301
+ publisher: options.publisher,
302
+ },
303
+ };
304
+
305
+ // Add optional tool fields
306
+ if (options.description) {
307
+ predicate.tool.description = options.description;
308
+ }
309
+
310
+ if (options.repository) {
311
+ predicate.tool.repository = options.repository;
312
+ }
313
+
314
+ // Add build information if provided
315
+ if (options.buildTimestamp || options.buildEnvironment || options.sourceCommit) {
316
+ predicate.build = {
317
+ timestamp: (options.buildTimestamp || new Date()).toISOString(),
318
+ };
319
+
320
+ if (options.buildEnvironment) {
321
+ predicate.build.environment = options.buildEnvironment;
322
+ }
323
+
324
+ if (options.sourceCommit) {
325
+ predicate.build.sourceCommit = options.sourceCommit;
326
+ }
327
+ }
328
+
329
+ return predicate;
330
+ }
331
+
332
+ /**
333
+ * Create an Enact tool attestation statement
334
+ *
335
+ * @param manifestContent - The tool manifest content
336
+ * @param options - Tool attestation options
337
+ * @returns The complete in-toto statement for the tool
338
+ */
339
+ export function createEnactToolStatement(
340
+ manifestContent: string | Buffer,
341
+ options: EnactToolAttestationOptions
342
+ ): InTotoStatement<EnactToolPredicate> {
343
+ const subject = createSubjectFromContent(`${options.name}@${options.version}`, manifestContent);
344
+ const predicate = createEnactToolPredicate(options);
345
+ return createStatement([subject], ENACT_TOOL_TYPE, predicate);
346
+ }
347
+
348
+ // ============================================================================
349
+ // Enact Audit Attestation
350
+ // ============================================================================
351
+
352
+ /**
353
+ * Options for creating an Enact audit attestation
354
+ */
355
+ export interface EnactAuditAttestationOptions {
356
+ /** Tool name being audited */
357
+ toolName: string;
358
+ /** Tool version being audited */
359
+ toolVersion: string;
360
+ /** Auditor identity (email or URI) */
361
+ auditor: string;
362
+ /** Audit result */
363
+ result: "passed" | "passed-with-warnings" | "failed";
364
+ /** Audit timestamp */
365
+ timestamp?: Date;
366
+ /** Audit notes */
367
+ notes?: string;
368
+ }
369
+
370
+ /**
371
+ * Audit predicate structure
372
+ */
373
+ export interface EnactAuditPredicate {
374
+ type: typeof ENACT_AUDIT_TYPE;
375
+ tool: {
376
+ name: string;
377
+ version: string;
378
+ };
379
+ audit: {
380
+ auditor: string;
381
+ timestamp: string;
382
+ result: "passed" | "passed-with-warnings" | "failed";
383
+ notes?: string;
384
+ };
385
+ }
386
+
387
+ /**
388
+ * Create an Enact audit attestation predicate
389
+ *
390
+ * @param options - Audit attestation options
391
+ * @returns The Enact audit predicate
392
+ */
393
+ export function createEnactAuditPredicate(
394
+ options: EnactAuditAttestationOptions
395
+ ): EnactAuditPredicate {
396
+ const predicate: EnactAuditPredicate = {
397
+ type: ENACT_AUDIT_TYPE,
398
+ tool: {
399
+ name: options.toolName,
400
+ version: options.toolVersion,
401
+ },
402
+ audit: {
403
+ auditor: options.auditor,
404
+ timestamp: (options.timestamp || new Date()).toISOString(),
405
+ result: options.result,
406
+ },
407
+ };
408
+
409
+ if (options.notes) {
410
+ predicate.audit.notes = options.notes;
411
+ }
412
+
413
+ return predicate;
414
+ }
415
+
416
+ /**
417
+ * Create an Enact audit attestation statement
418
+ *
419
+ * @param manifestContent - The tool manifest content being audited
420
+ * @param options - Audit attestation options
421
+ * @returns The complete in-toto statement for the audit
422
+ */
423
+ export function createEnactAuditStatement(
424
+ manifestContent: string | Buffer,
425
+ options: EnactAuditAttestationOptions
426
+ ): InTotoStatement<EnactAuditPredicate> {
427
+ const subject = createSubjectFromContent(
428
+ `${options.toolName}@${options.toolVersion}`,
429
+ manifestContent
430
+ );
431
+ const predicate = createEnactAuditPredicate(options);
432
+ return createStatement([subject], ENACT_AUDIT_TYPE, predicate);
433
+ }
434
+
435
+ // ============================================================================
436
+ // Resource Descriptors
437
+ // ============================================================================
438
+
439
+ /**
440
+ * Create a SLSA resource descriptor for a file
441
+ *
442
+ * @param filePath - Path to the file
443
+ * @param options - Additional descriptor options
444
+ * @returns Promise resolving to the resource descriptor
445
+ */
446
+ export async function createResourceDescriptorFromFile(
447
+ filePath: string,
448
+ options: {
449
+ uri?: string;
450
+ name?: string;
451
+ downloadLocation?: string;
452
+ mediaType?: string;
453
+ } = {}
454
+ ): Promise<SLSAResourceDescriptor> {
455
+ const hash = await hashFile(filePath, { algorithm: "sha256" });
456
+
457
+ const descriptor: SLSAResourceDescriptor = {
458
+ name: options.name || filePath,
459
+ digest: {
460
+ sha256: hash.digest,
461
+ },
462
+ };
463
+
464
+ if (options.uri) descriptor.uri = options.uri;
465
+ if (options.downloadLocation) descriptor.downloadLocation = options.downloadLocation;
466
+ if (options.mediaType) descriptor.mediaType = options.mediaType;
467
+
468
+ return descriptor;
469
+ }
470
+
471
+ /**
472
+ * Create a SLSA resource descriptor from content
473
+ *
474
+ * @param content - The content
475
+ * @param options - Descriptor options
476
+ * @returns The resource descriptor
477
+ */
478
+ export function createResourceDescriptorFromContent(
479
+ content: string | Buffer,
480
+ options: {
481
+ uri?: string;
482
+ name?: string;
483
+ downloadLocation?: string;
484
+ mediaType?: string;
485
+ } = {}
486
+ ): SLSAResourceDescriptor {
487
+ const hash = hashContent(content, "sha256");
488
+
489
+ const descriptor: SLSAResourceDescriptor = {
490
+ digest: {
491
+ sha256: hash.digest,
492
+ },
493
+ };
494
+
495
+ if (options.uri) descriptor.uri = options.uri;
496
+ if (options.name) descriptor.name = options.name;
497
+ if (options.downloadLocation) descriptor.downloadLocation = options.downloadLocation;
498
+ if (options.mediaType) descriptor.mediaType = options.mediaType;
499
+
500
+ return descriptor;
501
+ }