@ellistevo/openclaw-secure 1.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/bin/cli.js ADDED
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * OpenClaw Secure CLI
5
+ * Command-line tool for managing skill security
6
+ */
7
+
8
+ const { Command } = require('commander');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const yaml = require('yaml');
12
+ const crypto = require('crypto');
13
+
14
+ // Import our modules
15
+ const {
16
+ validateManifest,
17
+ generateKeyPair,
18
+ signManifest,
19
+ verifyManifest,
20
+ isSigned,
21
+ calculateTrustScore,
22
+ formatTrustScore,
23
+ getKeyFingerprint,
24
+ defaultPermissions,
25
+ defaultResources
26
+ } = require('../src');
27
+
28
+ const program = new Command();
29
+
30
+ // Colors for terminal output (basic ANSI)
31
+ const colors = {
32
+ reset: '\x1b[0m',
33
+ red: '\x1b[31m',
34
+ green: '\x1b[32m',
35
+ yellow: '\x1b[33m',
36
+ blue: '\x1b[34m',
37
+ cyan: '\x1b[36m',
38
+ gray: '\x1b[90m',
39
+ bold: '\x1b[1m'
40
+ };
41
+
42
+ function colorize(text, color) {
43
+ return `${colors[color] || ''}${text}${colors.reset}`;
44
+ }
45
+
46
+ function success(msg) {
47
+ console.log(colorize('✓ ' + msg, 'green'));
48
+ }
49
+
50
+ function error(msg) {
51
+ console.error(colorize('✗ ' + msg, 'red'));
52
+ }
53
+
54
+ function info(msg) {
55
+ console.log(colorize('ℹ ' + msg, 'cyan'));
56
+ }
57
+
58
+ function warn(msg) {
59
+ console.log(colorize('⚠ ' + msg, 'yellow'));
60
+ }
61
+
62
+ program
63
+ .name('openclaw-secure')
64
+ .description('Security toolkit for OpenClaw skills')
65
+ .version('1.0.0');
66
+
67
+ // === INIT COMMAND ===
68
+ program
69
+ .command('init')
70
+ .description('Initialize a new skill manifest (skill.yaml)')
71
+ .option('-n, --name <name>', 'Skill name')
72
+ .option('-a, --author <author>', 'Author name')
73
+ .option('-f, --force', 'Overwrite existing manifest')
74
+ .action((options) => {
75
+ const manifestPath = path.join(process.cwd(), 'skill.yaml');
76
+
77
+ if (fs.existsSync(manifestPath) && !options.force) {
78
+ error('skill.yaml already exists. Use --force to overwrite.');
79
+ process.exit(1);
80
+ }
81
+
82
+ const skillName = options.name || path.basename(process.cwd());
83
+ const authorName = options.author || process.env.USER || 'unknown';
84
+
85
+ const manifest = {
86
+ name: skillName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
87
+ version: '1.0.0',
88
+ display_name: skillName,
89
+ description: 'A secure OpenClaw skill',
90
+ author: {
91
+ name: authorName
92
+ },
93
+ license: 'MIT',
94
+ permissions: {
95
+ network: {
96
+ allow: [],
97
+ deny: []
98
+ },
99
+ filesystem: {
100
+ read: [],
101
+ write: [],
102
+ deny: ['~/.ssh', '~/.gnupg', '~/.config/openclaw']
103
+ },
104
+ shell: {
105
+ allowed: false,
106
+ commands: []
107
+ },
108
+ credentials: [],
109
+ capabilities: {
110
+ browser: false,
111
+ messaging: false,
112
+ cron: false,
113
+ spawn_agents: false
114
+ }
115
+ },
116
+ resources: {
117
+ max_memory_mb: 256,
118
+ max_cpu_percent: 25,
119
+ max_runtime_seconds: 60,
120
+ max_network_mb: 10
121
+ }
122
+ };
123
+
124
+ const yamlContent = yaml.stringify(manifest);
125
+ fs.writeFileSync(manifestPath, yamlContent);
126
+
127
+ success(`Created skill.yaml for "${manifest.name}"`);
128
+ info('Edit the manifest to declare your skill\'s permissions');
129
+ info('Then run: openclaw-secure sign');
130
+ });
131
+
132
+ // === VALIDATE COMMAND ===
133
+ program
134
+ .command('validate')
135
+ .description('Validate a skill manifest')
136
+ .argument('[path]', 'Path to skill.yaml', 'skill.yaml')
137
+ .action((manifestPath) => {
138
+ if (!fs.existsSync(manifestPath)) {
139
+ error(`File not found: ${manifestPath}`);
140
+ process.exit(1);
141
+ }
142
+
143
+ try {
144
+ const content = fs.readFileSync(manifestPath, 'utf8');
145
+ const manifest = yaml.parse(content);
146
+
147
+ const result = validateManifest(manifest);
148
+
149
+ if (result.valid) {
150
+ success('Manifest is valid');
151
+
152
+ // Show trust score
153
+ const trust = calculateTrustScore(result.manifest, {
154
+ signed: isSigned(result.manifest),
155
+ verified: false
156
+ });
157
+ console.log(formatTrustScore(trust));
158
+ } else {
159
+ error('Manifest validation failed:');
160
+ result.errors.forEach(err => {
161
+ console.log(` - ${err}`);
162
+ });
163
+ process.exit(1);
164
+ }
165
+ } catch (err) {
166
+ error(`Failed to parse manifest: ${err.message}`);
167
+ process.exit(1);
168
+ }
169
+ });
170
+
171
+ // === KEYGEN COMMAND ===
172
+ program
173
+ .command('keygen')
174
+ .description('Generate a new signing keypair')
175
+ .option('-o, --output <dir>', 'Output directory', path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw-secure'))
176
+ .option('-n, --name <name>', 'Key name', 'default')
177
+ .action((options) => {
178
+ const outputDir = options.output;
179
+ const keyName = options.name;
180
+
181
+ // Create output directory if needed
182
+ if (!fs.existsSync(outputDir)) {
183
+ fs.mkdirSync(outputDir, { recursive: true });
184
+ }
185
+
186
+ const secretKeyPath = path.join(outputDir, `${keyName}.key`);
187
+ const publicKeyPath = path.join(outputDir, `${keyName}.pub`);
188
+
189
+ if (fs.existsSync(secretKeyPath)) {
190
+ error(`Key already exists: ${secretKeyPath}`);
191
+ error('Use a different --name or delete the existing key');
192
+ process.exit(1);
193
+ }
194
+
195
+ // Generate keypair
196
+ const keyPair = generateKeyPair();
197
+
198
+ // Save keys
199
+ fs.writeFileSync(secretKeyPath, keyPair.secretKey, { mode: 0o600 });
200
+ fs.writeFileSync(publicKeyPath, keyPair.publicKey, { mode: 0o644 });
201
+
202
+ const fingerprint = getKeyFingerprint(keyPair.publicKey);
203
+
204
+ success('Generated new signing keypair');
205
+ info(`Secret key: ${secretKeyPath}`);
206
+ info(`Public key: ${publicKeyPath}`);
207
+ info(`Fingerprint: ${fingerprint}`);
208
+ warn('Keep your secret key safe! Anyone with it can sign skills as you.');
209
+ });
210
+
211
+ // === SIGN COMMAND ===
212
+ program
213
+ .command('sign')
214
+ .description('Sign a skill manifest')
215
+ .argument('[path]', 'Path to skill.yaml', 'skill.yaml')
216
+ .option('-k, --key <path>', 'Path to secret key')
217
+ .option('-n, --name <name>', 'Signer name (for signature block)')
218
+ .action((manifestPath, options) => {
219
+ if (!fs.existsSync(manifestPath)) {
220
+ error(`File not found: ${manifestPath}`);
221
+ process.exit(1);
222
+ }
223
+
224
+ // Find secret key
225
+ let secretKeyPath = options.key;
226
+ if (!secretKeyPath) {
227
+ const defaultKeyDir = path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw-secure');
228
+ secretKeyPath = path.join(defaultKeyDir, 'default.key');
229
+ }
230
+
231
+ if (!fs.existsSync(secretKeyPath)) {
232
+ error(`Secret key not found: ${secretKeyPath}`);
233
+ error('Run: openclaw-secure keygen');
234
+ process.exit(1);
235
+ }
236
+
237
+ try {
238
+ // Read manifest
239
+ const content = fs.readFileSync(manifestPath, 'utf8');
240
+ const manifest = yaml.parse(content);
241
+
242
+ // Validate first
243
+ const validation = validateManifest(manifest);
244
+ if (!validation.valid) {
245
+ error('Manifest validation failed. Fix errors before signing:');
246
+ validation.errors.forEach(err => console.log(` - ${err}`));
247
+ process.exit(1);
248
+ }
249
+
250
+ // Read secret key
251
+ const secretKey = fs.readFileSync(secretKeyPath, 'utf8').trim();
252
+
253
+ // Determine signer name
254
+ const signerName = options.name || manifest.author?.name || 'unknown';
255
+
256
+ // Sign manifest
257
+ const signedManifest = signManifest(validation.manifest, secretKey, signerName);
258
+
259
+ // Write back
260
+ const yamlContent = yaml.stringify(signedManifest);
261
+ fs.writeFileSync(manifestPath, yamlContent);
262
+
263
+ success(`Signed manifest as "${signerName}"`);
264
+ info(`Hash: ${signedManifest.signature.content_hash}`);
265
+ info(`Signed at: ${signedManifest.signature.signed_at}`);
266
+ } catch (err) {
267
+ error(`Signing failed: ${err.message}`);
268
+ process.exit(1);
269
+ }
270
+ });
271
+
272
+ // === VERIFY COMMAND ===
273
+ program
274
+ .command('verify')
275
+ .description('Verify a signed skill manifest')
276
+ .argument('[path]', 'Path to skill.yaml', 'skill.yaml')
277
+ .action((manifestPath) => {
278
+ if (!fs.existsSync(manifestPath)) {
279
+ error(`File not found: ${manifestPath}`);
280
+ process.exit(1);
281
+ }
282
+
283
+ try {
284
+ const content = fs.readFileSync(manifestPath, 'utf8');
285
+ const manifest = yaml.parse(content);
286
+
287
+ if (!isSigned(manifest)) {
288
+ error('Manifest is not signed');
289
+ process.exit(1);
290
+ }
291
+
292
+ const result = verifyManifest(manifest);
293
+
294
+ if (result.valid) {
295
+ success('Signature is valid');
296
+ info(`Signer: ${result.signer}`);
297
+ info(`Signed at: ${result.signed_at}`);
298
+ info(`Public key: ${result.public_key.substring(0, 16)}...`);
299
+
300
+ // Show trust score
301
+ const trust = calculateTrustScore(manifest, { signed: true, verified: true });
302
+ console.log(formatTrustScore(trust));
303
+ } else {
304
+ error(`Verification failed: ${result.error}`);
305
+ if (result.expected && result.computed) {
306
+ info(`Expected hash: ${result.expected}`);
307
+ info(`Computed hash: ${result.computed}`);
308
+ }
309
+ process.exit(1);
310
+ }
311
+ } catch (err) {
312
+ error(`Verification failed: ${err.message}`);
313
+ process.exit(1);
314
+ }
315
+ });
316
+
317
+ // === AUDIT COMMAND ===
318
+ program
319
+ .command('audit')
320
+ .description('Audit a skill and show trust score')
321
+ .argument('[path]', 'Path to skill.yaml', 'skill.yaml')
322
+ .option('-v, --verbose', 'Show detailed breakdown')
323
+ .action((manifestPath, options) => {
324
+ if (!fs.existsSync(manifestPath)) {
325
+ error(`File not found: ${manifestPath}`);
326
+ process.exit(1);
327
+ }
328
+
329
+ try {
330
+ const content = fs.readFileSync(manifestPath, 'utf8');
331
+ const manifest = yaml.parse(content);
332
+
333
+ // Validate
334
+ const validation = validateManifest(manifest);
335
+ if (!validation.valid) {
336
+ warn('Manifest has validation errors:');
337
+ validation.errors.forEach(err => console.log(` - ${err}`));
338
+ }
339
+
340
+ // Check signature
341
+ let signed = false;
342
+ let verified = false;
343
+
344
+ if (isSigned(manifest)) {
345
+ const verifyResult = verifyManifest(manifest);
346
+ signed = true;
347
+ verified = verifyResult.valid;
348
+
349
+ if (verified) {
350
+ success(`Signed by: ${verifyResult.signer}`);
351
+ } else {
352
+ warn(`Signature present but invalid: ${verifyResult.error}`);
353
+ }
354
+ } else {
355
+ warn('Manifest is not signed');
356
+ }
357
+
358
+ // Calculate trust score
359
+ const trust = calculateTrustScore(validation.manifest, { signed, verified });
360
+ console.log(formatTrustScore(trust));
361
+
362
+ // Verbose output
363
+ if (options.verbose) {
364
+ console.log('\n' + colorize('Permissions Detail:', 'bold'));
365
+ console.log(yaml.stringify(validation.manifest.permissions));
366
+ }
367
+
368
+ } catch (err) {
369
+ error(`Audit failed: ${err.message}`);
370
+ process.exit(1);
371
+ }
372
+ });
373
+
374
+ // === SHOW-PUBLIC-KEY COMMAND ===
375
+ program
376
+ .command('show-key')
377
+ .description('Show public key for sharing')
378
+ .option('-n, --name <name>', 'Key name', 'default')
379
+ .action((options) => {
380
+ const keyDir = path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw-secure');
381
+ const publicKeyPath = path.join(keyDir, `${options.name}.pub`);
382
+
383
+ if (!fs.existsSync(publicKeyPath)) {
384
+ error(`Public key not found: ${publicKeyPath}`);
385
+ error('Run: openclaw-secure keygen');
386
+ process.exit(1);
387
+ }
388
+
389
+ const publicKey = fs.readFileSync(publicKeyPath, 'utf8').trim();
390
+ const fingerprint = getKeyFingerprint(publicKey);
391
+
392
+ console.log('\n' + colorize('Your Public Key:', 'bold'));
393
+ console.log(publicKey);
394
+ console.log('\n' + colorize('Fingerprint:', 'bold'));
395
+ console.log(fingerprint);
396
+ console.log('\n' + colorize('Share this key so others can verify your signatures.', 'gray'));
397
+ });
398
+
399
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@ellistevo/openclaw-secure",
3
+ "version": "1.0.0",
4
+ "description": "Security toolkit for OpenClaw skills - signing, manifests, and verification",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "openclaw-secure": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test test/*.test.js",
11
+ "lint": "eslint src/ bin/",
12
+ "build": "echo 'No build step needed'",
13
+ "prepublishOnly": "npm test"
14
+ },
15
+ "keywords": [
16
+ "openclaw",
17
+ "ai",
18
+ "agent",
19
+ "security",
20
+ "signing",
21
+ "manifest"
22
+ ],
23
+ "author": "Sociable Inc <hello@sociable.social>",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/sociable-inc/openclaw-secure"
28
+ },
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ },
32
+ "dependencies": {
33
+ "yaml": "^2.3.4",
34
+ "ajv": "^8.12.0",
35
+ "ajv-formats": "^2.1.1",
36
+ "commander": "^11.1.0",
37
+ "tweetnacl": "^1.0.3",
38
+ "tweetnacl-util": "^0.15.1"
39
+ },
40
+ "devDependencies": {
41
+ "eslint": "^8.56.0"
42
+ }
43
+ }
package/src/index.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * OpenClaw Secure
3
+ * Security toolkit for OpenClaw skills - signing, manifests, and verification
4
+ *
5
+ * @author Sociable Inc <hello@sociable.social>
6
+ * @license MIT
7
+ */
8
+
9
+ const { manifestSchema, defaultPermissions, defaultResources } = require('./schema');
10
+ const { validateManifest, isValidManifest, assertValidManifest, applyDefaults } = require('./validator');
11
+ const {
12
+ generateKeyPair,
13
+ computeHash,
14
+ signManifest,
15
+ verifyManifest,
16
+ isSigned,
17
+ getSignableContent,
18
+ getKeyFingerprint
19
+ } = require('./signing');
20
+ const {
21
+ calculateTrustScore,
22
+ scoreToGrade,
23
+ gradeColor,
24
+ gradeEmoji,
25
+ formatTrustScore,
26
+ RISK_WEIGHTS
27
+ } = require('./trust');
28
+
29
+ module.exports = {
30
+ // Schema
31
+ manifestSchema,
32
+ defaultPermissions,
33
+ defaultResources,
34
+
35
+ // Validation
36
+ validateManifest,
37
+ isValidManifest,
38
+ assertValidManifest,
39
+ applyDefaults,
40
+
41
+ // Signing
42
+ generateKeyPair,
43
+ computeHash,
44
+ signManifest,
45
+ verifyManifest,
46
+ isSigned,
47
+ getSignableContent,
48
+ getKeyFingerprint,
49
+
50
+ // Trust scoring
51
+ calculateTrustScore,
52
+ scoreToGrade,
53
+ gradeColor,
54
+ gradeEmoji,
55
+ formatTrustScore,
56
+ RISK_WEIGHTS
57
+ };