I have the following code in an Angular app:
ethers.Wallet.fromEncryptedJson(encrypted_wallet.wallet_as_json, this.user_password).then(function(wallet) {
const message = ethers.utils.solidityKeccak256(
['string', 'bytes32', 'string', 'address'],
['\x19Ethereum Signed Message:\n32', '0x' + __this.register_hash, __this.register_description, wallet.address]);
console.log('message: ' + message);
/*const arrayified_message = ethers.utils.arrayify(message);
console.log('message: ' + arrayified_message);*/
wallet.signMessage(message).then( (signed_msg) => {
console.log(signer: ethers.utils.recoverAddress(message, signed_msg));
...
the last console log never outputs the correct address of the private key that signed the message
Your length is wrong in your prefixed message. It isn鈥檛 32 bytes long, which you have hardcoded into the string.
You should try using ethers.utils.hashMessage(ethers.utils.concat([ hash, string, address ]). Also you will need to make sure your Solidity hashing matches, which based on that format will require assembly, since you cannot set the payload length in the preamble otherwise.
It may be a good idea to drop the string from the message too; signing time becomes non-static, and if you have 2 strings, there are security implications, so it鈥檚 just safer to not include strings, in that way, at least.
@ricmoo Sorry, that snippet is misleading, this is what I'm actually doing (so hard-coding 32bytes into the prefix should be right, if the prefix is required at all in the new snippet below):
const message = ethers.utils.solidityKeccak256(
['bytes32', 'string', 'address'],
['0x' + __this.register_hash, __this.register_description, wallet.address]);
console.log('message: ' + message);
const message = ethers.utils.solidityKeccak256(
['string', 'string'],
['x19Ethereum Signed Message:n32', message]);
console.log('message: ' + message);
Two things:
Edit: also this snippet doesn't work... (taken from here: https://docs.ethers.io/ethers.js/html/cookbook-signing.html)
const message1 = '0x' + __this.register_hash;
const message2 = ethers.utils.arrayify(message1);
wallet.signMessage(message2).then( (signed_msg) => {
console.log('address: ' + ethers.utils.recoverAddress(message1, signed_msg));
Heya!
First I'll answer your second question. Basically any combination of a signature and a digest hash will produce a valid public key (from which we compute the address). But, if you pass in the wrong value for either, you will still get some address, but it won't be the address you expected.
The instance method walelt.signMessage will automatically prefix the message for you. This is required for security reasons. But Solidity does not. Here is an example of how you need to sign messages (with both Solidity and ethers): https://blog.ricmoo.com/verifying-messages-in-solidity-50a94f82b2ca
Also, keep in mind that signMessage can take in a string, which is treated as a UTF-8 string, or an ArrayLike, which is treated like binary data. A hash as a string is a 66 character string, which is likely not what you want, you probable want the 32 byte array. So you probably want something more like:
// 66 byte string, which represents 32 bytes of data
let messageHash = ethers.utils.solidityKeccak256( ...stuff here... );
// 32 bytes of data in Uint8Array
let messageHashBinary = ethers.utils.arrayify(messageHash);
// To sign the 32 bytes of data, make sure you pass in the data
let signature = await wallet.signMessage(messageHashBinary);
Thank you for your time and interest in assisting me, however, I am still running circles here...
I had already read your blog post (and your docs), let's put all those solidity keccak hashes aside for now and let us go back to ethers' own documentation; especially, here you write:
// The hash we wish to sign and verify
let messageHash = ethers.utils.id("Hello World");
// Note: messageHash is a string, that is 66-bytes long, to sign the
// binary value, we must convert it to the 32 byte Array that
// the string represents
//
// i.e.
// // 66-byte string
// "0x592fa743889fc7f92ac2a37bb1f5ba1daf2a5c84741ca0e0061d243a2e6707ba"
//
// ... vs ...
//
// // 32 entry Uint8Array
// [ 89, 47, 167, 67, 136, 159, 199, 249, 42, 194, 163,
// 123, 177, 245, 186, 29, 175, 42, 92, 132, 116, 28,
// 160, 224, 6, 29, 36, 58, 46, 103, 7, 186]
let messageHashBytes = ethers.utils.arrayify(messageHash)
// Sign the binary data
let flatSig = await wallet.signMessage(messageHashBytes);
can you show me how to correctly recover the signer address from your snippet above? I am asking this because I can't even get this test-snippet of mine to work:
const message1 = ethers.utils.id('Hello World');
const message_bytes = ethers.utils.arrayify(message1);
wallet.signMessage(message_bytes).then( (signed_msg) => {
console.log('address: ' + ethers.utils.recoverAddress(message1 || message_bytes, signed_msg));
...
Oh, recoverAddress (along with recoverPublicKey) is a low-level API for people that need to recover the address from a digest. The API you are likely interested in is ethers.utils.verifyMessage(message):
let recovered = ethers.utils.verifyMessage(messageHashBytes, flatSig);
console.log(recovered === wallet.address);
// true
Good Sunday,
well, ethers.utils.verifyMessage(msg, signed_msg) gives back wallet.address indeed, at least that's something.
So, I should probably have also mentioned that the contract's code is the following:
function registerOnBehalfOf(bytes32 hash, string memory description, address signer, bytes memory signature) public {
bytes32 message = this.ethSignedRegistration(hash, description); // require eth_sign behavior
address actualSigner = recoverSigner(message, signature);
require(actualSigner != address(0), "wrong signature");
require(actualSigner == signer, "wrong signer");
_register(hash, description, actualSigner);
}
function ethSignedRegistration(bytes32 hash, string memory description) public view returns (bytes32)
{
bytes32 messageHash = keccak256(abi.encodePacked(hash, description, address(this)));
return ethSignedMessage(messageHash);
}
function ethSignedMessage(bytes32 messageHash) public pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); // eth.sign behavior
}
// internals
function splitSignature(bytes memory sig) internal pure returns (uint8, bytes32, bytes32)
{
require(sig.length == 65);
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(sig, 32))
s := mload(add(sig, 64))
v := byte(0, mload(add(sig, 96)))
}
return (v, r, s);
}
function recoverSigner(bytes32 message, bytes memory sig) internal pure returns (address)
{
if (sig.length != 65) {
return (address(0));
}
uint8 v;
bytes32 r;
bytes32 s;
(v, r, s) = splitSignature(sig);
if (v < 27) {
v += 27;
}
if (v != 27 && v != 28) {
return (address(0));
}
return ecrecover(message, v, r, s);
}
With truffle, I am using ethereumjs-abi to pack/hash the abi and web3 to sign it:
var message = "0x" + ethereumjs_abi.soliditySHA3(
["bytes32", "string", "address"],
[hash, description1, instance.address]).toString("hex");
let signature = await web3.eth.sign(signer, message);
const message2 = ethers.utils.solidityKeccak256 (
['bytes32', 'string', 'address'],
[hash, description1, instance.address]);
...and everything works fine (the contract is able to recover the correct signer), however, that lib doesn't work in the browser (nor web3-eth-abi); so this is my code in the front-end (I added web3 too for comparison with ethers):
ethers.Wallet.fromEncryptedJson(encrypted_wallet.wallet_as_json, user_password).then(function(wallet) {
const message = ethers.utils.solidityKeccak256(
['bytes32', 'string', 'address'],
['0x' + __this.register_hash, __this.register_description, wallet.address]
);
// 32 bytes of data as Uint8Array
const message_binary = ethers.utils.arrayify(message);
const web3_signed_msg = __this.web3.eth.accounts.sign(message, wallet.privateKey).signature;
wallet.signMessage(message_binary).then( (signed_msg) => {
console.log('wallet.address == wallet.signMessage(): ' + (wallet.address === ethers.utils.verifyMessage(message_binary, signed_msg))); // true
console.log('wallet.address == web3.eth.accounts.sign(): ' + (wallet.address === ethers.utils.verifyMessage(message_binary, web3_signed_msg))); // true
...
As you know (and can see from the contract code), I just need to be able to pack the abi and sign its digest in a way that allows me to successfully use ethers.utils.recoverSigner() so that I know the contract will be able to as well...
Really appreciate all your patience
So, a quick note regarding your signature recovery is that it will be much more expensive than it needs to be, using bytes; instead just use bytes32 and a uint8, which also has the benefit of not requiring assembly which improves readability and reduces possible security issues. Here is a quick sample I made base on your code (deployed and tested on Ropsten; see the source for the address):
Solidity Contract:
contract TestSign {
event Registerd(bytes32 hash, string description, address signer);
function registerOnBehalfOf(bytes32 hash, string memory description, address signer, uint8 v, bytes32 r, bytes32 s) public {
// Notice; NOT using non-standard encoding as this is more secure
bytes32 payloadHash = keccak256(abi.encode(hash, description));
// If you *REALLY* want to use non-standard encoding, which will
// only save 6 gas and add potential security issues in the future
// versions of this contract.
//bytes32 payloadHash = keccak256(abi.encodePacked(hash, description));
bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", payloadHash));
address actualSigner = ecrecover(messageHash, v, r, s);
// Not that this step is acutally redundant; you could remove
// signer from the signature and skip this `require` entirely
require(signer == actualSigner);
_register(hash, description, actualSigner);
}
function _register(bytes32 hash, string memory description, address signer) public {
emit Registerd(hash, description, signer);
}
}
JavaScript:
const ethers = require("ethers");
const provider = ethers.getDefaultProvider("ropsten");
const wallet = ethers.Wallet.fromMnemonic(mnemonic).connect(provider);
console.log("Wallet:", wallet.address);
const address = "0xC5c38Dc1e7270DDB681BDbDc0D3c8043bf1Ac1b9";
const abi = [
"event Registerd(bytes32 hash, string description, address signer)",
"function registerOnBehalfOf(bytes32 hash, string description, address signer, uint8 v, bytes32 r, bytes32 s) public"
];
const contract = new ethers.Contract(address, abi, wallet);
let someHash = "0x0123456789012345678901234567890123456789012345678901234567890123";
let someDescr = "Hello World!";
(async function() {
let payload = ethers.utils.defaultAbiCoder.encode([ "bytes32", "string" ], [ someHash, someDescr ]);
console.log("Payload:", payload);
let payloadHash = ethers.utils.keccak256(payload);
console.log("PayloadHash:", payloadHash);
// See the note in the Solidity; basically this would save 6 gas and
// can potentially add security vulnerabilities in the future
// let payloadHash = ethers.utils.solidityKeccak256([ "bytes32", "string" ], [ someHash, someDescr ]);
// This adds the message prefix
let signature = await wallet.signMessage(ethers.utils.arrayify(payloadHash));
let sig = ethers.utils.splitSignature(signature);
console.log("Signature:", sig);
console.log("Recovered:", ethers.utils.verifyMessage(ethers.utils.arrayify(payloadHash), sig));
let tx = await contract.registerOnBehalfOf(someHash, someDescr, wallet.address, sig.v, sig.r, sig.s);
console.log("Transaction:", tx.hash);
let receipt = await tx.wait();
console.log("Receipt Status:", receipt.status);
receipt.events.forEach((event) => {
console.log("Event:", event.eventSignature, event.args);
});
})().then(() => {
console.log("done");
});
Output (trimmed some useless things out):
Wallet: 0x25Fdb3C8926091d47760CAb1c4C2b8299b8095e8
Payload: 0x01234567890123456789012345678901234567890123456789012345678901230000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000
PayloadHash: 0xa1382ed7e01ff69cec5538a443f2c84e80c9c4f4c999408e1862edb7e2bfd7cc
Signature: {
r: '0xb3a9b3b9fb706618a6608aafceb11fa2fb28dbc6b003df22de2c1177f546be23',
s: '0x24b8ad3defe80ab2e1c2f5882480ca118d57f868408e6a2991b1eb0acd4f567d',
recoveryParam: 0,
v: 27 }
Recovered: 0x25Fdb3C8926091d47760CAb1c4C2b8299b8095e8
Transaction: 0x993e1a2e56ce260f7cfb5e04db9010ecb4586eb13ae5901c777ae5acfc6d2587
Receipt Status: 1
Event: Registerd(bytes32,string,address) {
hash: '0x0123456789012345678901234567890123456789012345678901234567890123',
description: 'Hello World!',
signer: '0x25Fdb3C8926091d47760CAb1c4C2b8299b8095e8',
length: 3
}
done
Notice in the final event the correct signer was returned. This should be enough to get you on your way. :)
That said, in v5 I'm exposing compressed signatures, which are even cheaper and easier to use on the JavaScript side. Here is the draft EIP.
thank you a 1000 times for taking the time to write & test code ad hoc for my needs, I will definitely go your route now; and good luck with the EIP
Oh, one last small thing; in the JS code, wasn't it possible to use recoverAddress instead of verifyMessage, here:
console.log("Recovered:", ethers.utils.verifyMessage(ethers.utils.arrayify(payloadHash), sig));
No, the verifyMessage prefixes the message, and then hashes that payload. recoverAddress, is very low-level, and does not do any additional processing on the input. It is useful when you have a raw digest to use ecrecover on, such as a pre-hash for a transaction or an un-prefixed message from old faulty non-standards-compliant clients.
Basically, most people should never require recoverAddress, using it is generally an indication you are doing something dangerous. :)
The code here, you can see is quite simple otherwise though. :)
I think this is all figure out now, so I'm going to close it. If you have more issues though, please feel free to re-open or continue commenting (I monitor closed issues).
Thanks! :)
Most helpful comment
So, a quick note regarding your signature recovery is that it will be much more expensive than it needs to be, using bytes; instead just use bytes32 and a uint8, which also has the benefit of not requiring assembly which improves readability and reduces possible security issues. Here is a quick sample I made base on your code (deployed and tested on Ropsten; see the source for the address):
Solidity Contract:
JavaScript:
Output (trimmed some useless things out):
Notice in the final event the correct signer was returned. This should be enough to get you on your way. :)
That said, in v5 I'm exposing compressed signatures, which are even cheaper and easier to use on the JavaScript side. Here is the draft EIP.