lcnicolau
lcnicolau

Reputation: 4058

Ethereum: verifying signatures despite malicious transaction data modifications

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.

Upvotes: 0

Views: 122

Answers (1)

lcnicolau
lcnicolau

Reputation: 4058

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!");
}

Upvotes: 0

Related Questions