mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-18 06:41:27 +02:00
151 lines
7.3 KiB
JavaScript
151 lines
7.3 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { spawnSync } = require('child_process');
|
|
|
|
const rootPath = (...parts) => path.join(__dirname, '..', ...parts);
|
|
const helperPath = rootPath('scripts', 'deploy', 'wallet-keyfile-recovery-proof.py');
|
|
const fixturePath = rootPath('scripts', 'deploy', 'wallet-keyfile-recovery-fixture-test.py');
|
|
const runbookPath = rootPath('docs', 'WALLET_KEYFILE_RECOVERY_PROOF.md');
|
|
|
|
describe('fixed wallet keyfile/password recovery proof boundary', () => {
|
|
const helper = fs.readFileSync(helperPath, 'utf8');
|
|
const runbook = fs.readFileSync(runbookPath, 'utf8');
|
|
|
|
test('accepts only the two audited legacy profiles and fixed recovery artifacts', () => {
|
|
for (const expected of [
|
|
'monero-wallet-rpc.service',
|
|
'/usr/bin/monero-wallet-rpc',
|
|
'/home/jw/Programs/monero-x86_64-linux-gnu-v0.18.4.4/test',
|
|
'monero-stagenet-preprod-20260721T1140Z.wallet',
|
|
'monero-stagenet-preprod-20260721T1140Z.keys',
|
|
'/run/wowngeon-wallet-keyfile-proof-monero-stagenet',
|
|
'wownero-wallet-rpc.service',
|
|
'/usr/bin/wownero-wallet-rpc',
|
|
'/home/jw/Programs/wow-11.3.0/game',
|
|
'wownero-mainnet-preprod-20260721T1140Z.wallet',
|
|
'wownero-mainnet-preprod-20260721T1140Z.keys',
|
|
'/run/wowngeon-wallet-keyfile-proof-wownero-mainnet',
|
|
]) {
|
|
expect(helper).toContain(expected);
|
|
}
|
|
expect(helper).toContain('live_rpc_port=38083');
|
|
expect(helper).toContain('proof_port=39084');
|
|
expect(helper).toContain('live_rpc_port=34570');
|
|
expect(helper).toContain('proof_port=39570');
|
|
expect(helper).toContain('if len(arguments) != 1 or arguments[0] not in PROFILES:');
|
|
expect(helper).not.toContain('argparse');
|
|
});
|
|
|
|
test('validates live identity before extracting and wiping the exact inline password', () => {
|
|
expect(helper).toContain('systemctl("is-active", "--quiet", profile.unit)');
|
|
expect(helper).toContain('process_executable(pid) != profile.binary');
|
|
expect(helper).toContain('process_executable_identity(pid) != expected_binary');
|
|
expect(helper).toContain('process_has_unit_cgroup(pid, profile.unit)');
|
|
expect(helper).toContain('require_exact_listener(');
|
|
expect(helper).toContain('b"--wallet-file"');
|
|
expect(helper).toContain('b"--password"');
|
|
expect(helper).toContain('f"/proc/{pid}/cmdline"');
|
|
expect(helper).toContain('os.readv(descriptor, [buffer])');
|
|
expect(helper).toContain('wipe_buffer(buffer, buffer_is_protected)');
|
|
expect(helper).toContain('PR_SET_DUMPABLE');
|
|
expect(helper).toContain('PR_SET_NO_NEW_PRIVS');
|
|
expect(helper).toContain('MADV_DONTDUMP');
|
|
expect(helper).not.toMatch(/systemctl\([^\n]*(?:stop|start|restart|reload|enable|disable)/);
|
|
});
|
|
|
|
test('drops candidate privilege and uses only fixed read/proof RPC methods', () => {
|
|
expect(helper).toContain('os.setgroups([])');
|
|
expect(helper).toContain('os.setgid(expected_gid)');
|
|
expect(helper).toContain('os.setuid(expected_uid)');
|
|
expect(helper).toContain('os.getresuid() != (expected_uid, expected_uid, expected_uid)');
|
|
expect(helper).toContain('os.getresgid() != (expected_gid, expected_gid, expected_gid)');
|
|
expect(helper).toContain('if process_groups(process.pid):');
|
|
expect(helper).toContain('process_status_integer(process.pid, "NoNewPrivs") != 1');
|
|
expect(helper).toContain('("CapInh", "CapPrm", "CapEff", "CapAmb")');
|
|
expect(helper).toContain('LIBC.prctl(PR_SET_KEEPCAPS, 0, 0, 0, 0)');
|
|
expect(helper).toContain('publish_candidate_tree(profile, expected_uid, expected_gid)');
|
|
expect(helper).toContain('"method": "get_address"');
|
|
expect(helper).toContain('rpc_request("sign", {"data": SIGN_MESSAGE})');
|
|
expect(helper).toContain('"verify",');
|
|
expect(helper).toContain('signature_type not in (None, "spend")');
|
|
expect(helper).toContain('"--offline"');
|
|
expect(helper).toContain('"--log-file"');
|
|
expect(helper).toContain('"/dev/null"');
|
|
expect(helper).toContain('password-file=');
|
|
expect(helper).toContain('rpc-bind-ip=127.0.0.1');
|
|
expect(helper).toContain('rpc-login=');
|
|
expect(helper).toContain('if status != 401:');
|
|
expect(helper).toContain('if status != 200:');
|
|
expect(helper).toContain('hmac.compare_digest(live_address, restored_address)');
|
|
for (const forbiddenMethod of [
|
|
'query_key',
|
|
'get_seed',
|
|
'get_spend_key',
|
|
'get_view_key',
|
|
'transfer_split',
|
|
'sweep_all',
|
|
]) {
|
|
expect(helper).not.toContain(`"method": "${forbiddenMethod}"`);
|
|
}
|
|
});
|
|
|
|
test('fails closed around source links, races, port collisions, and exact cleanup', () => {
|
|
expect(helper).toContain('details.st_nlink != 1');
|
|
expect(helper).toContain('O_NOFOLLOW');
|
|
expect(helper).toContain('file_identity(os.fstat(source_fd)) != expected_source');
|
|
expect(helper).toContain('refuse("proof-port-collision")');
|
|
expect(helper).toContain('secure_remove_proof_directory(');
|
|
expect(helper).toContain('(details.st_dev, details.st_ino) != expected_identity');
|
|
expect(helper).toContain('result=passed');
|
|
expect(helper).toContain('result=refused');
|
|
expect(helper).not.toMatch(/(?:import traceback|traceback\.)/);
|
|
expect(helper).not.toMatch(/\bprint\(/);
|
|
});
|
|
|
|
test('runbook keeps seed custody and off-host restore explicitly unproven', () => {
|
|
expect(runbook).toContain('has not been installed on or run against');
|
|
expect(runbook).toContain('This is **not seed custody**');
|
|
expect(runbook).toContain('it is **not an off-host disaster-recovery test**');
|
|
expect(runbook).toMatch(/record seed custody and off-host recovery as\s+\*\*unproven\*\*/);
|
|
expect(runbook).toContain('The proof candidate is offline');
|
|
expect(runbook).toMatch(/Wallet-RPC never runs\s+as root/);
|
|
expect(runbook).toContain('not from an independent custody source');
|
|
expect(runbook).toContain('**not** an RPC-hardening or');
|
|
expect(runbook).toContain('not evidence they have been run');
|
|
expect(runbook).toContain('do not blindly remove it');
|
|
});
|
|
|
|
test('synthetic fixture passes without opening a real wallet', () => {
|
|
const result = spawnSync('/usr/bin/python3', [fixturePath], {
|
|
encoding: 'utf8',
|
|
timeout: 15000,
|
|
});
|
|
expect({ status: result.status, signal: result.signal, stdout: result.stdout }).toEqual({
|
|
status: 0,
|
|
signal: null,
|
|
stdout: '',
|
|
});
|
|
expect(result.stderr).toContain('OK');
|
|
});
|
|
|
|
test('invalid invocation renders only the refusal status', () => {
|
|
const result = spawnSync('/usr/bin/python3', [helperPath, 'not-a-profile'], {
|
|
encoding: 'utf8',
|
|
timeout: 5000,
|
|
});
|
|
expect({
|
|
status: result.status,
|
|
signal: result.signal,
|
|
stdout: result.stdout,
|
|
stderr: result.stderr,
|
|
}).toEqual({
|
|
status: 1,
|
|
signal: null,
|
|
stdout: 'result=refused\n',
|
|
stderr: '',
|
|
});
|
|
});
|
|
});
|