All checks were successful
check / check (push) Successful in 24s
The password no longer crosses the extension messaging boundary: the popup decrypts and signs, and sends only the raw signed transaction or the signature. The background re-derives the signer from the artifact and checks it against the approval it holds before broadcasting, so it is not a blind relay.
125 lines
4.5 KiB
JavaScript
125 lines
4.5 KiB
JavaScript
// 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 };
|