The purpose of signature verification is to ensure that the person submitting a HTTP request to your backend actually has ownership of the wallet address that he says he owns. This is done using Elliptic Curve Digital Signature Algorithm (ECDSA) to recover the address that could have signed a particular signature, and then checking if the address matches that was given in a HTTP request.
The idea
Here, we outline an example implementation of how this can be done. The idea is that we ask the user to provide his wallet address, and the timestamp of the HTTP request. You may ask for other information if you desire. Then, we concatenate these two fields together, and generate a hash for it. We then ask the user to sign this hash, and produce a signature signed using his wallet address.
Then, in the HTTP request, we attach the plaintext address and timestamp, as well as the signature, into the header of the request. On the backend, we then re-generate the hash that was signed by the user. Then, we use a special Web3 function (akin to the ecrecover function in Solidity) to recover the address that signed the signature given, by supplying the re-generated hash. If everything matches, and the timestamp has not expired, then we allow the request to go through.
Doing the verification this way avoids the need for a centralised, traditional OAuth API to authenticate users, as it commonly is for Web3 technologies. Everything is secured via cryptography, just like in the blockchain itself.
from web3 import Web3
from eth_account.messages import encode_defunct
REQUEST_TIMEOUT = 60 * 60 # 1 hour timeout
def verify_request(request):
# Write permissions are only allowed to the owner of the wallet address
try:
address = request.META.get('HTTP_X_ADDRESS', None)
timestamp = request.META.get('HTTP_X_TIMESTAMP', None)
signature = request.META.get('HTTP_X_SIGNATURE', None)
if (not (address and timestamp and signature)): # Missing fields
return False
if (Web3.toInt(hexstr=address) == 0): # Input address is 0
return False
# Use solidity keccak to hash the information
msg = Web3.solidityKeccak(['address', 'uint256'], [address, int(timestamp)])
# reconstruct signed hash using encode_defunct
hash_ = encode_defunct(primitive = msg)
# use ecrecover to recover the address that signed this signature
recovered_address = w3.eth.account.recover_message(hash_, signature=signature)
# get current time
curr_time = int(time.time())
# check if recovered address matches the address given, and that the timeout has not passed
return recovered_address == address and curr_time < (int(timestamp) + REQUEST_TIMEOUT)
except:
return False
Getting a smart contract instance with ethers
// account is optional
export function getContract(address: string, ABI: any, library: any, account: any) {
if (!isAddress(address) || address === ethers.constants.AddressZero) {
throw Error(`Invalid 'address' parameter '${address}'.`)
}
return new ethers.Contract(address, ABI, getProviderOrSigner(library, account))
}