Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

校验签名:

RSA

rsa非对称密钥体系 基于初等数论中的欧拉定理 并且建立在大整数因子分解的困难性问题上

加密/解密数据

examples

签名/解签

  • 签名数据 m
  • 签名数据 s = m^d % n
  • 验证签名: s^e % n = m %n
    • s^e = m^(ed)
    • s = buyerAddress ^ d % n
    • msg.sender == s ^ e % n

GasCost

Math

Solidity Slot最多支持32 bytes(256bit),不能直接进行模幂运算,在 EIP-198增加 0x5 的预编译合约进行幂运算。

    function modExp(
        uint256 base,
        uint256 exp,
        uint256 mod
    ) public view returns (uint256) {
        bytes memory precompileData = abi.encode(32, 32, 32, base, exp, mod);
        (bool ok, bytes memory data) = address(5).staticcall(precompileData);
        require(ok, "expMod failed");
        return abi.decode(data, (uint256));
    }

staticCall 调用预编译合约执行幂运算,需要从内存加载 base,exp,mod 参数

Key

RSA 加密依赖大数分解,其中 RSA2048 表示的是大数 n 具有 2048bit, 因此每bit 位数每增加一位,公钥的安全性就成指数增长。

为保证安全,如果采用 RSA1024,需要1024bit存储 n 的值,在 solidity中需要 4个 slot 插槽,sload n值需要 4 * 2100 = 8400 gas.

如果使用 immutable| constant 参数将公钥编码进 codes,但是这样没办法取消签名(ECDSA通过更换签名地址,Merkle通过更新root值取消旧的校验数据),但是将数据存储叜 codes中远比从slot 读取数据更节省 gas

  • extcodecopy读取codes的花销
  • minimum_word_size = (size + 31) / 32
    static_gas = 0
    dynamic_gas = 3 * minimum_word_size + memory_expansion_cost + address_access_cost
    
  • 如果后面更新签名数据,直接更换外部合约,重新部署合约代码,更新codes存储的 key
  • 通过 create2 可以部署相同的合约地址,因此在合约codes中分成两部分:
    • 第一部分执行 selfdestruct,然后在相同地址部署合约
    • 第二部分存储公钥 key,直接外部调用解码使用

warm access list

读取address数据时,将 code_access 的2600gas可以降低到 warm_access的100 gas

合约验证签名

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;


