Stagenet rehearsal passed
This commit is contained in:
Executable
+28
@@ -0,0 +1,28 @@
|
||||
FROM node:24-trixie-slim
|
||||
WORKDIR /app
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get upgrade -y -qq \
|
||||
&& apt-get install -y -qq --no-install-recommends ca-certificates curl gnupg openssl bzip2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN set -eu; \
|
||||
mkdir -m 700 -p /tmp/monero-verify /opt/monero; \
|
||||
cd /tmp/monero-verify; \
|
||||
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
|
||||
'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.5.4.tar.bz2' -o monero-linux-x64-v0.18.5.4.tar.bz2; \
|
||||
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
|
||||
'https://www.getmonero.org/downloads/hashes.txt' -o hashes.txt; \
|
||||
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
|
||||
'https://raw.githubusercontent.com/monero-project/monero/master/utils/gpg_keys/binaryfate.asc' -o binaryfate.asc; \
|
||||
actual_fingerprint="$(gpg --batch --show-keys --with-colons binaryfate.asc | awk -F: '$1 == "fpr" {print $10; exit}')"; \
|
||||
test "$actual_fingerprint" = '81AC591FE9C4B65C5806AFC3F0AF4D462A0BDF92'; \
|
||||
export GNUPGHOME=/tmp/monero-verify/gnupg; mkdir -m 700 -p "$GNUPGHOME"; \
|
||||
gpg --batch --import binaryfate.asc >/dev/null 2>&1; \
|
||||
gpg --batch --output hashes.verified.txt --decrypt hashes.txt >/dev/null 2>&1; \
|
||||
grep -Fx 'ba2928962c101b609434bf8c1b43ad59ebe96c5525977284ca599d8bf0f02d10 monero-linux-x64-v0.18.5.4.tar.bz2' hashes.verified.txt >/dev/null; \
|
||||
printf '%s %s\n' 'ba2928962c101b609434bf8c1b43ad59ebe96c5525977284ca599d8bf0f02d10' 'monero-linux-x64-v0.18.5.4.tar.bz2' | sha256sum --check --status; \
|
||||
tar -xjf monero-linux-x64-v0.18.5.4.tar.bz2 -C /opt/monero --strip-components=1; \
|
||||
rm -rf /tmp/monero-verify
|
||||
CMD ["node", "--experimental-strip-types", "scripts/monero-mtls-stagenet-rehearsal.mjs", "/opt/monero/monero-wallet-rpc"]
|
||||
@@ -5,6 +5,7 @@ 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 { request as httpsRequest } from 'node:https';
|
||||
import { createMoneroRpcGateway } from '../server/market/monero-rpc-gateway.ts';
|
||||
import { loadMoneroMtlsTransport } from '../server/market/monero-mtls-transport.ts';
|
||||
|
||||
@@ -42,18 +43,20 @@ async function makeCerts() {
|
||||
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);
|
||||
for (const file of Object.keys(certs)) await chmod(certs[file], 0o600);
|
||||
return new X509Certificate(await readFile(certs['client.crt'])).fingerprint256.replaceAll(':','').toLowerCase();
|
||||
}
|
||||
|
||||
async function rawRpc(method, params = {}) {
|
||||
console.log(`wallet-rpc: ${method}`);
|
||||
const id = randomUUID();
|
||||
const response = await fetch(rpcBase, { method: 'POST', redirect: 'error', signal: AbortSignal.timeout(5000),
|
||||
const response = await fetch(rpcBase, { method: 'POST', redirect: 'error', signal: AbortSignal.timeout(30_000),
|
||||
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; }
|
||||
console.log(`wallet-rpc: ${method} complete`);
|
||||
return data.result;
|
||||
}
|
||||
function startWallet(walletFile) {
|
||||
@@ -84,10 +87,24 @@ async function gatewayCall(transport,method,params={}) {
|
||||
const data=await response.json();
|
||||
return {status:response.status,data};
|
||||
}
|
||||
async function rawGatewayStatus(method, params, cert, key, ca) {
|
||||
const body=Buffer.from(JSON.stringify({jsonrpc:'2.0',id:randomUUID(),method,params}));
|
||||
const [certPem,keyPem,caPem]=await Promise.all([readFile(cert),readFile(key),readFile(ca)]);
|
||||
return new Promise((resolveStatus,reject)=>{
|
||||
const request=httpsRequest({hostname:'127.0.0.1',port:gatewayPort,path:'/json_rpc',method:'POST',cert:certPem,key:keyPem,ca:caPem,
|
||||
rejectUnauthorized:true,minVersion:'TLSv1.3',headers:{'content-type':'application/json','content-length':body.length}},response=>{
|
||||
response.resume(); response.once('end',()=>resolveStatus(response.statusCode));
|
||||
});
|
||||
request.once('error',reject); request.end(body);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('stage: generate ephemeral TLS certificates');
|
||||
const fingerprint = await makeCerts();
|
||||
console.log('stage: start offline Stagenet Wallet RPC');
|
||||
startWallet(); await waitWallet();
|
||||
console.log('stage: create watch-only disposable wallet');
|
||||
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;
|
||||
@@ -96,25 +113,31 @@ try {
|
||||
assert.equal(watch.address,primary);
|
||||
await assert.rejects(rawRpc('query_key',{key_type:'spend_key'}));
|
||||
await stopWallet(); startWallet(join(walletDir,walletName)); await waitWallet();
|
||||
console.log('stage: wallet is watch-only after restart');
|
||||
|
||||
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]});
|
||||
console.log('stage: mTLS gateway listening');
|
||||
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]});
|
||||
console.log('stage: authorized mTLS address read');
|
||||
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});
|
||||
console.log('stage: authorized mTLS subaddress creation');
|
||||
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 forbiddenStatus=await rawGatewayStatus('transfer',{destinations:[]},certs['client.crt'],certs['client.key'],certs['ca.crt']);
|
||||
assert.equal(forbiddenStatus,400,'gateway must reject transfer before Wallet RPC');
|
||||
console.log('stage: forbidden transfer blocked');
|
||||
|
||||
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]}));
|
||||
console.log('stage: untrusted client certificate rejected');
|
||||
await assert.rejects(fetch(gatewayUrl,{method:'POST',headers:{'content-type':'application/json'},body:'{}',
|
||||
signal:AbortSignal.timeout(3000)}),'gateway must reject clients without a certificate');
|
||||
console.log('stage: missing client certificate rejected');
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user