Files
clawsec/skills/clawsec-nanoclaw/mcp-tools/signature-verification.ts
T
davida-ps 81c2e60513 fix(ci): temporary clawhub publish workaround for MIT-0 consent (#117)
* fix(ci): patch clawhub publish payload for temporary MIT-0 consent workaround

* fix(ci): make clawhub publish patch self-contained for tag republish

* fix(clawsec-nanoclaw): harden signature verification boundaries

* chore(clawsec-nanoclaw): bump version to 0.0.3

* fix(clawsec-nanoclaw): normalize integrity policy and baseline paths
2026-03-09 19:30:22 +02:00

227 lines
6.9 KiB
TypeScript

/**
* ClawSec Skill Signature Verification MCP Tool for NanoClaw
*
* Add this tool to /workspace/project/container/agent-runner/src/ipc-mcp-stdio.ts
*
* This tool verifies Ed25519 signatures on skill packages to prevent supply chain attacks.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import fs from 'fs';
import path from 'path';
import { z } from 'zod';
// These variables are provided by the host environment (ipc-mcp-stdio.ts)
// when this code is integrated into the NanoClaw container agent.
declare const server: { tool: (...args: any[]) => void };
declare function writeIpcFile(dir: string, data: any): void;
declare const TASKS_DIR: string;
declare const groupFolder: string;
const ALLOWED_VERIFICATION_ROOTS = [
'/tmp',
'/var/tmp',
'/workspace/ipc',
'/workspace/project/data',
'/workspace/project/tmp',
'/workspace/project/downloads',
] as const;
const ALLOWED_PACKAGE_EXTENSIONS = ['.zip', '.tar', '.tgz', '.tar.gz'] as const;
function isWithinAllowedRoots(filePath: string): boolean {
return ALLOWED_VERIFICATION_ROOTS.some((root) => filePath === root || filePath.startsWith(`${root}/`));
}
function validatePackagePath(rawPath: string): string {
if (!path.isAbsolute(rawPath)) {
throw new Error('packagePath must be absolute');
}
const resolved = path.resolve(rawPath);
if (!isWithinAllowedRoots(resolved)) {
throw new Error(`packagePath must be under: ${ALLOWED_VERIFICATION_ROOTS.join(', ')}`);
}
if (!ALLOWED_PACKAGE_EXTENSIONS.some((ext) => resolved.endsWith(ext))) {
throw new Error(`packagePath must end with one of: ${ALLOWED_PACKAGE_EXTENSIONS.join(', ')}`);
}
return resolved;
}
function validateSignaturePath(rawPath: string): string {
if (!path.isAbsolute(rawPath)) {
throw new Error('signaturePath must be absolute');
}
const resolved = path.resolve(rawPath);
if (!isWithinAllowedRoots(resolved)) {
throw new Error(`signaturePath must be under: ${ALLOWED_VERIFICATION_ROOTS.join(', ')}`);
}
if (!resolved.endsWith('.sig')) {
throw new Error('signaturePath must end with .sig');
}
return resolved;
}
// Result waiting helper
async function waitForResult(requestId: string, timeoutMs: number = 5000): Promise<any> {
const resultDir = '/workspace/ipc/clawsec_results';
const resultPath = path.join(resultDir, `${requestId}.json`);
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
if (fs.existsSync(resultPath)) {
const result = JSON.parse(fs.readFileSync(resultPath, 'utf-8'));
fs.unlinkSync(resultPath); // Cleanup
return result;
}
await new Promise(resolve => setTimeout(resolve, 100)); // Poll every 100ms
}
throw new Error(`Timeout waiting for result: ${requestId}`);
}
// ============================================================================
// MCP Tool: clawsec_verify_skill_package
// ============================================================================
server.tool(
'clawsec_verify_skill_package',
'Verify Ed25519 signature of a skill package before installation. Prevents installation of tampered or malicious skill packages by checking ClawSec signatures.',
{
packagePath: z.string().describe('Absolute path to skill package (.tar.gz or .zip)'),
signaturePath: z.string().optional().describe('Path to signature file. If omitted, auto-detects <packagePath>.sig'),
},
async (args: { packagePath: string; signaturePath?: string }) => {
const requestId = `verify-signature-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
let packagePath: string;
let sigPath: string;
try {
packagePath = validatePackagePath(args.packagePath);
sigPath = validateSignaturePath(args.signaturePath || `${packagePath}.sig`);
} catch (error) {
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: false,
valid: false,
recommendation: 'block',
error: error instanceof Error ? error.message : String(error),
}, null, 2)
}],
isError: true
};
}
// Validate package file exists
if (!fs.existsSync(packagePath)) {
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: false,
valid: false,
recommendation: 'block',
error: `Package file not found: ${packagePath}`
}, null, 2)
}],
isError: true
};
}
// Write IPC request to host
writeIpcFile(TASKS_DIR, {
type: 'verify_skill_signature',
requestId,
groupFolder,
timestamp: new Date().toISOString(),
packagePath,
signaturePath: sigPath,
});
try {
// Wait for host to verify (5 second timeout)
const result = await waitForResult(requestId, 5000);
if (!result.success) {
// Service error or file not found
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: false,
valid: false,
recommendation: 'block',
packagePath,
signaturePath: sigPath,
error: result.message || 'Verification failed',
reason: result.error?.code || 'UNKNOWN_ERROR'
}, null, 2)
}],
isError: true
};
}
// Check if signature is valid
if (!result.data?.valid) {
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
valid: false,
recommendation: 'block',
packagePath,
signaturePath: sigPath,
reason: result.data?.error || 'Signature verification failed',
packageInfo: {
sha256: result.data?.packageHash || 'unknown'
}
}, null, 2)
}],
};
}
// Signature valid!
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: true,
valid: true,
recommendation: 'install',
packagePath,
signaturePath: sigPath,
signer: result.data.signer,
algorithm: result.data.algorithm,
verifiedAt: result.data.verifiedAt,
packageInfo: {
size: fs.statSync(packagePath).size,
sha256: result.data.packageHash
}
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
success: false,
valid: false,
recommendation: 'block',
error: `Verification timeout or error: ${error instanceof Error ? error.message : String(error)}`
}, null, 2)
}],
isError: true
};
}
}
);