@ellistevo/openclaw-secure 1.0.0 → 1.1.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 +80 -1
- package/bin/cli.js +181 -1
- package/package.json +1 -1
- package/src/index.js +14 -1
- package/src/signing.js +197 -1
- package/src/trust.js +61 -9
- package/test/all.test.js +347 -0
package/README.md
CHANGED
|
@@ -106,8 +106,86 @@ openclaw-secure audit
|
|
|
106
106
|
| `sign` | Sign manifest with your key |
|
|
107
107
|
| `verify` | Verify manifest signature |
|
|
108
108
|
| `audit` | Calculate trust score |
|
|
109
|
+
| `attest` | Add an auditor attestation (vouch for a skill) |
|
|
110
|
+
| `isnad` | Show the chain of trust (author → auditors) |
|
|
109
111
|
| `show-key` | Display your public key |
|
|
110
112
|
|
|
113
|
+
## Attestation Chains (Isnad) 🆕
|
|
114
|
+
|
|
115
|
+
Signing proves WHO wrote a skill. Attestations prove WHO REVIEWED it.
|
|
116
|
+
|
|
117
|
+
An **isnad** (from Arabic: سند, "chain of transmission") is a chain of trust showing:
|
|
118
|
+
1. Who authored the skill
|
|
119
|
+
2. Who audited/reviewed it
|
|
120
|
+
3. Who vouches for it
|
|
121
|
+
|
|
122
|
+
### Add an attestation (as an auditor)
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Review the skill, then attest it
|
|
126
|
+
openclaw-secure attest --name "YourAuditorName" --type security_audit --notes "Reviewed code, no malicious patterns"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### View the chain of trust
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
openclaw-secure isnad --verify
|
|
133
|
+
# 📜 Chain of Trust (Isnad):
|
|
134
|
+
# Provenance chain for this skill
|
|
135
|
+
#
|
|
136
|
+
# ├── AUTHOR: SkillAuthor ✓ verified
|
|
137
|
+
# │ Key: abc123...
|
|
138
|
+
# │ Time: 2026-02-05T...
|
|
139
|
+
#
|
|
140
|
+
# └── AUDITOR: SecurityExpert ✓ verified
|
|
141
|
+
# Key: def456...
|
|
142
|
+
# Time: 2026-02-06T...
|
|
143
|
+
# Type: security_audit
|
|
144
|
+
# Notes: Reviewed code, no malicious patterns
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Trust scoring with attestations
|
|
148
|
+
|
|
149
|
+
Attestations **reduce risk scores**:
|
|
150
|
+
|
|
151
|
+
| Attestation Type | Score Bonus |
|
|
152
|
+
|------------------|-------------|
|
|
153
|
+
| `security_audit` | -20 points |
|
|
154
|
+
| `code_review` | -15 points |
|
|
155
|
+
| `endorsement` | -10 points |
|
|
156
|
+
| From trusted auditor | -10 extra |
|
|
157
|
+
|
|
158
|
+
A skill with Grade C (45 points) + one security audit = Grade B (25 points).
|
|
159
|
+
|
|
160
|
+
### Programmatic attestation
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
const {
|
|
164
|
+
createAttestation,
|
|
165
|
+
addAttestation,
|
|
166
|
+
verifyAttestation,
|
|
167
|
+
verifyAllAttestations,
|
|
168
|
+
getIsnad
|
|
169
|
+
} = require('openclaw-secure');
|
|
170
|
+
|
|
171
|
+
// Create attestation
|
|
172
|
+
const attestation = createAttestation(signedManifest, auditorSecretKey, 'AuditorName', {
|
|
173
|
+
type: 'security_audit',
|
|
174
|
+
notes: 'Reviewed and approved'
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Add to manifest
|
|
178
|
+
const attested = addAttestation(signedManifest, attestation);
|
|
179
|
+
|
|
180
|
+
// Verify
|
|
181
|
+
const result = verifyAttestation(attestation, attested);
|
|
182
|
+
console.log(result.valid, result.auditor);
|
|
183
|
+
|
|
184
|
+
// View chain
|
|
185
|
+
const chain = getIsnad(attested);
|
|
186
|
+
chain.forEach(link => console.log(link.role, link.identity));
|
|
187
|
+
```
|
|
188
|
+
|
|
111
189
|
## Trust Grades
|
|
112
190
|
|
|
113
191
|
| Grade | Score | Meaning |
|
|
@@ -213,7 +291,8 @@ This doesn't sandbox execution (that's OpenClaw's job), but it enables:
|
|
|
213
291
|
|
|
214
292
|
PRs welcome! Areas we need help:
|
|
215
293
|
- [ ] Integration with OpenClaw core
|
|
216
|
-
- [
|
|
294
|
+
- [x] Attestation chains (isnad) ✅ v1.1.0
|
|
295
|
+
- [ ] Trusted key registry (public key lookup service)
|
|
217
296
|
- [ ] Automated auditing tools
|
|
218
297
|
- [ ] Better sandbox enforcement
|
|
219
298
|
|
package/bin/cli.js
CHANGED
|
@@ -22,7 +22,13 @@ const {
|
|
|
22
22
|
formatTrustScore,
|
|
23
23
|
getKeyFingerprint,
|
|
24
24
|
defaultPermissions,
|
|
25
|
-
defaultResources
|
|
25
|
+
defaultResources,
|
|
26
|
+
// Attestation chain functions
|
|
27
|
+
createAttestation,
|
|
28
|
+
addAttestation,
|
|
29
|
+
verifyAttestation,
|
|
30
|
+
verifyAllAttestations,
|
|
31
|
+
getIsnad
|
|
26
32
|
} = require('../src');
|
|
27
33
|
|
|
28
34
|
const program = new Command();
|
|
@@ -396,4 +402,178 @@ program
|
|
|
396
402
|
console.log('\n' + colorize('Share this key so others can verify your signatures.', 'gray'));
|
|
397
403
|
});
|
|
398
404
|
|
|
405
|
+
// === ATTEST COMMAND ===
|
|
406
|
+
program
|
|
407
|
+
.command('attest')
|
|
408
|
+
.description('Add an attestation (vouch for a skill as an auditor)')
|
|
409
|
+
.argument('[path]', 'Path to skill.yaml', 'skill.yaml')
|
|
410
|
+
.option('-k, --key <path>', 'Path to your secret key')
|
|
411
|
+
.option('-n, --name <name>', 'Your auditor name')
|
|
412
|
+
.option('-t, --type <type>', 'Attestation type (security_audit, code_review, endorsement)', 'endorsement')
|
|
413
|
+
.option('--notes <notes>', 'Optional notes about your review')
|
|
414
|
+
.action((manifestPath, options) => {
|
|
415
|
+
if (!fs.existsSync(manifestPath)) {
|
|
416
|
+
error(`File not found: ${manifestPath}`);
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Find secret key
|
|
421
|
+
let secretKeyPath = options.key;
|
|
422
|
+
if (!secretKeyPath) {
|
|
423
|
+
const defaultKeyDir = path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw-secure');
|
|
424
|
+
secretKeyPath = path.join(defaultKeyDir, 'default.key');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!fs.existsSync(secretKeyPath)) {
|
|
428
|
+
error(`Secret key not found: ${secretKeyPath}`);
|
|
429
|
+
error('Run: openclaw-secure keygen');
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
// Read manifest
|
|
435
|
+
const content = fs.readFileSync(manifestPath, 'utf8');
|
|
436
|
+
const manifest = yaml.parse(content);
|
|
437
|
+
|
|
438
|
+
// Must be signed first
|
|
439
|
+
if (!isSigned(manifest)) {
|
|
440
|
+
error('Cannot attest unsigned manifest');
|
|
441
|
+
error('The skill author must sign it first with: openclaw-secure sign');
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Verify current signature
|
|
446
|
+
const verifyResult = verifyManifest(manifest);
|
|
447
|
+
if (!verifyResult.valid) {
|
|
448
|
+
error(`Manifest signature is invalid: ${verifyResult.error}`);
|
|
449
|
+
error('Cannot attest a tampered manifest');
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Read secret key
|
|
454
|
+
const secretKey = fs.readFileSync(secretKeyPath, 'utf8').trim();
|
|
455
|
+
|
|
456
|
+
// Determine auditor name
|
|
457
|
+
const auditorName = options.name || process.env.USER || 'anonymous-auditor';
|
|
458
|
+
|
|
459
|
+
// Create attestation
|
|
460
|
+
const attestation = createAttestation(manifest, secretKey, auditorName, {
|
|
461
|
+
type: options.type,
|
|
462
|
+
notes: options.notes
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Add attestation to manifest
|
|
466
|
+
const attestedManifest = addAttestation(manifest, attestation);
|
|
467
|
+
|
|
468
|
+
// Write back
|
|
469
|
+
const yamlContent = yaml.stringify(attestedManifest);
|
|
470
|
+
fs.writeFileSync(manifestPath, yamlContent);
|
|
471
|
+
|
|
472
|
+
const attestationCount = attestedManifest.attestations.length;
|
|
473
|
+
|
|
474
|
+
success(`Added attestation as "${auditorName}" (${options.type})`);
|
|
475
|
+
info(`Total attestations: ${attestationCount}`);
|
|
476
|
+
if (options.notes) {
|
|
477
|
+
info(`Notes: ${options.notes}`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
} catch (err) {
|
|
481
|
+
error(`Attestation failed: ${err.message}`);
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// === ISNAD (CHAIN OF TRUST) COMMAND ===
|
|
487
|
+
program
|
|
488
|
+
.command('isnad')
|
|
489
|
+
.description('Show the chain of trust (author → auditors)')
|
|
490
|
+
.argument('[path]', 'Path to skill.yaml', 'skill.yaml')
|
|
491
|
+
.option('-v, --verify', 'Verify all signatures in the chain')
|
|
492
|
+
.action((manifestPath, options) => {
|
|
493
|
+
if (!fs.existsSync(manifestPath)) {
|
|
494
|
+
error(`File not found: ${manifestPath}`);
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
const content = fs.readFileSync(manifestPath, 'utf8');
|
|
500
|
+
const manifest = yaml.parse(content);
|
|
501
|
+
|
|
502
|
+
// Get the isnad (chain)
|
|
503
|
+
const chain = getIsnad(manifest);
|
|
504
|
+
|
|
505
|
+
if (chain.length === 0) {
|
|
506
|
+
warn('No chain of trust found (manifest is unsigned)');
|
|
507
|
+
process.exit(0);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
console.log('\n' + colorize('📜 Chain of Trust (Isnad):', 'bold'));
|
|
511
|
+
console.log(colorize(' Provenance chain for this skill\n', 'gray'));
|
|
512
|
+
|
|
513
|
+
// Verify if requested
|
|
514
|
+
let authorVerified = false;
|
|
515
|
+
let attestationsVerified = [];
|
|
516
|
+
|
|
517
|
+
if (options.verify) {
|
|
518
|
+
// Verify author signature
|
|
519
|
+
if (manifest.signature) {
|
|
520
|
+
const authorResult = verifyManifest(manifest);
|
|
521
|
+
authorVerified = authorResult.valid;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Verify attestations
|
|
525
|
+
if (manifest.attestations && manifest.attestations.length > 0) {
|
|
526
|
+
const attestResult = verifyAllAttestations(manifest);
|
|
527
|
+
attestationsVerified = attestResult.attestations;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Display chain
|
|
532
|
+
chain.forEach((link, index) => {
|
|
533
|
+
const isLast = index === chain.length - 1;
|
|
534
|
+
const prefix = isLast ? '└──' : '├──';
|
|
535
|
+
const indent = isLast ? ' ' : '│ ';
|
|
536
|
+
|
|
537
|
+
let status = '';
|
|
538
|
+
if (options.verify) {
|
|
539
|
+
if (link.role === 'author') {
|
|
540
|
+
status = authorVerified ? colorize(' ✓ verified', 'green') : colorize(' ✗ invalid', 'red');
|
|
541
|
+
} else {
|
|
542
|
+
const verified = attestationsVerified.find(a => a.auditor === link.identity);
|
|
543
|
+
status = verified ? colorize(' ✓ verified', 'green') : colorize(' ✗ invalid', 'red');
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const roleColor = link.role === 'author' ? 'cyan' : 'yellow';
|
|
548
|
+
console.log(` ${prefix} ${colorize(link.role.toUpperCase(), roleColor)}: ${link.identity}${status}`);
|
|
549
|
+
console.log(` ${indent} Key: ${link.public_key.substring(0, 16)}...`);
|
|
550
|
+
console.log(` ${indent} Time: ${link.timestamp}`);
|
|
551
|
+
if (link.type) {
|
|
552
|
+
console.log(` ${indent} Type: ${link.type}`);
|
|
553
|
+
}
|
|
554
|
+
if (link.notes) {
|
|
555
|
+
console.log(` ${indent} Notes: ${link.notes}`);
|
|
556
|
+
}
|
|
557
|
+
console.log();
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Summary
|
|
561
|
+
const authors = chain.filter(l => l.role === 'author').length;
|
|
562
|
+
const auditors = chain.filter(l => l.role === 'auditor').length;
|
|
563
|
+
|
|
564
|
+
console.log(colorize(' Summary:', 'bold'));
|
|
565
|
+
console.log(` ${authors} author(s), ${auditors} auditor(s)`);
|
|
566
|
+
|
|
567
|
+
if (auditors > 0) {
|
|
568
|
+
info('This skill has been reviewed by third-party auditors');
|
|
569
|
+
} else {
|
|
570
|
+
warn('No third-party auditors have vouched for this skill');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
} catch (err) {
|
|
574
|
+
error(`Failed to show isnad: ${err.message}`);
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
399
579
|
program.parse();
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -15,7 +15,13 @@ const {
|
|
|
15
15
|
verifyManifest,
|
|
16
16
|
isSigned,
|
|
17
17
|
getSignableContent,
|
|
18
|
-
getKeyFingerprint
|
|
18
|
+
getKeyFingerprint,
|
|
19
|
+
// Attestation chain (isnad) functions
|
|
20
|
+
createAttestation,
|
|
21
|
+
addAttestation,
|
|
22
|
+
verifyAttestation,
|
|
23
|
+
verifyAllAttestations,
|
|
24
|
+
getIsnad
|
|
19
25
|
} = require('./signing');
|
|
20
26
|
const {
|
|
21
27
|
calculateTrustScore,
|
|
@@ -47,6 +53,13 @@ module.exports = {
|
|
|
47
53
|
getSignableContent,
|
|
48
54
|
getKeyFingerprint,
|
|
49
55
|
|
|
56
|
+
// Attestation chains (isnad)
|
|
57
|
+
createAttestation,
|
|
58
|
+
addAttestation,
|
|
59
|
+
verifyAttestation,
|
|
60
|
+
verifyAllAttestations,
|
|
61
|
+
getIsnad,
|
|
62
|
+
|
|
50
63
|
// Trust scoring
|
|
51
64
|
calculateTrustScore,
|
|
52
65
|
scoreToGrade,
|
package/src/signing.js
CHANGED
|
@@ -182,6 +182,196 @@ function getKeyFingerprint(publicKeyBase64) {
|
|
|
182
182
|
return hash.substring(0, 16);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// ATTESTATION CHAINS (ISNAD)
|
|
187
|
+
// Allows auditors to vouch for skills, creating a chain of trust
|
|
188
|
+
// ============================================================================
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Create an attestation for a signed manifest
|
|
192
|
+
* An attestation is a signature over: content_hash + author_signature
|
|
193
|
+
* This creates a chain: content → author → auditor(s)
|
|
194
|
+
*
|
|
195
|
+
* @param {Object} manifest - The signed manifest to attest
|
|
196
|
+
* @param {string} auditorSecretKey - Base64-encoded auditor secret key
|
|
197
|
+
* @param {string} auditorName - Name/identity of the auditor
|
|
198
|
+
* @param {Object} options - Additional attestation options
|
|
199
|
+
* @param {string} [options.type] - Attestation type (security_audit, code_review, endorsement)
|
|
200
|
+
* @param {string} [options.notes] - Optional notes about the review
|
|
201
|
+
* @returns {Object} - The attestation object to add to manifest.attestations[]
|
|
202
|
+
*/
|
|
203
|
+
function createAttestation(manifest, auditorSecretKey, auditorName, options = {}) {
|
|
204
|
+
// Manifest must be signed first
|
|
205
|
+
if (!manifest.signature || !manifest.signature.signature) {
|
|
206
|
+
throw new Error('Cannot attest unsigned manifest - sign it first');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Create the attestation message: content_hash + author_signature
|
|
210
|
+
// This proves the auditor saw this exact content signed by this exact author
|
|
211
|
+
const attestationMessage = `${manifest.signature.content_hash}|${manifest.signature.signature}`;
|
|
212
|
+
const messageBytes = naclUtil.decodeUTF8(attestationMessage);
|
|
213
|
+
|
|
214
|
+
// Decode auditor's secret key
|
|
215
|
+
const secretKey = naclUtil.decodeBase64(auditorSecretKey);
|
|
216
|
+
const publicKey = secretKey.slice(32);
|
|
217
|
+
|
|
218
|
+
// Sign the attestation
|
|
219
|
+
const signatureBytes = nacl.sign.detached(messageBytes, secretKey);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
type: options.type || 'endorsement',
|
|
223
|
+
auditor: auditorName,
|
|
224
|
+
auditor_public_key: naclUtil.encodeBase64(publicKey),
|
|
225
|
+
attestation: naclUtil.encodeBase64(signatureBytes),
|
|
226
|
+
attested_at: new Date().toISOString(),
|
|
227
|
+
notes: options.notes || null,
|
|
228
|
+
// Include what was attested for verification
|
|
229
|
+
attested_content_hash: manifest.signature.content_hash,
|
|
230
|
+
attested_author_signature: manifest.signature.signature
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Add an attestation to a manifest
|
|
236
|
+
* @param {Object} manifest - The manifest to add attestation to
|
|
237
|
+
* @param {Object} attestation - The attestation from createAttestation()
|
|
238
|
+
* @returns {Object} - Manifest with attestation added
|
|
239
|
+
*/
|
|
240
|
+
function addAttestation(manifest, attestation) {
|
|
241
|
+
const attestations = manifest.attestations || [];
|
|
242
|
+
|
|
243
|
+
// Check for duplicate attestations from same auditor
|
|
244
|
+
const existingAuditor = attestations.find(a =>
|
|
245
|
+
a.auditor_public_key === attestation.auditor_public_key
|
|
246
|
+
);
|
|
247
|
+
if (existingAuditor) {
|
|
248
|
+
throw new Error(`Attestation from this auditor already exists (${attestation.auditor})`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
...manifest,
|
|
253
|
+
attestations: [...attestations, attestation]
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Verify an attestation
|
|
259
|
+
* @param {Object} attestation - The attestation to verify
|
|
260
|
+
* @param {Object} manifest - The manifest it attests
|
|
261
|
+
* @returns {Object} - { valid: boolean, error?: string, auditor?: string }
|
|
262
|
+
*/
|
|
263
|
+
function verifyAttestation(attestation, manifest) {
|
|
264
|
+
// Check manifest is signed
|
|
265
|
+
if (!manifest.signature || !manifest.signature.signature) {
|
|
266
|
+
return { valid: false, error: 'Manifest is not signed' };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check attestation matches current manifest
|
|
270
|
+
if (attestation.attested_content_hash !== manifest.signature.content_hash) {
|
|
271
|
+
return {
|
|
272
|
+
valid: false,
|
|
273
|
+
error: 'Attestation is for different content version',
|
|
274
|
+
attestedHash: attestation.attested_content_hash,
|
|
275
|
+
currentHash: manifest.signature.content_hash
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (attestation.attested_author_signature !== manifest.signature.signature) {
|
|
280
|
+
return {
|
|
281
|
+
valid: false,
|
|
282
|
+
error: 'Attestation is for different author signature'
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
// Recreate the attestation message
|
|
288
|
+
const attestationMessage = `${attestation.attested_content_hash}|${attestation.attested_author_signature}`;
|
|
289
|
+
const messageBytes = naclUtil.decodeUTF8(attestationMessage);
|
|
290
|
+
|
|
291
|
+
// Decode auditor's public key and signature
|
|
292
|
+
const publicKey = naclUtil.decodeBase64(attestation.auditor_public_key);
|
|
293
|
+
const signatureBytes = naclUtil.decodeBase64(attestation.attestation);
|
|
294
|
+
|
|
295
|
+
// Verify
|
|
296
|
+
const valid = nacl.sign.detached.verify(messageBytes, signatureBytes, publicKey);
|
|
297
|
+
|
|
298
|
+
if (valid) {
|
|
299
|
+
return {
|
|
300
|
+
valid: true,
|
|
301
|
+
auditor: attestation.auditor,
|
|
302
|
+
type: attestation.type,
|
|
303
|
+
attested_at: attestation.attested_at,
|
|
304
|
+
notes: attestation.notes
|
|
305
|
+
};
|
|
306
|
+
} else {
|
|
307
|
+
return { valid: false, error: 'Attestation signature verification failed' };
|
|
308
|
+
}
|
|
309
|
+
} catch (err) {
|
|
310
|
+
return { valid: false, error: `Attestation verification error: ${err.message}` };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Verify all attestations on a manifest
|
|
316
|
+
* @param {Object} manifest - The manifest with attestations
|
|
317
|
+
* @returns {Object} - { valid: boolean, attestations: Array, errors: Array }
|
|
318
|
+
*/
|
|
319
|
+
function verifyAllAttestations(manifest) {
|
|
320
|
+
const attestations = manifest.attestations || [];
|
|
321
|
+
|
|
322
|
+
if (attestations.length === 0) {
|
|
323
|
+
return { valid: true, attestations: [], errors: [], count: 0 };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const results = attestations.map((att, i) => ({
|
|
327
|
+
index: i,
|
|
328
|
+
...verifyAttestation(att, manifest)
|
|
329
|
+
}));
|
|
330
|
+
|
|
331
|
+
const valid = results.filter(r => r.valid);
|
|
332
|
+
const invalid = results.filter(r => !r.valid);
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
valid: invalid.length === 0,
|
|
336
|
+
attestations: valid,
|
|
337
|
+
errors: invalid,
|
|
338
|
+
count: valid.length
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get the isnad (chain of transmission) for a manifest
|
|
344
|
+
* Returns the provenance chain: author → auditor(s)
|
|
345
|
+
*/
|
|
346
|
+
function getIsnad(manifest) {
|
|
347
|
+
const chain = [];
|
|
348
|
+
|
|
349
|
+
// First link: author
|
|
350
|
+
if (manifest.signature) {
|
|
351
|
+
chain.push({
|
|
352
|
+
role: 'author',
|
|
353
|
+
identity: manifest.signature.signer,
|
|
354
|
+
public_key: manifest.signature.public_key,
|
|
355
|
+
timestamp: manifest.signature.signed_at
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Subsequent links: auditors
|
|
360
|
+
const attestations = manifest.attestations || [];
|
|
361
|
+
for (const att of attestations) {
|
|
362
|
+
chain.push({
|
|
363
|
+
role: 'auditor',
|
|
364
|
+
identity: att.auditor,
|
|
365
|
+
public_key: att.auditor_public_key,
|
|
366
|
+
timestamp: att.attested_at,
|
|
367
|
+
type: att.type,
|
|
368
|
+
notes: att.notes
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return chain;
|
|
373
|
+
}
|
|
374
|
+
|
|
185
375
|
module.exports = {
|
|
186
376
|
generateKeyPair,
|
|
187
377
|
computeHash,
|
|
@@ -189,5 +379,11 @@ module.exports = {
|
|
|
189
379
|
verifyManifest,
|
|
190
380
|
isSigned,
|
|
191
381
|
getSignableContent,
|
|
192
|
-
getKeyFingerprint
|
|
382
|
+
getKeyFingerprint,
|
|
383
|
+
// Attestation chain (isnad) functions
|
|
384
|
+
createAttestation,
|
|
385
|
+
addAttestation,
|
|
386
|
+
verifyAttestation,
|
|
387
|
+
verifyAllAttestations,
|
|
388
|
+
getIsnad
|
|
193
389
|
};
|
package/src/trust.js
CHANGED
|
@@ -38,6 +38,23 @@ const RISK_WEIGHTS = {
|
|
|
38
38
|
unverified: 30 // Signature present but not verified
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Attestation bonuses (reduce risk score)
|
|
43
|
+
* More attestations from trusted auditors = lower risk
|
|
44
|
+
*/
|
|
45
|
+
const ATTESTATION_BONUSES = {
|
|
46
|
+
// Per valid attestation
|
|
47
|
+
security_audit: -20, // Security audit (highest trust reduction)
|
|
48
|
+
code_review: -15, // Code review
|
|
49
|
+
endorsement: -10, // General endorsement
|
|
50
|
+
|
|
51
|
+
// Caps (can't go below 0 from attestations)
|
|
52
|
+
max_bonus: -50, // Maximum total attestation bonus
|
|
53
|
+
|
|
54
|
+
// Trusted auditor bonus (if public key matches known auditors)
|
|
55
|
+
trusted_auditor: -10 // Additional bonus for known auditors
|
|
56
|
+
};
|
|
57
|
+
|
|
41
58
|
/**
|
|
42
59
|
* Sensitive credential patterns
|
|
43
60
|
*/
|
|
@@ -54,7 +71,7 @@ const SENSITIVE_CREDENTIAL_PATTERNS = [
|
|
|
54
71
|
/**
|
|
55
72
|
* Calculate risk score for a manifest
|
|
56
73
|
* @param {Object} manifest - The manifest to score
|
|
57
|
-
* @param {Object} options - { signed: boolean, verified: boolean }
|
|
74
|
+
* @param {Object} options - { signed: boolean, verified: boolean, attestationsVerified: array, trustedAuditors: array }
|
|
58
75
|
* @returns {Object} - { score: number, grade: string, breakdown: object }
|
|
59
76
|
*/
|
|
60
77
|
function calculateTrustScore(manifest, options = {}) {
|
|
@@ -64,7 +81,8 @@ function calculateTrustScore(manifest, options = {}) {
|
|
|
64
81
|
shell: 0,
|
|
65
82
|
credentials: 0,
|
|
66
83
|
capabilities: 0,
|
|
67
|
-
signature: 0
|
|
84
|
+
signature: 0,
|
|
85
|
+
attestations: 0 // Bonus (negative number)
|
|
68
86
|
};
|
|
69
87
|
|
|
70
88
|
const permissions = manifest.permissions || {};
|
|
@@ -140,8 +158,31 @@ function calculateTrustScore(manifest, options = {}) {
|
|
|
140
158
|
breakdown.signature += RISK_WEIGHTS.unverified;
|
|
141
159
|
}
|
|
142
160
|
|
|
143
|
-
//
|
|
144
|
-
const
|
|
161
|
+
// === ATTESTATIONS (BONUS - reduces score) ===
|
|
162
|
+
const attestations = manifest.attestations || [];
|
|
163
|
+
const verifiedAttestations = options.attestationsVerified || [];
|
|
164
|
+
const trustedAuditors = options.trustedAuditors || [];
|
|
165
|
+
|
|
166
|
+
verifiedAttestations.forEach(att => {
|
|
167
|
+
// Get bonus based on attestation type
|
|
168
|
+
const type = att.type || 'endorsement';
|
|
169
|
+
const typeBonus = ATTESTATION_BONUSES[type] || ATTESTATION_BONUSES.endorsement;
|
|
170
|
+
breakdown.attestations += typeBonus;
|
|
171
|
+
|
|
172
|
+
// Extra bonus if auditor is in trusted list
|
|
173
|
+
if (trustedAuditors.includes(att.auditor) ||
|
|
174
|
+
trustedAuditors.includes(att.auditor_public_key)) {
|
|
175
|
+
breakdown.attestations += ATTESTATION_BONUSES.trusted_auditor;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Cap attestation bonus
|
|
180
|
+
if (breakdown.attestations < ATTESTATION_BONUSES.max_bonus) {
|
|
181
|
+
breakdown.attestations = ATTESTATION_BONUSES.max_bonus;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Calculate total (attestations is negative, so it reduces score)
|
|
185
|
+
const totalScore = Math.max(0, Object.values(breakdown).reduce((a, b) => a + b, 0));
|
|
145
186
|
|
|
146
187
|
// Determine grade
|
|
147
188
|
const grade = scoreToGrade(totalScore);
|
|
@@ -150,7 +191,8 @@ function calculateTrustScore(manifest, options = {}) {
|
|
|
150
191
|
score: totalScore,
|
|
151
192
|
grade,
|
|
152
193
|
breakdown,
|
|
153
|
-
|
|
194
|
+
attestationCount: verifiedAttestations.length,
|
|
195
|
+
summary: generateSummary(breakdown, grade, verifiedAttestations.length)
|
|
154
196
|
};
|
|
155
197
|
}
|
|
156
198
|
|
|
@@ -196,7 +238,7 @@ function gradeEmoji(grade) {
|
|
|
196
238
|
/**
|
|
197
239
|
* Generate human-readable summary
|
|
198
240
|
*/
|
|
199
|
-
function generateSummary(breakdown, grade) {
|
|
241
|
+
function generateSummary(breakdown, grade, attestationCount = 0) {
|
|
200
242
|
const concerns = [];
|
|
201
243
|
|
|
202
244
|
if (breakdown.shell > 0) {
|
|
@@ -218,6 +260,11 @@ function generateSummary(breakdown, grade) {
|
|
|
218
260
|
concerns.push('⚠️ Missing or unverified signature');
|
|
219
261
|
}
|
|
220
262
|
|
|
263
|
+
// Positive notes
|
|
264
|
+
if (breakdown.attestations < 0) {
|
|
265
|
+
concerns.push(`✅ ${attestationCount} verified attestation(s) (${breakdown.attestations} pts)`);
|
|
266
|
+
}
|
|
267
|
+
|
|
221
268
|
if (concerns.length === 0) {
|
|
222
269
|
concerns.push('✅ Minimal permissions requested');
|
|
223
270
|
}
|
|
@@ -246,7 +293,7 @@ function getGradeDescription(grade) {
|
|
|
246
293
|
* Format trust score for display
|
|
247
294
|
*/
|
|
248
295
|
function formatTrustScore(result) {
|
|
249
|
-
const { score, grade, breakdown, summary } = result;
|
|
296
|
+
const { score, grade, breakdown, summary, attestationCount } = result;
|
|
250
297
|
|
|
251
298
|
let output = `\n${gradeEmoji(grade)} Trust Score: ${grade} (${score} points)\n`;
|
|
252
299
|
output += ` ${summary.grade_description}\n\n`;
|
|
@@ -257,7 +304,11 @@ function formatTrustScore(result) {
|
|
|
257
304
|
output += ` - Shell: ${breakdown.shell} pts\n`;
|
|
258
305
|
output += ` - Credentials: ${breakdown.credentials} pts\n`;
|
|
259
306
|
output += ` - Capabilities: ${breakdown.capabilities} pts\n`;
|
|
260
|
-
output += ` - Signature: ${breakdown.signature} pts\n
|
|
307
|
+
output += ` - Signature: ${breakdown.signature} pts\n`;
|
|
308
|
+
if (breakdown.attestations !== 0) {
|
|
309
|
+
output += ` - Attestations: ${breakdown.attestations} pts (${attestationCount || 0} verified)\n`;
|
|
310
|
+
}
|
|
311
|
+
output += '\n';
|
|
261
312
|
|
|
262
313
|
if (summary.concerns.length > 0) {
|
|
263
314
|
output += ` Notes:\n`;
|
|
@@ -275,5 +326,6 @@ module.exports = {
|
|
|
275
326
|
gradeColor,
|
|
276
327
|
gradeEmoji,
|
|
277
328
|
formatTrustScore,
|
|
278
|
-
RISK_WEIGHTS
|
|
329
|
+
RISK_WEIGHTS,
|
|
330
|
+
ATTESTATION_BONUSES
|
|
279
331
|
};
|
package/test/all.test.js
CHANGED
|
@@ -526,4 +526,351 @@ describe('Integration: Full Workflow', () => {
|
|
|
526
526
|
});
|
|
527
527
|
});
|
|
528
528
|
|
|
529
|
+
// === ATTESTATION TESTS ===
|
|
530
|
+
|
|
531
|
+
const {
|
|
532
|
+
createAttestation,
|
|
533
|
+
addAttestation,
|
|
534
|
+
verifyAttestation,
|
|
535
|
+
verifyAllAttestations,
|
|
536
|
+
getIsnad
|
|
537
|
+
} = require('../src');
|
|
538
|
+
|
|
539
|
+
describe('Attestation: Create Attestation', () => {
|
|
540
|
+
|
|
541
|
+
it('should create attestation for signed manifest', () => {
|
|
542
|
+
const keyPair = generateKeyPair();
|
|
543
|
+
const auditorKeyPair = generateKeyPair();
|
|
544
|
+
|
|
545
|
+
const manifest = {
|
|
546
|
+
name: 'test-skill',
|
|
547
|
+
version: '1.0.0',
|
|
548
|
+
author: { name: 'Author' },
|
|
549
|
+
permissions: {}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const validation = validateManifest(manifest);
|
|
553
|
+
const signed = signManifest(validation.manifest, keyPair.secretKey, 'Author');
|
|
554
|
+
|
|
555
|
+
const attestation = createAttestation(signed, auditorKeyPair.secretKey, 'Auditor', {
|
|
556
|
+
type: 'security_audit',
|
|
557
|
+
notes: 'Reviewed and approved'
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
assert.strictEqual(attestation.auditor, 'Auditor');
|
|
561
|
+
assert.strictEqual(attestation.type, 'security_audit');
|
|
562
|
+
assert.strictEqual(attestation.notes, 'Reviewed and approved');
|
|
563
|
+
assert.ok(attestation.attestation);
|
|
564
|
+
assert.ok(attestation.attested_at);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('should throw when attesting unsigned manifest', () => {
|
|
568
|
+
const auditorKeyPair = generateKeyPair();
|
|
569
|
+
|
|
570
|
+
const manifest = {
|
|
571
|
+
name: 'unsigned-skill',
|
|
572
|
+
version: '1.0.0',
|
|
573
|
+
author: { name: 'Author' },
|
|
574
|
+
permissions: {}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
assert.throws(() => {
|
|
578
|
+
createAttestation(manifest, auditorKeyPair.secretKey, 'Auditor');
|
|
579
|
+
}, /Cannot attest unsigned manifest/);
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
describe('Attestation: Add Attestation', () => {
|
|
584
|
+
|
|
585
|
+
it('should add attestation to manifest', () => {
|
|
586
|
+
const keyPair = generateKeyPair();
|
|
587
|
+
const auditorKeyPair = generateKeyPair();
|
|
588
|
+
|
|
589
|
+
const manifest = {
|
|
590
|
+
name: 'test-skill',
|
|
591
|
+
version: '1.0.0',
|
|
592
|
+
author: { name: 'Author' },
|
|
593
|
+
permissions: {}
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const validation = validateManifest(manifest);
|
|
597
|
+
const signed = signManifest(validation.manifest, keyPair.secretKey, 'Author');
|
|
598
|
+
|
|
599
|
+
const attestation = createAttestation(signed, auditorKeyPair.secretKey, 'Auditor');
|
|
600
|
+
const attested = addAttestation(signed, attestation);
|
|
601
|
+
|
|
602
|
+
assert.ok(attested.attestations);
|
|
603
|
+
assert.strictEqual(attested.attestations.length, 1);
|
|
604
|
+
assert.strictEqual(attested.attestations[0].auditor, 'Auditor');
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('should prevent duplicate attestations from same auditor', () => {
|
|
608
|
+
const keyPair = generateKeyPair();
|
|
609
|
+
const auditorKeyPair = generateKeyPair();
|
|
610
|
+
|
|
611
|
+
const manifest = {
|
|
612
|
+
name: 'test-skill',
|
|
613
|
+
version: '1.0.0',
|
|
614
|
+
author: { name: 'Author' },
|
|
615
|
+
permissions: {}
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const validation = validateManifest(manifest);
|
|
619
|
+
const signed = signManifest(validation.manifest, keyPair.secretKey, 'Author');
|
|
620
|
+
|
|
621
|
+
const attestation1 = createAttestation(signed, auditorKeyPair.secretKey, 'Auditor');
|
|
622
|
+
const attested = addAttestation(signed, attestation1);
|
|
623
|
+
|
|
624
|
+
const attestation2 = createAttestation(attested, auditorKeyPair.secretKey, 'Auditor');
|
|
625
|
+
|
|
626
|
+
assert.throws(() => {
|
|
627
|
+
addAttestation(attested, attestation2);
|
|
628
|
+
}, /already exists/);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('should allow multiple attestations from different auditors', () => {
|
|
632
|
+
const keyPair = generateKeyPair();
|
|
633
|
+
const auditor1KeyPair = generateKeyPair();
|
|
634
|
+
const auditor2KeyPair = generateKeyPair();
|
|
635
|
+
|
|
636
|
+
const manifest = {
|
|
637
|
+
name: 'test-skill',
|
|
638
|
+
version: '1.0.0',
|
|
639
|
+
author: { name: 'Author' },
|
|
640
|
+
permissions: {}
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const validation = validateManifest(manifest);
|
|
644
|
+
const signed = signManifest(validation.manifest, keyPair.secretKey, 'Author');
|
|
645
|
+
|
|
646
|
+
const attestation1 = createAttestation(signed, auditor1KeyPair.secretKey, 'Auditor1');
|
|
647
|
+
let attested = addAttestation(signed, attestation1);
|
|
648
|
+
|
|
649
|
+
const attestation2 = createAttestation(attested, auditor2KeyPair.secretKey, 'Auditor2');
|
|
650
|
+
attested = addAttestation(attested, attestation2);
|
|
651
|
+
|
|
652
|
+
assert.strictEqual(attested.attestations.length, 2);
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
describe('Attestation: Verify Attestation', () => {
|
|
657
|
+
|
|
658
|
+
it('should verify valid attestation', () => {
|
|
659
|
+
const keyPair = generateKeyPair();
|
|
660
|
+
const auditorKeyPair = generateKeyPair();
|
|
661
|
+
|
|
662
|
+
const manifest = {
|
|
663
|
+
name: 'test-skill',
|
|
664
|
+
version: '1.0.0',
|
|
665
|
+
author: { name: 'Author' },
|
|
666
|
+
permissions: {}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const validation = validateManifest(manifest);
|
|
670
|
+
const signed = signManifest(validation.manifest, keyPair.secretKey, 'Author');
|
|
671
|
+
const attestation = createAttestation(signed, auditorKeyPair.secretKey, 'Auditor', {
|
|
672
|
+
type: 'security_audit'
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
const result = verifyAttestation(attestation, signed);
|
|
676
|
+
assert.strictEqual(result.valid, true);
|
|
677
|
+
assert.strictEqual(result.auditor, 'Auditor');
|
|
678
|
+
assert.strictEqual(result.type, 'security_audit');
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('should reject attestation for modified manifest', () => {
|
|
682
|
+
const keyPair = generateKeyPair();
|
|
683
|
+
const auditorKeyPair = generateKeyPair();
|
|
684
|
+
|
|
685
|
+
const manifest = {
|
|
686
|
+
name: 'test-skill',
|
|
687
|
+
version: '1.0.0',
|
|
688
|
+
author: { name: 'Author' },
|
|
689
|
+
permissions: {}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
const validation = validateManifest(manifest);
|
|
693
|
+
const signed = signManifest(validation.manifest, keyPair.secretKey, 'Author');
|
|
694
|
+
const attestation = createAttestation(signed, auditorKeyPair.secretKey, 'Auditor');
|
|
695
|
+
|
|
696
|
+
// Re-sign with different content (simulates modification)
|
|
697
|
+
const modified = { ...signed, name: 'malicious-skill' };
|
|
698
|
+
const reSigned = signManifest(modified, keyPair.secretKey, 'Author');
|
|
699
|
+
|
|
700
|
+
const result = verifyAttestation(attestation, reSigned);
|
|
701
|
+
assert.strictEqual(result.valid, false);
|
|
702
|
+
assert.ok(result.error.includes('different'));
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
describe('Attestation: Verify All Attestations', () => {
|
|
707
|
+
|
|
708
|
+
it('should verify all attestations at once', () => {
|
|
709
|
+
const keyPair = generateKeyPair();
|
|
710
|
+
const auditor1KeyPair = generateKeyPair();
|
|
711
|
+
const auditor2KeyPair = generateKeyPair();
|
|
712
|
+
|
|
713
|
+
const manifest = {
|
|
714
|
+
name: 'test-skill',
|
|
715
|
+
version: '1.0.0',
|
|
716
|
+
author: { name: 'Author' },
|
|
717
|
+
permissions: {}
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const validation = validateManifest(manifest);
|
|
721
|
+
const signed = signManifest(validation.manifest, keyPair.secretKey, 'Author');
|
|
722
|
+
|
|
723
|
+
const attestation1 = createAttestation(signed, auditor1KeyPair.secretKey, 'Auditor1', { type: 'security_audit' });
|
|
724
|
+
let attested = addAttestation(signed, attestation1);
|
|
725
|
+
|
|
726
|
+
const attestation2 = createAttestation(attested, auditor2KeyPair.secretKey, 'Auditor2', { type: 'code_review' });
|
|
727
|
+
attested = addAttestation(attested, attestation2);
|
|
728
|
+
|
|
729
|
+
const result = verifyAllAttestations(attested);
|
|
730
|
+
assert.strictEqual(result.valid, true);
|
|
731
|
+
assert.strictEqual(result.count, 2);
|
|
732
|
+
assert.strictEqual(result.attestations.length, 2);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it('should return empty for no attestations', () => {
|
|
736
|
+
const keyPair = generateKeyPair();
|
|
737
|
+
|
|
738
|
+
const manifest = {
|
|
739
|
+
name: 'test-skill',
|
|
740
|
+
version: '1.0.0',
|
|
741
|
+
author: { name: 'Author' },
|
|
742
|
+
permissions: {}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
const validation = validateManifest(manifest);
|
|
746
|
+
const signed = signManifest(validation.manifest, keyPair.secretKey, 'Author');
|
|
747
|
+
|
|
748
|
+
const result = verifyAllAttestations(signed);
|
|
749
|
+
assert.strictEqual(result.valid, true);
|
|
750
|
+
assert.strictEqual(result.count, 0);
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
describe('Attestation: Get Isnad (Chain)', () => {
|
|
755
|
+
|
|
756
|
+
it('should return chain with author and auditors', () => {
|
|
757
|
+
const keyPair = generateKeyPair();
|
|
758
|
+
const auditorKeyPair = generateKeyPair();
|
|
759
|
+
|
|
760
|
+
const manifest = {
|
|
761
|
+
name: 'test-skill',
|
|
762
|
+
version: '1.0.0',
|
|
763
|
+
author: { name: 'Author' },
|
|
764
|
+
permissions: {}
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
const validation = validateManifest(manifest);
|
|
768
|
+
const signed = signManifest(validation.manifest, keyPair.secretKey, 'Author');
|
|
769
|
+
const attestation = createAttestation(signed, auditorKeyPair.secretKey, 'Auditor');
|
|
770
|
+
const attested = addAttestation(signed, attestation);
|
|
771
|
+
|
|
772
|
+
const chain = getIsnad(attested);
|
|
773
|
+
|
|
774
|
+
assert.strictEqual(chain.length, 2);
|
|
775
|
+
assert.strictEqual(chain[0].role, 'author');
|
|
776
|
+
assert.strictEqual(chain[0].identity, 'Author');
|
|
777
|
+
assert.strictEqual(chain[1].role, 'auditor');
|
|
778
|
+
assert.strictEqual(chain[1].identity, 'Auditor');
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('should return empty chain for unsigned manifest', () => {
|
|
782
|
+
const manifest = {
|
|
783
|
+
name: 'unsigned-skill',
|
|
784
|
+
version: '1.0.0',
|
|
785
|
+
author: { name: 'Author' },
|
|
786
|
+
permissions: {}
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const chain = getIsnad(manifest);
|
|
790
|
+
assert.strictEqual(chain.length, 0);
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
describe('Trust Scoring with Attestations', () => {
|
|
795
|
+
|
|
796
|
+
it('should reduce score with verified attestations', () => {
|
|
797
|
+
const keyPair = generateKeyPair();
|
|
798
|
+
const auditorKeyPair = generateKeyPair();
|
|
799
|
+
|
|
800
|
+
const manifest = {
|
|
801
|
+
name: 'test-skill',
|
|
802
|
+
version: '1.0.0',
|
|
803
|
+
author: { name: 'Author' },
|
|
804
|
+
permissions: {
|
|
805
|
+
network: { allow: ['*'] } // Risky permission
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
const validation = validateManifest(manifest);
|
|
810
|
+
const signed = signManifest(validation.manifest, keyPair.secretKey, 'Author');
|
|
811
|
+
|
|
812
|
+
// Score without attestation
|
|
813
|
+
const scoreWithout = calculateTrustScore(signed, { signed: true, verified: true });
|
|
814
|
+
|
|
815
|
+
// Add attestation
|
|
816
|
+
const attestation = createAttestation(signed, auditorKeyPair.secretKey, 'Auditor', {
|
|
817
|
+
type: 'security_audit'
|
|
818
|
+
});
|
|
819
|
+
const attested = addAttestation(signed, attestation);
|
|
820
|
+
|
|
821
|
+
// Verify attestation
|
|
822
|
+
const verifyResult = verifyAttestation(attestation, attested);
|
|
823
|
+
|
|
824
|
+
// Score with attestation
|
|
825
|
+
const scoreWith = calculateTrustScore(attested, {
|
|
826
|
+
signed: true,
|
|
827
|
+
verified: true,
|
|
828
|
+
attestationsVerified: [verifyResult]
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
assert.ok(scoreWith.score < scoreWithout.score, 'Attested score should be lower');
|
|
832
|
+
assert.strictEqual(scoreWith.attestationCount, 1);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it('should give extra bonus for trusted auditors', () => {
|
|
836
|
+
const keyPair = generateKeyPair();
|
|
837
|
+
const trustedAuditorKeyPair = generateKeyPair();
|
|
838
|
+
|
|
839
|
+
const manifest = {
|
|
840
|
+
name: 'test-skill',
|
|
841
|
+
version: '1.0.0',
|
|
842
|
+
author: { name: 'Author' },
|
|
843
|
+
permissions: {
|
|
844
|
+
shell: { allowed: true, commands: ['echo'] }
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
const validation = validateManifest(manifest);
|
|
849
|
+
const signed = signManifest(validation.manifest, keyPair.secretKey, 'Author');
|
|
850
|
+
|
|
851
|
+
const attestation = createAttestation(signed, trustedAuditorKeyPair.secretKey, 'TrustedAuditor', {
|
|
852
|
+
type: 'security_audit'
|
|
853
|
+
});
|
|
854
|
+
const attested = addAttestation(signed, attestation);
|
|
855
|
+
const verifyResult = verifyAttestation(attestation, attested);
|
|
856
|
+
|
|
857
|
+
// Score without trusted list
|
|
858
|
+
const scoreNormal = calculateTrustScore(attested, {
|
|
859
|
+
signed: true,
|
|
860
|
+
verified: true,
|
|
861
|
+
attestationsVerified: [verifyResult]
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// Score with trusted list
|
|
865
|
+
const scoreTrusted = calculateTrustScore(attested, {
|
|
866
|
+
signed: true,
|
|
867
|
+
verified: true,
|
|
868
|
+
attestationsVerified: [verifyResult],
|
|
869
|
+
trustedAuditors: ['TrustedAuditor']
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
assert.ok(scoreTrusted.score < scoreNormal.score, 'Trusted auditor should give extra bonus');
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
|
|
529
876
|
console.log('Running OpenClaw Secure test suite...\n');
|