체인의정석

Solidity 컨트렉트에서 서명 검증하기 본문

블록체인/Solidity

Solidity 컨트렉트에서 서명 검증하기

체인의정석 2024. 8. 21. 13:55
728x90
반응형

이번에 서명 검증 로직을 구현하면서 오랜만에 활용 및 정리를 해봤다.

기본 예제 학습

일단 아래 예제가 검증 로직을 이해하기에 정말 편리하고 코드도 직관적이다.

https://solidity-by-example.org/signature/

 

Solidity by Example

 

solidity-by-example.org

// 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)
    }
}

example

https://github.com/t4sk/hello-erc20-permit/blob/main/test/verify-signature.js

 

hello-erc20-permit/test/verify-signature.js at main · t4sk/hello-erc20-permit

Contribute to t4sk/hello-erc20-permit development by creating an account on GitHub.

github.com

const { expect } = require("chai")
const { ethers } = require("hardhat")

describe("VerifySignature", function () {
  it("Check signature", async function () {
    const accounts = await ethers.getSigners(2)

    const VerifySignature = await ethers.getContractFactory("VerifySignature")
    const contract = await VerifySignature.deploy()
    await contract.deployed()

    // const PRIV_KEY = "0x..."
    // const signer = new ethers.Wallet(PRIV_KEY)
    const signer = accounts[0]
    const to = accounts[1].address
    const amount = 999
    const message = "Hello"
    const nonce = 123

    const hash = await contract.getMessageHash(to, amount, message, nonce)
    const sig = await signer.signMessage(ethers.utils.arrayify(hash))

    const ethHash = await contract.getEthSignedMessageHash(hash)

    console.log("signer          ", signer.address)
    console.log("recovered signer", await contract.recoverSigner(ethHash, sig))

    // Correct signature and message returns true
    expect(
      await contract.verify(signer.address, to, amount, message, nonce, sig)
    ).to.equal(true)

    // Incorrect message returns false
    expect(
      await contract.verify(signer.address, to, amount + 1, message, nonce, sig)
    ).to.equal(false)
  })
})

오픈제플린 모듈 살펴보기

위의 예제는 이해할때는 좋지만 실제로 사용할때는 open-zepplin을 사용하는 것이 좋다.

오픈제플린에서도 서명 관련 라이브러리들이 많으니 사용하면 좋다.
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/SignatureChecker.sol

 

openzeppelin-contracts/contracts/utils/cryptography/SignatureChecker.sol at master · OpenZeppelin/openzeppelin-contracts

OpenZeppelin Contracts is a library for secure smart contract development. - OpenZeppelin/openzeppelin-contracts

github.com

https://medium.com/@ajaotosinserah/mastering-libraries-in-solidity-36d783409dfc

 

Mastering Libraries In Solidity

Welcome back! In this article, we will explore the fascinating world of libraries in Solidity. We’ll uncover the advantages of utilizing…

medium.com

라이브러리 사용할때는 위의 블로그를 참고해서 라이브러리를 활용하면 더 좋다. 내가 참고한 대부분의 코드는 internal호출을 써서 그냥 improt만 해온 후에 ECDSA.recover 이런식으로 사용하였지만 bytes 타입에 using을 쓰면 더 간결하게도 작성이 가능하다. 이런 라이브러리들은 버전에 따라 내용이나 함수등이 다르므로 버전도 잘 기록하고 사용해야 한다.

정리



다음은 여러 라이브러리들을 보고 정리한 사항들이다.

1. 주석이나 설명 등등 살펴보면 대부분 EIP191 서명이다. (EOA에서 서명하는 경우)

2. 서명 검증의 문제로 원문을 그대로 서명하는 대신 보통 해시를 취한 상태에서 서명을 진행한다. (오픈제플린 주석을 포함한 다양한 예제에 써져있음)

3. 오픈제플린의 ECDSA.sol 을 쓰면 거의 다 사용이 가능하지만 컨트렉트 형태의 서명 표준 이나 EIP712서명도 쉽게 해주는 오픈제플린 라이브러리들도 존재한다. 만약 ECDSA를 사용한다면 191로 바꾸어주는 MessageHashUtils를 사용하면 된다.

import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

4. 기획할때 서명 타입을 모르거나 안알려주면 eip191서명을 생각하고 있다고 보면 된다. ethers에서 Signer를 써서 sign하는 함수도 eip191서명이다.

 

오픈제플린을 사용하여 만든 서명 검증 로직

이를 이용해서 컨트렉트 서명은 다음과 같이 검증한다.

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

contract EXAMPLE is {
    using ECDSA for bytes32;
    using MessageHashUtils for bytes32;
.
.
.

    function _verifySignature(
        address holder,
        bytes32 _hash,
        bytes memory signature
    ) public pure returns (bool) {
        if (_hash.toEthSignedMessageHash().recover(signature) != holder) return false;
        else return true;
    }


    function getMessageHash(string memory _message) public pure returns (bytes32) {
        return keccak256(bytes(_message));
    }

}

해당 코드에 input 값을 넣을때는 아래와 같이 값을 만들어서 넣어준다.

const hash = ethers.keccak256(ethers.toUtf8Bytes(inputString));
const signature = owner.signMessage(ethers.getBytes(hash));

위의 해시의 경우에는 getMessageHash를 사용하거나 위 코드의 ethers로 해시를 만들면 된다.


728x90
반응형
Comments