Harden treasury API authorization signatures
This commit is contained in:
+13
-1
@@ -19,10 +19,12 @@ const clientRpcUrl =
|
||||
const chainId = process.env.CHAIN_ID || "138";
|
||||
const contractAddress = process.env.CONTRACT_ADDRESS || "";
|
||||
const allowedOrigin = process.env.CORS_ORIGIN || "https://treasury.d-bis.org";
|
||||
const authDomain = process.env.AUTH_DOMAIN || new URL(allowedOrigin).host;
|
||||
const chain138 = defineChain({ id: 138, name: "Solace Chain 138", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: [rpcUrl] } } });
|
||||
const publicClient = createPublicClient({ chain: chain138, transport: viemHttp(rpcUrl) });
|
||||
const ownerAbi = [{ type: "function", name: "isOwner", stateMutability: "view", inputs: [{ name: "owner", type: "address" }], outputs: [{ type: "bool" }] }] as const;
|
||||
const requestWindows = new Map<string, { started: number; count: number }>();
|
||||
const usedAuthNonces = new Map<string, number>();
|
||||
const API_RATE_LIMIT = 120;
|
||||
|
||||
for (const envVar of ["DATABASE_URL", "RPC_URL", "CHAIN_ID"] as const) {
|
||||
@@ -149,9 +151,17 @@ const server = http.createServer(async (req, res) => {
|
||||
const rows = await treasuryRouter.getById(treasuryId);
|
||||
const treasury = rows[0];
|
||||
if (!treasury || typeof wallet !== "string" || typeof signature !== "string" || typeof message !== "string" || !isAddress(wallet) || !contractAddress || treasury.mainWallet !== contractAddress.toLowerCase()) { console.warn(JSON.stringify({ event: "treasury_authorization_denied", treasuryId, ip: req.socket.remoteAddress })); return null; }
|
||||
const timestamp = Number(message.match(/\nTimestamp: (\d+)$/)?.[1]);
|
||||
const expectedPath = `${url.pathname}${url.search}`;
|
||||
const authMatch = message.match(/^Solace Treasury API authorization\nDomain: ([^\n]+)\nChain ID: (\d+)\nWallet: (0x[a-fA-F0-9]{40})\nMethod: ([A-Z]+)\nPath: ([^\n]+)\nTimestamp: (\d+)\nNonce: ([a-fA-F0-9-]+)$/);
|
||||
if (!authMatch || authMatch[1] !== authDomain || Number(authMatch[2]) !== Number(chainId) || authMatch[3].toLowerCase() !== wallet.toLowerCase() || authMatch[4] !== req.method || authMatch[5] !== expectedPath) return null;
|
||||
const timestamp = Number(authMatch[6]);
|
||||
const nonce = authMatch[7];
|
||||
if (!Number.isFinite(timestamp) || Math.abs(Date.now() - timestamp) > 300000) return null;
|
||||
const nonceKey = `${wallet.toLowerCase()}:${nonce}`;
|
||||
for (const [key, expires] of usedAuthNonces) if (expires < Date.now()) usedAuthNonces.delete(key);
|
||||
if (usedAuthNonces.has(nonceKey)) return null;
|
||||
if (!await verifyMessage({ address: wallet, message, signature: signature as `0x${string}` })) return null;
|
||||
usedAuthNonces.set(nonceKey, timestamp + 300000);
|
||||
const owner = await publicClient.readContract({ address: contractAddress as `0x${string}`, abi: ownerAbi, functionName: "isOwner", args: [wallet] });
|
||||
return owner ? treasury : null;
|
||||
}
|
||||
@@ -167,6 +177,8 @@ const server = http.createServer(async (req, res) => {
|
||||
sendJson(res, 403, { error: "Wallet authorization required" });
|
||||
return;
|
||||
}
|
||||
const treasury = await treasuryRouter.getByWallet(wallet);
|
||||
if (!treasury || !(await authorizedTreasury(treasury.id))) { sendJson(res, 403, { error: "Wallet authorization required" }); return; }
|
||||
const ledger = await fetchTreasuryLedger(wallet);
|
||||
sendJson(res, 200, { ledger });
|
||||
return;
|
||||
|
||||
@@ -68,11 +68,13 @@ export interface LedgerEntry {
|
||||
blockNumber: number;
|
||||
}
|
||||
|
||||
async function signedHeaders(path: string, wallet?: string): Promise<Record<string, string> | undefined> {
|
||||
async function signedHeaders(path: string, wallet?: string, method = "GET"): Promise<Record<string, string> | undefined> {
|
||||
if (!wallet || typeof window === "undefined") return undefined;
|
||||
const ethereum = (window as Window & { ethereum?: { request: (args: { method: string; params: string[] }) => Promise<string> } }).ethereum;
|
||||
if (!ethereum) return undefined;
|
||||
const message = `Solace Treasury API authorization\nWallet: ${wallet.toLowerCase()}\nPath: ${path}\nTimestamp: ${Date.now()}`;
|
||||
const domain = window.location.host;
|
||||
const nonce = crypto.randomUUID();
|
||||
const message = `Solace Treasury API authorization\nDomain: ${domain}\nChain ID: 138\nWallet: ${wallet.toLowerCase()}\nMethod: ${method}\nPath: ${path}\nTimestamp: ${Date.now()}\nNonce: ${nonce}`;
|
||||
const signature = await ethereum.request({ method: "personal_sign", params: [message, wallet] });
|
||||
return { "X-Wallet-Address": wallet, "X-Wallet-Message": message, "X-Wallet-Signature": signature };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user