diff --git a/packages/cli/src/commands/core.ts b/packages/cli/src/commands/core.ts index 4d89313..246ddf2 100644 --- a/packages/cli/src/commands/core.ts +++ b/packages/cli/src/commands/core.ts @@ -5,7 +5,7 @@ import { resolve, extname } from "path"; import { EnactCore } from "@enactprotocol/shared/core"; import type { ToolSearchOptions, ToolExecuteOptions } from "@enactprotocol/shared/core"; import type { EnactTool } from "@enactprotocol/shared"; -import type { EnactToolDefinition } from "@enactprotocol/shared/api"; +import type { EnactToolDefinition, ToolSignaturePayload } from "@enactprotocol/shared/api"; import pc from "picocolors"; import * as p from "@clack/prompts"; import yaml from "yaml"; @@ -19,6 +19,7 @@ import { getAuthHeaders } from "./auth"; import { addToHistory, getFrontendUrl, getApiUrl } from "@enactprotocol/shared/utils"; import { getCurrentConfig } from "./config"; import stripAnsi from "strip-ansi"; +import { CryptoUtils, KeyManager, KeyMetadata, SecurityConfigManager, SigningService } from "@enactprotocol/security"; // Create core instance with configuration let core: EnactCore; @@ -61,6 +62,11 @@ interface CoreSearchOptions { author?: string; } +interface CoreSignOptions { + help?: boolean; + tool?: string; +} + interface CoreExecOptions { help?: boolean; input?: string; @@ -236,10 +242,14 @@ ${pc.bold("EXAMPLES:")} try { const tool = await apiClient.getTool(result.name); if (tool) { + const isValid = await EnactCore.checkToolVerificationStatus(tool); + // console.log("šŸ” TRACE: core.ts - Tool:", tool.name, "isValid:", isValid); // Convert to EnactTool format const enactTool: EnactTool = { + id: tool.id, name: tool.name, description: tool.description || "", + verified: isValid, command: tool.command, from: tool.from, version: tool.version || "1.0.0", @@ -508,6 +518,241 @@ function parseTimeout(timeout: string): number { } } +/** + * Sign a tool definition using local private key + */ +export async function handleSignToolCommand( + args: string[], + options: CoreSignOptions, +) { + if (options.help) { + console.error(` +Usage: enact sign [options] +Sign a tool definition using your private key +Options: + --help, -h Show this help message + --tool query Search for the tool to sign (name) +Examples: + enact sign --tool myorg/mytool + `); + return; + } + + // Show a spinner during search + + if (!options.tool){ + const inputSchema = { + type: "object", + properties: { + toolName: { + type: "string", + description: "The name of the tool to run" + } + }, + required: ["tool"] + }; + + const params = await collectParametersInteractively(inputSchema); + // console.log("Collected tool name:", params.toolName); + options.tool = params.toolName + + } + const spinner = p.spinner(); + spinner.start("Searching for tools..."); + const results: EnactTool[] = []; + try { + const searchOptions: ToolSearchOptions = { + query: options.tool ? options.tool : "", + limit: 20, + tags: undefined, + author: undefined, + format: "table" as any, + }; + + // Use API client directly for search - no need for EnactCore + const apiClient = await EnactApiClient.create(); + const searchResults = await apiClient.searchTools(searchOptions); + + // Convert API results to EnactTool format + for (const result of searchResults) { + if (result.name) { + try { + const tool = await apiClient.getTool(result.name); + if (tool) { + const documentForVerification = { + command: tool.command, + description: tool.description, + from: tool.from, + name: tool.name, + }; + const isValid = await EnactCore.checkToolVerificationStatus(tool); + // console.log("šŸ” TRACE: core.ts - Tool:", tool.name, "isValid:", isValid); + // Convert to EnactTool format + const enactTool: EnactTool = { + id: tool.id, + name: tool.name, + description: tool.description || "", + verified: isValid, + command: tool.command, + from: tool.from, + version: tool.version || "1.0.0", + timeout: tool.timeout, + tags: tool.tags || [], + inputSchema: tool.inputSchema, + outputSchema: tool.outputSchema, + env: tool.env_vars + ? Object.fromEntries( + Object.entries(tool.env_vars).map(([key, config]: [string, any]) => [ + key, + { ...config, source: config.source || "env" }, + ]), + ) + : undefined, + signature: tool.signature, + signatures: Array.isArray(tool.signatures) ? tool.signatures : + (tool.signatures ? Object.values(tool.signatures) : undefined), + namespace: tool.namespace, + resources: tool.resources, + license: tool.license, + authors: tool.authors, + examples: tool.examples, + annotations: tool.annotations, + }; + results.push(enactTool); + } + } catch (error) { + // Skip tools that can't be fetched + continue; + } + } + } + + spinner.stop( + `Found ${results.length} tool${results.length === 1 ? "" : "s"}`, + ); + + if (results.length === 0) { + p.note("No tools found matching your criteria.", "No Results"); + p.note( + "Try:\n• Broader keywords\n• Removing filters\n• Different spelling", + "Suggestions", + ); + + return; + } + }catch (error: any) { + spinner.stop("Search failed"); + + if ( + error.message?.includes("ENOTFOUND") || + error.message?.includes("ECONNREFUSED") + ) { + p.note( + "Could not connect to the Enact registry. Check your internet connection.", + "Connection Error", + ); + } else { + p.note(error instanceof Error ? error.message : String(error), "Error"); + } + + + console.error( + pc.red( + `Search failed: ${error instanceof Error ? error.message : String(error)}`, + ), + ); + process.exit(1); + } + + const selected_tool = await p.select({ + message: "Select a tool to sign:", + options: results.map((tool) => ({ + value: tool, + label: `${tool.name} - ${tool.description.substring(0, 60)}${tool.description.length > 60 ? "..." : ""} - Verified: ${tool.verified ? pc.green("Yes") : pc.red("No")}`, + })) + }) as EnactTool; + + const documentForVerification = { + command: selected_tool.command, + description: selected_tool.description, + from: selected_tool.from, + name: selected_tool.name, + } + + const privateKeys = await KeyManager.getAllPrivateKeys(); + + const privateKey = await p.select({ + message: "Select a private key to sign with:", + options: privateKeys.map((key) => ({ + value: key, + label: `${key.fileName}`, + })) + }) as CryptoUtils.PrivateKey; + + const correspondingPublicKey = CryptoUtils.getPublicKeyFromPrivate(privateKey.key); + + + if (selected_tool.signatures && selected_tool.signatures.length > 0) { + const alreadySigned = selected_tool.signatures.some(sig => { + const referenceSignature = { + signature: sig.value, + publicKey: "", // Correct public key for UUID 71e02e2c-148c-4534-9900-bd9646e99333 + algorithm: sig.algorithm, + timestamp: new Date(sig.created).getTime() + }; + + return SigningService.verifyDocumentWithPublicKey( + documentForVerification, + referenceSignature, + correspondingPublicKey, + { includeFields: ['command', 'description', 'from', 'name'] } + ); + }); + + if (alreadySigned) { + console.log(pc.green("āœ“ Tool has already been signed with the selected key.")); + return; + } + } + + const buffer = Buffer.from(correspondingPublicKey, "hex"); + const publickeyBase64 = buffer.toString("base64") + + const spinnerSign = p.spinner(); + console.error(spinnerSign.start("Signing tool...")); + const signature = await SigningService.signDocument(documentForVerification, privateKey.key, { includeFields: ['command', 'description', 'from', 'name']}); + spinnerSign.stop(pc.green("āœ“ Tool signed successfully")); + console.error(pc.cyan("\nSignature Details:")); + console.error(pc.cyan(`\tSignature: ${signature.signature}`)); + console.error(pc.cyan(`\tAlgorithm: ${signature.algorithm}`)); + console.error(pc.cyan(`\tCreated: ${new Date(signature.timestamp).toISOString()}`)); + console.error(pc.cyan(`\tPublic Key: ${publickeyBase64}`)); + + let keyId = "None" + let toolId = selected_tool.id; + + const user_id = "anon" + + const signaturePayload: ToolSignaturePayload = { + algorithm: "sha256", + created: new Date(signature.timestamp).toISOString(), // Time of signing + keyId: keyId, // ID of the private key + public_key: publickeyBase64, // The corresponding public key in base64 + role: "author", + signer: user_id, // The userID of the signer + timestamp: Math.floor(Date.now() / 1000), // Unix epoch + type: "ecdsa-p256", + value: signature.signature, // Signature + }; + console.log(toolId) + const apiClient = await EnactApiClient.create(); + await apiClient.signTool(toolId, signaturePayload); + + console.error(pc.red("\nSignature was successfully added to the Database!")); + return signature; + +}; + /** * Enhanced handle execute command using core library with full legacy feature parity */ @@ -872,8 +1117,10 @@ Examples: // Convert tool definition to EnactTool format for core const enactTool: EnactTool = { + id: toolDefinition.id, name: toolDefinition.name, description: toolDefinition.description || "", + verified: true, // Verification handled separately command: toolDefinition.command, from: toolDefinition.from, version: toolDefinition.version || "1.0.0", @@ -1095,10 +1342,14 @@ ${pc.bold("EXAMPLES:")} process.exit(1); } + const isValid = EnactCore.checkToolVerificationStatus(toolDefinition); + // Convert to EnactTool format const tool: EnactTool = { + id: toolDefinition.id, name: toolDefinition.name, description: toolDefinition.description || "", + verified: isValid, command: toolDefinition.command, from: toolDefinition.from, version: toolDefinition.version || "1.0.0", @@ -1145,6 +1396,12 @@ ${pc.bold("EXAMPLES:")} if (tool.command) { console.error(`${pc.bold("Command:")} ${pc.gray(tool.command)}`); } + + if (tool.verified) { + console.error(`${pc.bold("Verification:")} ${pc.green("Verified āœ“")}`); + } else { + console.error(`${pc.bold("Verification:")} ${pc.red("Unverified āœ—")}`); + } if (tool.from) { console.error(`${pc.bold("Container:")} ${pc.gray(tool.from)}`); @@ -1221,22 +1478,34 @@ ${pc.bold("EXAMPLES:")} */ function displayResultsTable(results: EnactTool[]): void { console.error("\n" + pc.bold("Search Results:")); - console.error("═".repeat(100)); - // Header const nameWidth = 40; + const statusWidth = 15; // moved up const descWidth = 45; const tagsWidth = 20; + // Dynamically calculate the total table width + const totalWidth = nameWidth + statusWidth + descWidth + tagsWidth + 9; + // 9 = 3 separators ( " │ " ) Ɨ 3 + + console.error("═".repeat(totalWidth)); + + // Header row console.error( pc.bold(pc.cyan("NAME".padEnd(nameWidth))) + + " │ " + + pc.bold(pc.cyan("STATUS".padEnd(statusWidth))) + " │ " + pc.bold(pc.cyan("DESCRIPTION".padEnd(descWidth))) + " │ " + pc.bold(pc.cyan("TAGS".padEnd(tagsWidth))), ); + + // Separator row console.error( "─".repeat(nameWidth) + + "─┼─" + + "─".repeat(statusWidth) + "─┼─" + "─".repeat(descWidth) + "─┼─" + @@ -1245,31 +1514,47 @@ function displayResultsTable(results: EnactTool[]): void { // Rows results.forEach((tool) => { - const name = + // Format name + const nameText = tool.name.length > nameWidth ? tool.name.substring(0, nameWidth - 3) + "..." - : tool.name.padEnd(nameWidth); + : tool.name.padEnd(nameWidth); + + // Format status + const statusText = tool.verified ? "Verified" : "Unverified"; + const statusColor = tool.verified ? pc.green : pc.red; + const status = statusColor(statusText.padEnd(statusWidth)); + + // const name = statusColor(nameText) + + // Format description const desc = tool.description.length > descWidth ? tool.description.substring(0, descWidth - 3) + "..." : tool.description.padEnd(descWidth); + + // Format tags const tags = (tool.tags || []).join(", "); const tagsDisplay = tags.length > tagsWidth ? tags.substring(0, tagsWidth - 3) + "..." : tags.padEnd(tagsWidth); + // Print row console.error( - pc.green(name) + " │ " + pc.dim(desc) + " │ " + pc.yellow(tagsDisplay), + pc.green(nameText) + " │ " + status + " │ " + pc.dim(desc) + " │ " + pc.yellow(tagsDisplay), ); }); - console.error("═".repeat(100)); + console.error("═".repeat(totalWidth)); console.error( pc.dim(`Total: ${results.length} tool${results.length === 1 ? "" : "s"}`), ); } + + + /** * Display results in a simple list format */ @@ -1279,7 +1564,7 @@ function displayResultsList(results: EnactTool[]): void { results.forEach((tool, index) => { console.error( - `${pc.cyan(`${index + 1}.`)} ${pc.bold(pc.green(tool.name))}`, + `${pc.cyan(`${index + 1}.`)} ${pc.bold((tool.verified ? pc.green(tool.name) : pc.red(tool.name)) + (tool.verified ? pc.green(" (verified)") : pc.red(" (unverified)")))}`, ); console.error(` ${pc.dim(tool.description)}`); if (tool.tags && tool.tags.length > 0) { diff --git a/packages/cli/src/commands/mcp.ts b/packages/cli/src/commands/mcp.ts index a6d2f28..b6ad8c7 100644 --- a/packages/cli/src/commands/mcp.ts +++ b/packages/cli/src/commands/mcp.ts @@ -76,6 +76,14 @@ const MCP_CLIENTS = { linux: join(homedir(), ".gemini/settings.json"), }, }, + "openai-codex": { + name: "OpenAI Codex", + configPaths: { + darwin: join(homedir(), ".codex/config.toml"), + win32: join(homedir(), ".codex/config.toml"), + linux: join(homedir(), ".codex/config.toml"), + } + } }; // MCP server configurations @@ -430,6 +438,51 @@ export async function installMcpServer(client: { return; } + if (client.id === "openai-codex") { + const configPath = client.configPath; + const configDir = join(configPath, ".."); + await mkdir(configDir, { recursive: true }); + + let content = ""; + if (existsSync(configPath)) { + content = await readFile(configPath, "utf-8"); + } + + // Helper to generate the TOML block + const generateTomlBlock = (name: string, pkg: string) => { + return [ + `\n\n[mcp_servers.${name}]`, + `command = "npx"`, + `args = ["-y", "${pkg}"]` + ].join('\n'); + }; + + const updates: { name: string; pkg: string }[] = []; + + if (serverType === "main" || serverType === "both") { + updates.push({ name: "enact", pkg: "@enactprotocol/mcp-server" }); + } + if (serverType === "dev" || serverType === "both") { + updates.push({ name: "enact-dev", pkg: "@enactprotocol/mcp-dev-server" }); + } + + for (const update of updates) { + // 1. Remove existing block if it exists (Regex to match [mcp_servers.name] until next [section] or EOF) + // This ensures we update args if they changed, and don't duplicate keys + const regex = new RegExp(`\\[mcp_servers\\.${update.name}\\][\\s\\S]*?(?=(\\n\\[|$))`, "g"); + content = content.replace(regex, "").trim(); + + // 2. Append new block + content += generateTomlBlock(update.name, update.pkg); + } + + // Ensure we have a trailing newline + if (!content.endsWith("\n")) content += "\n"; + + await writeFile(configPath, content.trimStart(), "utf-8"); + return; + } + // Original logic for file-based clients const configPath = client.configPath; @@ -521,6 +574,26 @@ export async function checkMcpServerInstalled(client: { } } + if (client.id === "openai-codex") { + if (!existsSync(client.configPath)) return false; + try { + const content = await readFile(client.configPath, "utf-8"); + + // We check for the specific section header based on server type + if (serverType === "main") { + return content.includes("[mcp_servers.enact]"); + } else if (serverType === "dev") { + return content.includes("[mcp_servers.enact-dev]"); + } else if (serverType === "both") { + return content.includes("[mcp_servers.enact]") && + content.includes("[mcp_servers.enact-dev]"); + } + return false; + } catch (error) { + return false; + } + } + if (!existsSync(client.configPath)) { return false; } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 027840e..e147c6c 100755 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -15,6 +15,7 @@ import { handleConfigCommand } from "./commands/config"; import { handleCoreSearchCommand, handleCoreExecCommand, + handleSignToolCommand, handleCoreGetCommand, handleCorePublishCommand, } from "./commands/core"; @@ -128,6 +129,9 @@ const { values, positionals } = parseArgs({ mount: { type: "string", }, + tool: { + type: "string", + }, }, allowPositionals: true, strict: false, @@ -218,6 +222,12 @@ async function main() { }); break; + case "sign": // Sign command - core library only + await handleSignToolCommand(commandArgs, { + help: values.help as boolean | undefined, + tool: values.tool as string | undefined, + }); + break; case "get": // New case for get command (core library only) await handleCoreGetCommand(commandArgs, { @@ -266,6 +276,7 @@ async function main() { { value: "publish", label: "šŸ“¤ Publish a tool" }, { value: "init", label: "šŸ“ Create a new tool definition" }, { value: "env", label: "šŸŒ Manage environment variables" }, + { value: "sign", label: "āœļø Sign a tool definition" }, { value: "config", label: "šŸ”§ Configure Enact settings" }, { value: "auth", label: "šŸ” Manage authentication" }, { value: "remote", label: "🌐 Manage remote servers" }, @@ -323,6 +334,11 @@ async function main() { } return; } + + if (action === "sign") { + // Sign a tool definition + await handleSignToolCommand([], {}); + } if (action === "auth") { // Show auth submenu diff --git a/packages/security/src/crypto.ts b/packages/security/src/crypto.ts index 4e9f588..e23adc8 100644 --- a/packages/security/src/crypto.ts +++ b/packages/security/src/crypto.ts @@ -147,4 +147,11 @@ export class CryptoUtils { static isPemFormat(key: string): boolean { return key.includes('-----BEGIN') && key.includes('-----END'); } +} + +export namespace CryptoUtils { + export type PrivateKey = { + fileName: string; // e.g., "id_key.pem" + key: string; // hex or PEM format + }; } \ No newline at end of file diff --git a/packages/security/src/index.ts b/packages/security/src/index.ts index 34946d4..ea1bd97 100644 --- a/packages/security/src/index.ts +++ b/packages/security/src/index.ts @@ -3,7 +3,6 @@ export { CryptoUtils } from './crypto'; export { KeyManager } from './keyManager'; export { SecurityConfigManager } from './securityConfigManager'; export { FieldSelector, EnactFieldSelector, GenericFieldSelector } from './fieldConfig'; -export type { EnactDocument, SigningOptions, Signature, KeyPair, SecurityConfig } from './types'; +export type { EnactDocument, SigningOptions, Signature, KeyMetadata, KeyPair, SecurityConfig } from './types'; export { DEFAULT_SECURITY_CONFIG } from './types'; -export type { FieldConfig, SigningFieldOptions } from './fieldConfig'; -export type { KeyMetadata } from './keyManager'; \ No newline at end of file +export type { FieldConfig, SigningFieldOptions } from './fieldConfig'; \ No newline at end of file diff --git a/packages/security/src/keyManager.ts b/packages/security/src/keyManager.ts index 780f8c8..b42e53a 100644 --- a/packages/security/src/keyManager.ts +++ b/packages/security/src/keyManager.ts @@ -1,16 +1,9 @@ import { CryptoUtils } from './crypto'; -import type { KeyPair } from './types'; +import type { KeyPair, KeyMetadata } from './types'; import fs from 'fs'; import path from 'path'; import os from 'os'; -export interface KeyMetadata { - keyId: string; - created: string; - algorithm: string; - description?: string; -} - export class KeyManager { // Storage paths private static readonly TRUSTED_KEYS_DIR = path.join(os.homedir(), '.enact', 'trusted-keys'); @@ -31,8 +24,8 @@ export class KeyManager { return path.join(this.PRIVATE_KEYS_DIR, `${keyId}-private.pem`); } - private static getMetadataPath(keyId: string): string { - return path.join(this.TRUSTED_KEYS_DIR, `${keyId}.meta`); + private static getMetadataPath(): string { + return path.join(this.PRIVATE_KEYS_DIR, `metadata.json`); } static generateAndStoreKey(keyId: string, description?: string): KeyPair { @@ -69,18 +62,18 @@ export class KeyManager { ); // Store metadata - const metadata: KeyMetadata = { - keyId, - created: new Date().toISOString(), - algorithm: 'secp256k1', - description - }; - - fs.writeFileSync( - this.getMetadataPath(keyId), - JSON.stringify(metadata, null, 2), - { mode: 0o644 } - ); + // const metadata: KeyMetadata = { + // keyId, + // created: new Date().toISOString(), + // algorithm: 'secp256k1', + // description + // }; + + // fs.writeFileSync( + // this.getMetadataPath(keyId), + // JSON.stringify(metadata, null, 2), + // { mode: 0o644 } + // ); } catch (error) { // Clean up on error @@ -129,18 +122,34 @@ export class KeyManager { } } - static getKeyMetadata(keyId: string): KeyMetadata | undefined { + static getKeyMetadata(): KeyMetadata[] | undefined { try { - const metadataPath = this.getMetadataPath(keyId); + const metadataPath = this.getMetadataPath(); if (!fs.existsSync(metadataPath)) { return undefined; } - const metadataJson = fs.readFileSync(metadataPath, 'utf8'); - return JSON.parse(metadataJson); + const rawData = fs.readFileSync(metadataPath, 'utf8'); + const metadata = JSON.parse(rawData); + + const keys: KeyMetadata[] = []; + + metadata.forEach((key: any) => { + const publicKeyBytes = key['publicKey']; + const publickeyBase64 = Buffer.from(publicKeyBytes).toString('utf-8'); + keys.push({ + keyId: key['id'], + publicKey: publickeyBase64, + created: key['createdAt'], + algorithm: 'secp256k1', + }); + }); + + return keys; + } catch (error) { - console.warn(`Failed to read metadata for key '${keyId}': ${error instanceof Error ? error.message : String(error)}`); + console.warn(`Failed to read metadata: ${error instanceof Error ? error.message : String(error)}`); return undefined; } } @@ -170,11 +179,11 @@ export class KeyManager { } // Remove metadata - const metadataPath = this.getMetadataPath(keyId); - if (fs.existsSync(metadataPath)) { - fs.unlinkSync(metadataPath); - removed = true; - } + // const metadataPath = this.getMetadataPath(keyId); + // if (fs.existsSync(metadataPath)) { + // fs.unlinkSync(metadataPath); + // removed = true; + // } return removed; } catch (error) { @@ -237,6 +246,50 @@ export class KeyManager { } } + static listPrivateKeys(): string[] { + try { + this.ensureDirectories(); + + // Return all private keys (including those without public keys) + return fs.readdirSync(this.PRIVATE_KEYS_DIR) + .filter(file => file.endsWith('-private.pem')) + .map(file => file.replace('-private.pem', '')); + } catch (error) { + console.warn(`Failed to list private keys: ${error instanceof Error ? error.message : String(error)}`); + return []; + } + } + + static getAllPrivateKeys(): CryptoUtils.PrivateKey[] { + try { + this.ensureDirectories(); + + // Return all private key values from private keys directory + return fs.readdirSync(this.PRIVATE_KEYS_DIR) + .filter(file => file.endsWith('.pem')) + .map(file => { + try { + const privatekey: CryptoUtils.PrivateKey = { + fileName: file, + key: "", + }; + // + const privateKeyPem = fs.readFileSync(path.join(this.PRIVATE_KEYS_DIR, file), 'utf8').trim(); + // Convert PEM back to hex for internal use + privatekey.key = CryptoUtils.pemToHex(privateKeyPem, 'PRIVATE'); + return privatekey; + } catch (error) { + console.warn(`Failed to read private key file ${file}: ${error instanceof Error ? error.message : String(error)}`); + return null; + } + }) + .filter(key => key !== null) as CryptoUtils.PrivateKey[]; + } catch (error) { + console.warn(`Failed to get all private keys: ${error instanceof Error ? error.message : String(error)}`); + return []; + } + } + static exportKey(keyId: string): KeyPair | undefined { return this.getKey(keyId); } @@ -265,24 +318,24 @@ export class KeyManager { ); // Store metadata - const metadata: KeyMetadata = { - keyId, - created: new Date().toISOString(), - algorithm: 'secp256k1', - description: description || 'Imported public key' - }; - - fs.writeFileSync( - this.getMetadataPath(keyId), - JSON.stringify(metadata, null, 2), - { mode: 0o644 } - ); + // const metadata: KeyMetadata = { + // keyId, + // created: new Date().toISOString(), + // algorithm: 'secp256k1', + // description: description || 'Imported public key' + // }; + + // fs.writeFileSync( + // this.getMetadataPath(keyId), + // JSON.stringify(metadata, null, 2), + // { mode: 0o644 } + // ); } catch (error) { // Clean up on error try { fs.unlinkSync(this.getPublicKeyPath(keyId)); - fs.unlinkSync(this.getMetadataPath(keyId)); + // fs.unlinkSync(this.getMetadataPath(keyId)); } catch {} throw new Error(`Failed to import public key '${keyId}': ${error instanceof Error ? error.message : String(error)}`); } @@ -299,7 +352,7 @@ export class KeyManager { // Backup/export functionality static exportKeyToFile(keyId: string, outputPath: string, includePrivateKey: boolean = false): void { const keyPair = this.getKey(keyId); - const metadata = this.getKeyMetadata(keyId); + const metadata = this.getKeyMetadata(); if (!keyPair) { throw new Error(`Key '${keyId}' not found`); diff --git a/packages/security/src/signing.ts b/packages/security/src/signing.ts index b3c006a..e6c49c4 100644 --- a/packages/security/src/signing.ts +++ b/packages/security/src/signing.ts @@ -87,7 +87,7 @@ export class SigningService { const trustedPublicKeys = KeyManager.getAllTrustedPublicKeys(); // All signatures must be valid and from trusted keys - return signatures.every(sig => { + return signatures.some(sig => { // Check if we have a valid public key in the signature const hasValidPublicKey = sig.publicKey && typeof sig.publicKey === 'string' && @@ -106,8 +106,9 @@ export class SigningService { // - signature.publicKey is null/undefined/empty // - signature.publicKey is invalid/corrupted // - we want to verify against any trusted key - return trustedPublicKeys.some(trustedKey => { + const validity = trustedPublicKeys.some(trustedKey => { try { + // console.log("The trusted key tried against the signature:", CryptoUtils.verify(trustedKey, messageHash, sig.signature)); return CryptoUtils.verify( trustedKey, messageHash, @@ -118,7 +119,69 @@ export class SigningService { return false; } }); + // console.log("Validity:", validity); + return validity; + } + }); + + } + + static verifyDocumentWithPublicKey( + document: EnactDocument, + signature: Signature, + publicKey: string, + options: SigningOptions = {}, + securityConfig?: SecurityConfig + ): boolean { + const { + useEnactDefaults = false, + includeFields, + excludeFields, + additionalCriticalFields + } = options; + + // Load security config from ~/.enact/security if not provided + const loadedConfig = securityConfig ?? SecurityConfigManager.loadConfig(); + const config = { ...DEFAULT_SECURITY_CONFIG, ...loadedConfig }; + + // Get signatures from document or use provided signature + const signatures = document.signatures || [signature]; + + // Check minimum signatures requirement + if (signatures.length < (config.minimumSignatures ?? 1)) { + // If allowLocalUnsigned is true and we have no signatures, allow it + if (config.allowLocalUnsigned && signatures.length === 0) { + return true; } + return false; + } + + // Verify each signature + const fieldSelector = useEnactDefaults ? EnactFieldSelector : GenericFieldSelector; + + const canonicalDocument = fieldSelector.createCanonicalObject(document, { + includeFields, + excludeFields, + additionalCriticalFields + }); + + const documentString = JSON.stringify(canonicalDocument); + const messageHash = CryptoUtils.hash(documentString); + + + return signatures.every(sig => { + + // Try verifying against trusted public key + try { + return CryptoUtils.verify( + publicKey, + messageHash, + sig.signature + ); + } catch { + return false; + } + }); } diff --git a/packages/security/src/types.ts b/packages/security/src/types.ts index 22fed9b..27fecaa 100644 --- a/packages/security/src/types.ts +++ b/packages/security/src/types.ts @@ -35,6 +35,14 @@ export interface Signature { timestamp: number; } +export interface KeyMetadata { + keyId: string; + publicKey: string, + created: string; + algorithm: string; + description?: string; +} + export interface KeyPair { privateKey: string; publicKey: string; diff --git a/packages/shared/src/api/enact-api.ts b/packages/shared/src/api/enact-api.ts index 749f69c..599af32 100644 --- a/packages/shared/src/api/enact-api.ts +++ b/packages/shared/src/api/enact-api.ts @@ -1,5 +1,6 @@ import { EnactToolDefinition, + ToolSignaturePayload, ToolUsage, ToolSearchQuery, CLITokenCreate, @@ -480,6 +481,20 @@ export class EnactApiClient { throw new EnactApiError("Unknown error occurred", 0); } } + + async signTool( + toolId: string, + payload: ToolSignaturePayload, + // token: string, + // tokenType: "jwt" | "cli" = "cli" +): Promise { + const endpoint = `/functions/v1/tools/${encodeURIComponent(toolId)}/anon-sign`; + + return this.makeRequest(endpoint, { + method: "POST", + body: JSON.stringify(payload), + }); +} // =================== // OAUTH FLOW HELPERS diff --git a/packages/shared/src/api/types.ts b/packages/shared/src/api/types.ts index 62c6a72..ad250d5 100644 --- a/packages/shared/src/api/types.ts +++ b/packages/shared/src/api/types.ts @@ -1,6 +1,8 @@ export interface EnactToolDefinition { + id: string, name: string; description: string; + verified?: boolean; // Indicates if the tool has been verified command: string; from?: string; version?: string; @@ -99,3 +101,15 @@ export interface EnactExecOptions { dangerouslySkipVerification?: boolean; // Skip all signature verification (DANGEROUS) mount?: string; // Mount local directory to container (format: "local:container") } + +export interface ToolSignaturePayload { + algorithm: "sha256"; + created: string; // Time of signing + keyId: string; // ID of the private key + public_key: string; // The corresponding public key in base64 + role: "author"; + signer: string; // The userID of the signer + timestamp: number; // Unix epoch + type: "ecdsa-p256"; + value: string; // Signature +} \ No newline at end of file diff --git a/packages/shared/src/core/EnactCore.ts b/packages/shared/src/core/EnactCore.ts index 8de7769..3d98d88 100644 --- a/packages/shared/src/core/EnactCore.ts +++ b/packages/shared/src/core/EnactCore.ts @@ -19,6 +19,7 @@ import fs from "fs"; import path from "path"; import { CryptoUtils, KeyManager, SecurityConfigManager, SigningService } from "@enactprotocol/security"; import { getFrontendUrl, getApiUrl } from "../utils/config"; +import { EnactToolDefinition } from "../api/types.js"; export interface EnactCoreOptions { apiUrl?: string; @@ -314,6 +315,7 @@ export class EnactCore { } else { // Fallback: map database fields to tool format (may cause signature verification issues) tool = { + id: response.id, name: response.name, description: response.description, command: response.command, @@ -408,6 +410,43 @@ export class EnactCore { } } +public static async checkToolVerificationStatus(tool: EnactToolDefinition): Promise { + const documentForVerification = { + command: tool.command, + description: tool.description, + from: tool.from, + name: tool.name, + signatures: tool.signatures?.map(sig => ({ + signature: sig.value, + publicKey: "", // TODO: Look up the correct public key + algorithm: sig.algorithm, + timestamp: new Date(sig.created).getTime(), + })), + }; + + let isValid = false; + + if (tool.signatures && tool.signatures.length > 0) { + isValid = tool.signatures.some(sig => { + const referenceSignature = { + signature: sig.value, + publicKey: "", // TODO: Lookup correct public key based on signature UUID + algorithm: sig.algorithm, + timestamp: new Date(sig.created).getTime() + }; + + return SigningService.verifyDocument( + documentForVerification, + referenceSignature, + { includeFields: ['command', 'description', 'from', 'name'] } + ); + }); + } + + return isValid; +} + + private async verifyTool(tool: EnactTool, dangerouslySkipVerification: boolean = false): Promise { if (dangerouslySkipVerification) { logger.warn(`Skipping signature verification for tool: ${tool.name}`); @@ -419,46 +458,42 @@ private async verifyTool(tool: EnactTool, dangerouslySkipVerification: boolean = throw new Error(`Tool ${tool.name} does not have any signatures`); } - const documentForVerification = { - command: tool.command, - description: tool.description, - from: tool.from, - name: tool.name, - }; - - const referenceSignature = { - signature: tool.signatures[0].value, - publicKey: "", // Correct public key for UUID 71e02e2c-148c-4534-9900-bd9646e99333 - algorithm: tool.signatures[0].algorithm, - timestamp: new Date(tool.signatures[0].created).getTime() - }; + // const documentForVerification = { + // command: tool.command, + // description: tool.description, + // from: tool.from, + // name: tool.name, + // }; + + // const referenceSignature = { + // signature: tool.signatures[0].value, + // publicKey: "", // Correct public key for UUID 71e02e2c-148c-4534-9900-bd9646e99333 + // algorithm: tool.signatures[0].algorithm, + // timestamp: new Date(tool.signatures[0].created).getTime() + // }; - // Check what canonical document looks like - const canonicalDoc = SigningService.getCanonicalDocument(documentForVerification, { includeFields: ['command', 'description', 'from', 'name'] } -); +// // Check what canonical document looks like +// const canonicalDoc = SigningService.getCanonicalDocument(documentForVerification, { includeFields: ['command', 'description', 'from', 'name'] } +// ); - const docString = JSON.stringify(canonicalDoc); - const messageHash = CryptoUtils.hash(docString); +// const docString = JSON.stringify(canonicalDoc); +// const messageHash = CryptoUtils.hash(docString); - // Test direct crypto verification - const directVerify = CryptoUtils.verify( - referenceSignature.publicKey, - messageHash, - referenceSignature.signature - ); +// // Test direct crypto verification +// const directVerify = CryptoUtils.verify( +// referenceSignature.publicKey, +// messageHash, +// referenceSignature.signature +// ); // Check trusted keys // const trustedKeys = KeyManager.getAllTrustedPublicKeys(); - const isValid = SigningService.verifyDocument( - documentForVerification, - referenceSignature, - { includeFields: ['command', 'description', 'from', 'name'] } - ); + const isValid = await EnactCore.checkToolVerificationStatus(tool); - // console.log("Final verification result:", isValid); + console.log("Final verification result:", isValid); if (!isValid) { throw new Error(`Tool ${tool.name} has invalid signatures`); diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 86b4656..69c0d7f 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -1,6 +1,7 @@ // src/types.ts - Type definitions for Enact CLI Core export interface EnactTool { // REQUIRED FIELDS + id: string, name: string; // Tool identifier with hierarchical path description: string; // Human-readable description command: string; // Shell command to execute with version pins