Paired-backup verifier now checks more than the manifest hash
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Pool, type PoolConfig } from 'pg';
|
||||
import { generateKeyPairSync, randomUUID, sign } from 'node:crypto';
|
||||
import { access, lstat, mkdtemp, rm, writeFile, mkdir, readFile } from 'node:fs/promises';
|
||||
import { createHash, generateKeyPairSync, randomUUID, sign } from 'node:crypto';
|
||||
import { access, chmod, lstat, mkdtemp, rm, writeFile, mkdir, readFile } from 'node:fs/promises';
|
||||
import { constants } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
@@ -414,10 +414,33 @@ test('coordinated node/PostgreSQL backup holds both writer fences and verifies m
|
||||
const env = { ...process.env, WIKIDECK_CUSTODY_PG_URL_FILE: runtimeUrl,
|
||||
WIKIDECK_CUSTODY_PG_BACKUP_URL_FILE: backupUrl, WIKIDECK_CUSTODY_PG_LOCAL_TEST: '1' };
|
||||
const destination = join(dir, 'paired-backup');
|
||||
const manifest = await createCoordinatedCustodyBackup(data, destination, env);
|
||||
const inheritedLibpq = { hostAddr: process.env.PGHOSTADDR, sslMode: process.env.PGSSLMODE, sslRoot: process.env.PGSSLROOTCERT };
|
||||
let manifest: Awaited<ReturnType<typeof createCoordinatedCustodyBackup>>;
|
||||
try {
|
||||
process.env.PGHOSTADDR = '192.0.2.123'; process.env.PGSSLMODE = 'disable'; process.env.PGSSLROOTCERT = 'invalid-inherited-root.pem';
|
||||
manifest = await createCoordinatedCustodyBackup(data, destination, env);
|
||||
} finally {
|
||||
if (inheritedLibpq.hostAddr === undefined) delete process.env.PGHOSTADDR; else process.env.PGHOSTADDR = inheritedLibpq.hostAddr;
|
||||
if (inheritedLibpq.sslMode === undefined) delete process.env.PGSSLMODE; else process.env.PGSSLMODE = inheritedLibpq.sslMode;
|
||||
if (inheritedLibpq.sslRoot === undefined) delete process.env.PGSSLROOTCERT; else process.env.PGSSLROOTCERT = inheritedLibpq.sslRoot;
|
||||
}
|
||||
assert.equal(manifest.custodianId, f.market.id); assert.equal(manifest.database, 'custody_test');
|
||||
assert.match(manifest.postgresServerVersion, /^\d+\.\d+/);
|
||||
assert.match(manifest.postgresServerVersionNum, /^\d{5,6}$/);
|
||||
assert.match(manifest.pgDumpVersion, /^pg_dump \(PostgreSQL\) /);
|
||||
assert.match(manifest.pgRestoreVersion, /^pg_restore \(PostgreSQL\) /);
|
||||
assert.equal(manifest.sqliteCutoverSourceDigest, cutoverDigest);
|
||||
assert.equal((await verifyCoordinatedCustodyBackup(destination)).nodeManifestSha256, manifest.nodeManifestSha256);
|
||||
if (process.platform !== 'win32') {
|
||||
const oldRestore = join(dir, 'old-pg-restore');
|
||||
await writeFile(oldRestore, '#!/bin/sh\nprintf "pg_restore (PostgreSQL) 9.6\\n"\n', { mode: 0o700 });
|
||||
await chmod(oldRestore, 0o700);
|
||||
await assert.rejects(verifyCoordinatedCustodyBackup(destination, oldRestore), /older than archive pg_dump/);
|
||||
}
|
||||
const extraNodeFile = join(destination, 'node', 'unmanifested.txt');
|
||||
await writeFile(extraNodeFile, 'not part of backup manifest');
|
||||
await assert.rejects(verifyCoordinatedCustodyBackup(destination), /Unexpected coordinated node backup contents/);
|
||||
await rm(extraNodeFile);
|
||||
await assert.rejects(lstat(join(destination, 'node', 'custody-sandbox.sqlite')), { code: 'ENOENT' });
|
||||
const restoredNodeDir = join(dir, 'restored-node');
|
||||
await restoreBackup(join(destination, 'node'), restoredNodeDir);
|
||||
@@ -454,8 +477,8 @@ test('coordinated node/PostgreSQL backup holds both writer fences and verifies m
|
||||
await assert.rejects(lstat(lockedDestination)); await rm(staleLock);
|
||||
|
||||
const failedDump = join(dir, 'failed-dump'), brokenEnv = { ...env, WIKIDECK_PG_DUMP_BINARY: join(dir, 'missing-pg-dump') };
|
||||
await assert.rejects(createCoordinatedCustodyBackup(data, failedDump, brokenEnv), /dump utility failed to start/);
|
||||
await assert.rejects(verifyCoordinatedCustodyBackup(failedDump), /manifest/);
|
||||
await assert.rejects(createCoordinatedCustodyBackup(data, failedDump, brokenEnv), /client utility failed to start/);
|
||||
await assert.rejects(verifyCoordinatedCustodyBackup(failedDump), /ENOENT/);
|
||||
// A failed external dump releases PostgreSQL's shared-writer fence; normal sandbox commands still work.
|
||||
const recoveredWrite = await f.execute(other, { type: 'mock-credit' }) as CustodyActivity;
|
||||
assert.equal(recoveredWrite.availableAtomicXmr, '1000000000000');
|
||||
@@ -465,11 +488,29 @@ test('coordinated node/PostgreSQL backup holds both writer fences and verifies m
|
||||
await writeFile(bindingPath, JSON.stringify({ version: 1, custodianId: randomUUID() }), { mode: 0o600 });
|
||||
await assert.rejects(createCoordinatedCustodyBackup(data, mismatchedBinding, env), /identities differ/);
|
||||
await writeFile(bindingPath, originalBinding, { mode: 0o600 });
|
||||
await assert.rejects(verifyCoordinatedCustodyBackup(mismatchedBinding), /manifest/);
|
||||
await assert.rejects(verifyCoordinatedCustodyBackup(mismatchedBinding), /ENOENT/);
|
||||
|
||||
const corruptDump = join(destination, 'custody.dump');
|
||||
await writeFile(corruptDump, 'corrupt');
|
||||
await assert.rejects(verifyCoordinatedCustodyBackup(destination), /hash\/size mismatch/);
|
||||
// Even if an operator or attacker rewrites the unsigned manifest hashes, pg_restore parsing rejects junk bytes.
|
||||
const manifestPath = join(destination, 'manifest.json'), altered = JSON.parse(await readFile(manifestPath, 'utf8'));
|
||||
altered.postgresDump.bytes = Buffer.byteLength('corrupt');
|
||||
altered.postgresDump.sha256 = createHash('sha256').update('corrupt').digest('hex');
|
||||
await writeFile(manifestPath, JSON.stringify(altered));
|
||||
await assert.rejects(verifyCoordinatedCustodyBackup(destination), /dump archive is invalid/);
|
||||
const unrelatedDatabase = `empty_${randomUUID().replaceAll('-', '')}`, unrelatedDump = join(dir, 'unrelated.dump');
|
||||
await f.admin.query(`CREATE DATABASE "${unrelatedDatabase}"`);
|
||||
try {
|
||||
await run('pg_dump', ['-h', '127.0.0.1', '-U', 'postgres', '-d', unrelatedDatabase, '-Fc', '-f', unrelatedDump]);
|
||||
await writeFile(corruptDump, await readFile(unrelatedDump));
|
||||
const validButWrong = JSON.parse(await readFile(manifestPath, 'utf8'));
|
||||
const unrelated = await readFile(unrelatedDump);
|
||||
validButWrong.postgresDump.bytes = unrelated.length;
|
||||
validButWrong.postgresDump.sha256 = createHash('sha256').update(unrelated).digest('hex');
|
||||
await writeFile(manifestPath, JSON.stringify(validButWrong));
|
||||
await assert.rejects(verifyCoordinatedCustodyBackup(destination), /missing required custody accounting tables/);
|
||||
} finally { await f.admin.query(`DROP DATABASE IF EXISTS "${unrelatedDatabase}" WITH (FORCE)`); }
|
||||
});
|
||||
|
||||
test('failed cutover freezes the source, rolls back the entire target, and resumes without duplicate balances', async t => {
|
||||
|
||||
Reference in New Issue
Block a user