iT邦幫忙

2024 iThome 鐵人賽

DAY 14
0
Modern Web

web3 短篇集系列 第 14

合約驗證 EOA 的簽章

  • 分享至 

  • xImage
  •  

EOA 可以用私鑰對訊息進行簽章,合約可以在鏈上對訊息雜湊後,配上簽章,導出 EOA 的地址,合約驗證是否為 EOA 本人的意圖,EOA 就不需要親自執行交易,透過提供簽章,他人代送交易的方式,得以進行鏈上活動。

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 (on-chain)
  2. Recover signer from signature and hash
  3. Compare recovered signer to claimed signer
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;

import "forge-std/Test.sol";

library SignatureUtils {
    function splitSignature(bytes memory signature) internal pure returns (uint8 v, bytes32 r, bytes32 s) {
        require(signature.length == 65, "Wrong length");

        assembly {
            r := mload(add(signature, 32))
            s := mload(add(signature, 64))
            v := byte(0, mload(add(signature, 96)))
        }
    }
}

contract A {
    address owner;

    constructor(address _owner) {
        owner = _owner;
    }

    function isValidSignatureFromEOA(bytes32 digest, bytes memory signature) public view returns (bool) {
        (uint8 v, bytes32 r, bytes32 s) = SignatureUtils.splitSignature(signature);
        address signer = ecrecover(digest, v, r, s);
        if (signer == owner) return true;
        return false;
    }
}

contract ATest is Test {
    address alice = vm.addr(0xbeef);
    A a = new A(alice);

    function testSign() public {
        bytes memory message = "hello world";
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(0xbeef, keccak256(message));
        bytes memory signature = abi.encodePacked(r, s, v);
        console.logBytes(signature);
    }

    function testIsValidSignatureFromEOA() public {
        bytes memory message = "hello world";
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(0xbeef, keccak256(message));
        bytes memory signature = abi.encodePacked(r, s, v);

        console.logBytes(signature);

        console.logBytes32(r);
        console.logBytes32(s);
        console.log(v);

        bool valid = a.isValidSignatureFromEOA(keccak256(message), signature);

        assertEq(valid, true);
    }
}

Signature 的長度為 65 個字元,組合的順序是 r,s,v。

0x9951afc45afbf8c6bbd2469649cedcfa6d8b039d50a5bb8bfe216c4338e0cbb2008d3e5243755a08bd89f269e31a71e7b7ebfe9a0abb367422b658bd8c9c4e871c

r: 32 bytes

0x9951afc45afbf8c6bbd2469649cedcfa6d8b039d50a5bb8bfe216c4338e0cbb2

s: 32 bytes

0x008d3e5243755a08bd89f269e31a71e7b7ebfe9a0abb367422b658bd8c9c4e87

v: 1 bytes

0x1c

Recover

鏈上 recover 時要填入雜湊後的訊息和拆開的簽章,參數的順序是 v,r,s。

ecrecover (elliptic curve recover):

ecrecover(digest, v, r, s);

openzeppelin:

ECDSA.recover(hash, v, r, s);

Split Signature

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

        assembly {
            r := mload(add(signature, 32))
            s := mload(add(signature, 64))
            v := byte(0, mload(add(signature, 96)))
        }
    }

因為 signature 的型別是 bytes memory,因此前 32 bytes 是陣列的長度,所以 signature 的起始位置要加 32 bytes,才是 r 的 offset,接著 32 bytes 就是 s。

最後 v 因為只有 1 byte,使用 BYTE 從 32 bytes 中提取 1 byte。

ERC-191

message

\x19Ethereum Signed Message:\n + message.length + message

ERC-721

message

"\x19\x01" + domain_separator + keccak256(abi.encode(TYPEHASH, ...args))

TYPEHASH example:

bytes32 private constant PERMIT_TYPEHASH =
        keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

ERC20Permit.sol

 function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public virtual {
        if (block.timestamp > deadline) {
            revert ERC2612ExpiredSignature(deadline);
        }

        bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));

        bytes32 hash = _hashTypedDataV4(structHash);

        address signer = ECDSA.recover(hash, v, r, s);
        if (signer != owner) {
            revert ERC2612InvalidSigner(signer, owner);
        }

        _approve(owner, spender, value);
    }

Signature 有關的安全議題

Smart Contract Security - Signature Related

  • 若是無效的 signature,ecrecover 會回傳 address(0) 且不會 revert。
  • Signature Replay: 沒有使用 nonce,同樣的 signature 被拿去重送。
  • Signature Malleablility: 如果給我一個有效的 signature,我可以改變 signature 的樣貌,但它依然有效。
  • 若驗證函式的參數直接是 hash 和 signature,沒有在合約對訊息進行雜湊,會讓攻擊者有機會拿去重複使用。

安全作法:

  • 使用 openzeppelin's library 來防止 malleability attacks 和 recover to zero 的問題。
  • signature 不能作為密碼,它沒有唯一性。
  • 使用 nonce 防止重送,最好使用 ERC712,讓用戶知道自己簽了什麼訊息。
  • 在鏈上對訊息進行雜湊

Reference


上一篇
專案管理筆記
下一篇
允許代付手續費的代幣 (ERC-2612)
系列文
web3 短篇集30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言