diff --git a/docker-compose.yaml b/docker-compose.yaml index 6af7e17c..d4435416 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -540,6 +540,8 @@ services: report-receiver: build: scripts/ command: ["serve-report-receiver"] + volumes: + - "config:/config" ports: - "127.0.0.1:9081:8080" diff --git a/scripts/config.ts b/scripts/config.ts index 38d30596..e2c2a036 100644 --- a/scripts/config.ts +++ b/scripts/config.ts @@ -922,7 +922,7 @@ function computeAddressHash(address: string, salt: string): string { const normalizedAddress = address.toLowerCase().replace('0x', ''); const saltBytes = Buffer.from(salt.replace(/-/g, ''), 'hex'); const addrBytes = Buffer.from(normalizedAddress, 'hex'); - return crypto.createHash('sha256').update(Buffer.concat([saltBytes, addrBytes])).digest('hex'); + return crypto.createHash('sha256').update(Buffer.concat([saltBytes, addrBytes] as Uint8Array[]) as Uint8Array).digest('hex'); } async function uploadFilteredAddressesToMinio() { @@ -1112,6 +1112,9 @@ function writeFilteringReportConfig() { "external-endpoint": { "url": "http://report-receiver:8080", "timeout": "10s" + }, + "signer": { + "pem-file": consts.filteringReportSignerPemPath } } }; @@ -1138,17 +1141,82 @@ function applyFilteringReportConfig(config: any) { }; } +// Self-signed Ed25519 cert (testnode skips the production CA chain): combined PEM for the forwarder, cert-only for the receiver to pin. +async function initFilteringReportSigner() { + require("reflect-metadata"); // @peculiar/x509 uses decorator metadata internally; must load first + const x509 = require("@peculiar/x509"); + const webcrypto = require("crypto").webcrypto; + x509.cryptoProvider.set(webcrypto); + + const keys = await webcrypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]); + const cert = await x509.X509CertificateGenerator.createSelfSigned({ + serialNumber: "01", + name: "CN=filtering-report", + notBefore: new Date(Date.now() - 24 * 60 * 60 * 1000), // backdated to absorb clock skew + notAfter: new Date(Date.now() + 100 * 365 * 24 * 60 * 60 * 1000), // far future so it never expires + keys, + }); + + const pkcs8 = await webcrypto.subtle.exportKey("pkcs8", keys.privateKey); + const keyPem = x509.PemConverter.encode(pkcs8, "PRIVATE KEY"); + fs.writeFileSync(consts.filteringReportSignerPemPath, keyPem + "\n" + cert.toString("pem") + "\n"); + fs.writeFileSync(consts.filteringReportSignerPubPath, cert.toString("pem") + "\n"); + console.log("Generated filtering-report signing cert in", consts.configpath); +} + +export const initFilteringReportSignerCommand = { + command: "init-filtering-report-signer", + describe: "generates the Ed25519 signing cert for filtering-report report forwarding", + handler: async () => { + await initFilteringReportSigner(); + } +} + +const REPORT_SIGNATURE_SKEW_MS = 5 * 60 * 1000; + +function verifyReportSignature(req: any, rawBody: Buffer, signerKey: crypto.KeyObject) { + const sigHeader = req.headers['x-signature']; + const tsHeader = req.headers['x-signature-timestamp']; + if (typeof sigHeader !== 'string' || typeof tsHeader !== 'string') { + throw new Error('missing signature headers'); + } + + const tsSeconds = Number(tsHeader); + if (!Number.isFinite(tsSeconds) || Math.abs(Date.now() - tsSeconds * 1000) > REPORT_SIGNATURE_SKEW_MS) { + throw new Error('timestamp outside tolerance'); + } + + const payload = Buffer.concat([Buffer.from(`${tsHeader}.`), rawBody] as Uint8Array[]); + if (!crypto.verify(null, payload as Uint8Array, signerKey, Buffer.from(sigHeader, 'base64') as Uint8Array)) { + throw new Error('signature verification failed'); + } +} + export const serveReportReceiverCommand = { command: "serve-report-receiver", - describe: "starts an HTTP server that receives and logs filtering reports", + describe: "starts an HTTP server that verifies signatures on and logs filtering reports", handler: async () => { const http = require('http'); + if (!fs.existsSync(consts.filteringReportSignerPubPath)) { + throw new Error(`signing cert not found at ${consts.filteringReportSignerPubPath}; run init-filtering-report-signer first`); + } + const signerKey = new crypto.X509Certificate(fs.readFileSync(consts.filteringReportSignerPubPath) as Uint8Array).publicKey; const reports: any[] = []; const server = http.createServer((req: any, res: any) => { if (req.method === 'POST') { - let body = ''; - req.on('data', (chunk: string) => body += chunk); + const chunks: Buffer[] = []; + req.on('data', (chunk: Buffer) => chunks.push(chunk)); req.on('end', () => { + const rawBody = Buffer.concat(chunks as Uint8Array[]); + try { + verifyReportSignature(req, rawBody, signerKey); + } catch (err: any) { + console.error('Rejected report with invalid signature:', err.message); + res.writeHead(401, {'Content-Type': 'application/json'}); + res.end(JSON.stringify({status: 'error', message: 'signature verification failed'})); + return; + } + const body = rawBody.toString(); console.log('Received report:', body); try { reports.push(JSON.parse(body)); diff --git a/scripts/consts.ts b/scripts/consts.ts index edfcedd6..bfadaaef 100644 --- a/scripts/consts.ts +++ b/scripts/consts.ts @@ -6,4 +6,7 @@ export const tokenbridgedatapath = "/tokenbridge-data"; export const l1mnemonic = "indoor dish desk flag debris potato excuse depart ticket judge file exit"; -export const ARB_OWNER = "0x0000000000000000000000000000000000000070"; \ No newline at end of file +export const ARB_OWNER = "0x0000000000000000000000000000000000000070"; + +export const filteringReportSignerPemPath = configpath + "/filtering_report_signer.pem"; +export const filteringReportSignerPubPath = configpath + "/filtering_report_signer_pub.pem"; \ No newline at end of file diff --git a/scripts/index.ts b/scripts/index.ts index ed25c576..fd42f7e1 100644 --- a/scripts/index.ts +++ b/scripts/index.ts @@ -20,6 +20,7 @@ import { removeFilteredAddressCommand, writeElasticMQConfigCommand, writeFilteringReportConfigCommand, + initFilteringReportSignerCommand, serveReportReceiverCommand, } from "./config"; import { @@ -93,6 +94,7 @@ async function main() { .command(removeFilteredAddressCommand) .command(writeElasticMQConfigCommand) .command(writeFilteringReportConfigCommand) + .command(initFilteringReportSignerCommand) .command(serveReportReceiverCommand) .command(grantFiltererRoleCommand) .command(printAddressCommand) diff --git a/scripts/package.json b/scripts/package.json index 63475ccd..89277d0c 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -11,10 +11,12 @@ "@aws-sdk/client-s3": "3.1050.0", "@node-redis/client": "^1.0.4", "@openzeppelin/contracts": "^4.9.3", + "@peculiar/x509": "2.0.0", "@types/node": "^17.0.22", "@types/yargs": "^17.0.10", "ethers": "^5.6.1", "path": "^0.12.7", + "reflect-metadata": "0.2.2", "typescript": "^4.6.2", "yargs": "^17.4.0" }, diff --git a/scripts/yarn.lock b/scripts/yarn.lock index 491eae3b..aecf9982 100644 --- a/scripts/yarn.lock +++ b/scripts/yarn.lock @@ -1143,6 +1143,135 @@ proper-lockfile "^4.1.1" solidity-ast "^0.4.51" +"@peculiar/asn1-cms@^2.6.0", "@peculiar/asn1-cms@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-cms/-/asn1-cms-2.7.0.tgz#8e0eb656f4fc85f7c621dd442fa2d298faa84984" + integrity sha512-hew63shtzzvBcSHbhm+cyAmKe6AIfinT9hzEqSPjDC6opTTMKmTkQ0gHuN2KsWlvqiKw1S/fS94fhag/FJkioQ== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/asn1-x509" "^2.7.0" + "@peculiar/asn1-x509-attr" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-csr@^2.6.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-csr/-/asn1-csr-2.7.0.tgz#1a03ac03f7571ea981f5d8377c6f4510c5d43411" + integrity sha512-VVsAyGqErT9D1SY4aEqozThXMVI+ssVRiv2DDeYuvpBKLIgZ3hYs3Ay3u/VSoKq6ESFi9cf6rf3IOOzfwh7oMA== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/asn1-x509" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-ecc@^2.6.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-ecc/-/asn1-ecc-2.7.0.tgz#c35b57859812ecd0c2ae7b2144855e8208c2cfee" + integrity sha512-n7KEs/Q/wrB415cxy4fHOBhegp4NdJ15fkJPwcB/3/8iNBQC2L/N7SChJPKDJPZGYH0jD4Tg4/0vnHmwghnbKw== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/asn1-x509" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-pfx@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-pfx/-/asn1-pfx-2.7.0.tgz#d00766b13ff49785684a604248e6aadd184b291f" + integrity sha512-V/nrlQVmhg7lYAsM7E13UDL5erAwFv6kCIVFqNaMIHSVi7dngcT839JkRTkQBqznMG98l2XjxYk74ZztAohZzA== + dependencies: + "@peculiar/asn1-cms" "^2.7.0" + "@peculiar/asn1-pkcs8" "^2.7.0" + "@peculiar/asn1-rsa" "^2.7.0" + "@peculiar/asn1-schema" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-pkcs8@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.7.0.tgz#5ee602d8a9a3e0a3f09f7b008ff644a657378692" + integrity sha512-9GTl1nE8Mx1kTZ+7QyYatDyKsm34QcWRBFkY1iPvWC3X4Dona5s/tlLiQsx5WzVdZqiMBZNYT0buyw4/vbhnjw== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/asn1-x509" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-pkcs9@^2.6.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.7.0.tgz#23b4eae41c2feb8df258aa69c502a70092026b0a" + integrity sha512-Bh7m+OuIaSEllPQcSd9OSp93F4ROWH7sbITWV8MI+8dwsjE5111/87VxiWVvYFKyww3vp39geLv9ENqhwWHcew== + dependencies: + "@peculiar/asn1-cms" "^2.7.0" + "@peculiar/asn1-pfx" "^2.7.0" + "@peculiar/asn1-pkcs8" "^2.7.0" + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/asn1-x509" "^2.7.0" + "@peculiar/asn1-x509-attr" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-rsa@^2.6.0", "@peculiar/asn1-rsa@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-rsa/-/asn1-rsa-2.7.0.tgz#6dc5c78c643264dd5a251a66f1dd9a38fcbba385" + integrity sha512-/qvENQrXyTZURjMqSeofHul0JJt2sNSzSwk36pl2olkHbaioMQgrASDZAlHXl0xUlnVbHj0uGgOrBMTb5x2aJQ== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/asn1-x509" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-schema@^2.6.0", "@peculiar/asn1-schema@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz#f2dcb25995ce7cac8687ba1039f043e5eff43820" + integrity sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg== + dependencies: + "@peculiar/utils" "^2.0.2" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-x509-attr@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.7.0.tgz#5ef2a10d3a78d4763b848a2cb56db4bb6b1e082a" + integrity sha512-NS8e7SOgXipkzUPLF/sce7ukpMpWjhxYsH0n6Y+bHYo4TTxOb95Zv7hqwSuL212mj5YxovjdOKQOgH1As3E94w== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/asn1-x509" "^2.7.0" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/asn1-x509@^2.6.0", "@peculiar/asn1-x509@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509/-/asn1-x509-2.7.0.tgz#84793efb7819dbc9526fd6b0a4ccd86f90464b96" + integrity sha512-mUn9RRrkGDnG4ALfunDmzyRW5dg+sWCj/pfnCCqEHYbkGxEpvUt6iVJv8Yw1cyp6SWZ26ZE5oSmI5SqEaen15g== + dependencies: + "@peculiar/asn1-schema" "^2.7.0" + "@peculiar/utils" "^2.0.2" + asn1js "^3.0.6" + tslib "^2.8.1" + +"@peculiar/utils@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@peculiar/utils/-/utils-2.0.3.tgz#a27ca4c4b73652e110f19a7d16d664f458a5528e" + integrity sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ== + dependencies: + tslib "^2.8.1" + +"@peculiar/x509@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@peculiar/x509/-/x509-2.0.0.tgz#237e782b114160c71c87ecbd15d907d08d818416" + integrity sha512-r10lkuy6BNfRmyYdRAfgu6dq0HOmyIV2OLhXWE3gDEPBdX1b8miztJVyX/UxWhLwemNyDP3CLZHpDxDwSY0xaA== + dependencies: + "@peculiar/asn1-cms" "^2.6.0" + "@peculiar/asn1-csr" "^2.6.0" + "@peculiar/asn1-ecc" "^2.6.0" + "@peculiar/asn1-pkcs9" "^2.6.0" + "@peculiar/asn1-rsa" "^2.6.0" + "@peculiar/asn1-schema" "^2.6.0" + "@peculiar/asn1-x509" "^2.6.0" + pvtsutils "^1.3.6" + tslib "^2.8.1" + tsyringe "^4.10.0" + "@smithy/core@^3.24.2", "@smithy/core@^3.24.6": version "3.24.6" resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.24.6.tgz#72891bad85d577b2e43f30a8fc67adf36d577798" @@ -1343,6 +1472,15 @@ arraybuffer.prototype.slice@^1.0.2: is-array-buffer "^3.0.2" is-shared-array-buffer "^1.0.2" +asn1js@^3.0.6: + version "3.0.10" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.10.tgz#df26c874c8a8b41ca605efea47b2ad07551013dd" + integrity sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg== + dependencies: + pvtsutils "^1.3.6" + pvutils "^1.1.5" + tslib "^2.8.1" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -2885,6 +3023,18 @@ puppeteer@^13.7.0: unbzip2-stream "1.4.3" ws "8.5.0" +pvtsutils@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.6.tgz#ec46e34db7422b9e4fdc5490578c1883657d6001" + integrity sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg== + dependencies: + tslib "^2.8.1" + +pvutils@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.5.tgz#84b0dea4a5d670249aa9800511804ee0b7c2809c" + integrity sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -2913,6 +3063,11 @@ redis-parser@3.0.0: dependencies: redis-errors "^1.0.0" +reflect-metadata@0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" + integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== + regexp.prototype.flags@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" @@ -3200,11 +3355,23 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -tslib@^2.6.2: +tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.6.2, tslib@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== +tsyringe@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/tsyringe/-/tsyringe-4.10.0.tgz#d0c95815d584464214060285eaaadd94aa03299c" + integrity sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw== + dependencies: + tslib "^1.9.3" + typed-array-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" diff --git a/test-node.bash b/test-node.bash index bb73a9a2..aa799ff9 100755 --- a/test-node.bash +++ b/test-node.bash @@ -685,6 +685,9 @@ if $force_init; then fi if $l2filteringreport; then + echo == Generating filtering-report signing PKI + run_script init-filtering-report-signer + echo == Starting report receiver docker compose up --wait report-receiver