@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,147 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { generateKeyPair, getKeyTypeFromPEM, isValidPEMKey } from "../src/keys";
3
+
4
+ describe("key management utilities", () => {
5
+ describe("generateKeyPair", () => {
6
+ test("generates RSA key pair", () => {
7
+ const keyPair = generateKeyPair({
8
+ type: "rsa",
9
+ modulusLength: 2048,
10
+ });
11
+
12
+ expect(keyPair.type).toBe("rsa");
13
+ expect(keyPair.format).toBe("pem");
14
+ expect(keyPair.publicKey).toContain("-----BEGIN PUBLIC KEY-----");
15
+ expect(keyPair.publicKey).toContain("-----END PUBLIC KEY-----");
16
+ expect(keyPair.privateKey).toContain("-----BEGIN PRIVATE KEY-----");
17
+ expect(keyPair.privateKey).toContain("-----END PRIVATE KEY-----");
18
+ });
19
+
20
+ test("generates Ed25519 key pair", () => {
21
+ const keyPair = generateKeyPair({
22
+ type: "ed25519",
23
+ });
24
+
25
+ expect(keyPair.type).toBe("ed25519");
26
+ expect(keyPair.format).toBe("pem");
27
+ expect(keyPair.publicKey).toContain("-----BEGIN PUBLIC KEY-----");
28
+ expect(keyPair.privateKey).toContain("-----BEGIN PRIVATE KEY-----");
29
+ });
30
+
31
+ test("generates ECDSA key pair", () => {
32
+ const keyPair = generateKeyPair({
33
+ type: "ecdsa",
34
+ });
35
+
36
+ expect(keyPair.type).toBe("ecdsa");
37
+ expect(keyPair.format).toBe("pem");
38
+ expect(keyPair.publicKey).toContain("-----BEGIN PUBLIC KEY-----");
39
+ expect(keyPair.privateKey).toContain("-----BEGIN PRIVATE KEY-----");
40
+ });
41
+
42
+ test("generates encrypted RSA key pair with passphrase", () => {
43
+ const keyPair = generateKeyPair({
44
+ type: "rsa",
45
+ modulusLength: 2048,
46
+ passphrase: "secret123",
47
+ });
48
+
49
+ expect(keyPair.privateKey).toContain("-----BEGIN ENCRYPTED PRIVATE KEY-----");
50
+ expect(keyPair.privateKey).toContain("-----END ENCRYPTED PRIVATE KEY-----");
51
+ });
52
+
53
+ test("generates different key pairs on each call", () => {
54
+ const keyPair1 = generateKeyPair({ type: "ed25519" });
55
+ const keyPair2 = generateKeyPair({ type: "ed25519" });
56
+
57
+ expect(keyPair1.privateKey).not.toBe(keyPair2.privateKey);
58
+ expect(keyPair1.publicKey).not.toBe(keyPair2.publicKey);
59
+ });
60
+
61
+ test("supports custom RSA modulus length", () => {
62
+ const keyPair4096 = generateKeyPair({
63
+ type: "rsa",
64
+ modulusLength: 4096,
65
+ });
66
+
67
+ // 4096-bit keys should be longer than 2048-bit keys
68
+ const keyPair2048 = generateKeyPair({
69
+ type: "rsa",
70
+ modulusLength: 2048,
71
+ });
72
+
73
+ expect(keyPair4096.privateKey.length).toBeGreaterThan(keyPair2048.privateKey.length);
74
+ });
75
+ });
76
+
77
+ describe("isValidPEMKey", () => {
78
+ test("validates public key PEM format", () => {
79
+ const keyPair = generateKeyPair({ type: "ed25519" });
80
+
81
+ expect(isValidPEMKey(keyPair.publicKey, "public")).toBe(true);
82
+ });
83
+
84
+ test("validates private key PEM format", () => {
85
+ const keyPair = generateKeyPair({ type: "ed25519" });
86
+
87
+ expect(isValidPEMKey(keyPair.privateKey, "private")).toBe(true);
88
+ });
89
+
90
+ test("validates encrypted private key PEM format", () => {
91
+ const keyPair = generateKeyPair({
92
+ type: "rsa",
93
+ passphrase: "secret",
94
+ });
95
+
96
+ expect(isValidPEMKey(keyPair.privateKey, "private")).toBe(true);
97
+ });
98
+
99
+ test("rejects invalid PEM format", () => {
100
+ expect(isValidPEMKey("not a valid key", "private")).toBe(false);
101
+ expect(isValidPEMKey("", "public")).toBe(false);
102
+ });
103
+
104
+ test("rejects public key when expecting private", () => {
105
+ const keyPair = generateKeyPair({ type: "ed25519" });
106
+
107
+ expect(isValidPEMKey(keyPair.publicKey, "private")).toBe(false);
108
+ });
109
+
110
+ test("rejects private key when expecting public", () => {
111
+ const keyPair = generateKeyPair({ type: "ed25519" });
112
+
113
+ expect(isValidPEMKey(keyPair.privateKey, "public")).toBe(false);
114
+ });
115
+ });
116
+
117
+ describe("getKeyTypeFromPEM", () => {
118
+ test("detects RSA key type", () => {
119
+ const keyPair = generateKeyPair({
120
+ type: "rsa",
121
+ modulusLength: 2048,
122
+ });
123
+
124
+ const detectedType = getKeyTypeFromPEM(keyPair.privateKey);
125
+ expect(detectedType).toBe("rsa");
126
+ });
127
+
128
+ test("detects Ed25519 key type", () => {
129
+ const keyPair = generateKeyPair({ type: "ed25519" });
130
+
131
+ const detectedType = getKeyTypeFromPEM(keyPair.privateKey);
132
+ expect(detectedType).toBe("ed25519");
133
+ });
134
+
135
+ test("detects ECDSA key type", () => {
136
+ const keyPair = generateKeyPair({ type: "ecdsa" });
137
+
138
+ const detectedType = getKeyTypeFromPEM(keyPair.privateKey);
139
+ expect(detectedType).toBe("ecdsa");
140
+ });
141
+
142
+ test("returns undefined for invalid PEM", () => {
143
+ const detectedType = getKeyTypeFromPEM("not a key");
144
+ expect(detectedType).toBeUndefined();
145
+ });
146
+ });
147
+ });
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Tests for Sigstore attestation module
3
+ */
4
+
5
+ import { describe, expect, it } from "bun:test";
6
+ import { unlinkSync, writeFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import {
10
+ ENACT_AUDIT_TYPE,
11
+ ENACT_TOOL_TYPE,
12
+ INTOTO_STATEMENT_TYPE,
13
+ SLSA_PROVENANCE_TYPE,
14
+ createEnactAuditPredicate,
15
+ createEnactAuditStatement,
16
+ createEnactToolPredicate,
17
+ createEnactToolStatement,
18
+ createResourceDescriptorFromContent,
19
+ createResourceDescriptorFromFile,
20
+ createSLSAProvenance,
21
+ createSLSAProvenanceStatement,
22
+ createStatement,
23
+ createSubjectFromContent,
24
+ createSubjectFromFile,
25
+ createSubjectWithMultipleDigests,
26
+ } from "../../src/sigstore/attestation";
27
+
28
+ describe("Sigstore Attestation", () => {
29
+ describe("Constants", () => {
30
+ it("should export in-toto statement type", () => {
31
+ expect(INTOTO_STATEMENT_TYPE).toBe("https://in-toto.io/Statement/v1");
32
+ });
33
+
34
+ it("should export SLSA provenance type", () => {
35
+ expect(SLSA_PROVENANCE_TYPE).toBe("https://slsa.dev/provenance/v1");
36
+ });
37
+
38
+ it("should export Enact tool type", () => {
39
+ expect(ENACT_TOOL_TYPE).toBe("https://enact.tools/attestation/tool/v1");
40
+ });
41
+
42
+ it("should export Enact audit type", () => {
43
+ expect(ENACT_AUDIT_TYPE).toBe("https://enact.tools/attestation/audit/v1");
44
+ });
45
+ });
46
+
47
+ describe("createSubjectFromContent", () => {
48
+ it("should create subject with sha256 digest", () => {
49
+ const subject = createSubjectFromContent("test.yaml", "hello world");
50
+
51
+ expect(subject.name).toBe("test.yaml");
52
+ expect(subject.digest.sha256).toBeDefined();
53
+ expect(subject.digest.sha256).toHaveLength(64); // SHA256 hex is 64 chars
54
+ });
55
+
56
+ it("should create consistent hashes for same content", () => {
57
+ const subject1 = createSubjectFromContent("file1.txt", "test content");
58
+ const subject2 = createSubjectFromContent("file2.txt", "test content");
59
+
60
+ expect(subject1.digest.sha256).toBe(subject2.digest.sha256);
61
+ });
62
+
63
+ it("should create different hashes for different content", () => {
64
+ const subject1 = createSubjectFromContent("file.txt", "content1");
65
+ const subject2 = createSubjectFromContent("file.txt", "content2");
66
+
67
+ expect(subject1.digest.sha256).not.toBe(subject2.digest.sha256);
68
+ });
69
+
70
+ it("should handle Buffer content", () => {
71
+ const buffer = Buffer.from("binary content");
72
+ const subject = createSubjectFromContent("binary.bin", buffer);
73
+
74
+ expect(subject.name).toBe("binary.bin");
75
+ expect(subject.digest.sha256).toBeDefined();
76
+ });
77
+ });
78
+
79
+ describe("createSubjectFromFile", () => {
80
+ const testFile = join(tmpdir(), "test-attestation-subject.txt");
81
+
82
+ it("should create subject from file", async () => {
83
+ writeFileSync(testFile, "file content for testing");
84
+
85
+ try {
86
+ const subject = await createSubjectFromFile("my-artifact", testFile);
87
+
88
+ expect(subject.name).toBe("my-artifact");
89
+ expect(subject.digest.sha256).toBeDefined();
90
+ expect(subject.digest.sha256).toHaveLength(64);
91
+ } finally {
92
+ unlinkSync(testFile);
93
+ }
94
+ });
95
+ });
96
+
97
+ describe("createSubjectWithMultipleDigests", () => {
98
+ it("should create subject with both sha256 and sha512", () => {
99
+ const subject = createSubjectWithMultipleDigests("artifact.tar.gz", "test content");
100
+
101
+ expect(subject.name).toBe("artifact.tar.gz");
102
+ expect(subject.digest.sha256).toBeDefined();
103
+ expect(subject.digest.sha512).toBeDefined();
104
+ expect(subject.digest.sha256).toHaveLength(64); // SHA256
105
+ expect(subject.digest.sha512).toHaveLength(128); // SHA512
106
+ });
107
+ });
108
+
109
+ describe("createStatement", () => {
110
+ it("should create in-toto statement with custom predicate", () => {
111
+ const subject = createSubjectFromContent("test.yaml", "content");
112
+ const predicate = { customField: "value", count: 42 };
113
+
114
+ const statement = createStatement([subject], "https://example.com/predicate/v1", predicate);
115
+
116
+ expect(statement._type).toBe(INTOTO_STATEMENT_TYPE);
117
+ expect(statement.subject).toHaveLength(1);
118
+ expect(statement.predicateType).toBe("https://example.com/predicate/v1");
119
+ expect(statement.predicate).toEqual(predicate);
120
+ });
121
+
122
+ it("should support multiple subjects", () => {
123
+ const subjects = [
124
+ createSubjectFromContent("file1.txt", "content1"),
125
+ createSubjectFromContent("file2.txt", "content2"),
126
+ ];
127
+
128
+ const statement = createStatement(subjects, "https://example.com/v1", {});
129
+
130
+ expect(statement.subject).toHaveLength(2);
131
+ });
132
+ });
133
+
134
+ describe("createSLSAProvenance", () => {
135
+ it("should create minimal SLSA provenance", () => {
136
+ const provenance = createSLSAProvenance({
137
+ buildType: "https://enact.tools/build/v1",
138
+ builderId: "https://github.com/enact-dev/cli@v2.0.0",
139
+ });
140
+
141
+ expect(provenance.buildDefinition.buildType).toBe("https://enact.tools/build/v1");
142
+ expect(provenance.buildDefinition.externalParameters).toEqual({});
143
+ expect(provenance.runDetails.builder.id).toBe("https://github.com/enact-dev/cli@v2.0.0");
144
+ });
145
+
146
+ it("should include external parameters", () => {
147
+ const provenance = createSLSAProvenance({
148
+ buildType: "https://enact.tools/build/v1",
149
+ builderId: "builder-id",
150
+ externalParameters: { manifestPath: "tool.yaml", version: "1.0.0" },
151
+ });
152
+
153
+ expect(provenance.buildDefinition.externalParameters).toEqual({
154
+ manifestPath: "tool.yaml",
155
+ version: "1.0.0",
156
+ });
157
+ });
158
+
159
+ it("should include internal parameters", () => {
160
+ const provenance = createSLSAProvenance({
161
+ buildType: "https://enact.tools/build/v1",
162
+ builderId: "builder-id",
163
+ internalParameters: { builderVersion: "2.0.0" },
164
+ });
165
+
166
+ expect(provenance.buildDefinition.internalParameters).toEqual({ builderVersion: "2.0.0" });
167
+ });
168
+
169
+ it("should include metadata with timestamps", () => {
170
+ const startTime = new Date("2024-01-01T10:00:00Z");
171
+ const endTime = new Date("2024-01-01T10:05:00Z");
172
+
173
+ const provenance = createSLSAProvenance({
174
+ buildType: "https://enact.tools/build/v1",
175
+ builderId: "builder-id",
176
+ invocationId: "build-123",
177
+ startedOn: startTime,
178
+ finishedOn: endTime,
179
+ });
180
+
181
+ expect(provenance.runDetails.metadata?.invocationId).toBe("build-123");
182
+ expect(provenance.runDetails.metadata?.startedOn).toBe("2024-01-01T10:00:00.000Z");
183
+ expect(provenance.runDetails.metadata?.finishedOn).toBe("2024-01-01T10:05:00.000Z");
184
+ });
185
+ });
186
+
187
+ describe("createSLSAProvenanceStatement", () => {
188
+ it("should create complete SLSA provenance statement", () => {
189
+ const subjects = [createSubjectFromContent("artifact.tar.gz", "artifact content")];
190
+
191
+ const statement = createSLSAProvenanceStatement(subjects, {
192
+ buildType: "https://enact.tools/build/v1",
193
+ builderId: "builder-id",
194
+ });
195
+
196
+ expect(statement._type).toBe(INTOTO_STATEMENT_TYPE);
197
+ expect(statement.predicateType).toBe(SLSA_PROVENANCE_TYPE);
198
+ expect(statement.subject).toEqual(subjects);
199
+ expect(statement.predicate.buildDefinition.buildType).toBe("https://enact.tools/build/v1");
200
+ });
201
+ });
202
+
203
+ describe("createEnactToolPredicate", () => {
204
+ it("should create minimal tool predicate", () => {
205
+ const predicate = createEnactToolPredicate({
206
+ name: "my-tool",
207
+ version: "1.0.0",
208
+ publisher: "user@example.com",
209
+ });
210
+
211
+ expect(predicate.type).toBe(ENACT_TOOL_TYPE);
212
+ expect(predicate.tool.name).toBe("my-tool");
213
+ expect(predicate.tool.version).toBe("1.0.0");
214
+ expect(predicate.tool.publisher).toBe("user@example.com");
215
+ });
216
+
217
+ it("should include optional tool fields", () => {
218
+ const predicate = createEnactToolPredicate({
219
+ name: "my-tool",
220
+ version: "1.0.0",
221
+ publisher: "user@example.com",
222
+ description: "A useful tool",
223
+ repository: "https://github.com/owner/repo",
224
+ });
225
+
226
+ expect(predicate.tool.description).toBe("A useful tool");
227
+ expect(predicate.tool.repository).toBe("https://github.com/owner/repo");
228
+ });
229
+
230
+ it("should include build information", () => {
231
+ const buildTime = new Date("2024-01-01T12:00:00Z");
232
+
233
+ const predicate = createEnactToolPredicate({
234
+ name: "my-tool",
235
+ version: "1.0.0",
236
+ publisher: "user@example.com",
237
+ buildTimestamp: buildTime,
238
+ buildEnvironment: { NODE_VERSION: "20.0.0" },
239
+ sourceCommit: "abc123def456",
240
+ });
241
+
242
+ expect(predicate.build?.timestamp).toBe("2024-01-01T12:00:00.000Z");
243
+ expect(predicate.build?.environment).toEqual({ NODE_VERSION: "20.0.0" });
244
+ expect(predicate.build?.sourceCommit).toBe("abc123def456");
245
+ });
246
+ });
247
+
248
+ describe("createEnactToolStatement", () => {
249
+ it("should create complete tool attestation statement", () => {
250
+ const manifestContent = "name: my-tool\nversion: 1.0.0";
251
+
252
+ const statement = createEnactToolStatement(manifestContent, {
253
+ name: "my-tool",
254
+ version: "1.0.0",
255
+ publisher: "user@example.com",
256
+ });
257
+
258
+ expect(statement._type).toBe(INTOTO_STATEMENT_TYPE);
259
+ expect(statement.predicateType).toBe(ENACT_TOOL_TYPE);
260
+ expect(statement.subject[0]?.name).toBe("my-tool@1.0.0");
261
+ expect(statement.predicate.tool.name).toBe("my-tool");
262
+ });
263
+ });
264
+
265
+ describe("createEnactAuditPredicate", () => {
266
+ it("should create audit predicate with passed result", () => {
267
+ const predicate = createEnactAuditPredicate({
268
+ toolName: "my-tool",
269
+ toolVersion: "1.0.0",
270
+ auditor: "auditor@security.org",
271
+ result: "passed",
272
+ });
273
+
274
+ expect(predicate.type).toBe(ENACT_AUDIT_TYPE);
275
+ expect(predicate.tool.name).toBe("my-tool");
276
+ expect(predicate.tool.version).toBe("1.0.0");
277
+ expect(predicate.audit.auditor).toBe("auditor@security.org");
278
+ expect(predicate.audit.result).toBe("passed");
279
+ expect(predicate.audit.timestamp).toBeDefined();
280
+ });
281
+
282
+ it("should include audit notes", () => {
283
+ const predicate = createEnactAuditPredicate({
284
+ toolName: "my-tool",
285
+ toolVersion: "1.0.0",
286
+ auditor: "auditor@security.org",
287
+ result: "passed-with-warnings",
288
+ notes: "Minor issues found but acceptable",
289
+ });
290
+
291
+ expect(predicate.audit.result).toBe("passed-with-warnings");
292
+ expect(predicate.audit.notes).toBe("Minor issues found but acceptable");
293
+ });
294
+ });
295
+
296
+ describe("createEnactAuditStatement", () => {
297
+ it("should create complete audit attestation statement", () => {
298
+ const manifestContent = "name: my-tool\nversion: 1.0.0";
299
+
300
+ const statement = createEnactAuditStatement(manifestContent, {
301
+ toolName: "my-tool",
302
+ toolVersion: "1.0.0",
303
+ auditor: "auditor@security.org",
304
+ result: "passed",
305
+ });
306
+
307
+ expect(statement._type).toBe(INTOTO_STATEMENT_TYPE);
308
+ expect(statement.predicateType).toBe(ENACT_AUDIT_TYPE);
309
+ expect(statement.subject[0]?.name).toBe("my-tool@1.0.0");
310
+ });
311
+ });
312
+
313
+ describe("createResourceDescriptorFromFile", () => {
314
+ const testFile = join(tmpdir(), "test-resource-descriptor.txt");
315
+
316
+ it("should create resource descriptor from file", async () => {
317
+ writeFileSync(testFile, "resource content");
318
+
319
+ try {
320
+ const descriptor = await createResourceDescriptorFromFile(testFile);
321
+
322
+ expect(descriptor.name).toBe(testFile);
323
+ expect(descriptor.digest?.sha256).toBeDefined();
324
+ } finally {
325
+ unlinkSync(testFile);
326
+ }
327
+ });
328
+
329
+ it("should include optional fields", async () => {
330
+ writeFileSync(testFile, "resource content");
331
+
332
+ try {
333
+ const descriptor = await createResourceDescriptorFromFile(testFile, {
334
+ uri: "https://example.com/resource",
335
+ name: "custom-name",
336
+ downloadLocation: "https://download.example.com/resource",
337
+ mediaType: "application/octet-stream",
338
+ });
339
+
340
+ expect(descriptor.uri).toBe("https://example.com/resource");
341
+ expect(descriptor.name).toBe("custom-name");
342
+ expect(descriptor.downloadLocation).toBe("https://download.example.com/resource");
343
+ expect(descriptor.mediaType).toBe("application/octet-stream");
344
+ } finally {
345
+ unlinkSync(testFile);
346
+ }
347
+ });
348
+ });
349
+
350
+ describe("createResourceDescriptorFromContent", () => {
351
+ it("should create resource descriptor from content", () => {
352
+ const descriptor = createResourceDescriptorFromContent("test content");
353
+
354
+ expect(descriptor.digest?.sha256).toBeDefined();
355
+ });
356
+
357
+ it("should include optional fields", () => {
358
+ const descriptor = createResourceDescriptorFromContent("test content", {
359
+ uri: "https://example.com/resource",
360
+ name: "resource-name",
361
+ mediaType: "text/plain",
362
+ });
363
+
364
+ expect(descriptor.uri).toBe("https://example.com/resource");
365
+ expect(descriptor.name).toBe("resource-name");
366
+ expect(descriptor.mediaType).toBe("text/plain");
367
+ });
368
+ });
369
+ });