contract Rsa {
    event Metamorphosed(address metamorphicContract);

    address public immutable owner;

    address public immutable metamorphicContractAddress;

    bytes32 private immutable salt;

    uint256 private immutable modLength;

    bytes currentImplementationCode;

    bytes32 public constant EXPONENT =
    0x0000000000000000000000000000000000000000000000000000000000000003;

    /**
     * @dev See README.md for bytecode breakdown
     */
    bytes32 private immutable _metamorphicContractInitializationCode;

    //------------------------------------------------------------\\
    constructor(bytes32 _salt, uint256 _modLength) {
        owner = msg.sender;
        salt = _salt;
        modLength = _modLength;

        // contract runtime code length (without modulus) = 51 bytes (0x33)
        bytes memory metamorphicInit = (
            abi.encodePacked(
            hex"630000000e60005261",
            /**
             *   must be 0x40(pointer + bytes length) + contract code length  + modulus length
             *   fixed 2 bytes as no mod length over 2 bytes in size is expected
             *   rsa-2048 = 0x0100 2 bytes length
             */
            uint16(0x73 + _modLength),
            hex"60006004601c335afa61",
            /**
             *  contract code length + modulus length
             *  fixed 2 bytes
             */
            uint16(0x33 + _modLength),
            hex"6040f3"
        )
        );

        /**
         * @dev stackMetaInit is a temp variable used to parse the length of the
         *      metamorphic init code from the actual code. The is required as
         *      'bytes memory metamorphicInit' has a dynamic length and will
         *      prepend the value with it's length.
         *
         *      It is parsed in assembly and then set outside the block as
         *      assembly access to immutable variables is not supported.
         */
        bytes32 stackMetaInit;
        assembly {
        // use assembly to prune bytes to just get the bytes data
            stackMetaInit := mload(add(metamorphicInit, 0x20))
        }
        _metamorphicContractInitializationCode = stackMetaInit;

        // Hash of metamorphic bytecode.
        bytes32 METAMORPHIC_INIT_HASH = keccak256(
            abi.encodePacked(stackMetaInit)
        );

        // Calculate and store as immutable, metamorphic contract address.
        metamorphicContractAddress = (
            address(
            uint160(
                uint256(
                    keccak256(
                        abi.encodePacked(
                            hex"ff",
                            address(this),
                            _salt,
                            METAMORPHIC_INIT_HASH
                        )
                    )
                )
            )
        )
        );
    }

    /**
     * @notice 'verifySignature' is the user facing function used to validate
     *          signed messages.
     *
     * @param  'sig' length must always be equal to the length of the public
     *          key(modulus).
     *
     * @dev     Exponent is hardcoded at top of contract. This may be altered
     *          for specific use cases but it must always be 32 bytes.
     *
     * @dev See below layout & link for memory when calling precompiled modular exponentiation contract (0x05)
     *      <length_of_BASE(signature)> <length_of_EXPONENT> <length_of_MODULUS> <BASE(signature)> <EXPONENT> <MODULUS>
     *
     *      https://github.com/ethereum/EIPs/blob/master/EIPS/eip-198.md
     */
    function verifySignature(bytes calldata sig) external view returns (bool) {
        require(sig.length == modLength);

        // Load immutable variable onto the stack.
        address _metamorphicContractAddress = metamorphicContractAddress;

        // no need to update pointer as all memory written here can be overwriten with no consequence
        assembly {
        /**
         * @dev No need to update free memory pointer as all memory written here
             *      can be overwriten with no consequence.
             *
             * @dev Store in memory, length of BASE(signature), EXPONENT, MODULUS.
             */
            mstore(0x80, sig.length)
            mstore(add(0x80, 0x20), 0x20)
            mstore(add(0x80, 0x40), sig.length)

        // Store in memory, BASE(signature), EXPONENT, MODULUS(public key).
            calldatacopy(0xe0, sig.offset, sig.length)
            mstore(add(0xe0, sig.length), EXPONENT)

        /**
         * @dev Calculate where in memory to copy modulus to (modPos). This must
             *      be dynamically determined as various size of signature may be used.
             */
            let modPos := add(0xe0, add(sig.length, 0x20))

        /**
         * @dev 0x33 is a precalulated value that is the offset of where the
             *      signature begins in the metamorphic bytecode.
             */
            extcodecopy(_metamorphicContractAddress, modPos, 0x33, sig.length)

        /**
         * @dev callDataSize must be dynamically calculated. It follows the
             *      previously mentioned memory layout including the length and
             *      value of the sig, exponent and modulus.
             */
            let callDataSize := add(0x80, mul(sig.length, 2))

        /**
         * @dev Call 0x05 precompile (modular exponentation) w/ the following
             *      args and revert on failure.
             *
             *      Args:
             *      gas,
             *      precomipled contract address,
             *      memory pointer of begin of calldata,
             *      size of call data (callDataSize),
             *      pointer for where to copy return,
             *      size of return data
             */
            if iszero(
                staticcall(gas(), 0x05, 0x80, callDataSize, 0x80, sig.length)
            ) {
                revert(0, 0)
            }


        /**
         * @dev Check all leading 32-byte chunk to ensure values are zeroed out.
             *      If a valid sig then only the last 20 bytes will contains non-zero bits.
             */
            let chunksToCheck := div(sig.length, 0x20)
            for { let i := 1 } lt(i, chunksToCheck) { i := add(i, 1) }
            {
                if  mload(add(0x60, mul(i, 0x20)))
                {
                    revert(0, 0)
                }
            }

        /**
         * @dev Decoded signature will always be contained in last 32-bytes.
             *      If msg.sender == decoded signature then return true, else false.
             */
            let decodedSig := mload(add(0x60, sig.length))
            if eq(caller(), decodedSig) {
            // Return true
                mstore(0x00, 0x01)
                return(0x00, 0x20)
            }
        // Else Return false
            mstore(0x00, 0x00)
            return(0x00, 0x20)
        }
    }

    modifier onlyOwner() {
        require(owner == msg.sender);
        _;
    }

    /**
     * @notice 'deployPublicKey' is used in initializing the metamorphic contract that
     *          stores the RSA modulus, n (public key).
     *
     * @dev     See Repo README for guide to generating public key via python script.
     *
     * https://github.com/RareSkills/RSA-presale-allowlist
     */
    function deployPublicKey(bytes calldata publicKey) external onlyOwner {
        require(publicKey.length == modLength, "incorrect publicKey length");

        // contract runtime code length (without modulus) = 51 bytes (0x33)
        bytes memory contractCode = abi.encodePacked(
            hex"3373",
            address(this),
            hex"14601b57fe5b73",
            address(this),
            hex"fffe",
            publicKey
        );

        // Code to be returned from metamorphic init callback. See README for explanation.
        currentImplementationCode = contractCode;

        // Load immutable variables onto the stack.
        bytes32 metaMorphicInitCode = _metamorphicContractInitializationCode;
        bytes32 _salt = salt;

        // Address metamorphic contract will be deployed to.
        address deployedMetamorphicContract;

        assembly {
        /**
         * Store metamorphic init code in scratch memory space.
         * This is previously dynamically created in constructor (based on public key size
         * and stored as an immutable variable.
         */
            mstore(0x00, metaMorphicInitCode)

        /**
         * CREATE2 args:
         *  value: value in wei to send to the new account,
         *  offset: byte offset in the memory in bytes, the initialisation code of the new account,
         *  size: byte size to copy (size of the initialisation code),
         *  salt: 32-byte value used to create the new contract at a deterministic address
         */
            deployedMetamorphicContract := create2(
                0,
                0x00,
                0x20, // as init code is stored as bytes32
                _salt
            )
        }

        // Ensure metamorphic deployment to address as calculated in constructor.
        require(
            deployedMetamorphicContract == metamorphicContractAddress,
            "Failed to deploy the new metamorphic contract."
        );

        emit Metamorphosed(deployedMetamorphicContract);
    }

    /**
     * @notice 'destroyContract' must be called before redeployment of public
     *          key contract.
     *
     * @dev     See Repo README.md process walk-through.
     *
     * https://github.com/RareSkills/RSA-presale-allowlist
     */
    function destroyContract() external onlyOwner {
        (bool success, ) = metamorphicContractAddress.call("");
        require(success);
    }

    /**
     * @notice 'callback19F236F3' is a critical step in the initialization of a
     *          metamorphic contract.
     *
     * @dev     The function selector for this is '0x0000000e'
     */
    function callback19F236F3() external view returns (bytes memory) {
        return currentImplementationCode;
    }
}