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