Started Walet RPC auth work without adding MD5 Digest support
This commit is contained in:
@@ -4,8 +4,11 @@ import { keccak } from 'hash-wasm';
|
||||
import { ADDITIONAL_MARKET_FEE } from '../shared/market.ts';
|
||||
import { createMoneroDepositAddressRpc, createMoneroWalletObserver, loadMoneroObserverConfig,
|
||||
moneroWalletFingerprint, type MoneroFixtureTransport } from '../server/market/monero-observer.ts';
|
||||
import { loadMoneroMtlsTransportConfig } from '../server/market/monero-mtls-transport.ts';
|
||||
import { createMoneroRpcGateway, loadMoneroRpcGatewayConfig,
|
||||
validateMoneroRpcGatewayRequest } from '../server/market/monero-rpc-gateway.ts';
|
||||
|
||||
const config = { network: 'mainnet' as const, url: 'http://127.0.0.1:18083/json_rpc', username: 'fixture', password: 'not-a-live-secret' };
|
||||
const config = { network: 'mainnet' as const, url: 'http://127.0.0.1:18083/json_rpc' };
|
||||
function response(id: string, result: Record<string, unknown>, status = 200, type = 'application/json') {
|
||||
return new Response(JSON.stringify({ jsonrpc: '2.0', id, result }), { status, headers: { 'content-type': type } });
|
||||
}
|
||||
@@ -55,7 +58,7 @@ test('read-only Monero observer preserves uint64 amount text and returns observa
|
||||
amountAtomicXmr: '9007199254740993', height: 91, confirmations: 10, timestamp: 1_800_000_000,
|
||||
unlockTime: '0', accountIndex: 0, addressIndex: 7, locked: false, doubleSpendSeen: false });
|
||||
assert.deepEqual(calls.map(call => call.method).sort(), ['get_address', 'get_height', 'get_transfers']);
|
||||
assert.ok(calls.every(call => call.auth === `Basic ${Buffer.from(`${config.username}:${config.password}`).toString('base64')}`));
|
||||
assert.ok(calls.every(call => call.auth === null), 'Wallet RPC credentials must never be sent as Basic/Digest headers');
|
||||
assert.deepEqual(calls.find(call => call.method === 'get_transfers')?.params,
|
||||
{ in: true, filter_by_height: true, min_height: 90, max_height: 100 });
|
||||
assert.equal(Object.keys(observer).some(key => /transfer|sweep|send|credit/i.test(key)), false);
|
||||
@@ -84,7 +87,7 @@ test('deposit-address RPC client is narrowly allow-listed and creates only one v
|
||||
assert.deepEqual(await rpc.getSubaddress(0, 7), { address: subaddress, label: 'wikideck-deposit-v1:fixture' });
|
||||
assert.deepEqual(await rpc.createSubaddress(0, 'wikideck-deposit-v1:fixture'), { address: subaddress, addressIndex: 7 });
|
||||
assert.deepEqual(calls, [
|
||||
{ method: 'get_address', params: { account_index: 0 } },
|
||||
{ method: 'get_address', params: { account_index: 0, address_index: [0] } },
|
||||
{ method: 'validate_address', params: { address: ADDITIONAL_MARKET_FEE.address } },
|
||||
{ method: 'get_address', params: { account_index: 0, address_index: [7] } },
|
||||
{ method: 'create_address', params: { account_index: 0, label: 'wikideck-deposit-v1:fixture', count: 1 } },
|
||||
@@ -125,11 +128,42 @@ test('official wallet not-yet-created address-index error is recoverable but unr
|
||||
await assert.rejects(createMoneroDepositAddressRpc(config, unrelated).getSubaddress(0, 1), /method failed \(-15\)/);
|
||||
});
|
||||
|
||||
test('mTLS gateway allowlists only bounded observation and one-subaddress allocation methods', () => {
|
||||
const request = (method: string, params: Record<string, unknown> = {}) =>
|
||||
({ jsonrpc: '2.0', id: 'fixture-id', method, params });
|
||||
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('get_height')));
|
||||
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('get_address', { account_index: 0, address_index: [0] })));
|
||||
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('validate_address', { address: 'x'.repeat(95) })));
|
||||
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('create_address', { account_index: 0, label: 'fixture', count: 1 })));
|
||||
assert.doesNotThrow(() => validateMoneroRpcGatewayRequest(request('get_transfers', {
|
||||
in: true, filter_by_height: true, min_height: 0, max_height: 100,
|
||||
})));
|
||||
for (const method of ['transfer', 'transfer_split', 'sweep_all', 'relay_tx', 'get_tx_key', 'query_key', 'open_wallet', 'get_balance']) {
|
||||
assert.throws(() => validateMoneroRpcGatewayRequest(request(method)), /not allow-listed/);
|
||||
}
|
||||
assert.throws(() => validateMoneroRpcGatewayRequest(request('get_address', { account_index: 0, address_index: [0], all: true })));
|
||||
assert.throws(() => validateMoneroRpcGatewayRequest(request('create_address', { account_index: 0, label: 'x', count: 2 })));
|
||||
assert.throws(() => validateMoneroRpcGatewayRequest(request('get_transfers', {
|
||||
in: true, filter_by_height: true, min_height: 0, max_height: 100_001,
|
||||
})), /range/);
|
||||
});
|
||||
|
||||
test('production mTLS transport config is mandatory and requires a bare HTTPS RPC endpoint', async () => {
|
||||
await assert.rejects(loadMoneroMtlsTransportConfig({}), /config file is required/);
|
||||
await assert.rejects(loadMoneroRpcGatewayConfig(''), /protected Monero RPC gateway configuration/);
|
||||
await assert.rejects(createMoneroRpcGateway({ listenHost: '203.0.113.10', listenPort: 18085,
|
||||
walletRpcUrl: 'http://127.0.0.1:18083/json_rpc', server: { certificateFile: 'missing', privateKeyFile: 'missing',
|
||||
clientCaFile: 'missing' }, clientFingerprints: ['a'.repeat(64)] }), /private interface IP/);
|
||||
await assert.rejects(createMoneroRpcGateway({ listenHost: '127.0.0.1', listenPort: 18085,
|
||||
walletRpcUrl: 'http://wallet.example/json_rpc', server: { certificateFile: 'missing', privateKeyFile: 'missing',
|
||||
clientCaFile: 'missing' }, clientFingerprints: ['a'.repeat(64)] }), /loopback-only/);
|
||||
});
|
||||
|
||||
test('observer refuses non-loopback plaintext, URL credentials and malformed height ranges', async () => {
|
||||
assert.throws(() => createMoneroWalletObserver({ ...config, url: 'http://wallet.example/json_rpc' }), /HTTPS/);
|
||||
assert.throws(() => createMoneroWalletObserver({ ...config, url: 'http://localhost/json_rpc' }), /HTTPS/);
|
||||
assert.throws(() => createMoneroWalletObserver({ ...config, url: 'https://user:pass@wallet.example/json_rpc' }), /bare/);
|
||||
const { transport } = fakeTransport(); const observer = createMoneroWalletObserver(config, transport);
|
||||
assert.throws(() => createMoneroWalletObserver({ ...config, url: 'http://wallet.example/json_rpc' }, transport), /HTTPS/);
|
||||
assert.throws(() => createMoneroWalletObserver({ ...config, url: 'http://localhost/json_rpc' }, transport), /HTTPS/);
|
||||
assert.throws(() => createMoneroWalletObserver({ ...config, url: 'https://user:pass@wallet.example/json_rpc' }, transport), /bare/);
|
||||
await assert.rejects(observer.scanIncoming(-1), /height/);
|
||||
await assert.rejects(observer.scanIncoming(101), /ahead/);
|
||||
});
|
||||
@@ -157,6 +191,7 @@ test('observer rejects unsafe amounts, duplicate observations, malformed transfe
|
||||
});
|
||||
|
||||
test('observer refuses redirects, wrong content type and oversized RPC response without exposing credentials', async () => {
|
||||
const secretSentinel = 'fixture-secret-never-log';
|
||||
const badResponses = [new Response('{}', { status: 302, headers: { 'content-type': 'application/json' } }),
|
||||
new Response('<html/>', { headers: { 'content-type': 'text/html' } }),
|
||||
new Response('x'.repeat(1024 * 1024 + 1), { headers: { 'content-type': 'application/json' } })];
|
||||
@@ -168,7 +203,7 @@ test('observer refuses redirects, wrong content type and oversized RPC response
|
||||
return bad;
|
||||
};
|
||||
await assert.rejects(createMoneroWalletObserver(config, transport).scanIncoming(90), error =>
|
||||
error instanceof Error && !error.message.includes(config.password));
|
||||
error instanceof Error && !error.message.includes(secretSentinel));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user