Verify and analyze Ethereum digital signatures
Digital signatures are cryptographic proofs that verify a message was signed by the owner of a specific private key, without revealing the key itself. In Ethereum and Web3, signatures enable secure authentication and authorization.
Ethereum uses the Elliptic Curve Digital Signature Algorithm (ECDSA) with the secp256k1 curve. The signing process:
To prevent signed messages from being valid transactions, Ethereum prepends a prefix:
"\x19Ethereum Signed Message:\n" + message.length + message
This ensures signatures created for off-chain purposes can't be replayed as transactions.
Total: 65 bytes (130 hex characters)
Sign-in with Ethereum (SIWE) allows users to authenticate with their wallet:
// Frontend (ethers.js)
const signer = provider.getSigner();
const message = "Sign in to MyApp";
const signature = await signer.signMessage(message);
// Backend verifies signature
const address = ethers.utils.verifyMessage(message, signature);
Users sign transaction intent off-chain, relayer submits on-chain:
Prove ownership of address without transaction:
EIP-2612 allows gasless approvals using signatures:
// User signs permit message
const signature = await token.signPermit(
owner, spender, value, deadline
);
// Contract verifies and sets allowance
token.permit(owner, spender, value, deadline, v, r, s);
Defines format for signed data with version byte:
Allows signing complex structured data with type information:
const domain = {
name: 'MyDApp',
version: '1',
chainId: 1,
verifyingContract: '0x...'
};
const types = {
Transfer: [
{ name: 'to', type: 'address' },
{ name: 'amount', type: 'uint256' }
]
};
const value = {
to: '0x...',
amount: 1000000
};
const signature = await signer._signTypedData(domain, types, value);
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
function verify(
string memory message,
bytes memory signature,
address expectedSigner
) public pure returns (bool) {
bytes32 messageHash = keccak256(abi.encodePacked(message));
bytes32 ethSignedHash = ECDSA.toEthSignedMessageHash(messageHash);
address recoveredSigner = ECDSA.recover(ethSignedHash, signature);
return recoveredSigner == expectedSigner;
}
function recoverSigner(
bytes32 messageHash,
bytes memory signature
) public pure returns (address) {
require(signature.length == 65, "Invalid signature length");
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(signature, 32))
s := mload(add(signature, 64))
v := byte(0, mload(add(signature, 96)))
}
return ecrecover(messageHash, v, r, s);
}
The 's' value can be flipped to create valid alternative signatures. Always validate:
require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, "Invalid s value");
Failed signature recovery returns address(0). Always check:
address signer = ECDSA.recover(hash, signature);
require(signer != address(0), "Invalid signature");