@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.
- package/dist/api/enact-api.js +2 -2
- package/dist/api/types.d.ts +10 -3
- package/dist/core/DaggerExecutionProvider.d.ts +1 -1
- package/dist/core/DaggerExecutionProvider.js +23 -19
- package/dist/core/EnactCore.d.ts +36 -19
- package/dist/core/EnactCore.js +157 -219
- package/dist/core/NativeExecutionProvider.d.ts +9 -0
- package/dist/core/NativeExecutionProvider.js +16 -0
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -4
- package/dist/lib/enact-direct.d.ts +0 -15
- package/dist/lib/enact-direct.js +0 -11
- package/dist/security/sign.d.ts +5 -5
- package/dist/security/sign.js +247 -113
- package/dist/security/verification-enforcer.d.ts +12 -0
- package/dist/security/verification-enforcer.js +26 -3
- package/dist/services/McpCoreService.d.ts +0 -12
- package/dist/services/McpCoreService.js +0 -9
- package/dist/types.d.ts +5 -4
- package/dist/utils/config.js +1 -1
- package/dist/utils/version.js +23 -2
- package/package.json +3 -6
- package/src/api/enact-api.ts +2 -2
- package/src/api/types.ts +11 -4
- package/src/core/DaggerExecutionProvider.ts +26 -13
- package/src/core/EnactCore.ts +226 -270
- package/src/index.ts +0 -5
- package/src/lib/enact-direct.ts +0 -21
- package/src/services/McpCoreService.ts +0 -20
- package/src/types.ts +10 -12
- package/src/utils/config.ts +1 -1
- package/src/utils/version.ts +23 -2
- package/src/security/index.ts +0 -3
- package/src/security/security.ts +0 -188
- package/src/security/sign.ts +0 -797
- package/src/security/verification-enforcer.ts +0 -268
package/dist/lib/enact-direct.js
CHANGED
|
@@ -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
|
*
|
package/dist/security/sign.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ interface EnactTool {
|
|
|
22
22
|
examples?: any;
|
|
23
23
|
resources?: any;
|
|
24
24
|
raw_content?: string;
|
|
25
|
-
signatures?:
|
|
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
|
-
*
|
|
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
|
|
49
|
-
* This
|
|
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
|
-
*
|
|
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;
|
package/dist/security/sign.js
CHANGED
|
@@ -1,82 +1,112 @@
|
|
|
1
|
-
// enact-signer.ts -
|
|
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
|
-
*
|
|
8
|
-
|
|
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
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
*
|
|
50
|
-
|
|
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
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
*
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
175
|
-
tool.signatures
|
|
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
|
-
*
|
|
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
|
|
198
|
-
const { signature, ...toolForSigning } = canonical;
|
|
199
|
-
//
|
|
200
|
-
const
|
|
201
|
-
|
|
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
|
|
215
|
-
* This
|
|
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
|
-
//
|
|
220
|
-
const
|
|
221
|
-
//
|
|
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
|
|
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
|
-
*
|
|
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 ||
|
|
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 =
|
|
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(
|
|
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 (
|
|
286
|
-
console.error("Original tool signature
|
|
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
|
-
|
|
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
|
-
//
|
|
309
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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
|
|
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 (
|
|
344
|
-
isValid = await verifyToolSignature(
|
|
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 (
|
|
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(
|
|
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
|