Started Walet RPC auth work without adding MD5 Digest support
This commit is contained in:
Executable
+126
@@ -0,0 +1,126 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash, randomBytes, randomUUID, X509Certificate } from 'node:crypto';
|
||||
import { execFile as execFileCallback, spawn } from 'node:child_process';
|
||||
import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
import { createMoneroRpcGateway } from '../server/market/monero-rpc-gateway.ts';
|
||||
import { loadMoneroMtlsTransport } from '../server/market/monero-mtls-transport.ts';
|
||||
|
||||
const execFile = promisify(execFileCallback);
|
||||
const walletRpcBinary = process.argv[2];
|
||||
if (!walletRpcBinary || !/monero-wallet-rpc(?:\.exe)?$/i.test(walletRpcBinary)) {
|
||||
throw new Error('Pass the verified monero-wallet-rpc binary path');
|
||||
}
|
||||
const binary = resolve(walletRpcBinary), temp = await mkdtemp(join(tmpdir(), 'wikideck-mtls-stagenet-'));
|
||||
const walletPort = 18083, gatewayPort = 18085;
|
||||
const rpcBase = `http://127.0.0.1:${walletPort}/json_rpc`;
|
||||
const gatewayUrl = `https://127.0.0.1:${gatewayPort}/json_rpc`;
|
||||
const walletDir = join(temp, 'wallets'), walletName = 'mtls-watch-only';
|
||||
const walletPassword = randomBytes(32).toString('hex');
|
||||
let walletRpc, gateway, passed = false;
|
||||
const certs = Object.fromEntries(['ca.key','ca.crt','server.key','server.csr','server.crt','server.ext',
|
||||
'client.key','client.csr','client.crt','client.ext','rogue.key','rogue.csr','rogue.crt','rogue.ext']
|
||||
.map(name => [name, join(temp, name)]));
|
||||
|
||||
async function runOpenSsl(args) {
|
||||
await execFile('openssl', args, { cwd: temp, windowsHide: true, timeout: 15_000 });
|
||||
}
|
||||
async function makeCerts() {
|
||||
await runOpenSsl(['req','-x509','-newkey','rsa:2048','-nodes','-sha256','-days','1','-keyout',certs['ca.key'],
|
||||
'-out',certs['ca.crt'],'-subj','/CN=Wikideck Disposable Test CA']);
|
||||
await runOpenSsl(['req','-newkey','rsa:2048','-nodes','-sha256','-keyout',certs['server.key'],
|
||||
'-out',certs['server.csr'],'-subj','/CN=127.0.0.1']);
|
||||
await writeFile(certs['server.ext'], 'subjectAltName=IP:127.0.0.1\nextendedKeyUsage=serverAuth\nkeyUsage=digitalSignature,keyEncipherment\n');
|
||||
await runOpenSsl(['x509','-req','-sha256','-days','1','-in',certs['server.csr'],'-CA',certs['ca.crt'],
|
||||
'-CAkey',certs['ca.key'],'-CAcreateserial','-out',certs['server.crt'],'-extfile',certs['server.ext']]);
|
||||
for (const name of ['client','rogue']) {
|
||||
await runOpenSsl(['req','-newkey','rsa:2048','-nodes','-sha256','-keyout',certs[`${name}.key`],
|
||||
'-out',certs[`${name}.csr`],'-subj',`/CN=wikideck-${name}`]);
|
||||
await writeFile(certs[`${name}.ext`], 'extendedKeyUsage=clientAuth\nkeyUsage=digitalSignature,keyEncipherment\n');
|
||||
await runOpenSsl(['x509','-req','-sha256','-days','1','-in',certs[`${name}.csr`],'-CA',certs['ca.crt'],
|
||||
'-CAkey',certs['ca.key'],'-CAcreateserial','-out',certs[`${name}.crt`],'-extfile',certs[`${name}.ext`]]);
|
||||
}
|
||||
for (const file of ['ca.key','server.key','client.key','rogue.key']) await chmod(certs[file], 0o600);
|
||||
return new X509Certificate(await readFile(certs['client.crt'])).fingerprint256.replaceAll(':','').toLowerCase();
|
||||
}
|
||||
|
||||
async function rawRpc(method, params = {}) {
|
||||
const id = randomUUID();
|
||||
const response = await fetch(rpcBase, { method: 'POST', redirect: 'error', signal: AbortSignal.timeout(5000),
|
||||
headers: { 'content-type':'application/json', accept:'application/json' },
|
||||
body: JSON.stringify({ jsonrpc:'2.0', id, method, params }) });
|
||||
assert.equal(response.status, 200);
|
||||
const data = await response.json(); assert.equal(data.id,id);
|
||||
if (data.error) { const error = new Error('wallet RPC method returned an error'); error.rpcCode = data.error.code; throw error; }
|
||||
return data.result;
|
||||
}
|
||||
function startWallet(walletFile) {
|
||||
const args = ['--stagenet','--offline','--rpc-bind-ip=127.0.0.1',`--rpc-bind-port=${walletPort}`,
|
||||
'--disable-rpc-login',`--wallet-dir=${walletDir}`,'--non-interactive','--no-initial-sync',
|
||||
'--max-concurrency=1','--log-level=0',`--log-file=${join(temp,'wallet-rpc.log')}`];
|
||||
if (walletFile) { args.splice(args.indexOf(`--wallet-dir=${walletDir}`),1); args.push(`--wallet-file=${walletFile}`,`--password=${walletPassword}`); }
|
||||
walletRpc = spawn(binary,args,{shell:false,windowsHide:true,stdio:'ignore',cwd:temp}); walletRpc.on('error',()=>{});
|
||||
}
|
||||
async function waitWallet() {
|
||||
const deadline=Date.now()+15_000;
|
||||
while(Date.now()<deadline) {
|
||||
if(walletRpc.exitCode!==null) throw new Error(`Wallet RPC exited during startup (${walletRpc.exitCode})`);
|
||||
try { await rawRpc('get_version'); return; } catch { await new Promise(r=>setTimeout(r,100)); }
|
||||
}
|
||||
throw new Error('Wallet RPC readiness timeout');
|
||||
}
|
||||
async function stopWallet() {
|
||||
if(!walletRpc||walletRpc.exitCode!==null) return;
|
||||
await rawRpc('store').catch(()=>{}); await rawRpc('stop_wallet').catch(()=>{});
|
||||
await Promise.race([new Promise(r=>walletRpc.once('exit',r)),new Promise(r=>setTimeout(r,5000))]);
|
||||
if(walletRpc.exitCode===null) walletRpc.kill('SIGKILL');
|
||||
}
|
||||
async function gatewayCall(transport,method,params={}) {
|
||||
const id=randomUUID();
|
||||
const response=await transport(gatewayUrl,{method:'POST',redirect:'error',headers:{'content-type':'application/json',accept:'application/json'},
|
||||
body:JSON.stringify({jsonrpc:'2.0',id,method,params})});
|
||||
const data=await response.json();
|
||||
return {status:response.status,data};
|
||||
}
|
||||
|
||||
try {
|
||||
const fingerprint = await makeCerts();
|
||||
startWallet(); await waitWallet();
|
||||
await rawRpc('create_wallet',{filename:'source',password:walletPassword,language:'English'});
|
||||
const primary=(await rawRpc('get_address',{account_index:0})).address;
|
||||
const viewKey=(await rawRpc('query_key',{key_type:'view_key'})).key;
|
||||
const watch=await rawRpc('generate_from_keys',{filename:walletName,address:primary,viewkey:viewKey,
|
||||
password:walletPassword,restore_height:0,autosave_current:true});
|
||||
assert.equal(watch.address,primary);
|
||||
await assert.rejects(rawRpc('query_key',{key_type:'spend_key'}));
|
||||
await stopWallet(); startWallet(join(walletDir,walletName)); await waitWallet();
|
||||
|
||||
gateway=await createMoneroRpcGateway({listenHost:'127.0.0.1',listenPort:gatewayPort,walletRpcUrl:rpcBase,
|
||||
server:{certificateFile:certs['server.crt'],privateKeyFile:certs['server.key'],clientCaFile:certs['ca.crt']},
|
||||
clientFingerprints:[fingerprint]});
|
||||
const validTransport=await loadMoneroMtlsTransport({url:gatewayUrl,clientCertificateFile:certs['client.crt'],
|
||||
clientPrivateKeyFile:certs['client.key'],serverCaFile:certs['ca.crt']});
|
||||
const info=await gatewayCall(validTransport,'get_address',{account_index:0,address_index:[0]});
|
||||
assert.equal(info.status,200); assert.equal(info.data.result.address,primary);
|
||||
const sub=await gatewayCall(validTransport,'create_address',{account_index:0,label:`wikideck-deposit-v1:${randomUUID()}`,count:1});
|
||||
assert.equal(sub.status,200); assert.equal(sub.data.result.address_index,1);
|
||||
const forbiddenResponse=await validTransport(gatewayUrl,{method:'POST',redirect:'error',headers:{'content-type':'application/json'},
|
||||
body:JSON.stringify({jsonrpc:'2.0',id:randomUUID(),method:'transfer',params:{destinations:[]}})});
|
||||
assert.equal(forbiddenResponse.status,400,'gateway must reject transfer before Wallet RPC');
|
||||
|
||||
const rogueTransport=await loadMoneroMtlsTransport({url:gatewayUrl,clientCertificateFile:certs['rogue.crt'],
|
||||
clientPrivateKeyFile:certs['rogue.key'],serverCaFile:certs['ca.crt']});
|
||||
await assert.rejects(gatewayCall(rogueTransport,'get_address',{account_index:0,address_index:[0]}));
|
||||
await assert.rejects(fetch(gatewayUrl,{method:'POST',headers:{'content-type':'application/json'},body:'{}',
|
||||
signal:AbortSignal.timeout(3000)}),'gateway must reject clients without a certificate');
|
||||
passed=true;
|
||||
console.log(JSON.stringify({result:'passed',moneroWalletRpc:'0.18.5.4',network:'Stagenet',containerNetwork:'none',
|
||||
rpcAuthentication:'mTLS',tlsMinimum:'1.3',approvedClientAccepted:true,untrustedClientRejected:true,
|
||||
noCertificateRejected:true,forbiddenTransferRejected:true,allowedAddressCreation:true,watchOnly:true,
|
||||
realFundsUsed:false,secretsOrAddressesLogged:false}));
|
||||
} finally {
|
||||
if(gateway) await new Promise(resolveClose=>gateway.close(()=>resolveClose()));
|
||||
await stopWallet(); await rm(temp,{recursive:true,force:true}); if(!passed) process.exitCode=1;
|
||||
}
|
||||
Reference in New Issue
Block a user