@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/src/schema.js ADDED
@@ -0,0 +1,269 @@
1
+ /**
2
+ * OpenClaw Secure Manifest Schema
3
+ * JSON Schema for validating skill.yaml manifests
4
+ */
5
+
6
+ const manifestSchema = {
7
+ $schema: 'http://json-schema.org/draft-07/schema#',
8
+ $id: 'https://openclaw-secure.sociable.social/manifest.schema.json',
9
+ title: 'OpenClaw Skill Manifest',
10
+ description: 'Security manifest for OpenClaw skills',
11
+ type: 'object',
12
+ required: ['name', 'version', 'author', 'permissions'],
13
+ properties: {
14
+ // === METADATA ===
15
+ name: {
16
+ type: 'string',
17
+ pattern: '^[a-z][a-z0-9-]*$',
18
+ minLength: 2,
19
+ maxLength: 64,
20
+ description: 'Unique skill identifier (lowercase, hyphens allowed)'
21
+ },
22
+ version: {
23
+ type: 'string',
24
+ pattern: '^\\d+\\.\\d+\\.\\d+',
25
+ description: 'Semantic version (e.g., 1.0.0)'
26
+ },
27
+ display_name: {
28
+ type: 'string',
29
+ maxLength: 128,
30
+ description: 'Human-readable name'
31
+ },
32
+ description: {
33
+ type: 'string',
34
+ maxLength: 500,
35
+ description: 'Brief description of what the skill does'
36
+ },
37
+ author: {
38
+ type: 'object',
39
+ required: ['name'],
40
+ properties: {
41
+ name: {
42
+ type: 'string',
43
+ minLength: 1,
44
+ maxLength: 64
45
+ },
46
+ moltbook: {
47
+ type: 'string',
48
+ description: 'Moltbook username for identity verification'
49
+ },
50
+ email: {
51
+ type: 'string',
52
+ format: 'email'
53
+ },
54
+ url: {
55
+ type: 'string',
56
+ format: 'uri'
57
+ }
58
+ }
59
+ },
60
+ license: {
61
+ type: 'string',
62
+ description: 'SPDX license identifier'
63
+ },
64
+ repository: {
65
+ type: 'string',
66
+ format: 'uri'
67
+ },
68
+ homepage: {
69
+ type: 'string',
70
+ format: 'uri'
71
+ },
72
+
73
+ // === PERMISSIONS ===
74
+ permissions: {
75
+ type: 'object',
76
+ required: [],
77
+ properties: {
78
+ network: {
79
+ type: 'object',
80
+ properties: {
81
+ allow: {
82
+ type: 'array',
83
+ items: { type: 'string' },
84
+ default: [],
85
+ description: 'Allowed domains (supports wildcards like *.example.com)'
86
+ },
87
+ deny: {
88
+ type: 'array',
89
+ items: { type: 'string' },
90
+ default: [],
91
+ description: 'Explicitly denied domains'
92
+ }
93
+ },
94
+ default: { allow: [], deny: [] }
95
+ },
96
+ filesystem: {
97
+ type: 'object',
98
+ properties: {
99
+ read: {
100
+ type: 'array',
101
+ items: { type: 'string' },
102
+ default: [],
103
+ description: 'Paths the skill can read'
104
+ },
105
+ write: {
106
+ type: 'array',
107
+ items: { type: 'string' },
108
+ default: [],
109
+ description: 'Paths the skill can write'
110
+ },
111
+ deny: {
112
+ type: 'array',
113
+ items: { type: 'string' },
114
+ default: ['~/.ssh', '~/.gnupg', '~/.config/openclaw'],
115
+ description: 'Paths that are always denied'
116
+ }
117
+ },
118
+ default: { read: [], write: [], deny: [] }
119
+ },
120
+ shell: {
121
+ type: 'object',
122
+ properties: {
123
+ allowed: {
124
+ type: 'boolean',
125
+ default: false,
126
+ description: 'Whether shell commands are allowed'
127
+ },
128
+ commands: {
129
+ type: 'array',
130
+ items: { type: 'string' },
131
+ default: [],
132
+ description: 'Allowlist of specific commands (if shell.allowed is true)'
133
+ }
134
+ },
135
+ default: { allowed: false, commands: [] }
136
+ },
137
+ credentials: {
138
+ type: 'array',
139
+ items: { type: 'string' },
140
+ default: [],
141
+ description: 'Environment variables this skill needs access to'
142
+ },
143
+ capabilities: {
144
+ type: 'object',
145
+ properties: {
146
+ browser: {
147
+ type: 'boolean',
148
+ default: false,
149
+ description: 'Can use browser automation'
150
+ },
151
+ messaging: {
152
+ type: 'boolean',
153
+ default: false,
154
+ description: 'Can send messages on behalf of user'
155
+ },
156
+ cron: {
157
+ type: 'boolean',
158
+ default: false,
159
+ description: 'Can schedule tasks'
160
+ },
161
+ spawn_agents: {
162
+ type: 'boolean',
163
+ default: false,
164
+ description: 'Can create sub-agents'
165
+ }
166
+ },
167
+ default: {
168
+ browser: false,
169
+ messaging: false,
170
+ cron: false,
171
+ spawn_agents: false
172
+ }
173
+ }
174
+ }
175
+ },
176
+
177
+ // === RESOURCE LIMITS ===
178
+ resources: {
179
+ type: 'object',
180
+ properties: {
181
+ max_memory_mb: {
182
+ type: 'integer',
183
+ minimum: 16,
184
+ maximum: 4096,
185
+ default: 256
186
+ },
187
+ max_cpu_percent: {
188
+ type: 'integer',
189
+ minimum: 1,
190
+ maximum: 100,
191
+ default: 25
192
+ },
193
+ max_runtime_seconds: {
194
+ type: 'integer',
195
+ minimum: 1,
196
+ maximum: 3600,
197
+ default: 60
198
+ },
199
+ max_network_mb: {
200
+ type: 'integer',
201
+ minimum: 0,
202
+ maximum: 1024,
203
+ default: 10
204
+ }
205
+ }
206
+ },
207
+
208
+ // === SIGNATURE ===
209
+ signature: {
210
+ type: 'object',
211
+ properties: {
212
+ algorithm: {
213
+ type: 'string',
214
+ enum: ['ed25519'],
215
+ default: 'ed25519'
216
+ },
217
+ signer: {
218
+ type: 'string',
219
+ description: 'Identity of the signer'
220
+ },
221
+ signed_at: {
222
+ type: 'string',
223
+ format: 'date-time'
224
+ },
225
+ public_key: {
226
+ type: 'string',
227
+ description: 'Base64-encoded public key'
228
+ },
229
+ signature: {
230
+ type: 'string',
231
+ description: 'Base64-encoded signature'
232
+ },
233
+ content_hash: {
234
+ type: 'string',
235
+ pattern: '^sha256:[a-f0-9]{64}$',
236
+ description: 'SHA256 hash of manifest content (excluding signature block)'
237
+ }
238
+ }
239
+ }
240
+ }
241
+ };
242
+
243
+ // Default permissions for minimal-risk skills
244
+ const defaultPermissions = {
245
+ network: { allow: [], deny: [] },
246
+ filesystem: { read: [], write: [], deny: ['~/.ssh', '~/.gnupg', '~/.config/openclaw'] },
247
+ shell: { allowed: false, commands: [] },
248
+ credentials: [],
249
+ capabilities: {
250
+ browser: false,
251
+ messaging: false,
252
+ cron: false,
253
+ spawn_agents: false
254
+ }
255
+ };
256
+
257
+ // Default resources
258
+ const defaultResources = {
259
+ max_memory_mb: 256,
260
+ max_cpu_percent: 25,
261
+ max_runtime_seconds: 60,
262
+ max_network_mb: 10
263
+ };
264
+
265
+ module.exports = {
266
+ manifestSchema,
267
+ defaultPermissions,
268
+ defaultResources
269
+ };
package/src/signing.js ADDED
@@ -0,0 +1,193 @@
1
+ /**
2
+ * OpenClaw Secure Signing Module
3
+ * Cryptographic signing and verification using Ed25519
4
+ */
5
+
6
+ const nacl = require('tweetnacl');
7
+ const naclUtil = require('tweetnacl-util');
8
+ const crypto = require('crypto');
9
+ const yaml = require('yaml');
10
+
11
+ /**
12
+ * Generate a new Ed25519 keypair
13
+ * @returns {Object} - { publicKey: string, secretKey: string } (base64 encoded)
14
+ */
15
+ function generateKeyPair() {
16
+ const keyPair = nacl.sign.keyPair();
17
+ return {
18
+ publicKey: naclUtil.encodeBase64(keyPair.publicKey),
19
+ secretKey: naclUtil.encodeBase64(keyPair.secretKey)
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Compute SHA256 hash of content
25
+ * @param {string} content - Content to hash
26
+ * @returns {string} - Hash in format "sha256:hexstring"
27
+ */
28
+ function computeHash(content) {
29
+ const hash = crypto.createHash('sha256').update(content, 'utf8').digest('hex');
30
+ return `sha256:${hash}`;
31
+ }
32
+
33
+ /**
34
+ * Get the signable content from a manifest (excluding signature block)
35
+ * @param {Object} manifest - The manifest object
36
+ * @returns {string} - YAML string of content to sign
37
+ */
38
+ function getSignableContent(manifest) {
39
+ // Clone and remove signature block
40
+ const { signature, ...contentToSign } = manifest;
41
+
42
+ // Sort keys for deterministic serialization
43
+ const sortedContent = sortObjectKeys(contentToSign);
44
+
45
+ // Serialize to YAML
46
+ return yaml.stringify(sortedContent);
47
+ }
48
+
49
+ /**
50
+ * Recursively sort object keys for deterministic serialization
51
+ */
52
+ function sortObjectKeys(obj) {
53
+ if (Array.isArray(obj)) {
54
+ return obj.map(sortObjectKeys);
55
+ }
56
+ if (obj !== null && typeof obj === 'object') {
57
+ const sorted = {};
58
+ Object.keys(obj).sort().forEach(key => {
59
+ sorted[key] = sortObjectKeys(obj[key]);
60
+ });
61
+ return sorted;
62
+ }
63
+ return obj;
64
+ }
65
+
66
+ /**
67
+ * Sign a manifest with a secret key
68
+ * @param {Object} manifest - The manifest to sign
69
+ * @param {string} secretKeyBase64 - Base64-encoded secret key
70
+ * @param {string} signerName - Name/identity of the signer
71
+ * @returns {Object} - Manifest with signature block added
72
+ */
73
+ function signManifest(manifest, secretKeyBase64, signerName) {
74
+ // Remove any existing signature
75
+ const { signature: _, ...contentToSign } = manifest;
76
+
77
+ // Get signable content and compute hash
78
+ const signableContent = getSignableContent(contentToSign);
79
+ const contentHash = computeHash(signableContent);
80
+
81
+ // Decode secret key
82
+ const secretKey = naclUtil.decodeBase64(secretKeyBase64);
83
+
84
+ // Extract public key from secret key (last 32 bytes of 64-byte secret key)
85
+ const publicKey = secretKey.slice(32);
86
+
87
+ // Sign the content hash
88
+ const messageBytes = naclUtil.decodeUTF8(contentHash);
89
+ const signatureBytes = nacl.sign.detached(messageBytes, secretKey);
90
+
91
+ // Create signature block
92
+ const signatureBlock = {
93
+ algorithm: 'ed25519',
94
+ signer: signerName,
95
+ signed_at: new Date().toISOString(),
96
+ public_key: naclUtil.encodeBase64(publicKey),
97
+ signature: naclUtil.encodeBase64(signatureBytes),
98
+ content_hash: contentHash
99
+ };
100
+
101
+ // Return manifest with signature
102
+ return {
103
+ ...contentToSign,
104
+ signature: signatureBlock
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Verify a signed manifest
110
+ * @param {Object} manifest - The manifest with signature block
111
+ * @param {string} [expectedPublicKey] - Optional: expected public key (base64)
112
+ * @returns {Object} - { valid: boolean, error?: string, signer?: string }
113
+ */
114
+ function verifyManifest(manifest) {
115
+ // Check if signature block exists
116
+ if (!manifest.signature) {
117
+ return { valid: false, error: 'No signature block found' };
118
+ }
119
+
120
+ const { signature } = manifest;
121
+
122
+ // Validate signature block fields
123
+ if (!signature.algorithm || signature.algorithm !== 'ed25519') {
124
+ return { valid: false, error: 'Unsupported signature algorithm' };
125
+ }
126
+ if (!signature.public_key || !signature.signature || !signature.content_hash) {
127
+ return { valid: false, error: 'Incomplete signature block' };
128
+ }
129
+
130
+ try {
131
+ // Recompute content hash
132
+ const signableContent = getSignableContent(manifest);
133
+ const computedHash = computeHash(signableContent);
134
+
135
+ // Check content hash matches
136
+ if (computedHash !== signature.content_hash) {
137
+ return {
138
+ valid: false,
139
+ error: 'Content hash mismatch - manifest may have been modified',
140
+ expected: signature.content_hash,
141
+ computed: computedHash
142
+ };
143
+ }
144
+
145
+ // Decode keys and signature
146
+ const publicKey = naclUtil.decodeBase64(signature.public_key);
147
+ const signatureBytes = naclUtil.decodeBase64(signature.signature);
148
+ const messageBytes = naclUtil.decodeUTF8(signature.content_hash);
149
+
150
+ // Verify signature
151
+ const valid = nacl.sign.detached.verify(messageBytes, signatureBytes, publicKey);
152
+
153
+ if (valid) {
154
+ return {
155
+ valid: true,
156
+ signer: signature.signer,
157
+ signed_at: signature.signed_at,
158
+ public_key: signature.public_key
159
+ };
160
+ } else {
161
+ return { valid: false, error: 'Signature verification failed' };
162
+ }
163
+ } catch (err) {
164
+ return { valid: false, error: `Verification error: ${err.message}` };
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Check if a manifest is signed
170
+ */
171
+ function isSigned(manifest) {
172
+ return !!(manifest.signature && manifest.signature.signature);
173
+ }
174
+
175
+ /**
176
+ * Get the public key fingerprint (first 8 chars of base64)
177
+ */
178
+ function getKeyFingerprint(publicKeyBase64) {
179
+ const hash = crypto.createHash('sha256')
180
+ .update(Buffer.from(publicKeyBase64, 'base64'))
181
+ .digest('hex');
182
+ return hash.substring(0, 16);
183
+ }
184
+
185
+ module.exports = {
186
+ generateKeyPair,
187
+ computeHash,
188
+ signManifest,
189
+ verifyManifest,
190
+ isSigned,
191
+ getSignableContent,
192
+ getKeyFingerprint
193
+ };