Ethereum Signature Verifier
Verify and analyze Ethereum digital signatures
About Ethereum 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.
How Ethereum Signatures Work
Ethereum uses the Elliptic Curve Digital Signature Algorithm (ECDSA) with the secp256k1 curve. The signing process:
- Message Hashing: Message is hashed using keccak256
- Prefix Addition: EIP-191 prefix prevents replay attacks
- Signing: Private key signs the hash
- Components: Produces r, s, v values
EIP-191 Message Prefix
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.
Signature Components
ECDSA Signature Parts
- r (32 bytes): X-coordinate of a random point on the curve
- s (32 bytes): Signature proof value
- v (1 byte): Recovery identifier (27 or 28, sometimes 0 or 1)
Total: 65 bytes (130 hex characters)
Common Use Cases
1. Wallet Authentication
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);
2. Gasless Transactions
Users sign transaction intent off-chain, relayer submits on-chain:
- User signs permit/approval message
- Relayer submits transaction with signature
- Contract verifies signature and executes
- User pays no gas
3. Message Verification
Prove ownership of address without transaction:
- Service provides challenge message
- User signs with wallet
- Service verifies signature
- Access granted if valid
4. NFT/Token Permits
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);
Signature Standards
EIP-191: Signed Data Standard
Defines format for signed data with version byte:
- 0x00: Data with intended validator
- 0x01: Structured data (EIP-712)
- 0x45: Personal message (most common)
EIP-712: Typed Structured Data
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);
Verification in Solidity
Using ECDSA Library
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;
}
Manual Recovery
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);
}
Security Considerations
Replay Protection
- Include nonce or timestamp in message
- Specify chain ID for multi-chain apps
- Add expiration time to prevent stale signatures
- Track used signatures on-chain
Signature Malleability
The 's' value can be flipped to create valid alternative signatures. Always validate:
require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, "Invalid s value");
Zero Address Check
Failed signature recovery returns address(0). Always check:
address signer = ECDSA.recover(hash, signature);
require(signer != address(0), "Invalid signature");
Best Practices
- Always use EIP-191 or EIP-712 for message signing
- Include domain separation in structured data
- Validate all signature components
- Implement replay protection
- Add expiration times to signatures
- Use established libraries (OpenZeppelin, ethers.js)
- Never sign arbitrary data from untrusted sources
Signature Format
- Total: 65 bytes (130 hex)
- r: 32 bytes (64 hex)
- s: 32 bytes (64 hex)
- v: 1 byte (2 hex)
- Prefix: 0x
- Example: 0x1234...5678
Common Issues
- Wrong message format
- Missing EIP-191 prefix
- Incorrect v value (27/28 vs 0/1)
- Signature from wrong network
- Using transaction signature for message
- Not checking zero address