@moonglade/provenance 0.0.0-stage → 2.6.1
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/LICENSE +21 -0
- package/NOTICE.md +9 -0
- package/README.md +92 -2
- package/SPEC.md +176 -0
- package/TIMESTAMP.md +21 -0
- package/dist/artifact-cli.d.ts +2 -0
- package/dist/artifact-cli.js +113 -0
- package/dist/artifactEnvelope.d.ts +38 -0
- package/dist/artifactEnvelope.js +61 -0
- package/dist/artifactHook.d.ts +30 -0
- package/dist/artifactHook.js +114 -0
- package/dist/badge.d.ts +83 -0
- package/dist/badge.js +403 -0
- package/dist/hookInstaller.d.ts +75 -0
- package/dist/hookInstaller.js +410 -0
- package/dist/index.d.ts +228 -0
- package/dist/index.js +782 -0
- package/dist/mcp.d.ts +87 -0
- package/dist/mcp.js +352 -0
- package/dist/merkle.d.ts +71 -0
- package/dist/merkle.js +176 -0
- package/dist/self-test.d.ts +21 -0
- package/dist/self-test.js +280 -0
- package/dist/timestamp.d.ts +274 -0
- package/dist/timestamp.js +1103 -0
- package/package.json +80 -4
- package/trust/README.md +19 -0
- package/trust/registry.json +12 -0
- package/trust/root-public.asc +10 -0
- package/wasm/mgc_wasm.d.ts +53 -0
- package/wasm/mgc_wasm.js +388 -0
- package/wasm/mgc_wasm_bg.wasm +0 -0
- package/wasm/mgc_wasm_bg.wasm.d.ts +20 -0
package/dist/merkle.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.nodeHash = exports.leafHash = exports.MERKLE_ALGORITHM = void 0;
|
|
37
|
+
exports.leavesFromRegistry = leavesFromRegistry;
|
|
38
|
+
exports.buildLevels = buildLevels;
|
|
39
|
+
exports.merkleRoot = merkleRoot;
|
|
40
|
+
exports.inclusionProof = inclusionProof;
|
|
41
|
+
exports.verifyProof = verifyProof;
|
|
42
|
+
const crypto = __importStar(require("crypto"));
|
|
43
|
+
/**
|
|
44
|
+
* RFC 6962 Merkle tree over vault artifacts, with inclusion proofs.
|
|
45
|
+
*
|
|
46
|
+
* WHY THIS REPLACES scanAndAudit's ROOT
|
|
47
|
+
* -------------------------------------
|
|
48
|
+
* `ProvenanceVault.scanAndAudit` computed its "merkleRoot" as a single SHA-256
|
|
49
|
+
* over every hash concatenated together. That is a manifest checksum. It detects
|
|
50
|
+
* that something changed and nothing else - in particular it cannot produce an
|
|
51
|
+
* inclusion proof, which is the only reason to use a tree in the first place.
|
|
52
|
+
*
|
|
53
|
+
* With a tree, a third party can verify ONE document against a published root
|
|
54
|
+
* without downloading the whole registry: log2(n) hashes, about 9 for a few
|
|
55
|
+
* hundred artifacts. With a linear digest they would need every artifact hash in
|
|
56
|
+
* the vault to check a single file.
|
|
57
|
+
*
|
|
58
|
+
* DOMAIN SEPARATION
|
|
59
|
+
* -----------------
|
|
60
|
+
* Leaves are hashed with a 0x00 prefix and internal nodes with 0x01. Without it
|
|
61
|
+
* an internal node can be presented as a leaf - a second-preimage attack on the
|
|
62
|
+
* tree shape.
|
|
63
|
+
*
|
|
64
|
+
* Odd nodes are PROMOTED, never duplicated. Duplicating the last leaf lets two
|
|
65
|
+
* distinct trees share a root, which is the Bitcoin CVE-2012-2459 class of bug.
|
|
66
|
+
*
|
|
67
|
+
* PARITY WITH THE PYTHON IMPLEMENTATION
|
|
68
|
+
* -------------------------------------
|
|
69
|
+
* This is a deliberate port of the vault's `_merkle.py` and must agree with it
|
|
70
|
+
* byte for byte - two disagreeing provenance implementations would be worse than
|
|
71
|
+
* one honest flat digest. Leaf ordering, leaf encoding (note the TWO spaces
|
|
72
|
+
* between hash and path) and the empty-tree value are all fixed by that
|
|
73
|
+
* requirement, and `merkle.test.ts` checks against roots generated by the Python.
|
|
74
|
+
*/
|
|
75
|
+
exports.MERKLE_ALGORITHM = 'merkle-sha256-rfc6962';
|
|
76
|
+
const LEAF_PREFIX = Buffer.from([0x00]);
|
|
77
|
+
const NODE_PREFIX = Buffer.from([0x01]);
|
|
78
|
+
const sha256 = (b) => crypto.createHash('sha256').update(b).digest();
|
|
79
|
+
const leafHash = (data) => sha256(Buffer.concat([LEAF_PREFIX, data]));
|
|
80
|
+
exports.leafHash = leafHash;
|
|
81
|
+
const nodeHash = (left, right) => sha256(Buffer.concat([NODE_PREFIX, left, right]));
|
|
82
|
+
exports.nodeHash = nodeHash;
|
|
83
|
+
/**
|
|
84
|
+
* Deterministic leaf order: sorted by path. Walk order is not stable across
|
|
85
|
+
* filesystems; sorting is.
|
|
86
|
+
*/
|
|
87
|
+
function leavesFromRegistry(artifacts) {
|
|
88
|
+
return Object.keys(artifacts).sort().map(p => [p, (0, exports.leafHash)(Buffer.from(`${artifacts[p].sha256} ${p}`, 'utf8'))]);
|
|
89
|
+
}
|
|
90
|
+
/** Every level, leaves first, root last. */
|
|
91
|
+
function buildLevels(leaves) {
|
|
92
|
+
if (leaves.length === 0)
|
|
93
|
+
return [[sha256(Buffer.alloc(0))]];
|
|
94
|
+
const levels = [leaves];
|
|
95
|
+
while (levels[levels.length - 1].length > 1) {
|
|
96
|
+
const cur = levels[levels.length - 1];
|
|
97
|
+
const next = [];
|
|
98
|
+
for (let i = 0; i + 1 < cur.length; i += 2)
|
|
99
|
+
next.push((0, exports.nodeHash)(cur[i], cur[i + 1]));
|
|
100
|
+
if (cur.length % 2)
|
|
101
|
+
next.push(cur[cur.length - 1]); // promote, never duplicate
|
|
102
|
+
levels.push(next);
|
|
103
|
+
}
|
|
104
|
+
return levels;
|
|
105
|
+
}
|
|
106
|
+
function merkleRoot(artifacts) {
|
|
107
|
+
const leaves = leavesFromRegistry(artifacts).map(([, h]) => h);
|
|
108
|
+
const levels = buildLevels(leaves);
|
|
109
|
+
return levels[levels.length - 1][0].toString('hex');
|
|
110
|
+
}
|
|
111
|
+
/** The sibling hashes needed to rebuild the root from one leaf. */
|
|
112
|
+
function inclusionProof(artifacts, target) {
|
|
113
|
+
const entries = leavesFromRegistry(artifacts);
|
|
114
|
+
const idx = entries.findIndex(([p]) => p === target);
|
|
115
|
+
if (idx === -1)
|
|
116
|
+
return null;
|
|
117
|
+
const levels = buildLevels(entries.map(([, h]) => h));
|
|
118
|
+
const proof = [];
|
|
119
|
+
let i = idx;
|
|
120
|
+
for (const level of levels.slice(0, -1)) {
|
|
121
|
+
const sibling = i ^ 1;
|
|
122
|
+
if (sibling < level.length) {
|
|
123
|
+
proof.push({
|
|
124
|
+
side: sibling > i ? 'right' : 'left',
|
|
125
|
+
hash: level[sibling].toString('hex')
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// A promoted node has no sibling at this level; the index still halves.
|
|
129
|
+
i = Math.floor(i / 2);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
algorithm: exports.MERKLE_ALGORITHM,
|
|
133
|
+
artifact: target,
|
|
134
|
+
artifact_sha256: artifacts[target].sha256,
|
|
135
|
+
leaf_sha256: entries[idx][1].toString('hex'),
|
|
136
|
+
leaf_index: idx,
|
|
137
|
+
tree_size: entries.length,
|
|
138
|
+
proof,
|
|
139
|
+
merkle_root: levels[levels.length - 1][0].toString('hex')
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Recompute the root from a leaf plus its audit path.
|
|
144
|
+
* If expectedRoot is provided, validates that the proof resolves to it.
|
|
145
|
+
*/
|
|
146
|
+
function verifyProof(proof, expectedRoot, expectedArtifact) {
|
|
147
|
+
try {
|
|
148
|
+
const hex = (value) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
149
|
+
if (!proof || proof.algorithm !== exports.MERKLE_ALGORITHM || typeof proof.artifact !== 'string' || !proof.artifact ||
|
|
150
|
+
!hex(proof.artifact_sha256) || !hex(proof.leaf_sha256) || !hex(proof.merkle_root) ||
|
|
151
|
+
!Number.isSafeInteger(proof.tree_size) || proof.tree_size < 1 || !Number.isSafeInteger(proof.leaf_index) || proof.leaf_index < 0 || proof.leaf_index >= proof.tree_size ||
|
|
152
|
+
!Array.isArray(proof.proof) || proof.proof.length > 53 || (expectedRoot !== undefined && (!hex(expectedRoot) || proof.merkle_root !== expectedRoot)))
|
|
153
|
+
return false;
|
|
154
|
+
if (expectedArtifact && (expectedArtifact.path !== proof.artifact || expectedArtifact.sha256 !== proof.artifact_sha256))
|
|
155
|
+
return false;
|
|
156
|
+
let h = (0, exports.leafHash)(Buffer.from(`${proof.artifact_sha256} ${proof.artifact}`, 'utf8'));
|
|
157
|
+
if (h.toString('hex') !== proof.leaf_sha256)
|
|
158
|
+
return false;
|
|
159
|
+
let index = proof.leaf_index, width = proof.tree_size, stepIndex = 0;
|
|
160
|
+
while (width > 1) {
|
|
161
|
+
if (index % 2 === 1 || index + 1 < width) {
|
|
162
|
+
const step = proof.proof[stepIndex++];
|
|
163
|
+
if (!step || !hex(step.hash) || step.side !== (index % 2 === 1 ? 'left' : 'right'))
|
|
164
|
+
return false;
|
|
165
|
+
const sibling = Buffer.from(step.hash, 'hex');
|
|
166
|
+
h = step.side === 'left' ? (0, exports.nodeHash)(sibling, h) : (0, exports.nodeHash)(h, sibling);
|
|
167
|
+
}
|
|
168
|
+
index = Math.floor(index / 2);
|
|
169
|
+
width = Math.ceil(width / 2);
|
|
170
|
+
}
|
|
171
|
+
return stepIndex === proof.proof.length && h.toString('hex') === proof.merkle_root;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface ConditionResult {
|
|
2
|
+
name: string;
|
|
3
|
+
passed: boolean;
|
|
4
|
+
durationMs: number;
|
|
5
|
+
error?: string;
|
|
6
|
+
details?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SelfTestReport {
|
|
9
|
+
passed: boolean;
|
|
10
|
+
version: string;
|
|
11
|
+
nodeVersion: string;
|
|
12
|
+
platform: string;
|
|
13
|
+
conditions: ConditionResult[];
|
|
14
|
+
totalDurationMs: number;
|
|
15
|
+
verifiedPackageArtifacts: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function runSelfTests(options?: {
|
|
18
|
+
packageRoot?: string;
|
|
19
|
+
verbose?: boolean;
|
|
20
|
+
}): Promise<SelfTestReport>;
|
|
21
|
+
export declare function printSelfTestReport(report: SelfTestReport): void;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runSelfTests = runSelfTests;
|
|
37
|
+
exports.printSelfTestReport = printSelfTestReport;
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
39
|
+
const fs = __importStar(require("fs/promises"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const index_1 = require("./index");
|
|
42
|
+
const merkle_1 = require("./merkle");
|
|
43
|
+
const artifactEnvelope_1 = require("./artifactEnvelope");
|
|
44
|
+
const timestamp_1 = require("./timestamp");
|
|
45
|
+
async function runSelfTests(options = {}) {
|
|
46
|
+
const startTime = Date.now();
|
|
47
|
+
const root = options.packageRoot ?? path.resolve(__dirname, '..');
|
|
48
|
+
const conditions = [];
|
|
49
|
+
// Helper to record condition results
|
|
50
|
+
async function runCondition(name, fn) {
|
|
51
|
+
const t0 = Date.now();
|
|
52
|
+
try {
|
|
53
|
+
const details = await fn();
|
|
54
|
+
conditions.push({
|
|
55
|
+
name,
|
|
56
|
+
passed: true,
|
|
57
|
+
durationMs: Date.now() - t0,
|
|
58
|
+
details: details || undefined
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
conditions.push({
|
|
63
|
+
name,
|
|
64
|
+
passed: false,
|
|
65
|
+
durationMs: Date.now() - t0,
|
|
66
|
+
error: err?.message || String(err)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Condition 1: Ed25519 Micro-Signatures & Structured V2 Framing
|
|
71
|
+
await runCondition('Ed25519 Micro-Signatures & V2 Framing', async () => {
|
|
72
|
+
const keyPair = index_1.ProvenanceEngine.generateEd25519KeyPair();
|
|
73
|
+
const digest = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
|
|
74
|
+
const payload = index_1.ProvenanceEngine.buildSigningPayload({
|
|
75
|
+
scope: 'moonglade:self-test:doc',
|
|
76
|
+
sha256: digest,
|
|
77
|
+
signer: 'test@moonglade.ai'
|
|
78
|
+
});
|
|
79
|
+
const signature = crypto.sign(null, payload, keyPair.privateKey);
|
|
80
|
+
const valid = crypto.verify(null, payload, keyPair.publicKey, signature);
|
|
81
|
+
if (!valid)
|
|
82
|
+
throw new Error('Valid signature failed verification');
|
|
83
|
+
// Reject tampered payload
|
|
84
|
+
const tamperedPayload = index_1.ProvenanceEngine.buildSigningPayload({
|
|
85
|
+
scope: 'moonglade:self-test:doc',
|
|
86
|
+
sha256: '0000000000000000000000000000000000000000000000000000000000000000',
|
|
87
|
+
signer: 'test@moonglade.ai'
|
|
88
|
+
});
|
|
89
|
+
const tamperedPayloadValid = crypto.verify(null, tamperedPayload, keyPair.publicKey, signature);
|
|
90
|
+
if (tamperedPayloadValid)
|
|
91
|
+
throw new Error('Tampered payload was accepted as valid');
|
|
92
|
+
// Reject tampered signature
|
|
93
|
+
const corruptedSig = Buffer.from(signature);
|
|
94
|
+
corruptedSig[0] ^= 0xff;
|
|
95
|
+
const tamperedSigValid = crypto.verify(null, payload, keyPair.publicKey, corruptedSig);
|
|
96
|
+
if (tamperedSigValid)
|
|
97
|
+
throw new Error('Corrupted signature was accepted as valid');
|
|
98
|
+
return 'Sign, verify, and tamper rejection passed';
|
|
99
|
+
});
|
|
100
|
+
// Condition 2: RFC 6962 Merkle Trees & Leaf Inclusion Proofs
|
|
101
|
+
await runCondition('RFC 6962 Merkle Trees & Inclusion Proofs', async () => {
|
|
102
|
+
const mockRegistry = {
|
|
103
|
+
'docs/a.md': { sha256: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' },
|
|
104
|
+
'docs/b.md': { sha256: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' },
|
|
105
|
+
'docs/c.md': { sha256: 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc' },
|
|
106
|
+
'docs/d.md': { sha256: 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd' }
|
|
107
|
+
};
|
|
108
|
+
const root = (0, merkle_1.merkleRoot)(mockRegistry);
|
|
109
|
+
if (!/^[0-9a-f]{64}$/.test(root))
|
|
110
|
+
throw new Error('Invalid Merkle root derived');
|
|
111
|
+
const proof = (0, merkle_1.inclusionProof)(mockRegistry, 'docs/c.md');
|
|
112
|
+
if (!proof)
|
|
113
|
+
throw new Error('Inclusion proof derivation failed for existing leaf');
|
|
114
|
+
const validProof = (0, merkle_1.verifyProof)(proof, root);
|
|
115
|
+
if (!validProof)
|
|
116
|
+
throw new Error('Valid inclusion proof failed mathematical verification');
|
|
117
|
+
// Corrupted proof must fail
|
|
118
|
+
const corruptedProof = JSON.parse(JSON.stringify(proof));
|
|
119
|
+
corruptedProof.proof[0].hash = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
120
|
+
if ((0, merkle_1.verifyProof)(corruptedProof, root))
|
|
121
|
+
throw new Error('Corrupted inclusion proof was accepted');
|
|
122
|
+
// Missing artifact returns null
|
|
123
|
+
if ((0, merkle_1.inclusionProof)(mockRegistry, 'docs/missing.md') !== null) {
|
|
124
|
+
throw new Error('Inclusion proof returned non-null for missing leaf');
|
|
125
|
+
}
|
|
126
|
+
return `Tree of size 4 verified; root ${root.slice(0, 12)}...`;
|
|
127
|
+
});
|
|
128
|
+
// Condition 3: Artifact Envelope v1 Contract & Canonical Byte Binding
|
|
129
|
+
await runCondition('Artifact Envelope v1 Schema & Hash Binding', async () => {
|
|
130
|
+
const rawBytes = Buffer.from('# Moonglade Self Test\nInvariant proof substrate.', 'utf8');
|
|
131
|
+
const expectedId = (0, artifactEnvelope_1.artifactId)(rawBytes);
|
|
132
|
+
const envelope = (0, artifactEnvelope_1.createArtifactEnvelope)({
|
|
133
|
+
artifact_type: 'release',
|
|
134
|
+
canonical_bytes: expectedId,
|
|
135
|
+
created_at: new Date().toISOString(),
|
|
136
|
+
parents: [],
|
|
137
|
+
attestations: [{ method: 'self-test', status: 'provisional' }],
|
|
138
|
+
status: 'signed',
|
|
139
|
+
evidence: { integrity: 'valid', signature: 'provisional', timestamp: 'none', scope: 'valid' },
|
|
140
|
+
generator: { agent: 'synthetic-test', model: 'self-test-harness' },
|
|
141
|
+
bytes: rawBytes
|
|
142
|
+
});
|
|
143
|
+
if (!(0, artifactEnvelope_1.validateArtifactEnvelope)(envelope))
|
|
144
|
+
throw new Error('Constructed envelope failed schema validation');
|
|
145
|
+
if (envelope.artifact_id !== expectedId)
|
|
146
|
+
throw new Error('Envelope artifact_id does not match canonical bytes hash');
|
|
147
|
+
// Tampered schema must fail
|
|
148
|
+
const tampered = { ...envelope, artifact_id: 'invalid-id' };
|
|
149
|
+
if ((0, artifactEnvelope_1.validateArtifactEnvelope)(tampered))
|
|
150
|
+
throw new Error('Invalid envelope format was accepted as valid');
|
|
151
|
+
return `Envelope ${envelope.artifact_id.slice(0, 19)}... verified`;
|
|
152
|
+
});
|
|
153
|
+
// Condition 4: Fail-Closed Gate Barriers & Anti-Tampering Rejection
|
|
154
|
+
await runCondition('Fail-Closed Gate Barriers & Mutation Rejection', async () => {
|
|
155
|
+
const tempDir = path.join(root, '.tmp-selftest-' + Date.now());
|
|
156
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
157
|
+
const testDoc = path.join(tempDir, 'gate-test.md');
|
|
158
|
+
const keyPair = index_1.ProvenanceEngine.generateEd25519KeyPair();
|
|
159
|
+
try {
|
|
160
|
+
await fs.writeFile(testDoc, '# Gate Assertion Test\nOriginal untouched bytes.\n', 'utf8');
|
|
161
|
+
await index_1.ProvenanceEngine.sealDocument(testDoc, keyPair.privateKey, 'test-signer', 'head', {
|
|
162
|
+
vaultRoot: tempDir,
|
|
163
|
+
timestamp: false
|
|
164
|
+
});
|
|
165
|
+
// Valid check must pass
|
|
166
|
+
const initialVerify = await index_1.ProvenanceEngine.verifyDocument(testDoc, keyPair.publicKey);
|
|
167
|
+
if (!initialVerify.verified) {
|
|
168
|
+
throw new Error(`Expected verification to succeed: ${initialVerify.error}`);
|
|
169
|
+
}
|
|
170
|
+
// Mutate the document
|
|
171
|
+
await fs.appendFile(testDoc, '\nMalicious unsealed line inserted.\n', 'utf8');
|
|
172
|
+
const tamperedVerify = await index_1.ProvenanceEngine.verifyDocument(testDoc, keyPair.publicKey);
|
|
173
|
+
if (tamperedVerify.verified) {
|
|
174
|
+
throw new Error('Mutated document passed verification; fail-closed gate violated');
|
|
175
|
+
}
|
|
176
|
+
if (!tamperedVerify.error?.includes('Hash mismatch')) {
|
|
177
|
+
throw new Error(`Expected hash mismatch error, got: ${tamperedVerify.error}`);
|
|
178
|
+
}
|
|
179
|
+
return 'Sealed artifact verified; post-seal modification rejected fail-closed';
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
// Condition 5: Concurrent Degrading Timestamping Architecture (DTA)
|
|
186
|
+
await runCondition('Concurrent Degrading Timestamp Architecture (DTA)', async () => {
|
|
187
|
+
const dummyDigest = crypto.createHash('sha256').update('dta-test').digest();
|
|
188
|
+
const dta = await (0, timestamp_1.concurrentDegradingTimestamp)(dummyDigest, { specifiedTier: 'L4_HOST_UNAUTHENTICATED' });
|
|
189
|
+
if (!dta.tier || dta.tier !== 'L4_HOST_UNAUTHENTICATED') {
|
|
190
|
+
throw new Error(`Expected L4 offline degradation tier, got ${dta.tier}`);
|
|
191
|
+
}
|
|
192
|
+
if (!dta.tsa_time || isNaN(Date.parse(dta.tsa_time))) {
|
|
193
|
+
throw new Error('Invalid ISO timestamp returned by DTA degradation fallback');
|
|
194
|
+
}
|
|
195
|
+
if (!Array.isArray(dta.probes) || dta.probes.length === 0) {
|
|
196
|
+
throw new Error('Expected probe audit telemetry in DTA result');
|
|
197
|
+
}
|
|
198
|
+
return `Degraded to ${dta.tier} at ${dta.tsa_time}`;
|
|
199
|
+
});
|
|
200
|
+
// Condition 6: Package Self-Provenance (All Shipped Items Carry Provenance)
|
|
201
|
+
let verifiedCount = 0;
|
|
202
|
+
await runCondition('Package Self-Provenance Verification', async () => {
|
|
203
|
+
// Look for all .provenance.json files directly in package root and docs
|
|
204
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
205
|
+
const sidecarFiles = [];
|
|
206
|
+
for (const ent of entries) {
|
|
207
|
+
if (ent.isFile() && ent.name.endsWith('.provenance.json')) {
|
|
208
|
+
sidecarFiles.push(path.join(root, ent.name));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Only package-root sidecars belong to this distribution inventory.
|
|
212
|
+
if (sidecarFiles.length === 0) {
|
|
213
|
+
return 'No shipped sidecars; distribution files are intentionally unsealed pending release approval';
|
|
214
|
+
}
|
|
215
|
+
for (const sidecarPath of sidecarFiles) {
|
|
216
|
+
const sidecarRaw = await fs.readFile(sidecarPath, 'utf8');
|
|
217
|
+
const sidecar = JSON.parse(sidecarRaw);
|
|
218
|
+
const targetPath = sidecarPath.replace(/\.provenance\.json$/, '');
|
|
219
|
+
let fileBytes;
|
|
220
|
+
try {
|
|
221
|
+
fileBytes = await fs.readFile(targetPath);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// Special case: package.provenance.json may target package.json
|
|
225
|
+
if (path.basename(sidecarPath) === 'package.provenance.json') {
|
|
226
|
+
fileBytes = await fs.readFile(path.join(root, 'package.json'));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
throw new Error(`Target artifact missing for sidecar: ${path.basename(sidecarPath)}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const compareBytes = index_1.ProvenanceEngine.canonicalizeBytes(fileBytes, path.basename(sidecarPath) === 'package.provenance.json' ? 'package.json' : targetPath);
|
|
233
|
+
const computedHash = crypto.createHash('sha256').update(compareBytes).digest('hex');
|
|
234
|
+
if (!/^[a-f0-9]{64}$/.test(sidecar.sha256_at_last_write) || sidecar.sha256_at_last_write !== computedHash) {
|
|
235
|
+
throw new Error(`Hash mismatch on shipped artifact ${path.basename(targetPath)}: sidecar has ${sidecar.sha256_at_last_write}, actual is ${computedHash}`);
|
|
236
|
+
}
|
|
237
|
+
if (sidecar.envelope) {
|
|
238
|
+
if (!(0, artifactEnvelope_1.validateArtifactEnvelope)(sidecar.envelope)) {
|
|
239
|
+
throw new Error(`Invalid envelope on shipped artifact ${path.basename(targetPath)}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
verifiedCount++;
|
|
243
|
+
}
|
|
244
|
+
return `${verifiedCount} shipped sidecar hashes compared; no package signature verification performed`;
|
|
245
|
+
});
|
|
246
|
+
const totalDurationMs = Date.now() - startTime;
|
|
247
|
+
const passed = conditions.every(c => c.passed);
|
|
248
|
+
return {
|
|
249
|
+
passed,
|
|
250
|
+
version: require('../package.json').version,
|
|
251
|
+
nodeVersion: process.version,
|
|
252
|
+
platform: `${process.platform}-${process.arch}`,
|
|
253
|
+
conditions,
|
|
254
|
+
totalDurationMs,
|
|
255
|
+
verifiedPackageArtifacts: verifiedCount
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function printSelfTestReport(report) {
|
|
259
|
+
console.log(`\nMoonglade Provenance Self-Test Suite v${report.version} (${report.platform}, Node ${report.nodeVersion})`);
|
|
260
|
+
console.log('='.repeat(75));
|
|
261
|
+
for (let i = 0; i < report.conditions.length; i++) {
|
|
262
|
+
const c = report.conditions[i];
|
|
263
|
+
const statusTag = c.passed ? '[PASS]' : '[FAIL]';
|
|
264
|
+
console.log(`${statusTag} ${i + 1}. ${c.name} (${c.durationMs}ms)`);
|
|
265
|
+
if (c.details) {
|
|
266
|
+
console.log(` Evidence: ${c.details}`);
|
|
267
|
+
}
|
|
268
|
+
if (c.error) {
|
|
269
|
+
console.log(` Error: ${c.error}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
console.log('-'.repeat(75));
|
|
273
|
+
if (report.passed) {
|
|
274
|
+
console.log(`All ${report.conditions.length} condition suites PASSED (${report.totalDurationMs}ms). Environment verified.`);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
const failedCount = report.conditions.filter(c => !c.passed).length;
|
|
278
|
+
console.error(`${failedCount} condition suite(s) FAILED (${report.totalDurationMs}ms). Verification failed.`);
|
|
279
|
+
}
|
|
280
|
+
}
|