@enactprotocol/shared 1.2.11 → 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.
Files changed (134) hide show
  1. package/README.md +44 -0
  2. package/package.json +16 -58
  3. package/src/config.ts +476 -0
  4. package/src/constants.ts +36 -0
  5. package/src/execution/command.ts +314 -0
  6. package/src/execution/index.ts +73 -0
  7. package/src/execution/runtime.ts +308 -0
  8. package/src/execution/types.ts +379 -0
  9. package/src/execution/validation.ts +508 -0
  10. package/src/index.ts +237 -30
  11. package/src/manifest/index.ts +36 -0
  12. package/src/manifest/loader.ts +187 -0
  13. package/src/manifest/parser.ts +173 -0
  14. package/src/manifest/validator.ts +309 -0
  15. package/src/paths.ts +108 -0
  16. package/src/registry.ts +219 -0
  17. package/src/resolver.ts +345 -0
  18. package/src/types/index.ts +30 -0
  19. package/src/types/manifest.ts +255 -0
  20. package/src/types.ts +5 -188
  21. package/src/utils/fs.ts +281 -0
  22. package/src/utils/logger.ts +270 -59
  23. package/src/utils/version.ts +304 -36
  24. package/tests/config.test.ts +515 -0
  25. package/tests/execution/command.test.ts +317 -0
  26. package/tests/execution/validation.test.ts +384 -0
  27. package/tests/fixtures/invalid-tool.yaml +4 -0
  28. package/tests/fixtures/valid-tool.md +62 -0
  29. package/tests/fixtures/valid-tool.yaml +40 -0
  30. package/tests/index.test.ts +8 -0
  31. package/tests/manifest/loader.test.ts +291 -0
  32. package/tests/manifest/parser.test.ts +345 -0
  33. package/tests/manifest/validator.test.ts +394 -0
  34. package/tests/manifest-types.test.ts +358 -0
  35. package/tests/paths.test.ts +153 -0
  36. package/tests/registry.test.ts +231 -0
  37. package/tests/resolver.test.ts +272 -0
  38. package/tests/utils/fs.test.ts +388 -0
  39. package/tests/utils/logger.test.ts +480 -0
  40. package/tests/utils/version.test.ts +390 -0
  41. package/tsconfig.json +12 -0
  42. package/tsconfig.tsbuildinfo +1 -0
  43. package/dist/LocalToolResolver.d.ts +0 -84
  44. package/dist/LocalToolResolver.js +0 -353
  45. package/dist/api/enact-api.d.ts +0 -130
  46. package/dist/api/enact-api.js +0 -428
  47. package/dist/api/index.d.ts +0 -2
  48. package/dist/api/index.js +0 -2
  49. package/dist/api/types.d.ts +0 -103
  50. package/dist/api/types.js +0 -1
  51. package/dist/constants.d.ts +0 -7
  52. package/dist/constants.js +0 -10
  53. package/dist/core/DaggerExecutionProvider.d.ts +0 -169
  54. package/dist/core/DaggerExecutionProvider.js +0 -1029
  55. package/dist/core/DirectExecutionProvider.d.ts +0 -23
  56. package/dist/core/DirectExecutionProvider.js +0 -406
  57. package/dist/core/EnactCore.d.ts +0 -162
  58. package/dist/core/EnactCore.js +0 -597
  59. package/dist/core/NativeExecutionProvider.d.ts +0 -9
  60. package/dist/core/NativeExecutionProvider.js +0 -16
  61. package/dist/core/index.d.ts +0 -3
  62. package/dist/core/index.js +0 -3
  63. package/dist/exec/index.d.ts +0 -3
  64. package/dist/exec/index.js +0 -3
  65. package/dist/exec/logger.d.ts +0 -11
  66. package/dist/exec/logger.js +0 -57
  67. package/dist/exec/validate.d.ts +0 -5
  68. package/dist/exec/validate.js +0 -167
  69. package/dist/index.d.ts +0 -21
  70. package/dist/index.js +0 -25
  71. package/dist/lib/enact-direct.d.ts +0 -150
  72. package/dist/lib/enact-direct.js +0 -159
  73. package/dist/lib/index.d.ts +0 -1
  74. package/dist/lib/index.js +0 -1
  75. package/dist/security/index.d.ts +0 -3
  76. package/dist/security/index.js +0 -3
  77. package/dist/security/security.d.ts +0 -23
  78. package/dist/security/security.js +0 -137
  79. package/dist/security/sign.d.ts +0 -103
  80. package/dist/security/sign.js +0 -666
  81. package/dist/security/verification-enforcer.d.ts +0 -53
  82. package/dist/security/verification-enforcer.js +0 -204
  83. package/dist/services/McpCoreService.d.ts +0 -98
  84. package/dist/services/McpCoreService.js +0 -124
  85. package/dist/services/index.d.ts +0 -1
  86. package/dist/services/index.js +0 -1
  87. package/dist/types.d.ts +0 -132
  88. package/dist/types.js +0 -3
  89. package/dist/utils/config.d.ts +0 -111
  90. package/dist/utils/config.js +0 -342
  91. package/dist/utils/env-loader.d.ts +0 -54
  92. package/dist/utils/env-loader.js +0 -270
  93. package/dist/utils/help.d.ts +0 -36
  94. package/dist/utils/help.js +0 -248
  95. package/dist/utils/index.d.ts +0 -7
  96. package/dist/utils/index.js +0 -7
  97. package/dist/utils/logger.d.ts +0 -35
  98. package/dist/utils/logger.js +0 -75
  99. package/dist/utils/silent-monitor.d.ts +0 -67
  100. package/dist/utils/silent-monitor.js +0 -242
  101. package/dist/utils/timeout.d.ts +0 -5
  102. package/dist/utils/timeout.js +0 -23
  103. package/dist/utils/version.d.ts +0 -4
  104. package/dist/utils/version.js +0 -35
  105. package/dist/web/env-manager-server.d.ts +0 -29
  106. package/dist/web/env-manager-server.js +0 -367
  107. package/dist/web/index.d.ts +0 -1
  108. package/dist/web/index.js +0 -1
  109. package/src/LocalToolResolver.ts +0 -424
  110. package/src/api/enact-api.ts +0 -604
  111. package/src/api/index.ts +0 -2
  112. package/src/api/types.ts +0 -114
  113. package/src/core/DaggerExecutionProvider.ts +0 -1357
  114. package/src/core/DirectExecutionProvider.ts +0 -484
  115. package/src/core/EnactCore.ts +0 -847
  116. package/src/core/index.ts +0 -3
  117. package/src/exec/index.ts +0 -3
  118. package/src/exec/logger.ts +0 -63
  119. package/src/exec/validate.ts +0 -238
  120. package/src/lib/enact-direct.ts +0 -254
  121. package/src/lib/index.ts +0 -1
  122. package/src/services/McpCoreService.ts +0 -201
  123. package/src/services/index.ts +0 -1
  124. package/src/utils/config.ts +0 -438
  125. package/src/utils/env-loader.ts +0 -370
  126. package/src/utils/help.ts +0 -257
  127. package/src/utils/index.ts +0 -7
  128. package/src/utils/silent-monitor.ts +0 -328
  129. package/src/utils/timeout.ts +0 -26
  130. package/src/web/env-manager-server.ts +0 -465
  131. package/src/web/index.ts +0 -1
  132. package/src/web/static/app.js +0 -663
  133. package/src/web/static/index.html +0 -117
  134. package/src/web/static/style.css +0 -291
