274 lines
9.9 KiB
Plaintext
274 lines
9.9 KiB
Plaintext
_section: Signing @<cookbook-signing>
|
|
|
|
Signing content and providing the content and signature to a
|
|
Contract allows on-chain validation that a signer has access
|
|
to the private key of a specific address.
|
|
|
|
The ecrecover algorithm allows the public key to be determined
|
|
given some message digest and the signature generated by the
|
|
private key for that digest. From the public key, the address
|
|
can then be computed.
|
|
|
|
How a digest is derived depends on the type of data being
|
|
signed and a variety of encoding formats are employed. Each
|
|
format is designed to ensure that they do not collide, so for
|
|
example, a user **cannot** be tricked into signing a message
|
|
which is actually a valid transaction.
|
|
|
|
For this reason, most APIs in Ethereum do not permit signing a
|
|
raw digest, and instead require a separate API for each format
|
|
type and require the related data be specified, protecting the
|
|
user from accidentally authorizing an action they didn't intend.
|
|
|
|
_subsection: Messages @<cookbook-signing-messages>
|
|
|
|
A signed message can be any data, but it is generally recommended
|
|
to use human-readable text, as this is easier for a user to
|
|
verify visually.
|
|
|
|
This technique could be used, for example, to sign into a service
|
|
by using the text ``"I am signing into ethers.org on 2023-06-04 12:57pm"``.
|
|
The user can then see the message in MetaMask or on a Ledger
|
|
Hardware Wallet and accept that they wish to sign the message which
|
|
the site can then authenticate them with. By providing a timestamp
|
|
the site can ensure that an older signed message cannot be used again
|
|
in the future.
|
|
|
|
The format that is signed uses [[link-eip-191]] with the
|
|
**personal sign** version code (``0x45``, or ``"E"``).
|
|
|
|
For those interested in the choice of this prefix, signed messages
|
|
began as a Bitcoin feature, which used ``"\\x18Bitcoin Signed Message:\\n"``,
|
|
which was a Bitcoin var-int length-prefixed string (as ``0x18`` is 24,
|
|
the length of ``"Bitcoin Signed Message:\\n"``.). When Ethereum adopted
|
|
the similar feature, the relevant string was ``"\\x19Ethereum Signed Message:\\n"``.
|
|
|
|
In one of the most brilliant instances of technical retcon-ing,
|
|
since 0x19 is invalid as the first byte of a transaction (in [[link-rlp]] it
|
|
indicates a single byte of value 25), the initial byte ``\\x19`` has
|
|
now been adopted as a prefix for //some sort of signed data//,
|
|
where the second byte determines how to interpret that data. If the
|
|
second byte is 69 (the letter ``"E"``, as in
|
|
``"Ethereum Signed Message:\\n"``), then the format is a
|
|
the above prefixed message format.
|
|
|
|
So, all existing messages, tools and instances using the signed
|
|
message format were already EIP-191 compliant, long before the
|
|
standard existed or was even conceived and allowed for an extensible
|
|
format for future formats (of which there now a few).
|
|
|
|
Anyways, the necessary JavaScript and Solidity are provided below.
|
|
|
|
_code: JavaScript @lang<javascript>
|
|
|
|
// The contract below is deployed to Sepolia at this address
|
|
contractAddress = "0xf554DA5e35b2e40C09DDB481545A395da1736513";
|
|
contract = new Contract(contractAddress, [
|
|
"function recoverStringFromCompact(string message, (bytes32 r, bytes32 yParityAndS) sig) pure returns (address)",
|
|
"function recoverStringFromExpanded(string message, (uint8 v, bytes32 r, bytes32 s) sig) pure returns (address)",
|
|
"function recoverStringFromVRS(string message, uint8 v, bytes32 r, bytes32 s) pure returns (address)",
|
|
"function recoverStringFromRaw(string message, bytes sig) pure returns (address)",
|
|
"function recoverHashFromCompact(bytes32 hash, (bytes32 r, bytes32 yParityAndS) sig) pure returns (address)"
|
|
], new ethers.InfuraProvider("sepolia"));
|
|
|
|
// The Signer; it does not need to be connected to a Provider to sign
|
|
signer = new Wallet(id("foobar"));
|
|
signer.address
|
|
//_result:
|
|
|
|
// Our message
|
|
message = "Hello World";
|
|
|
|
// The raw signature; 65 bytes
|
|
rawSig = await signer.signMessage(message);
|
|
//_result:
|
|
|
|
// Converting it to a Signature object provides more
|
|
// flexibility, such as using it as a struct
|
|
sig = Signature.from(rawSig);
|
|
//_result:
|
|
|
|
|
|
// If the signature matches the EIP-2098 format, a Signature
|
|
// can be passed as the struct value directly, since the
|
|
// parser will pull out the matching struct keys from sig.
|
|
await contract.recoverStringFromCompact(message, sig);
|
|
//_result:
|
|
|
|
// Likewise, if the struct keys match an expanded signature
|
|
// struct, it can also be passed as the struct value directly.
|
|
await contract.recoverStringFromExpanded(message, sig);
|
|
//_result:
|
|
|
|
// If using an older API which requires the v, r and s be passed
|
|
// separately, those members are present on the Signature.
|
|
await contract.recoverStringFromVRS(message, sig.v, sig.r, sig.s);
|
|
//_result:
|
|
|
|
// Or if using an API that expects a raw signature.
|
|
await contract.recoverStringFromRaw(message, rawSig);
|
|
//_result:
|
|
|
|
// Note: The above recovered addresses matches the signer address
|
|
|
|
_null:
|
|
|
|
The Solidity Contract has been deployed and verified on
|
|
the Sepolia testnet at the address
|
|
[0xf554DA5e35b2e40C09DDB481545A395da1736513](link-sol-recovermessage).
|
|
|
|
It provides a variety of examples using various Signature
|
|
encodings and formats, to recover the address for an [[link-eip-191]]
|
|
signed message.
|
|
|
|
_code: Solidity @lang<solidity>
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// For more info, see: https://docs.ethers.org
|
|
|
|
|
|
pragma solidity ^0.8.21;
|
|
|
|
// Returns the decimal string representation of value
|
|
function itoa(uint value) pure returns (string memory) {
|
|
|
|
// Count the length of the decimal string representation
|
|
uint length = 1;
|
|
uint v = value;
|
|
while ((v /= 10) != 0) { length++; }
|
|
|
|
// Allocated enough bytes
|
|
bytes memory result = new bytes(length);
|
|
|
|
// Place each ASCII string character in the string,
|
|
// right to left
|
|
while (true) {
|
|
length--;
|
|
|
|
// The ASCII value of the modulo 10 value
|
|
result[length] = bytes1(uint8(0x30 + (value % 10)));
|
|
|
|
value /= 10;
|
|
|
|
if (length == 0) { break; }
|
|
}
|
|
|
|
return string(result);
|
|
}
|
|
|
|
contract RecoverMessage {
|
|
|
|
// This is the EIP-2098 compact representation, which reduces gas costs
|
|
struct SignatureCompact {
|
|
bytes32 r;
|
|
bytes32 yParityAndS;
|
|
}
|
|
|
|
// This is an expaned Signature representation
|
|
struct SignatureExpanded {
|
|
uint8 v;
|
|
bytes32 r;
|
|
bytes32 s;
|
|
}
|
|
|
|
// Helper function
|
|
function _ecrecover(string memory message, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
|
|
// Compute the EIP-191 prefixed message
|
|
bytes memory prefixedMessage = abi.encodePacked(
|
|
"\x19Ethereum Signed Message:\n",
|
|
itoa(bytes(message).length),
|
|
message
|
|
);
|
|
|
|
// Compute the message digest
|
|
bytes32 digest = keccak256(prefixedMessage);
|
|
|
|
// Use the native ecrecover provided by the EVM
|
|
return ecrecover(digest, v, r, s);
|
|
}
|
|
|
|
// Recover the address from an EIP-2098 compact Signature, which packs the bit for
|
|
// v into an unused bit within s, which saves gas overall, costing a little extra
|
|
// in computation, but saves far more in calldata length.
|
|
//
|
|
// This Signature format is 64 bytes in length.
|
|
function recoverStringFromCompact(string calldata message, SignatureCompact calldata sig) public pure returns (address) {
|
|
|
|
// Decompose the EIP-2098 signature (the struct is 64 bytes in length)
|
|
uint8 v = 27 + uint8(uint256(sig.yParityAndS) >> 255);
|
|
bytes32 s = bytes32((uint256(sig.yParityAndS) << 1) >> 1);
|
|
|
|
return _ecrecover(message, v, sig.r, s);
|
|
}
|
|
|
|
// Recover the address from the expanded Signature struct.
|
|
//
|
|
// This Signature format is 96 bytes in length.
|
|
function recoverStringFromExpanded(string calldata message, SignatureExpanded calldata sig) public pure returns (address) {
|
|
|
|
// The v, r and s are included directly within the struct, which is 96 bytes in length
|
|
return _ecrecover(message, sig.v, sig.r, sig.s);
|
|
}
|
|
|
|
// Recover the address from a v, r and s passed directly into the method.
|
|
//
|
|
// This Signature format is 96 bytes in length.
|
|
function recoverStringFromVRS(string calldata message, uint8 v, bytes32 r, bytes32 s) public pure returns (address) {
|
|
|
|
// The v, r and s are included directly within the struct, which is 96 bytes in length
|
|
return _ecrecover(message, v, r, s);
|
|
}
|
|
|
|
// Recover the address from a raw signature. The signature is 65 bytes, which when
|
|
// ABI encoded is 160 bytes long (a pointer, a length and the padded 3 words of data).
|
|
//
|
|
// When using raw signatures, some tools return the v as 0 or 1. In this case you must
|
|
// add 27 to that value as v must be either 27 or 28.
|
|
//
|
|
// This Signature format is 65 bytes of data, but when ABI encoded is 160 bytes in length;
|
|
// a pointer (32 bytes), a length (32 bytes) and the padded 3 words of data (96 bytes).
|
|
function recoverStringFromRaw(string calldata message, bytes calldata sig) public pure returns (address) {
|
|
|
|
// Sanity check before using assembly
|
|
require(sig.length == 65, "invalid signature");
|
|
|
|
// Decompose the raw signature into r, s and v (note the order)
|
|
uint8 v;
|
|
bytes32 r;
|
|
bytes32 s;
|
|
assembly {
|
|
r := calldataload(sig.offset)
|
|
s := calldataload(add(sig.offset, 0x20))
|
|
v := calldataload(add(sig.offset, 0x21))
|
|
}
|
|
|
|
return _ecrecover(message, v, r, s);
|
|
}
|
|
|
|
// This is provided as a quick example for those that only need to recover a signature
|
|
// for a signed hash (highly discouraged; but common), which means we can hardcode the
|
|
// length in the prefix. This means we can drop the itoa and _ecrecover functions above.
|
|
function recoverHashFromCompact(bytes32 hash, SignatureCompact calldata sig) public pure returns (address) {
|
|
bytes memory prefixedMessage = abi.encodePacked(
|
|
// Notice the length of the message is hard-coded to 32
|
|
// here -----------------------v
|
|
"\x19Ethereum Signed Message:\n32",
|
|
hash
|
|
);
|
|
|
|
bytes32 digest = keccak256(prefixedMessage);
|
|
|
|
// Decompose the EIP-2098 signature
|
|
uint8 v = 27 + uint8(uint256(sig.yParityAndS) >> 255);
|
|
bytes32 s = bytes32((uint256(sig.yParityAndS) << 1) >> 1);
|
|
|
|
return ecrecover(digest, v, sig.r, s);
|
|
}
|
|
}
|
|
|
|
|
|
_subsection: EIP-712 Typed Data @<cookbook-signing-eip712>
|
|
|
|
//Coming soon...//
|