@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,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
|
+
}
|