how to work with them?

Cryptographic digital signatures are a key part of the blockchain. They are used to prove ownership without revealing the private key. Mainly used for transaction signaturesbut can also be used for custom message signatures. This in turn opens up various use cases in applications.

IN Ethereum documentation The following definition of a digital signature is given:

Digital signature – a short string of data that the user creates for a document using the private key. Anyone with the corresponding public key, signature and document can verify the following:

  1. The document was “signed” by the owner of that private key.

  2. The document was not changed after it was signed.

There are many cryptographic algorithms that are used for encryption and that can be used to create a digital signature. For example RSA And AES.

But to create a digital signature, a separate cryptographic algorithm called DSA (Digital Signature Algorithm). It is based on the use of a public and private key pair. The signature is created secretly using closed key, but is verified publicly open key. Thus, the private key remains unknown to anyone.

The Ethereum and Bitcoin networks use a more advanced digital signature algorithm, which is based on elliptic curves. It is called ECDSA(Elliptic Curve Digital Signature Algorithm)

Important! ECDSA is only an algorithm for digital signature. Unlike RSA and AES, it cannot be used for encryption.

To understand even better how this works, you can look videowhich will tell you in simple words about DSA and how DSA differs from ECDSA.

For decentralized applications, I would highlight two main scenarios for using a digital signature:

  1. Prove to the protocol that for your public address, you have a private key that you control (authentication)

  2. Verify that some action is actually authorized by you

Sign message and verify using ECDSA

ECDSA signatures consist of two numbers (integers): r And s. Ethereum also uses additional v variable (recovery ID). Such a signature can be designated as {r, s, v}.

To create a signature, you need to sign a message with a private key. The algorithm looks like this:

  1. The hash of the message is calculated. In Ethereum, the hash of a message is usually calculated using keccak256. Always added to the beginning of the message \x19Ethereum Signed Message:\n32". This ensures that the signature cannot be used outside of Ethereum.

    Keccak256("\x19Ethereum Signed Message:\n32" + Keccak256(message))
  2. A secure random value is generated. Let's call him secret. Using this random value allows you to get a different signature each time. When this variable is not secret or can be calculated, then the private key can also be calculated. This is completely unsafe for us.

  3. Calculate the point (x, y) on the elliptic curve by multiplying secret to a constant G elliptic curve. We remember that the ECDSA algorithm is a story about elliptic curves.

  4. Calculated r And s using special formulas based on the point (x, y) on the elliptic curve. We won’t dive into calculations; this requires deep knowledge of mathematics. If r or s are equal to zero, then we return to step 2.

Important! Let's repeat it again! Since we use random to obtain a signature secret, the signature will always be different. When secret not secret (not random or publicly known), it becomes possible to calculate the private key based on two signatures received from the same owner of the private key. However, there is a standard deterministic DSA signatures. According to the standard, you can choose a safe secret and always use only it to sign all your messages. With such secret it will be impossible to guess the private key.

Recovery ID ({v})

V is the last byte of the signature and has a value of 27 (0x1b) or 28 (0x1c). This ID is very important. To understand the importance, look at the formulas for calculating the value r.

r = x₁ mod n

As you noticed, r calculated only by value x on the horizontal axis. Vertical axis value y not used. Thus, if you look at the graph of an elliptic curve, you will understand that by one value x you can calculate two points r.

The graph of course describes the complete process of calculating the point r. But this is not so important to us now. We remember that in r information about a point is stored only along the x axis, and since the graph is curved, for such x there are two values ​​different in sign y.

Now look at the calculation formula s. And note that to calculate s value is used rof which we may have two, as you remember.

s = k⁻¹(e + rdₐ) mod n

The result may be two completely different public keys (that is, addresses) that can be recovered. And this is where the parameter comes into play vwhich indicates which of two possible values r need to be used.

Important! This parameter is required when restoring a public address from a digital signature. Solidity uses a built-in function for this ecrecover().

Sign message vs sign transaction

