@ruvector/edge-net 0.5.0 → 0.5.3
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 +281 -10
- package/core-invariants.js +942 -0
- package/models/adapter-hub.js +1008 -0
- package/models/adapter-security.js +792 -0
- package/models/benchmark.js +688 -0
- package/models/distribution.js +791 -0
- package/models/index.js +109 -0
- package/models/integrity.js +753 -0
- package/models/loader.js +725 -0
- package/models/microlora.js +1298 -0
- package/models/model-loader.js +922 -0
- package/models/model-optimizer.js +1245 -0
- package/models/model-registry.js +696 -0
- package/models/model-utils.js +548 -0
- package/models/models-cli.js +914 -0
- package/models/registry.json +214 -0
- package/models/training-utils.js +1418 -0
- package/models/wasm-core.js +1025 -0
- package/network-genesis.js +2847 -0
- package/onnx-worker.js +462 -8
- package/package.json +33 -3
- package/plugins/SECURITY-AUDIT.md +654 -0
- package/plugins/cli.js +43 -3
- package/plugins/implementations/e2e-encryption.js +57 -12
- package/plugins/plugin-loader.js +610 -21
- package/tests/model-optimizer.test.js +644 -0
- package/tests/network-genesis.test.js +562 -0
- package/tests/plugin-benchmark.js +1239 -0
- package/tests/plugin-system-test.js +163 -0
- package/tests/wasm-core.test.js +368 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ruvector/edge-net Model Integrity System
|
|
3
|
+
*
|
|
4
|
+
* Content-addressed integrity with:
|
|
5
|
+
* - Canonical JSON signing
|
|
6
|
+
* - Threshold signatures with trust roots
|
|
7
|
+
* - Merkle chunk verification for streaming
|
|
8
|
+
* - Transparency log integration
|
|
9
|
+
*
|
|
10
|
+
* Design principle: Manifest is truth, everything else is replaceable.
|
|
11
|
+
*
|
|
12
|
+
* @module @ruvector/edge-net/models/integrity
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createHash } from 'crypto';
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// CANONICAL JSON
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Canonical JSON encoding for deterministic signing.
|
|
23
|
+
* - Keys sorted lexicographically
|
|
24
|
+
* - No whitespace
|
|
25
|
+
* - Unicode escaped consistently
|
|
26
|
+
* - Numbers without trailing zeros
|
|
27
|
+
*/
|
|
28
|
+
export function canonicalize(obj) {
|
|
29
|
+
if (obj === null || obj === undefined) {
|
|
30
|
+
return 'null';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof obj === 'boolean') {
|
|
34
|
+
return obj ? 'true' : 'false';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof obj === 'number') {
|
|
38
|
+
if (!Number.isFinite(obj)) {
|
|
39
|
+
throw new Error('Cannot canonicalize Infinity or NaN');
|
|
40
|
+
}
|
|
41
|
+
// Use JSON for consistent number formatting
|
|
42
|
+
return JSON.stringify(obj);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (typeof obj === 'string') {
|
|
46
|
+
// Escape unicode consistently
|
|
47
|
+
return JSON.stringify(obj).replace(/[\u007f-\uffff]/g, (c) => {
|
|
48
|
+
return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(obj)) {
|
|
53
|
+
const elements = obj.map(canonicalize);
|
|
54
|
+
return '[' + elements.join(',') + ']';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof obj === 'object') {
|
|
58
|
+
const keys = Object.keys(obj).sort();
|
|
59
|
+
const pairs = keys
|
|
60
|
+
.filter(k => obj[k] !== undefined)
|
|
61
|
+
.map(k => canonicalize(k) + ':' + canonicalize(obj[k]));
|
|
62
|
+
return '{' + pairs.join(',') + '}';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
throw new Error(`Cannot canonicalize type: ${typeof obj}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Hash canonical JSON bytes
|
|
70
|
+
*/
|
|
71
|
+
export function hashCanonical(obj, algorithm = 'sha256') {
|
|
72
|
+
const canonical = canonicalize(obj);
|
|
73
|
+
const hash = createHash(algorithm);
|
|
74
|
+
hash.update(canonical, 'utf8');
|
|
75
|
+
return hash.digest('hex');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// TRUST ROOT
|
|
80
|
+
// ============================================================================
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Built-in root keys shipped with SDK.
|
|
84
|
+
* These are the only keys trusted by default.
|
|
85
|
+
*/
|
|
86
|
+
export const BUILTIN_ROOT_KEYS = Object.freeze({
|
|
87
|
+
'ruvector-root-2024': {
|
|
88
|
+
keyId: 'ruvector-root-2024',
|
|
89
|
+
algorithm: 'ed25519',
|
|
90
|
+
publicKey: 'MCowBQYDK2VwAyEAaGVsbG8td29ybGQta2V5LXBsYWNlaG9sZGVy', // Placeholder
|
|
91
|
+
validFrom: '2024-01-01T00:00:00Z',
|
|
92
|
+
validUntil: '2030-01-01T00:00:00Z',
|
|
93
|
+
capabilities: ['sign-manifest', 'sign-adapter', 'delegate'],
|
|
94
|
+
},
|
|
95
|
+
'ruvector-models-2024': {
|
|
96
|
+
keyId: 'ruvector-models-2024',
|
|
97
|
+
algorithm: 'ed25519',
|
|
98
|
+
publicKey: 'MCowBQYDK2VwAyEAbW9kZWxzLWtleS1wbGFjZWhvbGRlcg==', // Placeholder
|
|
99
|
+
validFrom: '2024-01-01T00:00:00Z',
|
|
100
|
+
validUntil: '2026-01-01T00:00:00Z',
|
|
101
|
+
capabilities: ['sign-manifest'],
|
|
102
|
+
delegatedBy: 'ruvector-root-2024',
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Trust root configuration
|
|
108
|
+
*/
|
|
109
|
+
export class TrustRoot {
|
|
110
|
+
constructor(options = {}) {
|
|
111
|
+
// Start with built-in keys
|
|
112
|
+
this.trustedKeys = new Map();
|
|
113
|
+
for (const [id, key] of Object.entries(BUILTIN_ROOT_KEYS)) {
|
|
114
|
+
this.trustedKeys.set(id, key);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Add enterprise keys if configured
|
|
118
|
+
if (options.enterpriseKeys) {
|
|
119
|
+
for (const key of options.enterpriseKeys) {
|
|
120
|
+
this.addEnterpriseKey(key);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Revocation list
|
|
125
|
+
this.revokedKeys = new Set(options.revokedKeys || []);
|
|
126
|
+
|
|
127
|
+
// Minimum signatures required for official releases
|
|
128
|
+
this.minimumSignaturesRequired = options.minimumSignaturesRequired || 1;
|
|
129
|
+
|
|
130
|
+
// Threshold for high-security operations (e.g., new root key)
|
|
131
|
+
this.thresholdSignaturesRequired = options.thresholdSignaturesRequired || 2;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Add an enterprise root key (for private deployments)
|
|
136
|
+
*/
|
|
137
|
+
addEnterpriseKey(key) {
|
|
138
|
+
if (!key.keyId || !key.publicKey) {
|
|
139
|
+
throw new Error('Enterprise key must have keyId and publicKey');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Verify delegation chain if not self-signed
|
|
143
|
+
if (key.delegatedBy && key.delegationSignature) {
|
|
144
|
+
const delegator = this.trustedKeys.get(key.delegatedBy);
|
|
145
|
+
if (!delegator) {
|
|
146
|
+
throw new Error(`Unknown delegator: ${key.delegatedBy}`);
|
|
147
|
+
}
|
|
148
|
+
if (!delegator.capabilities.includes('delegate')) {
|
|
149
|
+
throw new Error(`Key ${key.delegatedBy} cannot delegate`);
|
|
150
|
+
}
|
|
151
|
+
// In production, verify delegationSignature here
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.trustedKeys.set(key.keyId, {
|
|
155
|
+
...key,
|
|
156
|
+
isEnterprise: true,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Revoke a key
|
|
162
|
+
*/
|
|
163
|
+
revokeKey(keyId, reason) {
|
|
164
|
+
this.revokedKeys.add(keyId);
|
|
165
|
+
console.warn(`[TrustRoot] Key revoked: ${keyId} - ${reason}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if a key is trusted for a capability
|
|
170
|
+
*/
|
|
171
|
+
isKeyTrusted(keyId, capability = 'sign-manifest') {
|
|
172
|
+
if (this.revokedKeys.has(keyId)) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const key = this.trustedKeys.get(keyId);
|
|
177
|
+
if (!key) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check validity period
|
|
182
|
+
const now = new Date();
|
|
183
|
+
if (key.validFrom && new Date(key.validFrom) > now) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
if (key.validUntil && new Date(key.validUntil) < now) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check capability
|
|
191
|
+
if (!key.capabilities.includes(capability)) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get public key for verification
|
|
200
|
+
*/
|
|
201
|
+
getPublicKey(keyId) {
|
|
202
|
+
const key = this.trustedKeys.get(keyId);
|
|
203
|
+
if (!key || this.revokedKeys.has(keyId)) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return key.publicKey;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Verify signature set meets threshold
|
|
211
|
+
*/
|
|
212
|
+
verifySignatureThreshold(signatures, requiredCount = null) {
|
|
213
|
+
const required = requiredCount || this.minimumSignaturesRequired;
|
|
214
|
+
let validCount = 0;
|
|
215
|
+
const validSigners = [];
|
|
216
|
+
|
|
217
|
+
for (const sig of signatures) {
|
|
218
|
+
if (this.isKeyTrusted(sig.keyId, 'sign-manifest')) {
|
|
219
|
+
// In production, verify actual signature here
|
|
220
|
+
validCount++;
|
|
221
|
+
validSigners.push(sig.keyId);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
valid: validCount >= required,
|
|
227
|
+
validCount,
|
|
228
|
+
required,
|
|
229
|
+
validSigners,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Export current trust configuration
|
|
235
|
+
*/
|
|
236
|
+
export() {
|
|
237
|
+
return {
|
|
238
|
+
trustedKeys: Object.fromEntries(this.trustedKeys),
|
|
239
|
+
revokedKeys: Array.from(this.revokedKeys),
|
|
240
|
+
minimumSignaturesRequired: this.minimumSignaturesRequired,
|
|
241
|
+
thresholdSignaturesRequired: this.thresholdSignaturesRequired,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// MERKLE CHUNK VERIFICATION
|
|
248
|
+
// ============================================================================
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Compute Merkle tree from chunk hashes
|
|
252
|
+
*/
|
|
253
|
+
export function computeMerkleRoot(chunkHashes) {
|
|
254
|
+
if (chunkHashes.length === 0) {
|
|
255
|
+
return hashCanonical({ empty: true });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (chunkHashes.length === 1) {
|
|
259
|
+
return chunkHashes[0];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Build tree bottom-up
|
|
263
|
+
let level = [...chunkHashes];
|
|
264
|
+
|
|
265
|
+
while (level.length > 1) {
|
|
266
|
+
const nextLevel = [];
|
|
267
|
+
for (let i = 0; i < level.length; i += 2) {
|
|
268
|
+
const left = level[i];
|
|
269
|
+
const right = level[i + 1] || left; // Duplicate last if odd
|
|
270
|
+
const combined = createHash('sha256')
|
|
271
|
+
.update(left, 'hex')
|
|
272
|
+
.update(right, 'hex')
|
|
273
|
+
.digest('hex');
|
|
274
|
+
nextLevel.push(combined);
|
|
275
|
+
}
|
|
276
|
+
level = nextLevel;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return level[0];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Generate Merkle proof for a chunk
|
|
284
|
+
*/
|
|
285
|
+
export function generateMerkleProof(chunkHashes, chunkIndex) {
|
|
286
|
+
const proof = [];
|
|
287
|
+
let level = [...chunkHashes];
|
|
288
|
+
let index = chunkIndex;
|
|
289
|
+
|
|
290
|
+
while (level.length > 1) {
|
|
291
|
+
const isRight = index % 2 === 1;
|
|
292
|
+
const siblingIndex = isRight ? index - 1 : index + 1;
|
|
293
|
+
|
|
294
|
+
if (siblingIndex < level.length) {
|
|
295
|
+
proof.push({
|
|
296
|
+
hash: level[siblingIndex],
|
|
297
|
+
position: isRight ? 'left' : 'right',
|
|
298
|
+
});
|
|
299
|
+
} else {
|
|
300
|
+
// Odd number, sibling is self
|
|
301
|
+
proof.push({
|
|
302
|
+
hash: level[index],
|
|
303
|
+
position: 'right',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Move up
|
|
308
|
+
const nextLevel = [];
|
|
309
|
+
for (let i = 0; i < level.length; i += 2) {
|
|
310
|
+
const left = level[i];
|
|
311
|
+
const right = level[i + 1] || left;
|
|
312
|
+
nextLevel.push(
|
|
313
|
+
createHash('sha256')
|
|
314
|
+
.update(left, 'hex')
|
|
315
|
+
.update(right, 'hex')
|
|
316
|
+
.digest('hex')
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
level = nextLevel;
|
|
320
|
+
index = Math.floor(index / 2);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return proof;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Verify a chunk against Merkle root
|
|
328
|
+
*/
|
|
329
|
+
export function verifyMerkleProof(chunkHash, chunkIndex, proof, merkleRoot) {
|
|
330
|
+
let computed = chunkHash;
|
|
331
|
+
|
|
332
|
+
for (const step of proof) {
|
|
333
|
+
const left = step.position === 'left' ? step.hash : computed;
|
|
334
|
+
const right = step.position === 'right' ? step.hash : computed;
|
|
335
|
+
computed = createHash('sha256')
|
|
336
|
+
.update(left, 'hex')
|
|
337
|
+
.update(right, 'hex')
|
|
338
|
+
.digest('hex');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return computed === merkleRoot;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Chunk a buffer and compute hashes
|
|
346
|
+
*/
|
|
347
|
+
export function chunkAndHash(buffer, chunkSize = 256 * 1024) {
|
|
348
|
+
const chunks = [];
|
|
349
|
+
const hashes = [];
|
|
350
|
+
|
|
351
|
+
for (let offset = 0; offset < buffer.length; offset += chunkSize) {
|
|
352
|
+
const chunk = buffer.slice(offset, offset + chunkSize);
|
|
353
|
+
chunks.push(chunk);
|
|
354
|
+
hashes.push(
|
|
355
|
+
createHash('sha256').update(chunk).digest('hex')
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
chunks,
|
|
361
|
+
chunkHashes: hashes,
|
|
362
|
+
chunkSize,
|
|
363
|
+
chunkCount: chunks.length,
|
|
364
|
+
totalSize: buffer.length,
|
|
365
|
+
merkleRoot: computeMerkleRoot(hashes),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ============================================================================
|
|
370
|
+
// MANIFEST INTEGRITY
|
|
371
|
+
// ============================================================================
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Integrity block for manifests
|
|
375
|
+
*/
|
|
376
|
+
export function createIntegrityBlock(manifest, chunkInfo) {
|
|
377
|
+
// Create the signed payload (everything except signatures)
|
|
378
|
+
const signedPayload = {
|
|
379
|
+
model: manifest.model,
|
|
380
|
+
version: manifest.version,
|
|
381
|
+
artifacts: manifest.artifacts,
|
|
382
|
+
provenance: manifest.provenance,
|
|
383
|
+
capabilities: manifest.capabilities,
|
|
384
|
+
timestamp: new Date().toISOString(),
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const signedPayloadHash = hashCanonical(signedPayload);
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
manifestHash: hashCanonical(manifest),
|
|
391
|
+
signedPayloadHash,
|
|
392
|
+
merkleRoot: chunkInfo.merkleRoot,
|
|
393
|
+
chunking: {
|
|
394
|
+
chunkSize: chunkInfo.chunkSize,
|
|
395
|
+
chunkCount: chunkInfo.chunkCount,
|
|
396
|
+
chunkHashes: chunkInfo.chunkHashes,
|
|
397
|
+
},
|
|
398
|
+
signatures: [], // To be filled by signing process
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Provenance block for manifests
|
|
404
|
+
*/
|
|
405
|
+
export function createProvenanceBlock(options = {}) {
|
|
406
|
+
return {
|
|
407
|
+
builtBy: {
|
|
408
|
+
tool: options.tool || '@ruvector/model-optimizer',
|
|
409
|
+
version: options.toolVersion || '1.0.0',
|
|
410
|
+
commit: options.commit || 'unknown',
|
|
411
|
+
},
|
|
412
|
+
optimizationRecipeHash: options.recipeHash || null,
|
|
413
|
+
calibrationDatasetHash: options.calibrationHash || null,
|
|
414
|
+
parentLineage: options.parentLineage || null,
|
|
415
|
+
buildTimestamp: new Date().toISOString(),
|
|
416
|
+
environment: {
|
|
417
|
+
platform: process.platform,
|
|
418
|
+
arch: process.arch,
|
|
419
|
+
nodeVersion: process.version,
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Full manifest with integrity
|
|
426
|
+
*/
|
|
427
|
+
export function createSecureManifest(model, artifacts, options = {}) {
|
|
428
|
+
const manifest = {
|
|
429
|
+
schemaVersion: '2.0.0',
|
|
430
|
+
model: {
|
|
431
|
+
id: model.id,
|
|
432
|
+
name: model.name,
|
|
433
|
+
version: model.version,
|
|
434
|
+
type: model.type, // 'embedding' | 'generation'
|
|
435
|
+
tier: model.tier, // 'micro' | 'small' | 'large'
|
|
436
|
+
capabilities: model.capabilities || [],
|
|
437
|
+
memoryRequirement: model.memoryRequirement,
|
|
438
|
+
},
|
|
439
|
+
artifacts: artifacts.map(a => ({
|
|
440
|
+
path: a.path,
|
|
441
|
+
size: a.size,
|
|
442
|
+
sha256: a.sha256,
|
|
443
|
+
format: a.format,
|
|
444
|
+
quantization: a.quantization,
|
|
445
|
+
})),
|
|
446
|
+
distribution: {
|
|
447
|
+
gcs: options.gcsUrl,
|
|
448
|
+
ipfs: options.ipfsCid,
|
|
449
|
+
fallbackUrls: options.fallbackUrls || [],
|
|
450
|
+
},
|
|
451
|
+
provenance: createProvenanceBlock(options.provenance || {}),
|
|
452
|
+
capabilities: model.capabilities || [],
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// Add integrity block if chunk info provided
|
|
456
|
+
if (options.chunkInfo) {
|
|
457
|
+
manifest.integrity = createIntegrityBlock(manifest, options.chunkInfo);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Add trust metadata
|
|
461
|
+
manifest.trust = {
|
|
462
|
+
trustedKeySetId: options.trustedKeySetId || 'ruvector-default-2024',
|
|
463
|
+
minimumSignaturesRequired: options.minimumSignaturesRequired || 1,
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
return manifest;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ============================================================================
|
|
470
|
+
// MANIFEST VERIFICATION
|
|
471
|
+
// ============================================================================
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Verify a manifest's integrity
|
|
475
|
+
*/
|
|
476
|
+
export class ManifestVerifier {
|
|
477
|
+
constructor(trustRoot = null) {
|
|
478
|
+
this.trustRoot = trustRoot || new TrustRoot();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Full verification of a manifest
|
|
483
|
+
*/
|
|
484
|
+
verify(manifest) {
|
|
485
|
+
const errors = [];
|
|
486
|
+
const warnings = [];
|
|
487
|
+
|
|
488
|
+
// 1. Schema version check
|
|
489
|
+
if (!manifest.schemaVersion || manifest.schemaVersion < '2.0.0') {
|
|
490
|
+
warnings.push('Manifest uses old schema version');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// 2. Verify integrity block
|
|
494
|
+
if (manifest.integrity) {
|
|
495
|
+
// Check manifest hash
|
|
496
|
+
const computed = hashCanonical(manifest);
|
|
497
|
+
// Note: manifestHash is computed before adding integrity, so we skip this
|
|
498
|
+
|
|
499
|
+
// Check signed payload hash
|
|
500
|
+
const signedPayload = {
|
|
501
|
+
model: manifest.model,
|
|
502
|
+
version: manifest.version,
|
|
503
|
+
artifacts: manifest.artifacts,
|
|
504
|
+
provenance: manifest.provenance,
|
|
505
|
+
capabilities: manifest.capabilities,
|
|
506
|
+
timestamp: manifest.integrity.timestamp,
|
|
507
|
+
};
|
|
508
|
+
const computedPayloadHash = hashCanonical(signedPayload);
|
|
509
|
+
|
|
510
|
+
// 3. Verify signatures meet threshold
|
|
511
|
+
if (manifest.integrity.signatures?.length > 0) {
|
|
512
|
+
const sigResult = this.trustRoot.verifySignatureThreshold(
|
|
513
|
+
manifest.integrity.signatures,
|
|
514
|
+
manifest.trust?.minimumSignaturesRequired
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
if (!sigResult.valid) {
|
|
518
|
+
errors.push(`Insufficient valid signatures: ${sigResult.validCount}/${sigResult.required}`);
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
warnings.push('No signatures present');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// 4. Verify Merkle root matches chunk hashes
|
|
525
|
+
if (manifest.integrity.chunking) {
|
|
526
|
+
const computedRoot = computeMerkleRoot(manifest.integrity.chunking.chunkHashes);
|
|
527
|
+
if (computedRoot !== manifest.integrity.merkleRoot) {
|
|
528
|
+
errors.push('Merkle root mismatch');
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
warnings.push('No integrity block present');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// 5. Check provenance
|
|
536
|
+
if (!manifest.provenance) {
|
|
537
|
+
warnings.push('No provenance information');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// 6. Check required fields
|
|
541
|
+
if (!manifest.model?.id) errors.push('Missing model.id');
|
|
542
|
+
if (!manifest.model?.version) errors.push('Missing model.version');
|
|
543
|
+
if (!manifest.artifacts?.length) errors.push('No artifacts defined');
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
valid: errors.length === 0,
|
|
547
|
+
errors,
|
|
548
|
+
warnings,
|
|
549
|
+
trust: manifest.trust,
|
|
550
|
+
provenance: manifest.provenance,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Verify a single chunk during streaming download
|
|
556
|
+
*/
|
|
557
|
+
verifyChunk(chunkData, chunkIndex, manifest) {
|
|
558
|
+
if (!manifest.integrity?.chunking) {
|
|
559
|
+
return { valid: false, error: 'No chunking info in manifest' };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const expectedHash = manifest.integrity.chunking.chunkHashes[chunkIndex];
|
|
563
|
+
if (!expectedHash) {
|
|
564
|
+
return { valid: false, error: `No hash for chunk ${chunkIndex}` };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const actualHash = createHash('sha256').update(chunkData).digest('hex');
|
|
568
|
+
|
|
569
|
+
if (actualHash !== expectedHash) {
|
|
570
|
+
return {
|
|
571
|
+
valid: false,
|
|
572
|
+
error: `Chunk ${chunkIndex} hash mismatch`,
|
|
573
|
+
expected: expectedHash,
|
|
574
|
+
actual: actualHash,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return { valid: true, chunkIndex, hash: actualHash };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ============================================================================
|
|
583
|
+
// TRANSPARENCY LOG
|
|
584
|
+
// ============================================================================
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Entry in the transparency log
|
|
588
|
+
*/
|
|
589
|
+
export function createLogEntry(manifest, publisherKeyId) {
|
|
590
|
+
return {
|
|
591
|
+
manifestHash: hashCanonical(manifest),
|
|
592
|
+
modelId: manifest.model.id,
|
|
593
|
+
version: manifest.model.version,
|
|
594
|
+
publisherKeyId,
|
|
595
|
+
timestamp: new Date().toISOString(),
|
|
596
|
+
signedPayloadHash: manifest.integrity?.signedPayloadHash,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Simple append-only transparency log
|
|
602
|
+
* In production, this would be backed by a Merkle tree or blockchain
|
|
603
|
+
*/
|
|
604
|
+
export class TransparencyLog {
|
|
605
|
+
constructor(options = {}) {
|
|
606
|
+
this.entries = [];
|
|
607
|
+
this.indexByModel = new Map();
|
|
608
|
+
this.indexByHash = new Map();
|
|
609
|
+
this.logRoot = null;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Append an entry to the log
|
|
614
|
+
*/
|
|
615
|
+
append(entry) {
|
|
616
|
+
const index = this.entries.length;
|
|
617
|
+
|
|
618
|
+
// Compute log entry hash including previous
|
|
619
|
+
const logEntryHash = hashCanonical({
|
|
620
|
+
...entry,
|
|
621
|
+
index,
|
|
622
|
+
previousHash: this.logRoot,
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const fullEntry = {
|
|
626
|
+
...entry,
|
|
627
|
+
index,
|
|
628
|
+
previousHash: this.logRoot,
|
|
629
|
+
logEntryHash,
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
this.entries.push(fullEntry);
|
|
633
|
+
this.logRoot = logEntryHash;
|
|
634
|
+
|
|
635
|
+
// Update indexes
|
|
636
|
+
if (!this.indexByModel.has(entry.modelId)) {
|
|
637
|
+
this.indexByModel.set(entry.modelId, []);
|
|
638
|
+
}
|
|
639
|
+
this.indexByModel.get(entry.modelId).push(index);
|
|
640
|
+
this.indexByHash.set(entry.manifestHash, index);
|
|
641
|
+
|
|
642
|
+
return fullEntry;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Generate inclusion proof
|
|
647
|
+
*/
|
|
648
|
+
getInclusionProof(manifestHash) {
|
|
649
|
+
const index = this.indexByHash.get(manifestHash);
|
|
650
|
+
if (index === undefined) {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const entry = this.entries[index];
|
|
655
|
+
const proof = [];
|
|
656
|
+
|
|
657
|
+
// Simple chain proof (in production, use Merkle tree)
|
|
658
|
+
for (let i = index; i < this.entries.length; i++) {
|
|
659
|
+
proof.push({
|
|
660
|
+
index: i,
|
|
661
|
+
logEntryHash: this.entries[i].logEntryHash,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
entry,
|
|
667
|
+
proof,
|
|
668
|
+
currentRoot: this.logRoot,
|
|
669
|
+
logLength: this.entries.length,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Verify inclusion proof
|
|
675
|
+
*/
|
|
676
|
+
verifyInclusionProof(proof) {
|
|
677
|
+
if (!proof || !proof.entry || !proof.proof.length) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Verify chain
|
|
682
|
+
let expectedHash = proof.entry.logEntryHash;
|
|
683
|
+
for (let i = 1; i < proof.proof.length; i++) {
|
|
684
|
+
const entry = proof.proof[i];
|
|
685
|
+
// Verify chain continuity
|
|
686
|
+
if (i < proof.proof.length - 1) {
|
|
687
|
+
// Each entry should reference the previous
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return proof.proof[proof.proof.length - 1].logEntryHash === proof.currentRoot;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Get history for a model
|
|
696
|
+
*/
|
|
697
|
+
getModelHistory(modelId) {
|
|
698
|
+
const indices = this.indexByModel.get(modelId) || [];
|
|
699
|
+
return indices.map(i => this.entries[i]);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Export log for persistence
|
|
704
|
+
*/
|
|
705
|
+
export() {
|
|
706
|
+
return {
|
|
707
|
+
entries: this.entries,
|
|
708
|
+
logRoot: this.logRoot,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Import log
|
|
714
|
+
*/
|
|
715
|
+
import(data) {
|
|
716
|
+
this.entries = data.entries || [];
|
|
717
|
+
this.logRoot = data.logRoot;
|
|
718
|
+
|
|
719
|
+
// Rebuild indexes
|
|
720
|
+
this.indexByModel.clear();
|
|
721
|
+
this.indexByHash.clear();
|
|
722
|
+
|
|
723
|
+
for (let i = 0; i < this.entries.length; i++) {
|
|
724
|
+
const entry = this.entries[i];
|
|
725
|
+
if (!this.indexByModel.has(entry.modelId)) {
|
|
726
|
+
this.indexByModel.set(entry.modelId, []);
|
|
727
|
+
}
|
|
728
|
+
this.indexByModel.get(entry.modelId).push(i);
|
|
729
|
+
this.indexByHash.set(entry.manifestHash, i);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ============================================================================
|
|
735
|
+
// EXPORTS
|
|
736
|
+
// ============================================================================
|
|
737
|
+
|
|
738
|
+
export default {
|
|
739
|
+
canonicalize,
|
|
740
|
+
hashCanonical,
|
|
741
|
+
TrustRoot,
|
|
742
|
+
BUILTIN_ROOT_KEYS,
|
|
743
|
+
computeMerkleRoot,
|
|
744
|
+
generateMerkleProof,
|
|
745
|
+
verifyMerkleProof,
|
|
746
|
+
chunkAndHash,
|
|
747
|
+
createIntegrityBlock,
|
|
748
|
+
createProvenanceBlock,
|
|
749
|
+
createSecureManifest,
|
|
750
|
+
ManifestVerifier,
|
|
751
|
+
createLogEntry,
|
|
752
|
+
TransparencyLog,
|
|
753
|
+
};
|