Harden treasury API authorization signatures

This commit is contained in:
defiQUG
2026-09-13 20:50:10 -07:00
parent f252882f13
commit 113b0ee333
2 changed files with 17 additions and 3 deletions
+13 -1
View File
@@ -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;
+4 -2
View File
@@ -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 };
}