simplito / elliptic-php

Fast, general Elliptic Curve Cryptography library. Supports curves used in Bitcoin, Ethereum and other cryptocurrencies (secp256k1, ed25519, ..)
MIT License
220 stars 52 forks source link

Ethereum Signature failing #37

Closed kakashigr closed 2 years ago

kakashigr commented 2 years ago

I'm trying to use your example "Verifying Ethereum Signature"

In the client, using ethers.js I used this code:

    const Signer = await provider.getSigner();
    const userAddress = await Signer.getAddress();
    const numTokens = await ContractUser.balanceOf(userAddress);
    let currentTime = new Date();
    let month = currentTime.getMonth() + 1;
    let day = currentTime.getDate();
    let year = currentTime.getFullYear();
    let msgToSign = 'Connect: ' + userAddress + ' on ' + year + month + day;
    let messageHash = ethers.utils.solidityKeccak256(['string'],[msgToSign]);
    let signature = await Signer.signMessage(ethers.utils.arrayify(messageHash));

And then to verify it

    let recovered = ethers.utils.verifyMessage(ethers.utils.arrayify(messageHash), signature);

which works, it correctly returns the userAddress.

But then, using your example to verify the signature in php, I get a different address. I noticed the messageHash is different than the hash calculated in the php. When I removed the $message prefix (string and length) and just hash the message as is, I get the same hash as in javascript. However, in both cases, the address returned is different.

Can you help me?

ldudzsim commented 2 years ago

Hello, please paste here your js code and php code, with example signature and address, so we can try to reproduce your problem.

ldudzsim commented 2 years ago

In your js code you unnecessary hash the message before signing/verifying. Check the code below, it returns the same address in both js and php:

<?php
// composer require simplito/elliptic-php && composer require kornrunner/keccak

require_once("vendor/autoload.php");

use Elliptic\EC;
use kornrunner\Keccak;

function pubKeyToAddress($pubkey) {
    return "0x" . substr(Keccak::hash(substr(hex2bin($pubkey->encode("hex")), 1), 256), 24);
}

function recoverAddress($message, $signature) {
    $msglen = strlen($message);
    $hash   = Keccak::hash("\x19Ethereum Signed Message:\n{$msglen}{$message}", 256);
    $sign   = ["r" => substr($signature, 2, 64),
               "s" => substr($signature, 66, 64)];
    $recid  = ord(hex2bin(substr($signature, 130, 2))) - 27;
    if ($recid != ($recid & 1))
        return false;

    $ec = new EC('secp256k1');
    $pubKey = $ec->recoverPubKey($hash, $sign, $recid);
    return pubKeyToAddress($pubKey);
}

$message   = "I like signatures";
$signature = "0xacb175089543ac060ed48c3e25ada5ffeed6f008da9eaca3806e4acb707b9481401409ae1f5f9f290f54f29684e7bac1d79b2964e0edcb7f083bacd5fc48882e1b";

$address = recoverAddress($message, $signature);
echo "Address: " . $address . "\n"; // prints 0x5a214a45585b336a776b62a3a61dbafd39f9fa2a

and for js:

// npm i ethers
const ethers = require("ethers");
const message = Buffer.from("I like signatures", "utf8")
const signature = "0xacb175089543ac060ed48c3e25ada5ffeed6f008da9eaca3806e4acb707b9481401409ae1f5f9f290f54f29684e7bac1d79b2964e0edcb7f083bacd5fc48882e1b";
const address = ethers.utils.verifyMessage(message, signature);
console.log("Address: " + address); // prints 0x5a214a45585b336a776b62a3a61dbafd39f9fa2a
kakashigr commented 2 years ago

Thank you very much, this is indeed working!

The only problem I found is that pubKeytoAddress() function returns all lower-case so I need it to run it through a getChecksumAddress() function:

    public static function getChecksumAddress(string $address): string {

        $address = substr($address, 2);
        $addressHash = Keccak::hash(strtolower($address), 256);
        $addressArray = str_split($address);
        $addressHashArray = str_split($addressHash);

        $ret = '';
        for ($i = 0; $i < 40; $i++) {
            // the nth letter should be uppercase if the nth digit of casemap is 1
            if (intval($addressHashArray[$i], 16) > 7) {
                $ret .= strtoupper($addressArray[$i]);
            } else /*if (intval($addressHashArray[$i], 16) <= 7)*/ {
                $ret .= strtolower($addressArray[$i]);
            }
        }

        return '0x' . $ret;

    }