@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.
@@ -15,7 +15,6 @@ export class EnactDirect {
15
15
  "https://xjnhhxwxovjifdxdwzih.supabase.co",
16
16
  executionProvider: "direct",
17
17
  authToken: options.authToken || process.env.ENACT_AUTH_TOKEN,
18
- verificationPolicy: options.verificationPolicy || "permissive",
19
18
  defaultTimeout: options.defaultTimeout || "30s",
20
19
  });
21
20
  }
@@ -49,16 +48,6 @@ export class EnactDirect {
49
48
  async getToolInfo(name, version) {
50
49
  return this.core.getToolInfo(name, version);
51
50
  }
52
- /**
53
- * Verify a tool's cryptographic signatures
54
- *
55
- * @param name - Tool name
56
- * @param policy - Verification policy
57
- * @returns Verification result
58
- */
59
- async verifyTool(name, policy) {
60
- return this.core.verifyTool(name, policy);
61
- }
62
51
  /**
63
52
  * Execute a tool from raw YAML definition
64
53
  *
@@ -22,7 +22,7 @@ interface EnactTool {
22
22
  examples?: any;
23
23
  resources?: any;
24
24
  raw_content?: string;
25
- signatures?: Record<string, SignatureData>;
25
+ signatures?: Array<SignatureData>;
26
26
  [key: string]: any;
27
27
  }
28
28
  interface VerificationPolicy {
@@ -38,20 +38,20 @@ interface VerificationPolicy {
38
38
  export declare function getTrustedPublicKeysMap(): Map<string, string>;
39
39
  /**
40
40
  * Sign an Enact tool and add to the signatures map
41
- * Uses EXACT same process as webapp for perfect compatibility
41
+ * Signs only critical security fields for focused and reliable validation
42
42
  */
43
43
  export declare function signTool(toolPath: string, privateKeyPath: string, publicKeyPath: string, signerInfo: {
44
44
  id: string;
45
45
  role?: string;
46
46
  }, outputPath?: string): Promise<string>;
47
47
  /**
48
- * Verify tool signature using EXACT same process as webapp
49
- * This mirrors the webapp's verifyToolSignature function exactly
48
+ * Verify tool signature using critical security fields only
49
+ * This verifies signatures against only the security-critical fields
50
50
  */
51
51
  export declare function verifyToolSignature(toolObject: Record<string, unknown>, signatureB64: string, publicKeyObj: CryptoKey): Promise<boolean>;
52
52
  /**
53
53
  * Verify an Enact tool with embedded signatures against trusted keys
54
- * Uses the exact same canonical format and verification approach as the webapp
54
+ * Only verifies signatures against critical security fields for focused validation
55
55
  */
56
56
  export declare function verifyTool(toolYaml: string | EnactTool, policy?: VerificationPolicy): Promise<{
57
57
  isValid: boolean;
@@ -1,82 +1,112 @@
1
- // enact-signer.ts - Exact webapp compatibility
1
+ // enact-signer.ts - Critical field signing for focused security validation
2
2
  import * as crypto from "crypto";
3
3
  import { parse, stringify } from "yaml";
4
4
  import * as fs from "fs";
5
5
  import * as path from "path";
6
6
  /**
7
- * EXACT copy of webapp's createCanonicalToolDefinition
8
- * This MUST match the webapp's cryptoService function exactly
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
9
16
  */
10
17
  function createCanonicalToolDefinition(tool) {
11
18
  const canonical = {};
12
- // CRITICAL: These must be in the exact same order as the webapp
13
- const orderedFields = [
14
- "name",
15
- "description",
16
- "command",
17
- "protocol_version",
18
- "version",
19
- "timeout",
20
- "tags",
21
- "input_schema",
22
- "output_schema",
23
- "annotations",
24
- "env_vars",
25
- "examples",
26
- "resources",
27
- "doc",
28
- "authors",
29
- "enact",
30
- ];
31
- // Add fields in the specific order
32
- for (const field of orderedFields) {
33
- if (tool[field] !== undefined) {
34
- canonical[field] = tool[field];
35
- }
19
+ // Core required fields - only add if not empty
20
+ if (tool.name && !isEmpty(tool.name)) {
21
+ canonical.name = tool.name;
36
22
  }
37
- // Add any remaining fields not in the ordered list (sorted)
38
- const remainingFields = Object.keys(tool)
39
- .filter((key) => !orderedFields.includes(key))
40
- .sort();
41
- for (const field of remainingFields) {
42
- if (tool[field] !== undefined) {
43
- canonical[field] = tool[field];
44
- }
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;
45
59
  }
46
60
  return canonical;
47
61
  }
48
62
  /**
49
- * Create canonical tool JSON EXACTLY like the webapp does
50
- * This mirrors the webapp's createCanonicalToolJson function
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
51
97
  */
52
98
  function createCanonicalToolJson(toolData) {
53
- // Convert Tool to the format expected by createCanonicalToolDefinition
54
- // CRITICAL: Use the exact same field mapping as the webapp
55
- const toolRecord = {
56
- name: toolData.name,
57
- description: toolData.description,
58
- command: toolData.command,
59
- // Map database fields to canonical fields (EXACT webapp mapping)
60
- protocol_version: toolData.protocol_version,
61
- version: toolData.version,
62
- timeout: toolData.timeout,
63
- tags: toolData.tags,
64
- // Handle schema field mappings (use underscore versions like webapp)
65
- input_schema: toolData.input_schema, // NOT inputSchema
66
- output_schema: toolData.output_schema, // NOT outputSchema
67
- annotations: toolData.annotations,
68
- env_vars: toolData.env_vars, // NOT env
69
- examples: toolData.examples,
70
- resources: toolData.resources,
71
- doc: toolData.doc, // Use direct field, not from raw_content
72
- authors: toolData.authors, // Use direct field, not from raw_content
73
- // Add enact field if missing (webapp behavior)
74
- enact: toolData.enact || "1.0.0",
75
- };
76
- // Use the standardized canonical function from cryptoService
77
- const canonical = createCanonicalToolDefinition(toolRecord);
78
- // Return deterministic JSON with sorted keys EXACTLY like webapp
79
- return JSON.stringify(canonical, Object.keys(canonical).sort());
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);
80
110
  }
81
111
  const DEFAULT_POLICY = {
82
112
  minimumSignatures: 1,
@@ -127,7 +157,7 @@ function base64ToPem(base64) {
127
157
  }
128
158
  /**
129
159
  * Sign an Enact tool and add to the signatures map
130
- * Uses EXACT same process as webapp for perfect compatibility
160
+ * Signs only critical security fields for focused and reliable validation
131
161
  */
132
162
  export async function signTool(toolPath, privateKeyPath, publicKeyPath, signerInfo, outputPath) {
133
163
  // Read files
@@ -146,8 +176,10 @@ export async function signTool(toolPath, privateKeyPath, publicKeyPath, signerIn
146
176
  console.error("Canonical JSON (webapp format):", canonicalJson);
147
177
  console.error("Canonical JSON length:", canonicalJson.length);
148
178
  console.error("==========================================");
179
+ // Normalize the tool for hashing (convert to canonical field names)
180
+ const normalizedToolForSigning = normalizeToolForSigning(toolForSigning);
149
181
  // Create tool hash exactly like webapp (SHA-256 hash of canonical JSON)
150
- const toolHashBytes = await hashTool(toolForSigning);
182
+ const toolHashBytes = await hashTool(normalizedToolForSigning);
151
183
  // Sign using Web Crypto API to match webapp exactly
152
184
  const { webcrypto } = await import("node:crypto");
153
185
  // Import the private key for Web Crypto API
@@ -162,24 +194,23 @@ export async function signTool(toolPath, privateKeyPath, publicKeyPath, signerIn
162
194
  // Sign the hash bytes using Web Crypto API (produces IEEE P1363 format)
163
195
  const signatureArrayBuffer = await webcrypto.subtle.sign({ name: "ECDSA", hash: { name: "SHA-256" } }, privateKeyObj, toolHashBytes);
164
196
  const signature = new Uint8Array(signatureArrayBuffer);
165
- const signatureB64 = Buffer.from(signature).toString("base64");
197
+ // Use same Base64 encoding as frontend spec: btoa(String.fromCharCode(...))
198
+ const signatureB64 = btoa(String.fromCharCode(...signature));
166
199
  console.error("Generated signature (Web Crypto API):", signatureB64);
167
200
  console.error("Signature length:", signature.length, "bytes (should be 64 for P-256)");
168
- // Convert public key to base64 for map key
169
- const publicKeyBase64 = pemToBase64(publicKeyPem);
170
- // Initialize signatures object if it doesn't exist
201
+ // Initialize signatures array if it doesn't exist
171
202
  if (!tool.signatures) {
172
- tool.signatures = {};
203
+ tool.signatures = [];
173
204
  }
174
- // Add signature to the map using public key as key
175
- tool.signatures[publicKeyBase64] = {
205
+ // Add signature to the array
206
+ tool.signatures.push({
207
+ signer: signerInfo.id,
176
208
  algorithm: "sha256",
177
209
  type: "ecdsa-p256",
178
- signer: signerInfo.id,
179
- created: new Date().toISOString(),
180
210
  value: signatureB64,
211
+ created: new Date().toISOString(),
181
212
  ...(signerInfo.role && { role: signerInfo.role }),
182
- };
213
+ });
183
214
  // Convert back to YAML
184
215
  const signedToolYaml = stringify(tool);
185
216
  // Write to output file if specified
@@ -189,16 +220,54 @@ export async function signTool(toolPath, privateKeyPath, publicKeyPath, signerIn
189
220
  return signedToolYaml;
190
221
  }
191
222
  /**
192
- * Hash tool data for signing - EXACT copy of webapp's hashTool function
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
193
261
  */
194
262
  async function hashTool(tool) {
195
- // Create canonical representation
263
+ // Create canonical representation with only critical fields
196
264
  const canonical = createCanonicalToolDefinition(tool);
197
- // Remove signature if present to avoid circular dependency
198
- const { signature, ...toolForSigning } = canonical;
199
- // Create deterministic JSON with sorted keys
200
- const canonicalJson = JSON.stringify(toolForSigning, Object.keys(toolForSigning).sort());
201
- console.error("🔍 Canonical JSON for hashing:", canonicalJson);
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);
202
271
  console.error("🔍 Canonical JSON length:", canonicalJson.length);
203
272
  // Hash the canonical JSON
204
273
  const encoder = new TextEncoder();
@@ -211,20 +280,22 @@ async function hashTool(tool) {
211
280
  return hashBytes;
212
281
  }
213
282
  /**
214
- * Verify tool signature using EXACT same process as webapp
215
- * This mirrors the webapp's verifyToolSignature function exactly
283
+ * Verify tool signature using critical security fields only
284
+ * This verifies signatures against only the security-critical fields
216
285
  */
217
286
  export async function verifyToolSignature(toolObject, signatureB64, publicKeyObj) {
218
287
  try {
219
- // Hash the tool (same process as signing) - EXACT webapp logic
220
- const toolHash = await hashTool(toolObject);
221
- // Convert Base64 signature to bytes EXACTLY like webapp
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
222
293
  const signatureBytes = new Uint8Array(atob(signatureB64)
223
294
  .split("")
224
295
  .map((char) => char.charCodeAt(0)));
225
296
  console.error("🔍 Tool hash byte length:", toolHash.length, "(should be 32 for SHA-256)");
226
297
  console.error("🔍 Signature bytes length:", signatureBytes.length, "(should be 64 for P-256)");
227
- // Use Web Crypto API for verification (matches webapp exactly)
298
+ // Use Web Crypto API for verification
228
299
  const { webcrypto } = await import("node:crypto");
229
300
  const isValid = await webcrypto.subtle.verify({ name: "ECDSA", hash: { name: "SHA-256" } }, publicKeyObj, signatureBytes, toolHash);
230
301
  console.error("🎯 Web Crypto API verification result:", isValid);
@@ -237,9 +308,10 @@ export async function verifyToolSignature(toolObject, signatureB64, publicKeyObj
237
308
  }
238
309
  /**
239
310
  * Verify an Enact tool with embedded signatures against trusted keys
240
- * Uses the exact same canonical format and verification approach as the webapp
311
+ * Only verifies signatures against critical security fields for focused validation
241
312
  */
242
313
  export async function verifyTool(toolYaml, policy = DEFAULT_POLICY) {
314
+ console.error("🔍 TRACE: verifyTool() called in sign.ts");
243
315
  const errors = [];
244
316
  const verifiedSigners = [];
245
317
  try {
@@ -264,7 +336,7 @@ export async function verifyTool(toolYaml, policy = DEFAULT_POLICY) {
264
336
  // Parse the tool if it's YAML string
265
337
  const tool = typeof toolYaml === "string" ? parse(toolYaml) : toolYaml;
266
338
  // Check if tool has signatures
267
- if (!tool.signatures || Object.keys(tool.signatures).length === 0) {
339
+ if (!tool.signatures || tool.signatures.length === 0) {
268
340
  return {
269
341
  isValid: false,
270
342
  message: "No signatures found in the tool",
@@ -274,16 +346,18 @@ export async function verifyTool(toolYaml, policy = DEFAULT_POLICY) {
274
346
  errors: ["No signatures found"],
275
347
  };
276
348
  }
277
- const totalSignatures = Object.keys(tool.signatures).length;
349
+ const totalSignatures = tool.signatures.length;
278
350
  // Create canonical JSON for verification (without signatures)
279
351
  const toolForVerification = { ...tool };
280
352
  delete toolForVerification.signatures;
353
+ // Normalize the tool to match signing format (handle EnactTool vs canonical format)
354
+ const normalizedToolForVerification = normalizeToolForSigning(toolForVerification);
281
355
  // Use EXACT same canonical JSON creation as webapp
282
- const toolHashBytes = await hashTool(toolForVerification);
356
+ const toolHashBytes = await hashTool(normalizedToolForVerification);
283
357
  // Debug output for verification
284
358
  if (process.env.NODE_ENV === "development" || process.env.DEBUG) {
285
- console.error("=== VERIFICATION DEBUG (WEBAPP COMPATIBLE) ===");
286
- console.error("Original tool signature field:", Object.keys(tool.signatures || {}));
359
+ console.error("=== VERIFICATION DEBUG (CRITICAL FIELDS ONLY) ===");
360
+ console.error("Original tool signature count:", tool.signatures?.length || 0);
287
361
  console.error("Tool before removing signatures:", JSON.stringify(tool, null, 2));
288
362
  console.error("Tool for verification:", JSON.stringify(toolForVerification, null, 2));
289
363
  console.error("Tool hash bytes length:", toolHashBytes.length, "(should be 32 for SHA-256)");
@@ -291,29 +365,89 @@ export async function verifyTool(toolYaml, policy = DEFAULT_POLICY) {
291
365
  }
292
366
  // Verify each signature
293
367
  let validSignatures = 0;
294
- for (const [publicKeyBase64, signatureData] of Object.entries(tool.signatures)) {
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}`);
295
371
  try {
296
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}`);
297
377
  if (policy.allowedAlgorithms &&
298
378
  !policy.allowedAlgorithms.includes(signatureData.algorithm)) {
379
+ console.error(`🔍 TRACE: Algorithm ${signatureData.algorithm} not allowed!`);
299
380
  errors.push(`Signature by ${signatureData.signer}: unsupported algorithm ${signatureData.algorithm}`);
300
381
  continue;
301
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
+ }
302
428
  // Check if signer is trusted (if policy specifies trusted signers)
303
429
  if (policy.trustedSigners &&
304
430
  !policy.trustedSigners.includes(signatureData.signer)) {
305
431
  errors.push(`Signature by ${signatureData.signer}: signer not in trusted list`);
306
432
  continue;
307
433
  }
308
- // Check if we have this public key in our trusted keys
309
- const publicKeyPem = trustedKeys.get(publicKeyBase64);
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
+ }
310
448
  if (!publicKeyPem) {
311
- // Try to reconstruct PEM from base64 if not found directly
312
- const reconstructedPem = base64ToPem(publicKeyBase64);
313
- if (!trustedKeys.has(pemToBase64(reconstructedPem))) {
314
- errors.push(`Signature by ${signatureData.signer}: public key not trusted`);
315
- continue;
316
- }
449
+ errors.push(`Signature by ${signatureData.signer}: no trusted public key found`);
450
+ continue;
317
451
  }
318
452
  if (process.env.DEBUG) {
319
453
  console.error("Looking for public key:", publicKeyBase64);
@@ -329,7 +463,7 @@ export async function verifyTool(toolYaml, policy = DEFAULT_POLICY) {
329
463
  console.error("Public key base64:", publicKeyBase64);
330
464
  }
331
465
  if (signatureData.type === "ecdsa-p256") {
332
- // Use Web Crypto API to match webapp exactly
466
+ // Use Web Crypto API for critical fields verification
333
467
  const { webcrypto } = await import("node:crypto");
334
468
  // Import the public key (convert PEM to raw key data like webapp)
335
469
  const publicKeyData = crypto
@@ -340,16 +474,16 @@ export async function verifyTool(toolYaml, policy = DEFAULT_POLICY) {
340
474
  })
341
475
  .export({ format: "der", type: "spki" });
342
476
  const publicKeyObj = await webcrypto.subtle.importKey("spki", publicKeyData, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
343
- // Use the centralized verification function (webapp compatible)
344
- isValid = await verifyToolSignature(toolForVerification, signatureData.value, publicKeyObj);
477
+ // Use the centralized verification function (critical fields only)
478
+ isValid = await verifyToolSignature(normalizedToolForVerification, signatureData.value, publicKeyObj);
345
479
  if (process.env.DEBUG) {
346
- console.error("Web Crypto API verification result (webapp compatible):", isValid);
480
+ console.error("Web Crypto API verification result (critical fields):", isValid);
347
481
  }
348
482
  }
349
483
  else {
350
484
  // Fallback for other signature types
351
485
  const verify = crypto.createVerify("SHA256");
352
- const canonicalJson = createCanonicalToolJson(toolForVerification);
486
+ const canonicalJson = createCanonicalToolJson(normalizedToolForVerification);
353
487
  verify.update(canonicalJson, "utf8");
354
488
  const signature = Buffer.from(signatureData.value, "base64");
355
489
  isValid = verify.verify(publicKeyToUse, signature);
@@ -364,7 +498,7 @@ export async function verifyTool(toolYaml, policy = DEFAULT_POLICY) {
364
498
  verifiedSigners.push({
365
499
  signer: signatureData.signer,
366
500
  role: signatureData.role,
367
- keyId: publicKeyBase64.substring(0, 8), // First 8 chars as key ID
501
+ keyId: publicKeyBase64?.substring(0, 8) || signatureData.signer.substring(0, 8), // First 8 chars as key ID
368
502
  });
369
503
  }
370
504
  else {
@@ -515,18 +649,18 @@ export const VERIFICATION_POLICIES = {
515
649
  // Permissive: any valid signature from trusted key
516
650
  PERMISSIVE: {
517
651
  minimumSignatures: 1,
518
- allowedAlgorithms: ["sha256"],
652
+ allowedAlgorithms: ["sha256", "secp256k1"], // Support both legacy and enhanced algorithms
519
653
  },
520
654
  // Strict: require author + reviewer signatures
521
655
  ENTERPRISE: {
522
656
  minimumSignatures: 2,
523
657
  requireRoles: ["author", "reviewer"],
524
- allowedAlgorithms: ["sha256"],
658
+ allowedAlgorithms: ["sha256", "secp256k1"], // Support both legacy and enhanced algorithms
525
659
  },
526
660
  // Maximum security: require author + reviewer + approver
527
661
  PARANOID: {
528
662
  minimumSignatures: 3,
529
663
  requireRoles: ["author", "reviewer", "approver"],
530
- allowedAlgorithms: ["sha256"],
664
+ allowedAlgorithms: ["sha256", "secp256k1"], // Support both legacy and enhanced algorithms
531
665
  },
532
666
  };
@@ -1,9 +1,17 @@
1
1
  import type { EnactTool, ExecutionResult } from "../types";
2
+ export interface SecurityPolicy {
3
+ allowSkipVerification: boolean;
4
+ allowUnsigned: boolean;
5
+ requireInteractiveConfirmation: boolean;
6
+ defaultVerificationPolicy: "permissive" | "enterprise" | "paranoid";
7
+ }
2
8
  export interface VerificationEnforcementOptions {
3
9
  skipVerification?: boolean;
4
10
  verifyPolicy?: "permissive" | "enterprise" | "paranoid";
5
11
  force?: boolean;
6
12
  allowUnsigned?: boolean;
13
+ isLocalFile?: boolean;
14
+ interactive?: boolean;
7
15
  }
8
16
  export interface VerificationEnforcementResult {
9
17
  allowed: boolean;
@@ -30,6 +38,10 @@ export interface VerificationEnforcementResult {
30
38
  * Enforce mandatory signature verification for tool execution
31
39
  * This is the central function that should be called before ANY tool execution
32
40
  */
41
+ /**
42
+ * Get security policy based on execution context
43
+ */
44
+ export declare function getSecurityPolicy(options: VerificationEnforcementOptions): SecurityPolicy;
33
45
  export declare function enforceSignatureVerification(tool: EnactTool, options?: VerificationEnforcementOptions): Promise<VerificationEnforcementResult>;
34
46
  /**
35
47
  * Create an execution result for verification failure