A Point of Practice for Signature Verification in Solidity Contracts

Posted by devofash on Sun, 23 Jan 2022 12:06:15 +0100

background

With the current popularity of NFT concepts at home and abroad, a number of projects have emerged, especially in the public chain ether square, and the emergence of communities and new teams is endless and dazzling.

The success of a new project online is often closely related to its community support. Now many new projects have artificially set a whitelist as a way to get more heat, so we can see that people on Discord channel can rack their brains and forget to eat or sleep for the sake of being white. After all, people who get the whitelist are promised to pre-mint ahead, which is almost a solid investment for popular projects.

As for the content of ERC721 standard agreement, there is no white list, so from a technical point of view, how to achieve this function? In fact, this is a gradual process.

Whitelist

At the earliest time, when some project parties began to gradually use the whitelist mechanism, because the whitelist generally only gives a few hundred, the implementation was relatively original. Because the common project architecture at that time was that front-end web pages invoked smart contracts, and no back-end was introduced, it was often written to the contract directly by the project side, and then the method used to determine whether the user's address was in the list of addresses when a pre-mint request was made.

This approach is of course not a problem in principle, but also reflects the non-tampering, open and transparent nature of block chains. However, due to the high gas fees in Taifang and the number of whitelists currently in general is thousands, almost all projects have gradually abandoned this method for their own cost and used two other mechanisms instead:

  1. A single whitelist address is signed under the chain (that is, back-end service), and the contract only needs to store the signature address.
  2. Build the Merkle tree for the Whitelist Address List as a whole, and the contract only needs to store Merkle's root hash.

This paper describes and practices the first kind of signature under chain and verification on chain. Users need to be familiar with solidity and block chain concepts.
The overall process is roughly as follows:

  1. When the user initiates a pre-mint for a front-end web page operation, a pop-up message prompts the user to sign the request
  2. Requests (including addresses, signatures, signature content) are sent to the back-end. After verifying the signature, query whether the address is in the whitelist list.
  3. If it does exist, the user address is signed by the private key of the back-end specific address, and the signature is returned to the front-end.
  4. The front end calls the wallet and passes the signature data returned by the back end as a parameter to the contract pre-mint method
  5. If the contract verifies that the signature was signed at a specific back-end address and that the content matches the user's address, it passes the verification and saves the address into the contract to avoid user duplication.

contract

In popular third-party libraries OpenZeppelin In fact, the contract validation method has been implemented, and only ECDSA library can be introduced into user-defined contracts. The source code to verify the signature is as follows:

function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) {
        // Check the signature length
        // - case 65: r,s,v signature (standard)
        // - case 64: r,vs signature (cf https://eips.ethereum.org/EIPS/eip-2098) _Available since v4.1._
        if (signature.length == 65) {
            bytes32 r;
            bytes32 s;
            uint8 v;
            // ecrecover takes the signature parameters, and the only way to get them
            // currently is to use assembly.
            assembly {
                r := mload(add(signature, 0x20))
                s := mload(add(signature, 0x40))
                v := byte(0, mload(add(signature, 0x60)))
            }
            return tryRecover(hash, v, r, s);
        } else if (signature.length == 64) {
            bytes32 r;
            bytes32 vs;
            // ecrecover takes the signature parameters, and the only way to get them
            // currently is to use assembly.
            assembly {
                r := mload(add(signature, 0x20))
                vs := mload(add(signature, 0x40))
            }
            return tryRecover(hash, r, vs);
        } else {
            return (address(0), RecoverError.InvalidSignatureLength);
        }
    }
}

The specific verification logic is not detailed here. The main logic in the method is related to the structure of the signature. Because the signature in Taifang is composed of three parts with fixed length of r, s and v, it is restored by length here, and then the address of the signature can be restored.
Notice that the method parameter, the first named hash, has a fixed length of 32 bytes, which means that the back end should sign a hash value (mentioned later).

Examples of contract codes for verifying signatures are as follows:

address signer = 0xXXXX;
 
function _verify(bytes32 dataHash, bytes memory signature, address account) private pure returns (bool) {
	return dataHash.toEthSignedMessageHash().recover(signature) == account;
}
 
function pubVerify(bytes memory signature, bytes32 msgHash) public view returns (bool) {
	bool r = _verify(msgHash, signature, signer);
	return r;
}

Be careful:

  1. signer is the backend specific address written in the contract
  2. The recover method wraps the tryRecover above
  3. The original signature msgStr hash (keccak256) was processed using the toEthSignedMessageHash method. This is due to the presence of multiple chains, which require a splice of prefix in the Taifang specification. The method source code for the OpenZeppelin library is as follows:
function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) {
	return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}
 
function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) {
	return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", Strings.toString(s.length), s));
}
  1. There are two overload methods above. If the first one is used, after splicing, a hash is taken on the whole, so the corresponding back end should also be hash twice. If the second is true, it does not need hash on the original text, but can be passed in directly. However, due to the transparency of transaction parameters and the protection of user privacy, we often do not want to pass the original text, so here we choose the first one.

back-end

The backend logically processes the signature of the original data according to the validation process of the contract as a whole. This uses the web3j libraries commonly used in java. The overall signature code is as follows:

import org.web3j.crypto.*;
import org.web3j.utils.Numeric;
import org.web3j.crypto.Sign.SignatureData;
public static String sign(String msg,  String pwd, String path){
	try {
		Credentials ownerCredentials = WalletUtils.loadCredentials(pwd, path);
		byte[] sha3Msg = Hash.sha3(msg.getBytes());
		Sign.SignatureData signMessage = Sign.signPrefixedMessage(sha3Msg, ownerCredentials.getEcKeyPair());

		byte[] signatureBytes = new byte[65];
		System.arraycopy(signMessage.getR(),0, signatureBytes,0, signMessage.getR().length);
		System.arraycopy(signMessage.getS(),0, signatureBytes,32, signMessage.getS().length);
		signatureBytes[64] = signMessage.getV()[0];
		return Numeric.toHexString(signatureBytes);
	}catch (Exception e){
		log.error(e.getMessage());
	}
	return null;

}

The steps are:

  1. Load a local wallet with a password and keystore file path, the back-end specific address mentioned earlier.
  2. Hash for the original text (Hash.sha3, equivalent to keccak256 in the contract)
  3. Sign the original text with a specific prefix using the private key
  4. Using the rsv field of the signature, construct the 16-bit string of the signature
  5. Return the hexadecimal signature and the original hash to the front end

advantage

Not only the whitelist, but also the scenarios where the project party is required to provide data can be signed under the chain and verified on the chain. Many chain games do this, for example, many token-earning scenarios in the game itself are not linked up, but just like traditional games, they are stored in the back-end database. When users really want to extract the currency and go to the trading market, they can submit requests, and the game back-end server can query the amount that the user can extract. Signed and transferred to the contract for chain Token.

defect

This signature verification method requires the project party to save the keystore file on the server. Many project parties write the password directly in the configuration file or even save the private key directly on the server. Security is not guaranteed. Once leaked, an attacker can initiate any signature-related attack request.

Reference resources

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol

https://blog.csdn.net/topc2000/article/details/119921231

Topics: Blockchain