// Verification of the signed artifacts produced by the approval popup. // // Signing happens in the popup, where the password is entered; the background // only broadcasts the raw transaction and resolves the pending approval back // to the requesting page. So that moving the signing out of the background // does not turn the background into a blind relay, the background re-derives // the signer from the artifact and checks it against the approval it is // holding before acting on it. All recovery is delegated to ethers. // // Every failure message is a full sentence, because these strings are shown to // the user and returned to the dApp. const { Transaction, getAddress, getBytes, verifyMessage, verifyTypedData, } = require("ethers"); // Case-insensitive address comparison that tolerates absent values on either // side. Two absent addresses compare equal (contract creation has no `to`). function sameAddress(a, b) { const aMissing = a === null || a === undefined || a === ""; const bMissing = b === null || b === undefined || b === ""; if (aMissing || bMissing) return aMissing && bMissing; try { return getAddress(a) === getAddress(b); } catch { return String(a).toLowerCase() === String(b).toLowerCase(); } } // Normalize a transaction value (hex string, decimal string, number or // bigint) to a bigint. An absent value is zero, matching ethers. function normalizeValue(v) { if (v === null || v === undefined || v === "") return 0n; return BigInt(v); } // Normalize call data to a lowercase hex string. Absent data is "0x". function normalizeData(v) { if (v === null || v === undefined || v === "" || v === "0x") return "0x"; return String(v).toLowerCase(); } // Assert that a raw signed transaction is the transaction the user approved, // signed by the address the approval was raised for. Returns the parsed // ethers Transaction on success, throws otherwise. function verifySignedTx(rawSignedTx, txParams, expectedFrom) { if (typeof rawSignedTx !== "string" || !rawSignedTx.startsWith("0x")) { throw new Error("The signed transaction is missing or malformed."); } let parsed; try { parsed = Transaction.from(rawSignedTx); } catch { throw new Error("The signed transaction could not be decoded."); } if (!parsed.from) { throw new Error("The signed transaction carries no valid signature."); } if (!sameAddress(parsed.from, expectedFrom)) { throw new Error( "The signed transaction was signed by a different address than the one that was approved.", ); } if (!sameAddress(parsed.to, txParams.to)) { throw new Error( "The signed transaction does not go to the approved recipient.", ); } if (normalizeValue(parsed.value) !== normalizeValue(txParams.value)) { throw new Error( "The signed transaction does not carry the approved value.", ); } if (normalizeData(parsed.data) !== normalizeData(txParams.data)) { throw new Error( "The signed transaction does not carry the approved call data.", ); } return parsed; } // Assert that a signature over the approved message or typed data was // produced by the address the approval was raised for. Returns the recovered // address on success, throws otherwise. function verifySignature(signParams, signature, expectedFrom) { if (typeof signature !== "string" || !signature.startsWith("0x")) { throw new Error("The signature is missing or malformed."); } let recovered; try { if ( signParams.method === "personal_sign" || signParams.method === "eth_sign" ) { recovered = verifyMessage(getBytes(signParams.message), signature); } else { const typedData = JSON.parse(signParams.typedData); const { domain, types, message } = typedData; // ethers derives EIP712Domain itself and rejects it as an input. delete types.EIP712Domain; recovered = verifyTypedData(domain, types, message, signature); } } catch { throw new Error("The signature could not be verified."); } if (!sameAddress(recovered, expectedFrom)) { throw new Error( "The signature was produced by a different address than the one that was approved.", ); } return recovered; } module.exports = { verifySignedTx, verifySignature, sameAddress };