Ethereum: verifying signatures despite malicious transaction data modifications

75 views Asked by At

Let's say we have this code to sign a simple transaction in Ethereum and then verify the signature, using the Elliptic Curve Digital Signature Algorithm (ECDSA), just like in real life:

const { secp256k1 } = require("ethereum-cryptography/secp256k1");
const { keccak256 } = require("ethereum-cryptography/keccak");
const { toHex, utf8ToBytes } = require("ethereum-cryptography/utils");

const privateKey = secp256k1.utils.randomPrivateKey();
console.log('private key :  ', toHex(privateKey));

const publicKey = secp256k1.getPublicKey(privateKey);
console.log('public key  :', toHex(publicKey));

const transaction = { message: 'hello world!' };
const data = JSON.stringify(transaction);
const hash = keccak256(utf8ToBytes(data));
const signature = secp256k1.sign(hash, privateKey);
console.log("payload     :", { transaction, signature });

Then I send the transaction and signature through the network and use this code to recover the public key and verify the signature:

const pkHonest = signature.recoverPublicKey(hash).toRawBytes();
console.log('recovered   :', toHex(publicKey) === toHex(pkHonest));           // true
console.log('verified    :', secp256k1.verify(signature, hash, pkHonest));    // true

So far so good.

But what if I (or someone in the middle) change the content of the transaction? You would expect the signature verification to fail, but it retrieves a different public key and the verification is successful.

const dataEvil = "modified";
const hashEvil = keccak256(utf8ToBytes(dataEvil));
const pkEvil = signature.recoverPublicKey(hashEvil).toRawBytes();
console.log('recovered   :', toHex(publicKey) === toHex(pkEvil));             // false
console.log('verified    :', secp256k1.verify(signature, hashEvil, pkEvil));  // true... why?

It doesn't seem right to me, but I can't figure out what I'm doing wrong.

1

There are 1 answers

0
lcnicolau On

As mentioned in the comments, you can send the public key (or address) as part of the transaction body (normally the address is sent instead of the public key, I have simplified this example for convenience).

const transaction = {
  from: toHex(publicKey),
  to: '...',
  value: '...',
  nonce: 0
};

Then you can check the recovered public key as part of the transaction verification:

const getTransactionCount = from => 0; // TODO: Implement properly
const getBalance = from => 0;          // TODO: Implement properly

const { from, to, value, nonce } = transaction;
const recovered = signature.recoverPublicKey(hash).toRawBytes();

if (toHex(recovered) !== from) {
  console.error("Sender does not match");
} else if (secp256k1.verify(signature, hash, recovered) !== true) {
  console.error("Invalid signature");
} else if (nonce !== getTransactionCount(from)) {
  console.error("Invalid nonce");
} else if (getBalance(from) < value) {
  console.error("Not enough funds");
} else {
  console.log("Transaction verified!");
}