Before this, we only talked about signing messages. To sign a message, we calculate the hash of the message and use the private key to calculate the digital signature.

For signing transactions, things are a little more complicated. Transactions are encoded using RLP. Coding includes all transaction parameters (nonce, gas price, gas limit, to, value, data) and signature (v, r, s).

We can code the signed transaction as follows:

  1. Encode transaction parameters:

    RLP(nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0).
  2. Get hash Keccak256 unsigned transaction in RLP encoding.

  3. Sign the hash with the private key using the ECDSA algorithm.

  4. Encode the signed transaction.

    RLP(nonce, gasPrice, gasLimit, to, value, data, v, r, s).

By decrypting the RLP-encoded transaction data, the raw transaction parameters and signature can be retrieved again.

Important! This is used within the Ethereum network to exchange data between nodes. This reduces node operating costs and storage requirements, and increases network throughput through efficient use of memory.

More information about RLP and data serialization in general can be found in this excellent article.

How do wallets with a signature work?

Signature {r, s, v} combined into one sequence of bytes. The length of the sequence is 65 bytes:

  • 32 bytes for r

  • 32 bytes for s

  • 1 byte for v.

If we encode this as a hex string, we get a string that is 130 characters long (not counting the 0x at the beginning). This type of signature is used by most wallets and interfaces. For example, a full signature might look like this:

signature: 0x0f1928d8f26b2d9260929425bdc6ac922f7d787fd73b42afe2548776a0e858016f52826d8ab67e1c84e6e6778fa4769d8aa4f014bf76b3280be77e4e0c447f9b1c
r: 0x0f1928d8f26b2d9260929425bdc6ac922f7d787fd73b42afe2548776a0e85801
s: 0x6f52826d8ab67e1c84e6e6778fa4769d8aa4f014bf76b3280be77e4e0c447f9b
v: 1c(в hex) или 28(в decimal)

Standardization for working with signatures

Personal_sign

Personal_sign is the generic name for the message signing process we described above. Let us repeat the algorithm in general terms. The message is usually pre-hashed, so its length can be a fixed 32 bytes:

"\x19Ethereum Signed Message:\n32" + Keccak256(message)

This hash is then signed. This works great for proving ownership of something.

However, if the user A signs the message and sends it to the contract Xuser B can copy this signed message and send it to the contract Y. It is called repeated attack.

If you are wondering what happened before the appearance personal_signyou can read this article.

EIP-191: Signed Data Standard

This standard is a very simple proposal to solve the problem of replay attacks. It defines the version number and data related to the version. The format looks like this:

0x19 <1 byte version> <version specific data> <data to sign>

0x19 at the beginning is intended to ensure that signed data can never be recognized by the RLP scheme. This means that data signed in this way can never be a transaction.

Then comes 1 byte for the version. There are currently three versions of the standard:

Version

EIP

Description

0x00

191

Validator address. The signature data can be anything and only the validator knows how to work with it

0x01

712

Data is structured

0x45

191

personal_sign

You can see the standard for more details. here.

EIP-712: Ethereum typed structured data hashing and signing

This standard for typing signed data. This makes signature data more verifiable by presenting it in a human-readable form.

EIP-712 defines a new method. It replaced personal_sign and was called eth_signTypedData. For this method, we must specify all the properties (eg to, amount and nonce) with their corresponding types (eg address, uint256). In the screenshots below we can see the difference in the signed data.

Example of personal-sign in Metamask wallet

Example of personal-sign in Metamask wallet

Example signTypedData in Metamask wallet

Example signTypedData in Metamask wallet

Metamask has prepared something good demo. Here you can experiment and see the difference between the signatures.

Additionally, according to the standard, it is necessary to specify basic information about the application, called domain.

Domain contains the following information:

  1. string name Application or protocol name

  2. string version Version of signature used. Signature data can be changed and versioned.

  3. uint256 chainId Network ID.

  4. address verifyingContract Address of the contract that will verify the signature

  5. bytes32 salt Additional field salt. Can be used to differentiate domain.

