@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,260 @@
1
+ /**
2
+ * Tests for Sigstore trust policy module
3
+ */
4
+
5
+ import { describe, expect, it } from "bun:test";
6
+ import { ENACT_AUDIT_TYPE, ENACT_TOOL_TYPE } from "../../src/sigstore/attestation";
7
+ import {
8
+ DEFAULT_TRUST_POLICY,
9
+ PERMISSIVE_POLICY,
10
+ STRICT_POLICY,
11
+ createIdentityRule,
12
+ createTrustPolicy,
13
+ deserializeTrustPolicy,
14
+ evaluateTrustPolicy,
15
+ isTrusted,
16
+ serializeTrustPolicy,
17
+ } from "../../src/sigstore/policy";
18
+
19
+ describe("Trust Policy", () => {
20
+ describe("Default Policies", () => {
21
+ it("should have default policy requiring tool attestation", () => {
22
+ expect(DEFAULT_TRUST_POLICY.name).toBe("default");
23
+ expect(DEFAULT_TRUST_POLICY.requiredAttestations).toContain(ENACT_TOOL_TYPE);
24
+ expect(DEFAULT_TRUST_POLICY.allowUnsigned).toBe(false);
25
+ expect(DEFAULT_TRUST_POLICY.minimumSLSALevel).toBe(0);
26
+ });
27
+
28
+ it("should have permissive policy allowing unsigned", () => {
29
+ expect(PERMISSIVE_POLICY.name).toBe("permissive");
30
+ expect(PERMISSIVE_POLICY.allowUnsigned).toBe(true);
31
+ expect(PERMISSIVE_POLICY.requiredAttestations).toEqual([]);
32
+ });
33
+
34
+ it("should have strict policy requiring auditor", () => {
35
+ expect(STRICT_POLICY.name).toBe("strict");
36
+ expect(STRICT_POLICY.requiredAttestations).toContain(ENACT_TOOL_TYPE);
37
+ expect(STRICT_POLICY.requiredAttestations).toContain(ENACT_AUDIT_TYPE);
38
+ expect(STRICT_POLICY.minimumSLSALevel).toBe(2);
39
+ expect(STRICT_POLICY.allowUnsigned).toBe(false);
40
+ });
41
+ });
42
+
43
+ describe("createTrustPolicy", () => {
44
+ it("should create policy with defaults", () => {
45
+ const policy = createTrustPolicy({ name: "my-policy" });
46
+
47
+ expect(policy.name).toBe("my-policy");
48
+ expect(policy.version).toBe("1.0");
49
+ expect(policy.trustedPublishers).toEqual([]);
50
+ expect(policy.trustedAuditors).toEqual([]);
51
+ expect(policy.allowUnsigned).toBe(false);
52
+ });
53
+
54
+ it("should override default values", () => {
55
+ const policy = createTrustPolicy({
56
+ name: "custom-policy",
57
+ allowUnsigned: true,
58
+ minimumSLSALevel: 2,
59
+ trustedPublishers: [{ name: "Test Publisher", type: "email", pattern: "*@example.com" }],
60
+ });
61
+
62
+ expect(policy.name).toBe("custom-policy");
63
+ expect(policy.allowUnsigned).toBe(true);
64
+ expect(policy.minimumSLSALevel).toBe(2);
65
+ expect(policy.trustedPublishers).toHaveLength(1);
66
+ });
67
+
68
+ it("should allow custom version", () => {
69
+ const policy = createTrustPolicy({
70
+ name: "versioned-policy",
71
+ version: "2.0",
72
+ });
73
+
74
+ expect(policy.version).toBe("2.0");
75
+ });
76
+ });
77
+
78
+ describe("createIdentityRule", () => {
79
+ it("should create email identity rule", () => {
80
+ const rule = createIdentityRule("My Team", "email", "*@myorg.com");
81
+
82
+ expect(rule.name).toBe("My Team");
83
+ expect(rule.type).toBe("email");
84
+ expect(rule.pattern).toBe("*@myorg.com");
85
+ });
86
+
87
+ it("should create GitHub workflow rule", () => {
88
+ const rule = createIdentityRule("GitHub CI", "github-workflow", "myorg/*", {
89
+ issuer: "https://token.actions.githubusercontent.com",
90
+ });
91
+
92
+ expect(rule.type).toBe("github-workflow");
93
+ expect(rule.pattern).toBe("myorg/*");
94
+ expect(rule.issuer).toBe("https://token.actions.githubusercontent.com");
95
+ });
96
+
97
+ it("should include required claims", () => {
98
+ const rule = createIdentityRule("Specific Workflow", "github-workflow", "myorg/repo", {
99
+ requiredClaims: {
100
+ ref: "refs/heads/main",
101
+ event_name: ["push", "workflow_dispatch"],
102
+ },
103
+ });
104
+
105
+ expect(rule.requiredClaims?.ref).toBe("refs/heads/main");
106
+ expect(rule.requiredClaims?.event_name).toEqual(["push", "workflow_dispatch"]);
107
+ });
108
+ });
109
+
110
+ describe("evaluateTrustPolicy", () => {
111
+ it("should trust empty bundles with permissive policy", async () => {
112
+ const result = await evaluateTrustPolicy([], PERMISSIVE_POLICY);
113
+
114
+ expect(result.trusted).toBe(true);
115
+ expect(result.trustLevel).toBe(0);
116
+ expect(result.details.warnings).toContain(
117
+ "No attestations found - trusting unsigned artifact"
118
+ );
119
+ });
120
+
121
+ it("should reject empty bundles with default policy", async () => {
122
+ const result = await evaluateTrustPolicy([], DEFAULT_TRUST_POLICY);
123
+
124
+ expect(result.trusted).toBe(false);
125
+ expect(result.trustLevel).toBe(0);
126
+ expect(result.details.violations).toContain(
127
+ "No attestations found and policy requires signed artifacts"
128
+ );
129
+ });
130
+ });
131
+
132
+ describe("isTrusted", () => {
133
+ it("should return boolean for quick check", async () => {
134
+ const result = await isTrusted([], PERMISSIVE_POLICY);
135
+ expect(result).toBe(true);
136
+ });
137
+
138
+ it("should use default policy when not specified", async () => {
139
+ const result = await isTrusted([]);
140
+ expect(result).toBe(false);
141
+ });
142
+ });
143
+
144
+ describe("serializeTrustPolicy", () => {
145
+ it("should serialize policy to JSON", () => {
146
+ const policy = createTrustPolicy({
147
+ name: "test-policy",
148
+ trustedPublishers: [{ name: "Publisher", type: "email", pattern: "*@example.com" }],
149
+ });
150
+
151
+ const json = serializeTrustPolicy(policy);
152
+ const parsed = JSON.parse(json);
153
+
154
+ expect(parsed.name).toBe("test-policy");
155
+ expect(parsed.trustedPublishers).toHaveLength(1);
156
+ });
157
+
158
+ it("should format JSON with indentation", () => {
159
+ const json = serializeTrustPolicy(DEFAULT_TRUST_POLICY);
160
+
161
+ expect(json).toContain("\n");
162
+ expect(json).toContain(" ");
163
+ });
164
+ });
165
+
166
+ describe("deserializeTrustPolicy", () => {
167
+ it("should deserialize valid JSON", () => {
168
+ const json = JSON.stringify({
169
+ name: "deserialized-policy",
170
+ version: "1.5",
171
+ trustedPublishers: [],
172
+ trustedAuditors: [],
173
+ });
174
+
175
+ const policy = deserializeTrustPolicy(json);
176
+
177
+ expect(policy.name).toBe("deserialized-policy");
178
+ expect(policy.version).toBe("1.5");
179
+ });
180
+
181
+ it("should apply defaults for missing fields", () => {
182
+ const json = JSON.stringify({
183
+ name: "minimal-policy",
184
+ trustedPublishers: [],
185
+ trustedAuditors: [],
186
+ });
187
+
188
+ const policy = deserializeTrustPolicy(json);
189
+
190
+ expect(policy.allowUnsigned).toBe(false);
191
+ expect(policy.cacheResults).toBe(true);
192
+ });
193
+
194
+ it("should throw on missing name", () => {
195
+ const json = JSON.stringify({
196
+ trustedPublishers: [],
197
+ trustedAuditors: [],
198
+ });
199
+
200
+ expect(() => deserializeTrustPolicy(json)).toThrow("missing or invalid name");
201
+ });
202
+
203
+ it("should throw on missing trustedPublishers", () => {
204
+ const json = JSON.stringify({
205
+ name: "bad-policy",
206
+ trustedAuditors: [],
207
+ });
208
+
209
+ expect(() => deserializeTrustPolicy(json)).toThrow("trustedPublishers must be an array");
210
+ });
211
+
212
+ it("should throw on missing trustedAuditors", () => {
213
+ const json = JSON.stringify({
214
+ name: "bad-policy",
215
+ trustedPublishers: [],
216
+ });
217
+
218
+ expect(() => deserializeTrustPolicy(json)).toThrow("trustedAuditors must be an array");
219
+ });
220
+
221
+ it("should roundtrip serialize/deserialize", () => {
222
+ const original = createTrustPolicy({
223
+ name: "roundtrip-policy",
224
+ version: "2.0",
225
+ minimumSLSALevel: 2,
226
+ trustedPublishers: [{ name: "Publisher", type: "email", pattern: "*@example.com" }],
227
+ trustedAuditors: [{ name: "Auditor", type: "github-workflow", pattern: "auditor-org/*" }],
228
+ });
229
+
230
+ const json = serializeTrustPolicy(original);
231
+ const restored = deserializeTrustPolicy(json);
232
+
233
+ expect(restored.name).toBe(original.name);
234
+ expect(restored.version).toBe(original.version);
235
+ expect(restored.minimumSLSALevel).toBe(original.minimumSLSALevel);
236
+ expect(restored.trustedPublishers).toEqual(original.trustedPublishers);
237
+ expect(restored.trustedAuditors).toEqual(original.trustedAuditors);
238
+ });
239
+ });
240
+
241
+ describe("Pattern Matching", () => {
242
+ // These tests verify the internal pattern matching behavior through policy evaluation
243
+ // We test by creating policies with specific patterns and checking if they would match
244
+
245
+ it("should support wildcard patterns", () => {
246
+ const rule = createIdentityRule("Wildcard", "email", "*@example.com");
247
+ expect(rule.pattern).toBe("*@example.com");
248
+ });
249
+
250
+ it("should support single character wildcard", () => {
251
+ const rule = createIdentityRule("Single Char", "email", "user?@example.com");
252
+ expect(rule.pattern).toBe("user?@example.com");
253
+ });
254
+
255
+ it("should support exact match", () => {
256
+ const rule = createIdentityRule("Exact", "email", "specific@example.com");
257
+ expect(rule.pattern).toBe("specific@example.com");
258
+ });
259
+ });
260
+ });
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Tests for Sigstore signing module
3
+ */
4
+
5
+ import { describe, expect, it } from "bun:test";
6
+ import {
7
+ FULCIO_PUBLIC_URL,
8
+ OIDC_ISSUERS,
9
+ REKOR_PUBLIC_URL,
10
+ TSA_PUBLIC_URL,
11
+ detectOIDCProvider,
12
+ extractCertificateFromBundle,
13
+ extractIdentityFromBundle,
14
+ extractOIDCIdentity,
15
+ getOIDCTokenFromEnvironment,
16
+ } from "../../src/sigstore/signing";
17
+ import type { SigstoreBundle } from "../../src/sigstore/types";
18
+
19
+ // Helper to create test bundles - using unknown to bypass complex sigstore types
20
+ function createTestBundle(overrides: Record<string, unknown> = {}): SigstoreBundle {
21
+ return {
22
+ mediaType: "application/vnd.dev.sigstore.bundle.v0.3+json",
23
+ verificationMaterial: {
24
+ tlogEntries: [],
25
+ timestampVerificationData: undefined,
26
+ },
27
+ ...overrides,
28
+ } as unknown as SigstoreBundle;
29
+ }
30
+
31
+ describe("Sigstore Signing", () => {
32
+ describe("Constants", () => {
33
+ it("should export correct Fulcio URL", () => {
34
+ expect(FULCIO_PUBLIC_URL).toBe("https://fulcio.sigstore.dev");
35
+ });
36
+
37
+ it("should export correct Rekor URL", () => {
38
+ expect(REKOR_PUBLIC_URL).toBe("https://rekor.sigstore.dev");
39
+ });
40
+
41
+ it("should export correct TSA URL", () => {
42
+ expect(TSA_PUBLIC_URL).toBe("https://timestamp.sigstore.dev");
43
+ });
44
+
45
+ it("should export OIDC issuer URLs for all providers", () => {
46
+ expect(OIDC_ISSUERS.github).toBe("https://token.actions.githubusercontent.com");
47
+ expect(OIDC_ISSUERS.google).toBe("https://accounts.google.com");
48
+ expect(OIDC_ISSUERS.microsoft).toBe("https://login.microsoftonline.com");
49
+ expect(OIDC_ISSUERS.gitlab).toBe("https://gitlab.com");
50
+ expect(OIDC_ISSUERS.custom).toBe("");
51
+ });
52
+ });
53
+
54
+ describe("detectOIDCProvider", () => {
55
+ it("should detect GitHub provider", () => {
56
+ expect(detectOIDCProvider("https://token.actions.githubusercontent.com")).toBe("github");
57
+ });
58
+
59
+ it("should detect Google provider", () => {
60
+ expect(detectOIDCProvider("https://accounts.google.com")).toBe("google");
61
+ });
62
+
63
+ it("should detect Microsoft provider", () => {
64
+ expect(detectOIDCProvider("https://login.microsoftonline.com/tenant")).toBe("microsoft");
65
+ });
66
+
67
+ it("should detect GitLab provider", () => {
68
+ expect(detectOIDCProvider("https://gitlab.com")).toBe("gitlab");
69
+ });
70
+
71
+ it("should return custom for unknown issuers", () => {
72
+ expect(detectOIDCProvider("https://unknown-issuer.com")).toBe("custom");
73
+ });
74
+ });
75
+
76
+ describe("extractOIDCIdentity", () => {
77
+ // Create a valid JWT token for testing
78
+ function createTestJWT(claims: Record<string, unknown>): string {
79
+ const header = { alg: "RS256", typ: "JWT" };
80
+ const headerB64 = Buffer.from(JSON.stringify(header)).toString("base64url");
81
+ const payloadB64 = Buffer.from(JSON.stringify(claims)).toString("base64url");
82
+ const signature = "fake-signature";
83
+ return `${headerB64}.${payloadB64}.${signature}`;
84
+ }
85
+
86
+ it("should extract identity from GitHub OIDC token", () => {
87
+ const token = createTestJWT({
88
+ iss: "https://token.actions.githubusercontent.com",
89
+ sub: "repo:owner/repo:ref:refs/heads/main",
90
+ email: "test@example.com",
91
+ repository: "owner/repo",
92
+ ref: "refs/heads/main",
93
+ event_name: "push",
94
+ });
95
+
96
+ const identity = extractOIDCIdentity(token);
97
+
98
+ expect(identity.provider).toBe("github");
99
+ expect(identity.issuer).toBe("https://token.actions.githubusercontent.com");
100
+ expect(identity.email).toBe("test@example.com");
101
+ expect(identity.workflowRepository).toBe("owner/repo");
102
+ expect(identity.workflowRef).toBe("refs/heads/main");
103
+ expect(identity.workflowTrigger).toBe("push");
104
+ });
105
+
106
+ it("should extract identity from Google OIDC token", () => {
107
+ const token = createTestJWT({
108
+ iss: "https://accounts.google.com",
109
+ sub: "123456789",
110
+ email: "user@gmail.com",
111
+ });
112
+
113
+ const identity = extractOIDCIdentity(token);
114
+
115
+ expect(identity.provider).toBe("google");
116
+ expect(identity.issuer).toBe("https://accounts.google.com");
117
+ expect(identity.subject).toBe("123456789");
118
+ expect(identity.email).toBe("user@gmail.com");
119
+ });
120
+
121
+ it("should throw on invalid JWT format", () => {
122
+ expect(() => extractOIDCIdentity("not-a-jwt")).toThrow("Invalid JWT format");
123
+ });
124
+
125
+ it("should throw on invalid JWT with only two parts", () => {
126
+ expect(() => extractOIDCIdentity("header.payload")).toThrow("Invalid JWT format");
127
+ });
128
+
129
+ it("should throw on invalid payload", () => {
130
+ const invalidToken = "aGVhZGVy.aW52YWxpZA==.c2lnbmF0dXJl";
131
+ expect(() => extractOIDCIdentity(invalidToken)).toThrow("Failed to decode JWT payload");
132
+ });
133
+ });
134
+
135
+ describe("getOIDCTokenFromEnvironment", () => {
136
+ it("should return undefined for github without env var", () => {
137
+ const originalEnv = process.env.ACTIONS_ID_TOKEN;
138
+ process.env.ACTIONS_ID_TOKEN = undefined;
139
+
140
+ const token = getOIDCTokenFromEnvironment("github");
141
+ expect(token).toBeUndefined();
142
+
143
+ if (originalEnv) process.env.ACTIONS_ID_TOKEN = originalEnv;
144
+ });
145
+
146
+ it("should return undefined for gitlab without env vars", () => {
147
+ const originalV2 = process.env.CI_JOB_JWT_V2;
148
+ const originalV1 = process.env.CI_JOB_JWT;
149
+ process.env.CI_JOB_JWT_V2 = undefined;
150
+ process.env.CI_JOB_JWT = undefined;
151
+
152
+ const token = getOIDCTokenFromEnvironment("gitlab");
153
+ expect(token).toBeUndefined();
154
+
155
+ if (originalV2) process.env.CI_JOB_JWT_V2 = originalV2;
156
+ if (originalV1) process.env.CI_JOB_JWT = originalV1;
157
+ });
158
+
159
+ it("should return undefined for custom provider", () => {
160
+ const token = getOIDCTokenFromEnvironment("custom");
161
+ expect(token).toBeUndefined();
162
+ });
163
+ });
164
+
165
+ describe("extractCertificateFromBundle", () => {
166
+ it("should return undefined for bundle without certificate", () => {
167
+ const bundle = createTestBundle();
168
+
169
+ const cert = extractCertificateFromBundle(bundle);
170
+ expect(cert).toBeUndefined();
171
+ });
172
+
173
+ it("should extract certificate from bundle with rawBytes", () => {
174
+ const rawBytes = Buffer.from("test-certificate-data").toString("base64");
175
+
176
+ const bundle = createTestBundle({
177
+ verificationMaterial: {
178
+ certificate: { rawBytes },
179
+ tlogEntries: [],
180
+ timestampVerificationData: undefined,
181
+ },
182
+ });
183
+
184
+ const cert = extractCertificateFromBundle(bundle);
185
+
186
+ expect(cert).toBeDefined();
187
+ expect(cert?.certificateChain).toHaveLength(1);
188
+ expect(cert?.certificateChain[0]).toContain("-----BEGIN CERTIFICATE-----");
189
+ expect(cert?.certificateChain[0]).toContain("-----END CERTIFICATE-----");
190
+ expect(cert?.issuer).toBe("sigstore");
191
+ });
192
+ });
193
+
194
+ describe("extractIdentityFromBundle", () => {
195
+ it("should return undefined for bundle without certificate", () => {
196
+ const bundle = createTestBundle();
197
+
198
+ const identity = extractIdentityFromBundle(bundle);
199
+ expect(identity).toBeUndefined();
200
+ });
201
+
202
+ it("should return identity from bundle with certificate", () => {
203
+ const rawBytes = Buffer.from("test-certificate-data").toString("base64");
204
+
205
+ const bundle = createTestBundle({
206
+ verificationMaterial: {
207
+ certificate: { rawBytes },
208
+ tlogEntries: [],
209
+ timestampVerificationData: undefined,
210
+ },
211
+ });
212
+
213
+ const identity = extractIdentityFromBundle(bundle);
214
+
215
+ // The simplified implementation returns a default identity
216
+ expect(identity).toBeDefined();
217
+ expect(identity?.provider).toBe("custom");
218
+ });
219
+ });
220
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "composite": true,
7
+ "noEmit": false
8
+ },
9
+ "include": ["src/**/*"],
10
+ "exclude": ["node_modules", "dist", "tests"]
11
+ }