@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,450 @@
1
+ /**
2
+ * Trust policy evaluation module
3
+ *
4
+ * This module provides functions for creating and evaluating trust policies
5
+ * that determine whether an artifact should be trusted based on its attestations.
6
+ */
7
+
8
+ import { ENACT_AUDIT_TYPE, ENACT_TOOL_TYPE, SLSA_PROVENANCE_TYPE } from "./attestation";
9
+ import { extractIdentityFromBundle } from "./signing";
10
+ import type {
11
+ InTotoStatement,
12
+ OIDCIdentity,
13
+ SigstoreBundle,
14
+ TrustPolicy,
15
+ TrustPolicyResult,
16
+ TrustedIdentityRule,
17
+ VerifiedAttestation,
18
+ } from "./types";
19
+ import { verifyBundle } from "./verification";
20
+
21
+ // ============================================================================
22
+ // Default Policy
23
+ // ============================================================================
24
+
25
+ /**
26
+ * Default trust policy - requires publisher attestation
27
+ */
28
+ export const DEFAULT_TRUST_POLICY: TrustPolicy = {
29
+ name: "default",
30
+ version: "1.0",
31
+ trustedPublishers: [],
32
+ trustedAuditors: [],
33
+ requiredAttestations: [ENACT_TOOL_TYPE],
34
+ minimumSLSALevel: 0,
35
+ allowUnsigned: false,
36
+ cacheResults: true,
37
+ };
38
+
39
+ /**
40
+ * Permissive policy - allows unsigned tools (for development)
41
+ */
42
+ export const PERMISSIVE_POLICY: TrustPolicy = {
43
+ name: "permissive",
44
+ version: "1.0",
45
+ trustedPublishers: [],
46
+ trustedAuditors: [],
47
+ requiredAttestations: [],
48
+ minimumSLSALevel: 0,
49
+ allowUnsigned: true,
50
+ cacheResults: false,
51
+ };
52
+
53
+ /**
54
+ * Strict policy - requires publisher + auditor attestations and SLSA level 2+
55
+ */
56
+ export const STRICT_POLICY: TrustPolicy = {
57
+ name: "strict",
58
+ version: "1.0",
59
+ trustedPublishers: [],
60
+ trustedAuditors: [],
61
+ requiredAttestations: [ENACT_TOOL_TYPE, ENACT_AUDIT_TYPE],
62
+ minimumSLSALevel: 2,
63
+ allowUnsigned: false,
64
+ cacheResults: true,
65
+ };
66
+
67
+ // ============================================================================
68
+ // Policy Creation
69
+ // ============================================================================
70
+
71
+ /**
72
+ * Create a trust policy
73
+ *
74
+ * @param options - Policy options
75
+ * @returns The trust policy
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * const policy = createTrustPolicy({
80
+ * name: "my-org-policy",
81
+ * trustedPublishers: [
82
+ * { name: "My Team", type: "email", pattern: "*@myorg.com" }
83
+ * ],
84
+ * minimumSLSALevel: 1
85
+ * });
86
+ * ```
87
+ */
88
+ export function createTrustPolicy(options: Partial<TrustPolicy> & { name: string }): TrustPolicy {
89
+ return {
90
+ ...DEFAULT_TRUST_POLICY,
91
+ ...options,
92
+ version: options.version || "1.0",
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Create a trusted identity rule
98
+ *
99
+ * @param name - Rule name
100
+ * @param type - Identity type
101
+ * @param pattern - Pattern to match
102
+ * @param options - Additional options
103
+ * @returns The identity rule
104
+ */
105
+ export function createIdentityRule(
106
+ name: string,
107
+ type: TrustedIdentityRule["type"],
108
+ pattern: string,
109
+ options: { issuer?: string; requiredClaims?: Record<string, string | string[]> } = {}
110
+ ): TrustedIdentityRule {
111
+ const rule: TrustedIdentityRule = {
112
+ name,
113
+ type,
114
+ pattern,
115
+ };
116
+
117
+ if (options.issuer) {
118
+ rule.issuer = options.issuer;
119
+ }
120
+
121
+ if (options.requiredClaims) {
122
+ rule.requiredClaims = options.requiredClaims;
123
+ }
124
+
125
+ return rule;
126
+ }
127
+
128
+ // ============================================================================
129
+ // Policy Evaluation
130
+ // ============================================================================
131
+
132
+ /**
133
+ * Evaluate trust policy for a set of attestations
134
+ *
135
+ * @param attestationBundles - Array of Sigstore bundles containing attestations
136
+ * @param policy - The trust policy to evaluate against
137
+ * @returns The trust policy evaluation result
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * const result = await evaluateTrustPolicy(bundles, myPolicy);
142
+ * if (result.trusted) {
143
+ * console.log(`Trusted at level ${result.trustLevel}`);
144
+ * }
145
+ * ```
146
+ */
147
+ export async function evaluateTrustPolicy(
148
+ attestationBundles: SigstoreBundle[],
149
+ policy: TrustPolicy
150
+ ): Promise<TrustPolicyResult> {
151
+ const result: TrustPolicyResult = {
152
+ trusted: false,
153
+ trustLevel: 0,
154
+ matchedAuditors: [],
155
+ details: {
156
+ attestations: [],
157
+ violations: [],
158
+ warnings: [],
159
+ },
160
+ };
161
+
162
+ // If no attestations and unsigned allowed, trust with level 0
163
+ if (attestationBundles.length === 0) {
164
+ if (policy.allowUnsigned) {
165
+ result.trusted = true;
166
+ result.details.warnings.push("No attestations found - trusting unsigned artifact");
167
+ return result;
168
+ }
169
+
170
+ result.details.violations.push("No attestations found and policy requires signed artifacts");
171
+ return result;
172
+ }
173
+
174
+ // Verify all attestation bundles and extract information
175
+ const verifiedAttestations: VerifiedAttestation[] = [];
176
+
177
+ for (const bundle of attestationBundles) {
178
+ try {
179
+ const verificationResult = await verifyBundle(bundle);
180
+
181
+ if (!verificationResult.verified) {
182
+ result.details.violations.push(
183
+ `Attestation verification failed: ${verificationResult.error}`
184
+ );
185
+ continue;
186
+ }
187
+
188
+ // Extract attestation from DSSE envelope
189
+ const attestation = extractAttestationFromBundle(bundle);
190
+ if (!attestation) {
191
+ result.details.warnings.push("Could not extract attestation from bundle");
192
+ continue;
193
+ }
194
+
195
+ const identity = extractIdentityFromBundle(bundle);
196
+ if (!identity) {
197
+ result.details.warnings.push("Could not extract identity from bundle");
198
+ continue;
199
+ }
200
+
201
+ verifiedAttestations.push({
202
+ type: attestation.predicateType,
203
+ predicateType: attestation.predicateType,
204
+ signer: identity,
205
+ verifiedAt: new Date(),
206
+ attestation,
207
+ });
208
+ } catch (error) {
209
+ const message = error instanceof Error ? error.message : String(error);
210
+ result.details.violations.push(`Attestation verification error: ${message}`);
211
+ }
212
+ }
213
+
214
+ result.details.attestations = verifiedAttestations;
215
+
216
+ // Check required attestation types
217
+ if (policy.requiredAttestations && policy.requiredAttestations.length > 0) {
218
+ const foundTypes = new Set(verifiedAttestations.map((a) => a.predicateType));
219
+
220
+ for (const required of policy.requiredAttestations) {
221
+ if (!foundTypes.has(required)) {
222
+ result.details.violations.push(`Required attestation type not found: ${required}`);
223
+ }
224
+ }
225
+ }
226
+
227
+ // Find matching publisher
228
+ const publisherAttestation = verifiedAttestations.find(
229
+ (a) => a.predicateType === ENACT_TOOL_TYPE
230
+ );
231
+
232
+ if (publisherAttestation) {
233
+ const matchedPublisher = findMatchingRule(
234
+ publisherAttestation.signer,
235
+ policy.trustedPublishers
236
+ );
237
+
238
+ if (matchedPublisher) {
239
+ result.matchedPublisher = matchedPublisher;
240
+ result.trustLevel = Math.max(result.trustLevel, 1) as 0 | 1 | 2 | 3 | 4;
241
+ } else if (policy.trustedPublishers.length > 0) {
242
+ result.details.violations.push(
243
+ "Publisher identity does not match any trusted publisher rule"
244
+ );
245
+ }
246
+ }
247
+
248
+ // Find matching auditors
249
+ const auditorAttestations = verifiedAttestations.filter(
250
+ (a) => a.predicateType === ENACT_AUDIT_TYPE
251
+ );
252
+
253
+ for (const auditorAttestation of auditorAttestations) {
254
+ const matchedAuditor = findMatchingRule(auditorAttestation.signer, policy.trustedAuditors);
255
+
256
+ if (matchedAuditor) {
257
+ result.matchedAuditors.push(matchedAuditor);
258
+ result.trustLevel = Math.max(result.trustLevel, 2) as 0 | 1 | 2 | 3 | 4;
259
+ }
260
+ }
261
+
262
+ // Check SLSA provenance for higher trust levels
263
+ const provenanceAttestation = verifiedAttestations.find(
264
+ (a) => a.predicateType === SLSA_PROVENANCE_TYPE
265
+ );
266
+
267
+ if (provenanceAttestation) {
268
+ const slsaLevel = determineSLSALevel(provenanceAttestation.attestation);
269
+ result.trustLevel = Math.max(result.trustLevel, slsaLevel) as 0 | 1 | 2 | 3 | 4;
270
+ }
271
+
272
+ // Check minimum SLSA level
273
+ if (policy.minimumSLSALevel && result.trustLevel < policy.minimumSLSALevel) {
274
+ result.details.violations.push(
275
+ `Trust level ${result.trustLevel} is below minimum required ${policy.minimumSLSALevel}`
276
+ );
277
+ }
278
+
279
+ // Determine final trust status
280
+ result.trusted = result.details.violations.length === 0;
281
+
282
+ return result;
283
+ }
284
+
285
+ /**
286
+ * Quick check if an artifact should be trusted
287
+ *
288
+ * @param attestationBundles - Array of Sigstore bundles
289
+ * @param policy - Trust policy (defaults to DEFAULT_TRUST_POLICY)
290
+ * @returns True if artifact is trusted
291
+ */
292
+ export async function isTrusted(
293
+ attestationBundles: SigstoreBundle[],
294
+ policy: TrustPolicy = DEFAULT_TRUST_POLICY
295
+ ): Promise<boolean> {
296
+ const result = await evaluateTrustPolicy(attestationBundles, policy);
297
+ return result.trusted;
298
+ }
299
+
300
+ // ============================================================================
301
+ // Helper Functions
302
+ // ============================================================================
303
+
304
+ /**
305
+ * Find a matching identity rule for the given identity
306
+ */
307
+ function findMatchingRule(
308
+ identity: OIDCIdentity,
309
+ rules: TrustedIdentityRule[]
310
+ ): TrustedIdentityRule | undefined {
311
+ for (const rule of rules) {
312
+ if (matchesIdentityRule(identity, rule)) {
313
+ return rule;
314
+ }
315
+ }
316
+ return undefined;
317
+ }
318
+
319
+ /**
320
+ * Check if an identity matches a rule
321
+ */
322
+ function matchesIdentityRule(identity: OIDCIdentity, rule: TrustedIdentityRule): boolean {
323
+ // Check issuer first if specified
324
+ if (rule.issuer && identity.issuer !== rule.issuer) {
325
+ return false;
326
+ }
327
+
328
+ // Match based on rule type
329
+ switch (rule.type) {
330
+ case "email":
331
+ return matchesPattern(identity.email || "", rule.pattern);
332
+
333
+ case "github-workflow":
334
+ return matchesPattern(identity.workflowRepository || "", rule.pattern);
335
+
336
+ case "gitlab-pipeline":
337
+ // GitLab uses subject for pipeline identity
338
+ return matchesPattern(identity.subject, rule.pattern);
339
+
340
+ case "uri":
341
+ return matchesPattern(identity.subject, rule.pattern);
342
+
343
+ default:
344
+ return false;
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Match a value against a glob-like pattern
350
+ * Supports * for any characters and ? for single character
351
+ */
352
+ function matchesPattern(value: string, pattern: string): boolean {
353
+ // Convert glob pattern to regex
354
+ const regexPattern = pattern
355
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&") // Escape regex special chars
356
+ .replace(/\*/g, ".*") // * matches any characters
357
+ .replace(/\?/g, "."); // ? matches single character
358
+
359
+ const regex = new RegExp(`^${regexPattern}$`, "i");
360
+ return regex.test(value);
361
+ }
362
+
363
+ /**
364
+ * Extract in-toto statement from a Sigstore bundle
365
+ */
366
+ function extractAttestationFromBundle(bundle: SigstoreBundle): InTotoStatement | undefined {
367
+ if (!bundle.dsseEnvelope?.payload) {
368
+ return undefined;
369
+ }
370
+
371
+ try {
372
+ const payloadJson = Buffer.from(bundle.dsseEnvelope.payload, "base64").toString("utf8");
373
+ return JSON.parse(payloadJson) as InTotoStatement;
374
+ } catch {
375
+ return undefined;
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Determine SLSA level from provenance attestation
381
+ */
382
+ function determineSLSALevel(attestation: InTotoStatement): 0 | 1 | 2 | 3 | 4 {
383
+ if (attestation.predicateType !== SLSA_PROVENANCE_TYPE) {
384
+ return 0;
385
+ }
386
+
387
+ // biome-ignore lint/suspicious/noExplicitAny: Predicate structure varies
388
+ const predicate = attestation.predicate as any;
389
+
390
+ // SLSA Level 1: Provenance exists
391
+ if (!predicate?.buildDefinition || !predicate?.runDetails) {
392
+ return 0;
393
+ }
394
+
395
+ let level: 0 | 1 | 2 | 3 | 4 = 1;
396
+
397
+ // SLSA Level 2: Hosted build platform
398
+ if (predicate.runDetails?.builder?.id) {
399
+ level = 2;
400
+ }
401
+
402
+ // SLSA Level 3: Hardened builds (check for specific builder features)
403
+ if (
404
+ predicate.buildDefinition?.internalParameters &&
405
+ predicate.buildDefinition?.resolvedDependencies
406
+ ) {
407
+ level = 3;
408
+ }
409
+
410
+ // SLSA Level 4: Would require additional verification of builder security
411
+ // This is simplified - real implementation would check builder attestations
412
+
413
+ return level;
414
+ }
415
+
416
+ // ============================================================================
417
+ // Policy Serialization
418
+ // ============================================================================
419
+
420
+ /**
421
+ * Serialize a trust policy to JSON
422
+ */
423
+ export function serializeTrustPolicy(policy: TrustPolicy): string {
424
+ return JSON.stringify(policy, null, 2);
425
+ }
426
+
427
+ /**
428
+ * Deserialize a trust policy from JSON
429
+ */
430
+ export function deserializeTrustPolicy(json: string): TrustPolicy {
431
+ const parsed = JSON.parse(json);
432
+
433
+ // Validate required fields
434
+ if (!parsed.name || typeof parsed.name !== "string") {
435
+ throw new Error("Invalid trust policy: missing or invalid name");
436
+ }
437
+
438
+ if (!Array.isArray(parsed.trustedPublishers)) {
439
+ throw new Error("Invalid trust policy: trustedPublishers must be an array");
440
+ }
441
+
442
+ if (!Array.isArray(parsed.trustedAuditors)) {
443
+ throw new Error("Invalid trust policy: trustedAuditors must be an array");
444
+ }
445
+
446
+ return {
447
+ ...DEFAULT_TRUST_POLICY,
448
+ ...parsed,
449
+ };
450
+ }