Addition domain solves the problem of a potential replay attack.

Checking signatures on a contract

Solidity has a built-in function called ecrecover(). In fact, it is a precompiled contract at 0x1. Using this function helps to recover the public address of the private key with which the message was signed.

However, there are pitfalls in using ecrecover(). According to EIP-2, the Ethereum network still allows some flexibility in signing for ecrecover(). The ECDSA library from OpenZeppelin allows you to remove this feature and make the signature unique. For safe implementation ecrecover() you can see here.

Examples

Verification on the side of smart contracts

Verifying message signature
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

/**
 * @notice Контракт проверяет подписанное приватным ключом произвольное сообщение
 * @dev Используется встроенная функция ecrecover()
 */
contract SignatureVerifier {
    /// @notice Префикс для обозначения, что эта подпись будет использоваться только внутри сети Ethereum
    bytes32 constant public PREFIX = "\x19Ethereum Signed Message:\n32";

    /// @notice Проверяет была ли подпись сделана адресом signer
    function isValid(address signer, bytes32 hash, uint8 v, bytes32 r, bytes32 s) external pure returns (bool) {
        return _recover(hash, v, r, s) == signer;
    }

    /// @notice Восстанавливает публичный адрес приватного ключа, которым была сделана передаваямая подпись
    function _recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) private pure returns (address) {
        bytes32 prefixedHash = keccak256(abi.encodePacked(PREFIX, hash));

        return ecrecover(prefixedHash, v, r, s);
    }
}
Signature verification according to EIP-712 standard
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

import {ECDSA} from "openzeppelin-contracts/utils/cryptography/ECDSA.sol";

/**
 * @notice Контракт проверяет подписанное приватным ключом сообщение c типизированными данными согласно EIP-712.
 * @dev Используется библиотека от OpenZeppelin ECDSA
 */
contract EIP712 {
    bytes32 public constant IS_VALID_TYPEHASH = keccak256("isValid(uint256 nonce)");

    /// @notice Счетчик проверки подписи. Позволяет быть уверенным, что одна и таже подпись не бует использована дважды
    uint256 public signatureNonce;

    error SignatureIsInvalid();

    /// @notice 32-байтовый разделитель домена. Используется для определения свойств конкретного приложения.
    /// Другими словами подпись может использоваться только для этого приложения
    function DOMAIN_SEPARATOR() public view returns (bytes32) {
        return keccak256(
            abi.encode(
                keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
                keccak256("EIP712"),
                keccak256("1"),
                block.chainid,
                address(this)
            )
        );
    }

    /// @notice hashStruct. Используется для определения типизированных данных подписи
    function _getDigest(bytes32 typeHash) private view returns (bytes32) {
        return keccak256(
            abi.encodePacked(
                "\x19\x01", // Согласно EIP-191. Фиксированное значение версии. Определяет "Structured data" EIP-712
                DOMAIN_SEPARATOR(),
                keccak256(
                    abi.encode(
                        typeHash,
                        signatureNonce + 1
                    )
                )
            )
        );
    }

    /**
     * @notice Проверяет была ли подпись сделана адресом signer
     * @param signer Публичный адрес для проверки, подписавший сообщение
     * @param signature Проверяемая подпись (abi.encoded(r, s, v))
     */
    function isValid(address signer, bytes memory signature) public view returns (bool) {
        bytes32 digest = _getDigest(IS_VALID_TYPEHASH);
        address recoveredSigner = ECDSA.recover(digest, signature);

        return signer == recoveredSigner;
    }

    function useSignature(address signer, bytes memory signature) external {
        if (!isValid(signer, signature)) {
            revert SignatureIsInvalid();
        }

        signatureNonce += 1;
    }
}
Example taken from Solidity by Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/* Signature Verification

How to Sign and Verify
# Signing
1. Create message to sign
2. Hash the message
3. Sign the hash (off chain, keep your private key secret)

# Verify
1. Recreate hash from the original message
2. Recover signer from signature and hash
3. Compare recovered signer to claimed signer
*/

