@enactprotocol/shared 1.0.13 → 1.2.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.
@@ -1,797 +0,0 @@
1
- // enact-signer.ts - Exact webapp compatibility
2
-
3
- import * as crypto from "crypto";
4
- import { parse, stringify } from "yaml";
5
- import * as fs from "fs";
6
- import * as path from "path";
7
-
8
- /**
9
- * EXACT copy of webapp's createCanonicalToolDefinition
10
- * This MUST match the webapp's cryptoService function exactly
11
- */
12
- function createCanonicalToolDefinition(
13
- tool: Record<string, unknown>,
14
- ): Record<string, unknown> {
15
- const canonical: Record<string, unknown> = {};
16
-
17
- // CRITICAL: These must be in the exact same order as the webapp
18
- const orderedFields = [
19
- "name",
20
- "description",
21
- "command",
22
- "protocol_version",
23
- "version",
24
- "timeout",
25
- "tags",
26
- "input_schema",
27
- "output_schema",
28
- "annotations",
29
- "env_vars",
30
- "examples",
31
- "resources",
32
- "doc",
33
- "authors",
34
- "enact",
35
- ];
36
-
37
- // Add fields in the specific order
38
- for (const field of orderedFields) {
39
- if (tool[field] !== undefined) {
40
- canonical[field] = tool[field];
41
- }
42
- }
43
-
44
- // Add any remaining fields not in the ordered list (sorted)
45
- const remainingFields = Object.keys(tool)
46
- .filter((key) => !orderedFields.includes(key))
47
- .sort();
48
-
49
- for (const field of remainingFields) {
50
- if (tool[field] !== undefined) {
51
- canonical[field] = tool[field];
52
- }
53
- }
54
-
55
- return canonical;
56
- }
57
-
58
- /**
59
- * Create canonical tool JSON EXACTLY like the webapp does
60
- * This mirrors the webapp's createCanonicalToolJson function
61
- */
62
- function createCanonicalToolJson(toolData: any): string {
63
- // Convert Tool to the format expected by createCanonicalToolDefinition
64
- // CRITICAL: Use the exact same field mapping as the webapp
65
- const toolRecord: Record<string, unknown> = {
66
- name: toolData.name,
67
- description: toolData.description,
68
- command: toolData.command,
69
- // Map database fields to canonical fields (EXACT webapp mapping)
70
- protocol_version: toolData.protocol_version,
71
- version: toolData.version,
72
- timeout: toolData.timeout,
73
- tags: toolData.tags,
74
- // Handle schema field mappings (use underscore versions like webapp)
75
- input_schema: toolData.input_schema, // NOT inputSchema
76
- output_schema: toolData.output_schema, // NOT outputSchema
77
- annotations: toolData.annotations,
78
- env_vars: toolData.env_vars, // NOT env
79
- examples: toolData.examples,
80
- resources: toolData.resources,
81
- doc: toolData.doc, // Use direct field, not from raw_content
82
- authors: toolData.authors, // Use direct field, not from raw_content
83
- // Add enact field if missing (webapp behavior)
84
- enact: toolData.enact || "1.0.0",
85
- };
86
-
87
- // Use the standardized canonical function from cryptoService
88
- const canonical = createCanonicalToolDefinition(toolRecord);
89
-
90
- // Return deterministic JSON with sorted keys EXACTLY like webapp
91
- return JSON.stringify(canonical, Object.keys(canonical).sort());
92
- }
93
-
94
- // Updated interfaces for new protocol
95
- interface SignatureData {
96
- algorithm: string;
97
- type: string;
98
- signer: string;
99
- created: string;
100
- value: string;
101
- role?: string;
102
- }
103
-
104
- interface EnactTool {
105
- name: string;
106
- description: string;
107
- command: string;
108
- timeout?: string;
109
- tags?: string[];
110
- version?: string;
111
- enact?: string;
112
- protocol_version?: string;
113
- input_schema?: any; // Use underscore version
114
- output_schema?: any; // Use underscore version
115
- annotations?: any;
116
- env_vars?: Record<string, any>; // Use underscore version (not env)
117
- examples?: any;
118
- resources?: any;
119
- raw_content?: string;
120
- // New multi-signature format: public key -> signature data
121
- signatures?: Record<string, SignatureData>;
122
- [key: string]: any;
123
- }
124
-
125
- // Verification policies
126
- interface VerificationPolicy {
127
- requireRoles?: string[]; // Require signatures with specific roles
128
- minimumSignatures?: number; // Minimum number of valid signatures
129
- trustedSigners?: string[]; // Only accept signatures from these signers
130
- allowedAlgorithms?: string[]; // Allowed signature algorithms
131
- }
132
-
133
- const DEFAULT_POLICY: VerificationPolicy = {
134
- minimumSignatures: 1,
135
- allowedAlgorithms: ["sha256"],
136
- };
137
-
138
- // Default directory for trusted keys
139
- const TRUSTED_KEYS_DIR = path.join(
140
- process.env.HOME || ".",
141
- ".enact",
142
- "trusted-keys",
143
- );
144
-
145
- /**
146
- * Get all trusted public keys mapped by their base64 representation
147
- * @returns Map of base64 public key -> PEM content
148
- */
149
- export function getTrustedPublicKeysMap(): Map<string, string> {
150
- const trustedKeys = new Map<string, string>();
151
-
152
- // Load keys from the filesystem
153
- if (fs.existsSync(TRUSTED_KEYS_DIR)) {
154
- try {
155
- const files = fs.readdirSync(TRUSTED_KEYS_DIR);
156
-
157
- for (const file of files) {
158
- if (file.endsWith(".pem")) {
159
- const keyPath = path.join(TRUSTED_KEYS_DIR, file);
160
- const pemContent = fs.readFileSync(keyPath, "utf8");
161
-
162
- // Convert PEM to base64 for map key
163
- const base64Key = pemToBase64(pemContent);
164
- trustedKeys.set(base64Key, pemContent);
165
- }
166
- }
167
- } catch (error) {
168
- console.error(`Error reading trusted keys: ${(error as Error).message}`);
169
- }
170
- }
171
-
172
- return trustedKeys;
173
- }
174
-
175
- /**
176
- * Convert PEM public key to base64 format for use as map key
177
- */
178
- function pemToBase64(pem: string): string {
179
- return pem
180
- .replace(/-----BEGIN PUBLIC KEY-----/, "")
181
- .replace(/-----END PUBLIC KEY-----/, "")
182
- .replace(/\s/g, "");
183
- }
184
-
185
- /**
186
- * Convert base64 key back to PEM format
187
- */
188
- function base64ToPem(base64: string): string {
189
- return `-----BEGIN PUBLIC KEY-----\n${base64.match(/.{1,64}/g)?.join("\n")}\n-----END PUBLIC KEY-----`;
190
- }
191
-
192
- /**
193
- * Sign an Enact tool and add to the signatures map
194
- * Uses EXACT same process as webapp for perfect compatibility
195
- */
196
- export async function signTool(
197
- toolPath: string,
198
- privateKeyPath: string,
199
- publicKeyPath: string,
200
- signerInfo: { id: string; role?: string },
201
- outputPath?: string,
202
- ): Promise<string> {
203
- // Read files
204
- const toolYaml = fs.readFileSync(toolPath, "utf8");
205
- const privateKey = fs.readFileSync(privateKeyPath, "utf8");
206
- const publicKeyPem = fs.readFileSync(publicKeyPath, "utf8");
207
-
208
- // Parse the YAML
209
- const tool = parse(toolYaml) as EnactTool;
210
-
211
- // Create a copy for signing (without signatures)
212
- const toolForSigning: EnactTool = { ...tool };
213
- delete toolForSigning.signatures;
214
-
215
- // Use EXACT same canonical JSON creation as webapp
216
- const canonicalJson = createCanonicalToolJson(toolForSigning);
217
-
218
- console.error("=== SIGNING DEBUG (WEBAPP COMPATIBLE) ===");
219
- console.error("Tool for signing:", JSON.stringify(toolForSigning, null, 2));
220
- console.error("Canonical JSON (webapp format):", canonicalJson);
221
- console.error("Canonical JSON length:", canonicalJson.length);
222
- console.error("==========================================");
223
-
224
- // Create tool hash exactly like webapp (SHA-256 hash of canonical JSON)
225
- const toolHashBytes = await hashTool(toolForSigning);
226
-
227
- // Sign using Web Crypto API to match webapp exactly
228
- const { webcrypto } = await import("node:crypto");
229
-
230
- // Import the private key for Web Crypto API
231
- const privateKeyData = crypto
232
- .createPrivateKey({
233
- key: privateKey,
234
- format: "pem",
235
- type: "pkcs8",
236
- })
237
- .export({ format: "der", type: "pkcs8" });
238
-
239
- const privateKeyObj = await webcrypto.subtle.importKey(
240
- "pkcs8",
241
- privateKeyData,
242
- { name: "ECDSA", namedCurve: "P-256" },
243
- false,
244
- ["sign"],
245
- );
246
-
247
- // Sign the hash bytes using Web Crypto API (produces IEEE P1363 format)
248
- const signatureArrayBuffer = await webcrypto.subtle.sign(
249
- { name: "ECDSA", hash: { name: "SHA-256" } },
250
- privateKeyObj,
251
- toolHashBytes,
252
- );
253
-
254
- const signature = new Uint8Array(signatureArrayBuffer);
255
- const signatureB64 = Buffer.from(signature).toString("base64");
256
-
257
- console.error("Generated signature (Web Crypto API):", signatureB64);
258
- console.error(
259
- "Signature length:",
260
- signature.length,
261
- "bytes (should be 64 for P-256)",
262
- );
263
-
264
- // Convert public key to base64 for map key
265
- const publicKeyBase64 = pemToBase64(publicKeyPem);
266
-
267
- // Initialize signatures object if it doesn't exist
268
- if (!tool.signatures) {
269
- tool.signatures = {};
270
- }
271
-
272
- // Add signature to the map using public key as key
273
- tool.signatures[publicKeyBase64] = {
274
- algorithm: "sha256",
275
- type: "ecdsa-p256",
276
- signer: signerInfo.id,
277
- created: new Date().toISOString(),
278
- value: signatureB64,
279
- ...(signerInfo.role && { role: signerInfo.role }),
280
- };
281
-
282
- // Convert back to YAML
283
- const signedToolYaml = stringify(tool);
284
-
285
- // Write to output file if specified
286
- if (outputPath) {
287
- fs.writeFileSync(outputPath, signedToolYaml);
288
- }
289
-
290
- return signedToolYaml;
291
- }
292
-
293
- /**
294
- * Hash tool data for signing - EXACT copy of webapp's hashTool function
295
- */
296
- async function hashTool(tool: Record<string, unknown>): Promise<Uint8Array> {
297
- // Create canonical representation
298
- const canonical = createCanonicalToolDefinition(tool);
299
-
300
- // Remove signature if present to avoid circular dependency
301
- const { signature, ...toolForSigning } = canonical;
302
-
303
- // Create deterministic JSON with sorted keys
304
- const canonicalJson = JSON.stringify(
305
- toolForSigning,
306
- Object.keys(toolForSigning).sort(),
307
- );
308
-
309
- console.error("🔍 Canonical JSON for hashing:", canonicalJson);
310
- console.error("🔍 Canonical JSON length:", canonicalJson.length);
311
-
312
- // Hash the canonical JSON
313
- const encoder = new TextEncoder();
314
- const data = encoder.encode(canonicalJson);
315
-
316
- // Use Web Crypto API for hashing to match webapp exactly
317
- const { webcrypto } = await import("node:crypto");
318
- const hashBuffer = await webcrypto.subtle.digest("SHA-256", data);
319
-
320
- const hashBytes = new Uint8Array(hashBuffer);
321
- console.error(
322
- "🔍 SHA-256 hash length:",
323
- hashBytes.length,
324
- "bytes (should be 32)",
325
- );
326
-
327
- return hashBytes;
328
- }
329
-
330
- /**
331
- * Verify tool signature using EXACT same process as webapp
332
- * This mirrors the webapp's verifyToolSignature function exactly
333
- */
334
- export async function verifyToolSignature(
335
- toolObject: Record<string, unknown>,
336
- signatureB64: string,
337
- publicKeyObj: CryptoKey,
338
- ): Promise<boolean> {
339
- try {
340
- // Hash the tool (same process as signing) - EXACT webapp logic
341
- const toolHash = await hashTool(toolObject);
342
-
343
- // Convert Base64 signature to bytes EXACTLY like webapp
344
- const signatureBytes = new Uint8Array(
345
- atob(signatureB64)
346
- .split("")
347
- .map((char) => char.charCodeAt(0)),
348
- );
349
-
350
- console.error(
351
- "🔍 Tool hash byte length:",
352
- toolHash.length,
353
- "(should be 32 for SHA-256)",
354
- );
355
- console.error(
356
- "🔍 Signature bytes length:",
357
- signatureBytes.length,
358
- "(should be 64 for P-256)",
359
- );
360
-
361
- // Use Web Crypto API for verification (matches webapp exactly)
362
- const { webcrypto } = await import("node:crypto");
363
- const isValid = await webcrypto.subtle.verify(
364
- { name: "ECDSA", hash: { name: "SHA-256" } },
365
- publicKeyObj,
366
- signatureBytes,
367
- toolHash,
368
- );
369
-
370
- console.error("🎯 Web Crypto API verification result:", isValid);
371
- return isValid;
372
- } catch (error) {
373
- console.error("❌ Verification error:", error);
374
- return false;
375
- }
376
- }
377
-
378
- /**
379
- * Verify an Enact tool with embedded signatures against trusted keys
380
- * Uses the exact same canonical format and verification approach as the webapp
381
- */
382
- export async function verifyTool(
383
- toolYaml: string | EnactTool,
384
- policy: VerificationPolicy = DEFAULT_POLICY,
385
- ): Promise<{
386
- isValid: boolean;
387
- message: string;
388
- validSignatures: number;
389
- totalSignatures: number;
390
- verifiedSigners: Array<{ signer: string; role?: string; keyId: string }>;
391
- errors: string[];
392
- }> {
393
- const errors: string[] = [];
394
- const verifiedSigners: Array<{
395
- signer: string;
396
- role?: string;
397
- keyId: string;
398
- }> = [];
399
-
400
- try {
401
- // Get trusted public keys
402
- const trustedKeys = getTrustedPublicKeysMap();
403
- if (trustedKeys.size === 0) {
404
- return {
405
- isValid: false,
406
- message: "No trusted public keys available",
407
- validSignatures: 0,
408
- totalSignatures: 0,
409
- verifiedSigners: [],
410
- errors: ["No trusted keys configured"],
411
- };
412
- }
413
-
414
- if (process.env.DEBUG) {
415
- console.error("Trusted keys available:");
416
- for (const [key, pem] of trustedKeys.entries()) {
417
- console.error(` Key: ${key.substring(0, 20)}...`);
418
- }
419
- }
420
-
421
- // Parse the tool if it's YAML string
422
- const tool: EnactTool =
423
- typeof toolYaml === "string" ? parse(toolYaml) : toolYaml;
424
-
425
- // Check if tool has signatures
426
- if (!tool.signatures || Object.keys(tool.signatures).length === 0) {
427
- return {
428
- isValid: false,
429
- message: "No signatures found in the tool",
430
- validSignatures: 0,
431
- totalSignatures: 0,
432
- verifiedSigners: [],
433
- errors: ["No signatures found"],
434
- };
435
- }
436
-
437
- const totalSignatures = Object.keys(tool.signatures).length;
438
-
439
- // Create canonical JSON for verification (without signatures)
440
- const toolForVerification: EnactTool = { ...tool };
441
- delete toolForVerification.signatures;
442
-
443
- // Use EXACT same canonical JSON creation as webapp
444
- const toolHashBytes = await hashTool(toolForVerification);
445
-
446
- // Debug output for verification
447
- if (process.env.NODE_ENV === "development" || process.env.DEBUG) {
448
- console.error("=== VERIFICATION DEBUG (WEBAPP COMPATIBLE) ===");
449
- console.error(
450
- "Original tool signature field:",
451
- Object.keys(tool.signatures || {}),
452
- );
453
- console.error(
454
- "Tool before removing signatures:",
455
- JSON.stringify(tool, null, 2),
456
- );
457
- console.error(
458
- "Tool for verification:",
459
- JSON.stringify(toolForVerification, null, 2),
460
- );
461
- console.error(
462
- "Tool hash bytes length:",
463
- toolHashBytes.length,
464
- "(should be 32 for SHA-256)",
465
- );
466
- console.error("==============================================");
467
- }
468
-
469
- // Verify each signature
470
- let validSignatures = 0;
471
-
472
- for (const [publicKeyBase64, signatureData] of Object.entries(
473
- tool.signatures,
474
- )) {
475
- try {
476
- // Check if algorithm is allowed
477
- if (
478
- policy.allowedAlgorithms &&
479
- !policy.allowedAlgorithms.includes(signatureData.algorithm)
480
- ) {
481
- errors.push(
482
- `Signature by ${signatureData.signer}: unsupported algorithm ${signatureData.algorithm}`,
483
- );
484
- continue;
485
- }
486
-
487
- // Check if signer is trusted (if policy specifies trusted signers)
488
- if (
489
- policy.trustedSigners &&
490
- !policy.trustedSigners.includes(signatureData.signer)
491
- ) {
492
- errors.push(
493
- `Signature by ${signatureData.signer}: signer not in trusted list`,
494
- );
495
- continue;
496
- }
497
-
498
- // Check if we have this public key in our trusted keys
499
- const publicKeyPem = trustedKeys.get(publicKeyBase64);
500
- if (!publicKeyPem) {
501
- // Try to reconstruct PEM from base64 if not found directly
502
- const reconstructedPem = base64ToPem(publicKeyBase64);
503
- if (!trustedKeys.has(pemToBase64(reconstructedPem))) {
504
- errors.push(
505
- `Signature by ${signatureData.signer}: public key not trusted`,
506
- );
507
- continue;
508
- }
509
- }
510
-
511
- if (process.env.DEBUG) {
512
- console.error("Looking for public key:", publicKeyBase64);
513
- console.error("Key found in trusted keys:", !!publicKeyPem);
514
- }
515
-
516
- // Verify the signature using Web Crypto API (webapp compatible)
517
- let isValid = false;
518
- try {
519
- const publicKeyToUse = publicKeyPem || base64ToPem(publicKeyBase64);
520
-
521
- if (process.env.DEBUG) {
522
- console.error("Signature base64:", signatureData.value);
523
- console.error(
524
- "Signature buffer length (should be 64):",
525
- Buffer.from(signatureData.value, "base64").length,
526
- );
527
- console.error("Public key base64:", publicKeyBase64);
528
- }
529
-
530
- if (signatureData.type === "ecdsa-p256") {
531
- // Use Web Crypto API to match webapp exactly
532
- const { webcrypto } = await import("node:crypto");
533
-
534
- // Import the public key (convert PEM to raw key data like webapp)
535
- const publicKeyData = crypto
536
- .createPublicKey({
537
- key: publicKeyToUse,
538
- format: "pem",
539
- type: "spki",
540
- })
541
- .export({ format: "der", type: "spki" });
542
-
543
- const publicKeyObj = await webcrypto.subtle.importKey(
544
- "spki",
545
- publicKeyData,
546
- { name: "ECDSA", namedCurve: "P-256" },
547
- false,
548
- ["verify"],
549
- );
550
-
551
- // Use the centralized verification function (webapp compatible)
552
- isValid = await verifyToolSignature(
553
- toolForVerification,
554
- signatureData.value,
555
- publicKeyObj,
556
- );
557
-
558
- if (process.env.DEBUG) {
559
- console.error(
560
- "Web Crypto API verification result (webapp compatible):",
561
- isValid,
562
- );
563
- }
564
- } else {
565
- // Fallback for other signature types
566
- const verify = crypto.createVerify("SHA256");
567
- const canonicalJson = createCanonicalToolJson(toolForVerification);
568
- verify.update(canonicalJson, "utf8");
569
- const signature = Buffer.from(signatureData.value, "base64");
570
- isValid = verify.verify(publicKeyToUse, signature);
571
- }
572
- } catch (verifyError) {
573
- errors.push(
574
- `Signature by ${signatureData.signer}: verification error - ${(verifyError as Error).message}`,
575
- );
576
- continue;
577
- }
578
-
579
- if (isValid) {
580
- validSignatures++;
581
- verifiedSigners.push({
582
- signer: signatureData.signer,
583
- role: signatureData.role,
584
- keyId: publicKeyBase64.substring(0, 8), // First 8 chars as key ID
585
- });
586
- } else {
587
- errors.push(
588
- `Signature by ${signatureData.signer}: cryptographic verification failed`,
589
- );
590
- }
591
- } catch (error) {
592
- errors.push(
593
- `Signature by ${signatureData.signer}: verification error - ${(error as Error).message}`,
594
- );
595
- }
596
- }
597
-
598
- // Apply policy checks
599
- const policyErrors: string[] = [];
600
-
601
- // Check minimum signatures
602
- if (
603
- policy.minimumSignatures &&
604
- validSignatures < policy.minimumSignatures
605
- ) {
606
- policyErrors.push(
607
- `Policy requires ${policy.minimumSignatures} signatures, but only ${validSignatures} valid`,
608
- );
609
- }
610
-
611
- // Check required roles
612
- if (policy.requireRoles && policy.requireRoles.length > 0) {
613
- const verifiedRoles = verifiedSigners.map((s) => s.role).filter(Boolean);
614
- const missingRoles = policy.requireRoles.filter(
615
- (role) => !verifiedRoles.includes(role),
616
- );
617
- if (missingRoles.length > 0) {
618
- policyErrors.push(`Policy requires roles: ${missingRoles.join(", ")}`);
619
- }
620
- }
621
-
622
- const isValid = policyErrors.length === 0 && validSignatures > 0;
623
- const allErrors = [...errors, ...policyErrors];
624
-
625
- let message: string;
626
- if (isValid) {
627
- message = `Tool "${tool.name}" verified with ${validSignatures}/${totalSignatures} valid signatures`;
628
- if (verifiedSigners.length > 0) {
629
- const signerInfo = verifiedSigners
630
- .map((s) => `${s.signer}${s.role ? ` (${s.role})` : ""}`)
631
- .join(", ");
632
- message += ` from: ${signerInfo}`;
633
- }
634
- } else {
635
- message = `Tool "${tool.name}" verification failed: ${allErrors[0] || "Unknown error"}`;
636
- }
637
-
638
- return {
639
- isValid,
640
- message,
641
- validSignatures,
642
- totalSignatures,
643
- verifiedSigners,
644
- errors: allErrors,
645
- };
646
- } catch (error) {
647
- return {
648
- isValid: false,
649
- message: `Verification error: ${(error as Error).message}`,
650
- validSignatures: 0,
651
- totalSignatures: 0,
652
- verifiedSigners: [],
653
- errors: [(error as Error).message],
654
- };
655
- }
656
- }
657
-
658
- /**
659
- * Check if a tool should be executed based on verification policy
660
- * @param tool Tool to check
661
- * @param policy Verification policy
662
- * @returns Whether execution should proceed
663
- */
664
- export async function shouldExecuteTool(
665
- tool: EnactTool,
666
- policy: VerificationPolicy = DEFAULT_POLICY,
667
- ): Promise<{ allowed: boolean; reason: string }> {
668
- const verification = await verifyTool(tool, policy);
669
-
670
- if (verification.isValid) {
671
- return {
672
- allowed: true,
673
- reason: `Verified: ${verification.message}`,
674
- };
675
- } else {
676
- return {
677
- allowed: false,
678
- reason: `Verification failed: ${verification.message}`,
679
- };
680
- }
681
- }
682
-
683
- /**
684
- * Generate a new ECC key pair
685
- */
686
- export function generateKeyPair(
687
- outputDir: string,
688
- prefix = "enact",
689
- ): { privateKeyPath: string; publicKeyPath: string } {
690
- if (!fs.existsSync(outputDir)) {
691
- fs.mkdirSync(outputDir, { recursive: true });
692
- }
693
-
694
- const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", {
695
- namedCurve: "prime256v1",
696
- publicKeyEncoding: { type: "spki", format: "pem" },
697
- privateKeyEncoding: { type: "pkcs8", format: "pem" },
698
- });
699
-
700
- const privateKeyPath = path.join(outputDir, `${prefix}-private.pem`);
701
- const publicKeyPath = path.join(outputDir, `${prefix}-public.pem`);
702
-
703
- fs.writeFileSync(privateKeyPath, privateKey);
704
- fs.writeFileSync(publicKeyPath, publicKey);
705
-
706
- return { privateKeyPath, publicKeyPath };
707
- }
708
-
709
- /**
710
- * Add a public key to trusted keys
711
- */
712
- export function addTrustedKey(keyPath: string, keyName?: string): string {
713
- if (!fs.existsSync(TRUSTED_KEYS_DIR)) {
714
- fs.mkdirSync(TRUSTED_KEYS_DIR, { recursive: true });
715
- }
716
-
717
- const keyContent = fs.readFileSync(keyPath, "utf8");
718
- const fileName = keyName || `trusted-key-${Date.now()}.pem`;
719
- const trustedKeyPath = path.join(TRUSTED_KEYS_DIR, fileName);
720
-
721
- fs.writeFileSync(trustedKeyPath, keyContent);
722
- return trustedKeyPath;
723
- }
724
-
725
- /**
726
- * List all trusted keys with their base64 representations
727
- */
728
- export function listTrustedKeys(): Array<{
729
- id: string;
730
- filename: string;
731
- base64Key: string;
732
- fingerprint: string;
733
- }> {
734
- const keyInfo: Array<{
735
- id: string;
736
- filename: string;
737
- base64Key: string;
738
- fingerprint: string;
739
- }> = [];
740
-
741
- if (fs.existsSync(TRUSTED_KEYS_DIR)) {
742
- try {
743
- const files = fs.readdirSync(TRUSTED_KEYS_DIR);
744
-
745
- for (const file of files) {
746
- if (file.endsWith(".pem")) {
747
- const keyPath = path.join(TRUSTED_KEYS_DIR, file);
748
- const keyContent = fs.readFileSync(keyPath, "utf8");
749
- const base64Key = pemToBase64(keyContent);
750
-
751
- const fingerprint = crypto
752
- .createHash("sha256")
753
- .update(keyContent)
754
- .digest("hex")
755
- .substring(0, 16);
756
-
757
- keyInfo.push({
758
- id: base64Key.substring(0, 8),
759
- filename: file,
760
- base64Key,
761
- fingerprint,
762
- });
763
- }
764
- }
765
- } catch (error) {
766
- console.error(`Error reading trusted keys: ${(error as Error).message}`);
767
- }
768
- }
769
-
770
- return keyInfo;
771
- }
772
-
773
- // Export verification policies for use in CLI/MCP server
774
- export const VERIFICATION_POLICIES = {
775
- // Permissive: any valid signature from trusted key
776
- PERMISSIVE: {
777
- minimumSignatures: 1,
778
- allowedAlgorithms: ["sha256"],
779
- } as VerificationPolicy,
780
-
781
- // Strict: require author + reviewer signatures
782
- ENTERPRISE: {
783
- minimumSignatures: 2,
784
- requireRoles: ["author", "reviewer"],
785
- allowedAlgorithms: ["sha256"],
786
- } as VerificationPolicy,
787
-
788
- // Maximum security: require author + reviewer + approver
789
- PARANOID: {
790
- minimumSignatures: 3,
791
- requireRoles: ["author", "reviewer", "approver"],
792
- allowedAlgorithms: ["sha256"],
793
- } as VerificationPolicy,
794
- };
795
-
796
- // Export types for use in other modules
797
- export type { EnactTool, VerificationPolicy, SignatureData };