@@ -1,666 +0,0 @@
1
- // enact-signer.ts - Critical field signing for focused security validation
2
- import * as crypto from "crypto";
3
- import { parse, stringify } from "yaml";
4
- import * as fs from "fs";
5
- import * as path from "path";
6
- /**
7
- * Crea // Use canonical JSON creation with only critical fields
8
- const canonicalJson = createCanonicalToolJson(toolForSigning);
9
-
10
- console.error("=== SIGNING DEBUG (CRITICAL FIELDS ONLY) ===");
11
- console.error("Tool for signing:", JSON.stringify(toolForSigning, null, 2));
12
- console.error("Critical-fields-only canonical JSON:", canonicalJson);
13
- console.error("Canonical JSON length:", canonicalJson.length);
14
- console.error("==========================================");ical tool definition with ONLY critical security fields
15
- * This signs only the fields that are critical for security and tool identity
16
- */
17
- function createCanonicalToolDefinition(tool) {
18
- const canonical = {};
19
- // Core required fields - only add if not empty
20
- if (tool.name && !isEmpty(tool.name)) {
21
- canonical.name = tool.name;
22
- }
23
- if (tool.description && !isEmpty(tool.description)) {
24
- canonical.description = tool.description;
25
- }
26
- if (tool.command && !isEmpty(tool.command)) {
27
- canonical.command = tool.command;
28
- }
29
- // Protocol version mapping: protocol_version OR enact → enact
30
- const enactValue = tool.enact || tool.protocol_version;
31
- if (enactValue && !isEmpty(enactValue)) {
32
- canonical.enact = enactValue;
33
- }
34
- // Tool version
35
- if (tool.version && !isEmpty(tool.version)) {
36
- canonical.version = tool.version;
37
- }
38
- // Container/execution environment
39
- if (tool.from && !isEmpty(tool.from)) {
40
- canonical.from = tool.from;
41
- }
42
- // Execution timeout
43
- if (tool.timeout && !isEmpty(tool.timeout)) {
44
- canonical.timeout = tool.timeout;
45
- }
46
- // Input schema mapping: input_schema OR inputSchema → inputSchema
47
- const inputSchemaValue = tool.input_schema || tool.inputSchema;
48
- if (inputSchemaValue && !isEmpty(inputSchemaValue)) {
49
- canonical.inputSchema = inputSchemaValue;
50
- }
51
- // Environment variables mapping: env_vars OR env → env
52
- const envValue = tool.env_vars || tool.env;
53
- if (envValue && !isEmpty(envValue)) {
54
- canonical.env = envValue;
55
- }
56
- // Execution metadata/annotations
57
- if (tool.annotations && !isEmpty(tool.annotations)) {
58
- canonical.annotations = tool.annotations;
59
- }
60
- return canonical;
61
- }
62
- /**
63
- * Check if a value is empty (null, undefined, empty object, empty array, empty string)
64
- */
65
- function isEmpty(value) {
66
- if (value === null || value === undefined || value === '') {
67
- return true;
68
- }
69
- if (typeof value === 'object' && value !== null) {
70
- if (Array.isArray(value)) {
71
- return value.length === 0;
72
- }
73
- return Object.keys(value).length === 0;
74
- }
75
- return false;
76
- }
77
- /**
78
- * Recursively sort all object keys alphabetically for deterministic JSON
79
- */
80
- function deepSortKeys(obj) {
81
- if (obj === null || typeof obj !== 'object') {
82
- return obj;
83
- }
84
- if (Array.isArray(obj)) {
85
- return obj.map(deepSortKeys);
86
- }
87
- const sortedObj = {};
88
- const keys = Object.keys(obj).sort();
89
- for (const key of keys) {
90
- sortedObj[key] = deepSortKeys(obj[key]);
91
- }
92
- return sortedObj;
93
- }
94
- /**
95
- * Create canonical tool JSON exactly matching frontend implementation
96
- * Uses two-phase approach: canonical creation + extra cleaning + individual value sorting
97
- */
98
- function createCanonicalToolJson(toolData) {
99
- // Step 1: Create canonical representation with field filtering (same as createCanonicalToolDefinition)
100
- const canonical = createCanonicalToolDefinition(toolData);
101
- // Step 2: Extra cleaning step - remove any remaining empty objects
102
- const cleanedCanonical = {};
103
- for (const [key, value] of Object.entries(canonical)) {
104
- if (!isEmpty(value)) {
105
- cleanedCanonical[key] = deepSortKeys(value); // Sort individual values
106
- }
107
- }
108
- // Step 3: Create deterministic JSON
109
- return JSON.stringify(cleanedCanonical);
110
- }
111
- const DEFAULT_POLICY = {
112
- minimumSignatures: 1,
113
- allowedAlgorithms: ["sha256"],
114
- };
115
- // Default directory for trusted keys
116
- const TRUSTED_KEYS_DIR = path.join(process.env.HOME || ".", ".enact", "trusted-keys");
117
- /**
118
- * Get all trusted public keys mapped by their base64 representation
119
- * @returns Map of base64 public key -> PEM content
120
- */
121
- export function getTrustedPublicKeysMap() {
122
- const trustedKeys = new Map();
123
- // Load keys from the filesystem
124
- if (fs.existsSync(TRUSTED_KEYS_DIR)) {
125
- try {
126
- const files = fs.readdirSync(TRUSTED_KEYS_DIR);
127
- for (const file of files) {
128
- if (file.endsWith(".pem")) {
129
- const keyPath = path.join(TRUSTED_KEYS_DIR, file);
130
- const pemContent = fs.readFileSync(keyPath, "utf8");
131
- // Convert PEM to base64 for map key
132
- const base64Key = pemToBase64(pemContent);
133
- trustedKeys.set(base64Key, pemContent);
134
- }
135
- }
136
- }
137
- catch (error) {
138
- console.error(`Error reading trusted keys: ${error.message}`);
139
- }
140
- }
141
- return trustedKeys;
142
- }
143
- /**
144
- * Convert PEM public key to base64 format for use as map key
145
- */
146
- function pemToBase64(pem) {
147
- return pem
148
- .replace(/-----BEGIN PUBLIC KEY-----/, "")
149
- .replace(/-----END PUBLIC KEY-----/, "")
150
- .replace(/\s/g, "");
151
- }
152
- /**
153
- * Convert base64 key back to PEM format
154
- */
155
- function base64ToPem(base64) {
156
- return `-----BEGIN PUBLIC KEY-----\n${base64.match(/.{1,64}/g)?.join("\n")}\n-----END PUBLIC KEY-----`;
157
- }
158
- /**
159
- * Sign an Enact tool and add to the signatures map
160
- * Signs only critical security fields for focused and reliable validation
161
- */
162
- export async function signTool(toolPath, privateKeyPath, publicKeyPath, signerInfo, outputPath) {
163
- // Read files
164
- const toolYaml = fs.readFileSync(toolPath, "utf8");
165
- const privateKey = fs.readFileSync(privateKeyPath, "utf8");
166
- const publicKeyPem = fs.readFileSync(publicKeyPath, "utf8");
167
- // Parse the YAML
168
- const tool = parse(toolYaml);
169
- // Create a copy for signing (without signatures)
170
- const toolForSigning = { ...tool };
171
- delete toolForSigning.signatures;
172
- // Use EXACT same canonical JSON creation as webapp
173
- const canonicalJson = createCanonicalToolJson(toolForSigning);
174
- console.error("=== SIGNING DEBUG (WEBAPP COMPATIBLE) ===");
175
- console.error("Tool for signing:", JSON.stringify(toolForSigning, null, 2));
176
- console.error("Canonical JSON (webapp format):", canonicalJson);
177
- console.error("Canonical JSON length:", canonicalJson.length);
178
- console.error("==========================================");
179
- // Normalize the tool for hashing (convert to canonical field names)
180
- const normalizedToolForSigning = normalizeToolForSigning(toolForSigning);
181
- // Create tool hash exactly like webapp (SHA-256 hash of canonical JSON)
182
- const toolHashBytes = await hashTool(normalizedToolForSigning);
183
- // Sign using Web Crypto API to match webapp exactly
184
- const { webcrypto } = await import("node:crypto");
185
- // Import the private key for Web Crypto API
186
- const privateKeyData = crypto
187
- .createPrivateKey({
188
- key: privateKey,
189
- format: "pem",
190
- type: "pkcs8",
191
- })
192
- .export({ format: "der", type: "pkcs8" });
193
- const privateKeyObj = await webcrypto.subtle.importKey("pkcs8", privateKeyData, { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
194
- // Sign the hash bytes using Web Crypto API (produces IEEE P1363 format)
195
- const signatureArrayBuffer = await webcrypto.subtle.sign({ name: "ECDSA", hash: { name: "SHA-256" } }, privateKeyObj, toolHashBytes);
196
- const signature = new Uint8Array(signatureArrayBuffer);
197
- // Use same Base64 encoding as frontend spec: btoa(String.fromCharCode(...))
198
- const signatureB64 = btoa(String.fromCharCode(...signature));
199
- console.error("Generated signature (Web Crypto API):", signatureB64);
200
- console.error("Signature length:", signature.length, "bytes (should be 64 for P-256)");
201
- // Initialize signatures array if it doesn't exist
202
- if (!tool.signatures) {
203
- tool.signatures = [];
204
- }
205
- // Add signature to the array
206
- tool.signatures.push({
207
- signer: signerInfo.id,
208
- algorithm: "sha256",
209
- type: "ecdsa-p256",
210
- value: signatureB64,
211
- created: new Date().toISOString(),
212
- ...(signerInfo.role && { role: signerInfo.role }),
213
- });
214
- // Convert back to YAML
215
- const signedToolYaml = stringify(tool);
216
- // Write to output file if specified
217
- if (outputPath) {
218
- fs.writeFileSync(outputPath, signedToolYaml);
219
- }
220
- return signedToolYaml;
221
- }
222
- /**
223
- * Normalize tool object to contain only critical security fields for signing
224
- * Maps between different field name formats and extracts only security-critical fields
225
- */
226
- function normalizeToolForSigning(tool) {
227
- const normalized = {};
228
- // Core required fields
229
- normalized.enact = tool.enact || tool.protocol_version || "1.0.0";
230
- normalized.name = tool.name;
231
- normalized.description = tool.description;
232
- normalized.command = tool.command;
233
- // Optional critical security fields (only include if present)
234
- if (tool.from)
235
- normalized.from = tool.from;
236
- if (tool.version)
237
- normalized.version = tool.version;
238
- if (tool.timeout)
239
- normalized.timeout = tool.timeout;
240
- if (tool.annotations)
241
- normalized.annotations = tool.annotations;
242
- // Handle environment variables (both formats)
243
- if (tool.env) {
244
- normalized.env = tool.env;
245
- }
246
- else if (tool.env_vars) {
247
- normalized.env = tool.env_vars;
248
- }
249
- // Handle input schema (both formats)
250
- if (tool.inputSchema) {
251
- normalized.inputSchema = tool.inputSchema;
252
- }
253
- else if (tool.input_schema) {
254
- normalized.inputSchema = tool.input_schema;
255
- }
256
- return normalized;
257
- }
258
- /**
259
- * Hash tool data for signing - only includes critical security fields
260
- * Creates a deterministic hash of only the security-critical fields
261
- */
262
- async function hashTool(tool) {
263
- // Create canonical representation with only critical fields
264
- const canonical = createCanonicalToolDefinition(tool);
265
- // Remove signature and signatures to avoid circular dependency
266
- const { signature, signatures, ...toolForSigning } = canonical;
267
- // Deep sort all keys recursively for deterministic JSON (same as createCanonicalToolJson)
268
- const deeplySorted = deepSortKeys(toolForSigning);
269
- const canonicalJson = JSON.stringify(deeplySorted);
270
- console.error("🔍 Critical-fields-only canonical JSON for hashing:", canonicalJson);
271
- console.error("🔍 Canonical JSON length:", canonicalJson.length);
272
- // Hash the canonical JSON
273
- const encoder = new TextEncoder();
274
- const data = encoder.encode(canonicalJson);
275
- // Use Web Crypto API for hashing to match webapp exactly
276
- const { webcrypto } = await import("node:crypto");
277
- const hashBuffer = await webcrypto.subtle.digest("SHA-256", data);
278
- const hashBytes = new Uint8Array(hashBuffer);
279
- console.error("🔍 SHA-256 hash length:", hashBytes.length, "bytes (should be 32)");
280
- return hashBytes;
281
- }
282
- /**
283
- * Verify tool signature using critical security fields only
284
- * This verifies signatures against only the security-critical fields
285
- */
286
- export async function verifyToolSignature(toolObject, signatureB64, publicKeyObj) {
287
- try {
288
- // Normalize the tool to match signing format (handle EnactTool vs canonical format)
289
- const normalizedTool = normalizeToolForSigning(toolObject);
290
- // Hash the tool (same process as signing) - critical fields only
291
- const toolHash = await hashTool(normalizedTool);
292
- // Convert Base64 signature to bytes
293
- const signatureBytes = new Uint8Array(atob(signatureB64)
294
- .split("")
295
- .map((char) => char.charCodeAt(0)));
296
- console.error("🔍 Tool hash byte length:", toolHash.length, "(should be 32 for SHA-256)");
297
- console.error("🔍 Signature bytes length:", signatureBytes.length, "(should be 64 for P-256)");
298
- // Use Web Crypto API for verification
299
- const { webcrypto } = await import("node:crypto");
300
- const isValid = await webcrypto.subtle.verify({ name: "ECDSA", hash: { name: "SHA-256" } }, publicKeyObj, signatureBytes, toolHash);
301
- console.error("🎯 Web Crypto API verification result:", isValid);
302
- return isValid;
303
- }
304
- catch (error) {
305
- console.error("❌ Verification error:", error);
306
- return false;
307
- }
308
- }
309
- /**
310
- * Verify an Enact tool with embedded signatures against trusted keys
311
- * Only verifies signatures against critical security fields for focused validation
312
- */
313
- export async function verifyTool(toolYaml, policy = DEFAULT_POLICY) {
314
- console.error("🔍 TRACE: verifyTool() called in sign.ts");
315
- const errors = [];
316
- const verifiedSigners = [];
317
- try {
318
- // Get trusted public keys
319
- const trustedKeys = getTrustedPublicKeysMap();
320
- if (trustedKeys.size === 0) {
321
- return {
322
- isValid: false,
323
- message: "No trusted public keys available",
324
- validSignatures: 0,
325
- totalSignatures: 0,
326
- verifiedSigners: [],
327
- errors: ["No trusted keys configured"],
328
- };
329
- }
330
- if (process.env.DEBUG) {
331
- console.error("Trusted keys available:");
332
- for (const [key, pem] of trustedKeys.entries()) {
333
- console.error(` Key: ${key.substring(0, 20)}...`);
334
- }
335
- }
336
- // Parse the tool if it's YAML string
337
- const tool = typeof toolYaml === "string" ? parse(toolYaml) : toolYaml;
338
- // Check if tool has signatures
339
- if (!tool.signatures || tool.signatures.length === 0) {
340
- return {
341
- isValid: false,
342
- message: "No signatures found in the tool",
343
- validSignatures: 0,
344
- totalSignatures: 0,
345
- verifiedSigners: [],
346
- errors: ["No signatures found"],
347
- };
348
- }
349
- const totalSignatures = tool.signatures.length;
350
- // Create canonical JSON for verification (without signatures)
351
- const toolForVerification = { ...tool };
352
- delete toolForVerification.signatures;
353
- // Normalize the tool to match signing format (handle EnactTool vs canonical format)
354
- const normalizedToolForVerification = normalizeToolForSigning(toolForVerification);
355
- // Use EXACT same canonical JSON creation as webapp
356
- const toolHashBytes = await hashTool(normalizedToolForVerification);
357
- // Debug output for verification
358
- if (process.env.NODE_ENV === "development" || process.env.DEBUG) {
359
- console.error("=== VERIFICATION DEBUG (CRITICAL FIELDS ONLY) ===");
360
- console.error("Original tool signature count:", tool.signatures?.length || 0);
361
- console.error("Tool before removing signatures:", JSON.stringify(tool, null, 2));
362
- console.error("Tool for verification:", JSON.stringify(toolForVerification, null, 2));
363
- console.error("Tool hash bytes length:", toolHashBytes.length, "(should be 32 for SHA-256)");
364
- console.error("==============================================");
365
- }
366
- // Verify each signature
367
- let validSignatures = 0;
368
- console.error("🔍 TRACE: Processing signatures:", tool.signatures.length);
369
- for (const signatureData of tool.signatures) {
370
- console.error(`🔍 TRACE: Processing signature from ${signatureData.signer}, type: ${signatureData.type}, algorithm: ${signatureData.algorithm}`);
371
- try {
372
- // Check if algorithm is allowed
373
- console.error(`🔍 TRACE: Checking algorithm ${signatureData.algorithm} against allowed:`, policy.allowedAlgorithms);
374
- const hasAllowedAlgorithms = !!policy.allowedAlgorithms;
375
- const algorithmAllowed = policy.allowedAlgorithms?.includes(signatureData.algorithm);
376
- console.error(`🔍 TRACE: hasAllowedAlgorithms: ${hasAllowedAlgorithms}, algorithmAllowed: ${algorithmAllowed}`);
377
- if (policy.allowedAlgorithms &&
378
- !policy.allowedAlgorithms.includes(signatureData.algorithm)) {
379
- console.error(`🔍 TRACE: Algorithm ${signatureData.algorithm} not allowed!`);
380
- errors.push(`Signature by ${signatureData.signer}: unsupported algorithm ${signatureData.algorithm}`);
381
- continue;
382
- }
383
- console.error(`🔍 TRACE: Algorithm ${signatureData.algorithm} is allowed, continuing...`);
384
- // Handle enhanced secp256k1 signatures with @enactprotocol/security FIRST
385
- if (signatureData.type === "ecdsa-secp256k1" && signatureData.algorithm === "secp256k1") {
386
- console.error("🔍 TRACE: Using @enactprotocol/security for secp256k1 verification");
387
- // Use @enactprotocol/security for enhanced secp256k1 verification
388
- try {
389
- const { SigningService } = await import("@enactprotocol/security");
390
- // Create signature object for verification (secp256k1 may not need publicKey)
391
- const signature = {
392
- signature: signatureData.value,
393
- algorithm: signatureData.algorithm,
394
- timestamp: new Date(signatureData.created).getTime(),
395
- publicKey: signatureData.signer, // Use signer as publicKey for secp256k1
396
- };
397
- // Verify using @enactprotocol/security with Enact defaults
398
- // Use the original tool without legacy normalization to match signing
399
- console.error("🔍 TRACE: Tool keys for verification:", Object.keys(tool).join(", "));
400
- const isValid = SigningService.verifyDocument(tool, signature, {
401
- useEnactDefaults: true,
402
- });
403
- console.error("🔍 TRACE: @enactprotocol/security verification result:", isValid);
404
- // For secp256k1 signatures, we trust @enactprotocol/security verification
405
- // and don't require legacy trusted keys check
406
- if (isValid) {
407
- console.error("🔍 TRACE: secp256k1 signature verified successfully!");
408
- verifiedSigners.push({
409
- signer: signatureData.signer,
410
- role: signatureData.role,
411
- keyId: signatureData.signer.substring(0, 8), // Use signer ID as key ID
412
- });
413
- validSignatures++;
414
- continue; // Skip the legacy trusted keys check
415
- }
416
- else {
417
- console.error("🔍 TRACE: secp256k1 signature verification failed");
418
- errors.push(`Signature by ${signatureData.signer}: @enactprotocol/security verification failed`);
419
- continue;
420
- }
421
- }
422
- catch (securityError) {
423
- console.error("🔍 TRACE: @enactprotocol/security error:", securityError.message);
424
- errors.push(`Signature by ${signatureData.signer}: @enactprotocol/security verification error - ${securityError.message}`);
425
- continue;
426
- }
427
- }
428
- // Check if signer is trusted (if policy specifies trusted signers)
429
- if (policy.trustedSigners &&
430
- !policy.trustedSigners.includes(signatureData.signer)) {
431
- errors.push(`Signature by ${signatureData.signer}: signer not in trusted list`);
432
- continue;
433
- }
434
- // For ecdsa-p256 signatures, we need to find the public key from trusted keys
435
- // Since we don't have the public key embedded in array format,
436
- // we'll need to match by signer ID or try all trusted keys
437
- let publicKeyPem;
438
- let publicKeyBase64;
439
- // Try to find trusted key by checking all keys
440
- // This is a temporary approach - in production you'd want a signer->key mapping
441
- for (const [keyBase64, keyPem] of trustedKeys.entries()) {
442
- // For now, we'll try each trusted key to see if verification works
443
- // In practice, you'd have a mapping from signer ID to public key
444
- publicKeyBase64 = keyBase64;
445
- publicKeyPem = keyPem;
446
- break; // For now, just use the first trusted key
447
- }
448
- if (!publicKeyPem) {
449
- errors.push(`Signature by ${signatureData.signer}: no trusted public key found`);
450
- continue;
451
- }
452
- if (process.env.DEBUG) {
453
- console.error("Looking for public key:", publicKeyBase64);
454
- console.error("Key found in trusted keys:", !!publicKeyPem);
455
- }
456
- // Verify the signature using Web Crypto API (webapp compatible)
457
- let isValid = false;
458
- try {
459
- const publicKeyToUse = publicKeyPem || base64ToPem(publicKeyBase64);
460
- if (process.env.DEBUG) {
461
- console.error("Signature base64:", signatureData.value);
462
- console.error("Signature buffer length (should be 64):", Buffer.from(signatureData.value, "base64").length);
463
- console.error("Public key base64:", publicKeyBase64);
464
- }
465
- if (signatureData.type === "ecdsa-p256") {
466
- // Use Web Crypto API for critical fields verification
467
- const { webcrypto } = await import("node:crypto");
468
- // Import the public key (convert PEM to raw key data like webapp)
469
- const publicKeyData = crypto
470
- .createPublicKey({
471
- key: publicKeyToUse,
472
- format: "pem",
473
- type: "spki",
474
- })
475
- .export({ format: "der", type: "spki" });
476
- const publicKeyObj = await webcrypto.subtle.importKey("spki", publicKeyData, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
477
- // Use the centralized verification function (critical fields only)
478
- isValid = await verifyToolSignature(normalizedToolForVerification, signatureData.value, publicKeyObj);
479
- if (process.env.DEBUG) {
480
- console.error("Web Crypto API verification result (critical fields):", isValid);
481
- }
482
- }
483
- else {
484
- // Fallback for other signature types
485
- const verify = crypto.createVerify("SHA256");
486
- const canonicalJson = createCanonicalToolJson(normalizedToolForVerification);
487
- verify.update(canonicalJson, "utf8");
488
- const signature = Buffer.from(signatureData.value, "base64");
489
- isValid = verify.verify(publicKeyToUse, signature);
490
- }
491
- }
492
- catch (verifyError) {
493
- errors.push(`Signature by ${signatureData.signer}: verification error - ${verifyError.message}`);
494
- continue;
495
- }
496
- if (isValid) {
497
- validSignatures++;
498
- verifiedSigners.push({
499
- signer: signatureData.signer,
500
- role: signatureData.role,
501
- keyId: publicKeyBase64?.substring(0, 8) || signatureData.signer.substring(0, 8), // First 8 chars as key ID
502
- });
503
- }
504
- else {
505
- errors.push(`Signature by ${signatureData.signer}: cryptographic verification failed`);
506
- }
507
- }
508
- catch (error) {
509
- errors.push(`Signature by ${signatureData.signer}: verification error - ${error.message}`);
510
- }
511
- }
512
- // Apply policy checks
513
- const policyErrors = [];
514
- // Check minimum signatures
515
- if (policy.minimumSignatures &&
516
- validSignatures < policy.minimumSignatures) {
517
- policyErrors.push(`Policy requires ${policy.minimumSignatures} signatures, but only ${validSignatures} valid`);
518
- }
519
- // Check required roles
520
- if (policy.requireRoles && policy.requireRoles.length > 0) {
521
- const verifiedRoles = verifiedSigners.map((s) => s.role).filter(Boolean);
522
- const missingRoles = policy.requireRoles.filter((role) => !verifiedRoles.includes(role));
523
- if (missingRoles.length > 0) {
524
- policyErrors.push(`Policy requires roles: ${missingRoles.join(", ")}`);
525
- }
526
- }
527
- const isValid = policyErrors.length === 0 && validSignatures > 0;
528
- const allErrors = [...errors, ...policyErrors];
529
- let message;
530
- if (isValid) {
531
- message = `Tool "${tool.name}" verified with ${validSignatures}/${totalSignatures} valid signatures`;
532
- if (verifiedSigners.length > 0) {
533
- const signerInfo = verifiedSigners
534
- .map((s) => `${s.signer}${s.role ? ` (${s.role})` : ""}`)
535
- .join(", ");
536
- message += ` from: ${signerInfo}`;
537
- }
538
- }
539
- else {
540
- message = `Tool "${tool.name}" verification failed: ${allErrors[0] || "Unknown error"}`;
541
- }
542
- return {
543
- isValid,
544
- message,
545
- validSignatures,
546
- totalSignatures,
547
- verifiedSigners,
548
- errors: allErrors,
549
- };
550
- }
551
- catch (error) {
552
- return {
553
- isValid: false,
554
- message: `Verification error: ${error.message}`,
555
- validSignatures: 0,
556
- totalSignatures: 0,
557
- verifiedSigners: [],
558
- errors: [error.message],
559
- };
560
- }
561
- }
562
- /**
563
- * Check if a tool should be executed based on verification policy
564
- * @param tool Tool to check
565
- * @param policy Verification policy
566
- * @returns Whether execution should proceed
567
- */
568
- export async function shouldExecuteTool(tool, policy = DEFAULT_POLICY) {
569
- const verification = await verifyTool(tool, policy);
570
- if (verification.isValid) {
571
- return {
572
- allowed: true,
573
- reason: `Verified: ${verification.message}`,
574
- };
575
- }
576
- else {
577
- return {
578
- allowed: false,
579
- reason: `Verification failed: ${verification.message}`,
580
- };
581
- }
582
- }
583
- /**
584
- * Generate a new ECC key pair
585
- */
586
- export function generateKeyPair(outputDir, prefix = "enact") {
587
- if (!fs.existsSync(outputDir)) {
588
- fs.mkdirSync(outputDir, { recursive: true });
589
- }
590
- const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", {
591
- namedCurve: "prime256v1",
592
- publicKeyEncoding: { type: "spki", format: "pem" },
593
- privateKeyEncoding: { type: "pkcs8", format: "pem" },
594
- });
595
- const privateKeyPath = path.join(outputDir, `${prefix}-private.pem`);
596
- const publicKeyPath = path.join(outputDir, `${prefix}-public.pem`);
597
- fs.writeFileSync(privateKeyPath, privateKey);
598
- fs.writeFileSync(publicKeyPath, publicKey);
599
- return { privateKeyPath, publicKeyPath };
600
- }
601
- /**
602
- * Add a public key to trusted keys
603
- */
604
- export function addTrustedKey(keyPath, keyName) {
605
- if (!fs.existsSync(TRUSTED_KEYS_DIR)) {
606
- fs.mkdirSync(TRUSTED_KEYS_DIR, { recursive: true });
607
- }
608
- const keyContent = fs.readFileSync(keyPath, "utf8");
609
- const fileName = keyName || `trusted-key-${Date.now()}.pem`;
610
- const trustedKeyPath = path.join(TRUSTED_KEYS_DIR, fileName);
611
- fs.writeFileSync(trustedKeyPath, keyContent);
612
- return trustedKeyPath;
613
- }
614
- /**
615
- * List all trusted keys with their base64 representations
616
- */
617
- export function listTrustedKeys() {
618
- const keyInfo = [];
619
- if (fs.existsSync(TRUSTED_KEYS_DIR)) {
620
- try {
621
- const files = fs.readdirSync(TRUSTED_KEYS_DIR);
622
- for (const file of files) {
623
- if (file.endsWith(".pem")) {
624
- const keyPath = path.join(TRUSTED_KEYS_DIR, file);
625
- const keyContent = fs.readFileSync(keyPath, "utf8");
626
- const base64Key = pemToBase64(keyContent);
627
- const fingerprint = crypto
628
- .createHash("sha256")
629
- .update(keyContent)
630
- .digest("hex")
631
- .substring(0, 16);
632
- keyInfo.push({
633
- id: base64Key.substring(0, 8),
634
- filename: file,
635
- base64Key,
636
- fingerprint,
637
- });
638
- }
639
- }
640
- }
641
- catch (error) {
642
- console.error(`Error reading trusted keys: ${error.message}`);
643
- }
644
- }
645
- return keyInfo;
646
- }
647
- // Export verification policies for use in CLI/MCP server
648
- export const VERIFICATION_POLICIES = {
649
- // Permissive: any valid signature from trusted key
650
- PERMISSIVE: {
651
- minimumSignatures: 1,
652
- allowedAlgorithms: ["sha256", "secp256k1"], // Support both legacy and enhanced algorithms
653
- },
654
- // Strict: require author + reviewer signatures
655
- ENTERPRISE: {
656
- minimumSignatures: 2,
657
- requireRoles: ["author", "reviewer"],
658
- allowedAlgorithms: ["sha256", "secp256k1"], // Support both legacy and enhanced algorithms
659
- },
660
- // Maximum security: require author + reviewer + approver
661
- PARANOID: {
662
- minimumSignatures: 3,
663
- requireRoles: ["author", "reviewer", "approver"],
664
- allowedAlgorithms: ["sha256", "secp256k1"], // Support both legacy and enhanced algorithms
665
- },
666
- };