contract VerifySignature {
    /* 1. Unlock MetaMask account
    ethereum.enable()
    */

    /* 2. Get message hash to sign
    getMessageHash(
        0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C,
        123,
        "coffee and donuts",
        1
    )

    hash = "0xcf36ac4f97dc10d91fc2cbb20d718e94a8cbfe0f82eaedc6a4aa38946fb797cd"
    */
    function getMessageHash(
        address _to,
        uint256 _amount,
        string memory _message,
        uint256 _nonce
    ) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(_to, _amount, _message, _nonce));
    }

    /* 3. Sign message hash
    # using browser
    account = "copy paste account of signer here"
    ethereum.request({ method: "personal_sign", params: [account, hash]}).then(console.log)

    # using web3
    web3.personal.sign(hash, web3.eth.defaultAccount, console.log)

    Signature will be different for different accounts
    0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
    */
    function getEthSignedMessageHash(bytes32 _messageHash)
        public
        pure
        returns (bytes32)
    {
        /*
        Signature is produced by signing a keccak256 hash with the following format:
        "\x19Ethereum Signed Message\n" + len(msg) + msg
        */
        return keccak256(
            abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash)
        );
    }

    /* 4. Verify signature
    signer = 0xB273216C05A8c0D4F0a4Dd0d7Bae1D2EfFE636dd
    to = 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C
    amount = 123
    message = "coffee and donuts"
    nonce = 1
    signature =
        0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
    */
    function verify(
        address _signer,
        address _to,
        uint256 _amount,
        string memory _message,
        uint256 _nonce,
        bytes memory signature
    ) public pure returns (bool) {
        bytes32 messageHash = getMessageHash(_to, _amount, _message, _nonce);
        bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);

        return recoverSigner(ethSignedMessageHash, signature) == _signer;
    }

    function recoverSigner(
        bytes32 _ethSignedMessageHash,
        bytes memory _signature
    ) public pure returns (address) {
        (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);

        return ecrecover(_ethSignedMessageHash, v, r, s);
    }

    function splitSignature(bytes memory sig)
        public
        pure
        returns (bytes32 r, bytes32 s, uint8 v)
    {
        require(sig.length == 65, "invalid signature length");

        assembly {
            /*
            First 32 bytes stores the length of the signature

            add(sig, 32) = pointer of sig + 32
            effectively, skips first 32 bytes of signature

            mload(p) loads next 32 bytes starting at the memory address p into memory
            */

            // first 32 bytes, after the length prefix
            r := mload(add(sig, 32))
            // second 32 bytes
            s := mload(add(sig, 64))
            // final byte (first byte of the next 32 bytes)
            v := byte(0, mload(add(sig, 96)))
        }

        // implicitly return (r, s, v)
    }
}

Real life examples

  1. ERC-2612: Permit Extension for EIP-20 Signed Approvals.. This standard is based on EIP-712. Good articlewhich provides an explanation of the standard.

  2. UniswapV2ERC20.sol contract extends contract UniswapV2Pair.sol and allows you to work with signatures in your peripheral contracts. On contract UniswapV2Router01.sol you can call the function removeLiquidityWithPermit().

  3. Permit2 from Uniswap. The code can be found here. The idea is that permit will be available for an ERC-20 token regardless of whether the token supports ERC-2612.

  4. Open GSN uses signature verification in its contract Forwarder.sol

Generating a signature externally

  1. Ethers js. Sign message

  2. Metamask. Signing data

  3. Open Ethereum. API

  4. Example from EIP-712

Links

The first two articles are cool. They will explain the basic concepts of cryptographic signatures in simple terms.

  1. The Magic of Digital Signatures on Ethereum

  2. Intro to Cryptography and Signatures in Ethereum

  3. EIP-191

  4. EIP-712

  5. ECDSA Contract to verify signatures

  6. Mathematical and cryptographic functions. Solidity docs. You can see the description ecrecover(), keccak256() etc.

  7. Testing EIP-712 Signatures

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *