Skip to main content

Overview

The Digital Signature Module analyzes how contracts handle cryptographic signatures, detecting vulnerabilities that could allow signature replay attacks, forgery, or unauthorized access.

What Are Digital Signatures?

Digital signatures in blockchain provide:
  • Authentication: Proof that a message came from a specific address
  • Integrity: Assurance the message hasn’t been altered
  • Non-repudiation: Signer cannot deny signing
// Basic signature verification
function verify(
    bytes32 messageHash,
    bytes memory signature,
    address expectedSigner
) public pure returns (bool) {
    return recoverSigner(messageHash, signature) == expectedSigner;
}

Common Vulnerabilities

Signature Replay Attack

Using the same signature multiple times.
// VULNERABLE: No replay protection
function claim(uint256 amount, bytes memory signature) public {
    bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount));
    require(verify(hash, signature, signer), "Invalid signature");
    _mint(msg.sender, amount);  // Can be called multiple times!
}
Attack: User claims rewards repeatedly with the same signature.

Cross-Chain Replay

Signature valid on multiple chains.
// VULNERABLE: No chain ID in signature
function execute(address to, uint256 value, bytes memory signature) public {
    bytes32 hash = keccak256(abi.encodePacked(to, value));
    require(verify(hash, signature, owner), "Invalid signature");
    payable(to).transfer(value);
}
Attack: Signature from mainnet replayed on testnet or L2.

Missing Nonce

No mechanism to invalidate old signatures.
// VULNERABLE: Signatures never expire
function withdraw(uint256 amount, bytes memory signature) public {
    bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount));
    require(verify(hash, signature, admin), "Invalid");
    // No nonce - signature is valid forever
    _transfer(address(this), msg.sender, amount);
}

Signature Malleability

ECDSA signatures can be modified while remaining valid.
// VULNERABLE: No malleability check
function verifySignature(bytes32 hash, uint8 v, bytes32 r, bytes32 s)
    public pure returns (address)
{
    return ecrecover(hash, v, r, s);  // Malleable!
}

Safe Patterns

Nonce-Based Replay Protection

// SAFE: Nonce prevents replay
mapping(address => uint256) public nonces;

function claim(uint256 amount, uint256 nonce, bytes memory signature) public {
    require(nonce == nonces[msg.sender], "Invalid nonce");

    bytes32 hash = keccak256(abi.encodePacked(
        msg.sender,
        amount,
        nonce,
        block.chainid,  // Chain-specific
        address(this)   // Contract-specific
    ));

    require(verify(hash, signature, signer), "Invalid signature");
    nonces[msg.sender]++;  // Increment nonce
    _mint(msg.sender, amount);
}

EIP-712 Typed Data

// SAFE: Structured, typed signing
bytes32 public constant CLAIM_TYPEHASH =
    keccak256("Claim(address recipient,uint256 amount,uint256 nonce)");

function claim(uint256 amount, uint256 nonce, bytes memory signature) public {
    bytes32 structHash = keccak256(abi.encode(
        CLAIM_TYPEHASH,
        msg.sender,
        amount,
        nonce
    ));

    bytes32 hash = _hashTypedDataV4(structHash);  // Includes domain separator
    address signer = ECDSA.recover(hash, signature);

    require(signer == authorizedSigner, "Invalid signature");
    require(nonce == nonces[msg.sender]++, "Invalid nonce");

    _mint(msg.sender, amount);
}

OpenZeppelin ECDSA

// SAFE: Using audited library
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

using ECDSA for bytes32;

function verify(bytes32 hash, bytes memory signature) public view returns (bool) {
    // Handles malleability, returns address(0) on failure
    return hash.toEthSignedMessageHash().recover(signature) == trustedSigner;
}

Detection Tags

TagSeverityDescription
signature_replayHighNo replay protection detected
missing_chainidMediumCross-chain replay possible
missing_nonceHighSignatures never invalidated
signature_malleabilityMediumRaw ecrecover without checks
weak_signature_schemeMediumNon-standard signature verification

API Response Example

{
  "issues": [
    {
      "tag": "signature_replay",
      "severity": "high",
      "description": "Signature can be reused - no nonce or invalidation",
      "location": "claim(uint256,bytes)"
    },
    {
      "tag": "missing_chainid",
      "severity": "medium",
      "description": "Signature hash does not include chain ID",
      "location": "execute(address,uint256,bytes)"
    }
  ]
}

Best Practices

  • Include nonce in all signed messages
  • Include block.chainid to prevent cross-chain replay
  • Include contract address to prevent cross-contract replay
  • Use EIP-712 for structured, typed signing
  • Use OpenZeppelin’s ECDSA library
  • Mark used signatures/nonces as consumed
  • Consider signature expiration timestamps