|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +const crypto = require('crypto'); |
| 4 | +const { |
| 5 | + OpenExecutionVerifier, |
| 6 | + canonicalize, |
| 7 | + hash, |
| 8 | + verifySignature, |
| 9 | + verifyContentIntegrity, |
| 10 | + verifyTimestamp, |
| 11 | + verifyBlockchain, |
| 12 | + HASH_MAP, |
| 13 | + SIGNATURE_ALGORITHMS, |
| 14 | +} = require('../src/verify.js'); |
| 15 | + |
| 16 | +const assert = require('assert'); |
| 17 | + |
| 18 | +// ── Canonicalize (JCS / RFC 8785) ── |
| 19 | + |
| 20 | +assert.strictEqual(canonicalize(null), 'null'); |
| 21 | +assert.strictEqual(canonicalize(42), '42'); |
| 22 | +assert.strictEqual(canonicalize('hello'), '"hello"'); |
| 23 | +assert.strictEqual(canonicalize([1, 2]), '[1,2]'); |
| 24 | +assert.strictEqual( |
| 25 | + canonicalize({ b: 2, a: 1 }), |
| 26 | + '{"a":1,"b":2}', |
| 27 | + 'Keys must be sorted' |
| 28 | +); |
| 29 | +assert.strictEqual( |
| 30 | + canonicalize({ b: undefined, a: 1 }), |
| 31 | + '{"a":1}', |
| 32 | + 'Undefined values omitted per RFC 8785' |
| 33 | +); |
| 34 | + |
| 35 | +// ── Hash ── |
| 36 | + |
| 37 | +const sha256Hello = hash('hello', 'sha256'); |
| 38 | +assert.strictEqual(sha256Hello.length, 64); |
| 39 | +assert.strictEqual( |
| 40 | + sha256Hello, |
| 41 | + crypto.createHash('sha256').update('hello').digest('hex') |
| 42 | +); |
| 43 | + |
| 44 | +for (const algo of Object.keys(HASH_MAP)) { |
| 45 | + const h = hash('test', algo); |
| 46 | + assert.ok(h.length > 0, `hash(${algo}) should produce output`); |
| 47 | +} |
| 48 | + |
| 49 | +assert.throws(() => hash('x', 'md5'), /Unsupported hash algorithm/); |
| 50 | + |
| 51 | +// ── Signature ── |
| 52 | + |
| 53 | +const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519'); |
| 54 | +const data = Buffer.from('test-payload'); |
| 55 | +const sig = crypto.sign(null, data, privateKey).toString('hex'); |
| 56 | + |
| 57 | +assert.strictEqual(verifySignature(data, sig, publicKey, 'ed25519'), true); |
| 58 | +assert.strictEqual(verifySignature(Buffer.from('wrong'), sig, publicKey, 'ed25519'), false); |
| 59 | +assert.strictEqual(verifySignature(data, 'bad', publicKey, 'ed25519'), false); |
| 60 | + |
| 61 | +// ── Chain Integrity ── |
| 62 | + |
| 63 | +function buildChain(n, hashAlgorithm = 'sha256') { |
| 64 | + const genesisLen = hashAlgorithm === 'sha256' ? 64 : 128; |
| 65 | + let prevHash = '0'.repeat(genesisLen); |
| 66 | + const events = []; |
| 67 | + for (let i = 1; i <= n; i++) { |
| 68 | + const eventData = { |
| 69 | + seq: i, |
| 70 | + event_type: 'test', |
| 71 | + actor_id: 'system', |
| 72 | + timestamp: new Date(1700000000000 + i * 1000).toISOString(), |
| 73 | + payload: { value: i }, |
| 74 | + prev_hash: prevHash, |
| 75 | + }; |
| 76 | + const eventHash = hash(canonicalize(eventData), hashAlgorithm); |
| 77 | + events.push({ |
| 78 | + seq: i, |
| 79 | + event_type: 'test', |
| 80 | + actor_id: 'system', |
| 81 | + created_at: eventData.timestamp, |
| 82 | + payload: { value: i }, |
| 83 | + prev_hash: prevHash, |
| 84 | + event_hash: eventHash, |
| 85 | + }); |
| 86 | + prevHash = eventHash; |
| 87 | + } |
| 88 | + return events; |
| 89 | +} |
| 90 | + |
| 91 | +const chain3 = buildChain(3); |
| 92 | +const result3 = OpenExecutionVerifier.verifyChainIntegrity(chain3); |
| 93 | +assert.strictEqual(result3.is_valid, true); |
| 94 | +assert.strictEqual(result3.event_count, 3); |
| 95 | +assert.strictEqual(result3.errors.length, 0); |
| 96 | + |
| 97 | +// Tampered chain |
| 98 | +const tampered = JSON.parse(JSON.stringify(chain3)); |
| 99 | +tampered[1].event_hash = 'deadbeef'.repeat(8); |
| 100 | +const resultTampered = OpenExecutionVerifier.verifyChainIntegrity(tampered); |
| 101 | +assert.strictEqual(resultTampered.is_valid, false); |
| 102 | +assert.ok(resultTampered.errors.length > 0); |
| 103 | + |
| 104 | +// Empty chain |
| 105 | +const resultEmpty = OpenExecutionVerifier.verifyChainIntegrity([]); |
| 106 | +assert.strictEqual(resultEmpty.is_valid, true); |
| 107 | +assert.strictEqual(resultEmpty.event_count, 0); |
| 108 | + |
| 109 | +// ── Chain Hash ── |
| 110 | + |
| 111 | +const hashes = chain3.map(e => e.event_hash); |
| 112 | +const chainHash = OpenExecutionVerifier.computeChainHash(hashes); |
| 113 | +assert.strictEqual(chainHash.length, 64); |
| 114 | +assert.strictEqual(chainHash, hash(hashes.join(''), 'sha256')); |
| 115 | + |
| 116 | +// ── Content Integrity (Merkle) ── |
| 117 | + |
| 118 | +const leaf1 = hash('data1', 'sha256'); |
| 119 | +const leaf2 = hash('data2', 'sha256'); |
| 120 | +const LEAF_PREFIX = Buffer.from([0x00]); |
| 121 | +const NODE_PREFIX = Buffer.from([0x01]); |
| 122 | +const h1 = crypto.createHash('sha256').update(Buffer.concat([LEAF_PREFIX, Buffer.from(leaf1, 'hex')])).digest(); |
| 123 | +const h2 = crypto.createHash('sha256').update(Buffer.concat([LEAF_PREFIX, Buffer.from(leaf2, 'hex')])).digest(); |
| 124 | +const root = crypto.createHash('sha256').update(Buffer.concat([NODE_PREFIX, h1, h2])).digest('hex'); |
| 125 | + |
| 126 | +const ciResult = verifyContentIntegrity({ |
| 127 | + type: 'ContentIntegrity', |
| 128 | + root_hash: root, |
| 129 | + leaves: [leaf1, leaf2], |
| 130 | +}); |
| 131 | +assert.strictEqual(ciResult.valid, true); |
| 132 | + |
| 133 | +const ciBadRoot = verifyContentIntegrity({ |
| 134 | + type: 'ContentIntegrity', |
| 135 | + root_hash: 'wrong', |
| 136 | + leaves: [leaf1, leaf2], |
| 137 | +}); |
| 138 | +assert.strictEqual(ciBadRoot.valid, false); |
| 139 | + |
| 140 | +// Empty leaves |
| 141 | +const ciEmpty = verifyContentIntegrity({ |
| 142 | + type: 'ContentIntegrity', |
| 143 | + root_hash: '0'.repeat(64), |
| 144 | + leaves: [], |
| 145 | +}); |
| 146 | +assert.strictEqual(ciEmpty.valid, true); |
| 147 | + |
| 148 | +// ── Timestamp ── |
| 149 | + |
| 150 | +const ts = new Date().toISOString(); |
| 151 | +const tsHash = hash(ts, 'sha256'); |
| 152 | +const tsResult = verifyTimestamp({ type: 'Timestamp', timestamp: ts, hash: tsHash }); |
| 153 | +assert.strictEqual(tsResult.valid, true); |
| 154 | +assert.strictEqual(tsResult.hash_valid, true); |
| 155 | + |
| 156 | +const tsBad = verifyTimestamp({ type: 'Timestamp', timestamp: ts, hash: 'wrong' }); |
| 157 | +assert.strictEqual(tsBad.valid, false); |
| 158 | + |
| 159 | +// ── Blockchain ── |
| 160 | + |
| 161 | +const bcResult = verifyBlockchain( |
| 162 | + { type: 'Blockchain', chain_hash: 'abc', tx_hash: '0x123', network: 'ethereum' }, |
| 163 | + 'abc' |
| 164 | +); |
| 165 | +assert.strictEqual(bcResult.valid, true); |
| 166 | + |
| 167 | +const bcMismatch = verifyBlockchain( |
| 168 | + { type: 'Blockchain', chain_hash: 'abc', tx_hash: '0x123' }, |
| 169 | + 'xyz' |
| 170 | +); |
| 171 | +assert.strictEqual(bcMismatch.valid, false); |
| 172 | + |
| 173 | +// ── Bundle ── |
| 174 | + |
| 175 | +const chainEvents = buildChain(2); |
| 176 | +const eventHashes = chainEvents.map(e => e.event_hash); |
| 177 | +const bundleChainHash = OpenExecutionVerifier.computeChainHash(eventHashes); |
| 178 | +const cert = { chain_hash: bundleChainHash, scope: 'test' }; |
| 179 | +const certCanonical = canonicalize(cert); |
| 180 | +const certSig = crypto.sign(null, Buffer.from(certCanonical), privateKey).toString('hex'); |
| 181 | +const pemKey = publicKey.export({ type: 'spki', format: 'pem' }); |
| 182 | + |
| 183 | +const bundle = { |
| 184 | + certificate: cert, |
| 185 | + certificate_signature: certSig, |
| 186 | + chain: { events: chainEvents, hash_algorithm: 'sha256', signature_algorithm: 'ed25519', canonicalization: 'jcs' }, |
| 187 | + public_key: pemKey, |
| 188 | +}; |
| 189 | +const bundleResult = OpenExecutionVerifier.verifyBundle(bundle); |
| 190 | +assert.strictEqual(bundleResult.valid, true); |
| 191 | +assert.strictEqual(bundleResult.certificate_signature_valid, true); |
| 192 | +assert.strictEqual(bundleResult.chain_integrity.is_valid, true); |
| 193 | +assert.strictEqual(bundleResult.chain_hash_valid, true); |
| 194 | + |
| 195 | +console.log('All 25 tests passed.'); |
0 commit comments