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