4

I am trying to recreate effect of web3's eth.accounts.sign in PHP and I struggle to find good resources how to do it properly.

I have seen this (found it thanks to this thread) and based on what I figured out, I've put together script, that outputs something but it's definitely invalid signature.

<?php

use kornrunner\Keccak;
use Elliptic\EC;

//used libraries simplito/elliptic-php kornrunner/keccak

require_once "vendor/autoload.php";

$ec = new EC('secp256k1');
$pkey = "cdf6a858b71520aedaf50442a761af94a516a3d0e36f2b5b6c9bcf0bfbe45820";
//$addr = "0x703632A0b52244fAbca04aaE138fA8EcaF72dCBC";

$ecPrivateKey = $ec->keyFromPrivate($pkey, 'hex');

$hash = Keccak::hash('Hello World', 256);

$signature = $ecPrivateKey->sign($hash, ['canonical' => true]);

$r = $signature->r->toString(16);
$s = $signature->s->toString(16);
$v = $signature->recoveryParam + 35;

echo "Signed Hello world is:\n";
echo "Using my script:\n";
echo "0x" . $r . $s . $v . "\n";
echo "Using MEW:\n";
echo "0x2f52dfb196b75398b78c0e6c6aee8dc08d7279f2f88af5588ad7728f1e93dd0a479a710365c91ba649deb6c56e2e16836ffc5857cfd1130f159aebd05377d3a01c\n";

There are some confusing parts of this, I am unsure why 35 is added to recoveryParam and why it's allways 0 from get-go. Also, apparently, chain ID influences V, but this behaviour isn't present in MEW signing. I don't know which order is correct - r s v, or s r v, or v s r, I've seen it used in seemingly random orders (except v is allways last or first), I tried with adding 0x before pkey, and at this point Im out of ideas.

My question is either what am I doing wrong, or where can I learn how to achieve desired effect (without reading tons of JavaScript code that I struggle to understand).

Background: I need this to generate server-side parameter to smart contract (that's signature to other parameters for smart contract) that will be given to user, and smart contract must be able to trust this parameter. Signing it server-side will prevent potentially malicious actor to for example burp proxy the website and mix up said parameters.

2 Answers 2

4

The accepted answer is slightly incorrect. EC library would sometimes (around 1 in 100 in my randomized testing) return ->r or ->s parameters shorter than 64 characters. Which is different from the JS implementation.

So you have to left-zero-pad them:

    $r = str_pad($signature->r->toString(16), 64, '0', STR_PAD_LEFT);
    $s = str_pad($signature->s->toString(16), 64, '0', STR_PAD_LEFT);

Examples of data that show this strange behavior:

$message = '6ef2cd055fde37f0fbd4f33555f667c150765693fafe04744e7eb3e33b42a41c';
$pkey = '0x3b0bf03e36b60dfffe21e894df22af79162549999c864963b5dce06867a32315';

or

$message = '19a1a0769a2baa13dbc180549039650e66e5735ba749036db447b256305df34c';
$pkey = '0x0a451782bdd735677c2a4e28fcf925a0a8059570485e148f22c7f5b940ad4d19';
0
3

So method was in the most part correct, but I was wrong in 2 parts. First, v is not +35, but it's +27, and it's supposed to be changed to hexes (hexdec()).

More importantly, ethereum signed message must contain something that resembles "magic bytes" - it's

$message = "\x19Ethereum Signed Message:\n" . strlen($message) . $message;

So working and complete code is:

<?php

use kornrunner\Keccak;
use Elliptic\EC;

require_once "vendor/autoload.php";

$ec = new EC('secp256k1');
$pkey = "0xcdf6a858b71520aedaf50442a761af94a516a3d0e36f2b5b6c9bcf0bfbe45820";
//$addr = "0x703632A0b52244fAbca04aaE138fA8EcaF72dCBC";

$ecPrivateKey = $ec->keyFromPrivate($pkey, 'hex');

$message = "Hello World";

$message = "\x19Ethereum Signed Message:\n" . strlen($message) . $message;

$hash = Keccak::hash($message, 256);

$signature = $ecPrivateKey->sign($hash, ['canonical' => true]);


$r = str_pad($signature->r->toString(16), 64, '0', STR_PAD_LEFT);
$s = str_pad($signature->s->toString(16), 64, '0', STR_PAD_LEFT);
$v = dechex($signature->recoveryParam + 27);

echo "Signed Hello world is:\n";
echo "Using my script:\n";
echo "0x$r$s$v\n";
echo "Using MEW:\n";
echo "0x2f52dfb196b75398b78c0e6c6aee8dc08d7279f2f88af5588ad7728f1e93dd0a479a710365c91ba649deb6c56e2e16836ffc5857cfd1130f159aebd05377d3a01c\n";

Now It can be validated by simple smart contract.

EDIT: As requested in comment, Im including solidity code that validates arbitrary signature. It's much shortened version of orginal code that I used when asking this question, so it uses a little bit older version of libraries and solidity itself.

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/utils/Strings.sol";
import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/cryptography/ECDSA.sol";

contract SigTest {
    address _owner;

    constructor(address owner) public {
        _owner = owner;
    }

    function IsSigValid (string memory message, bytes memory signature) public view returns(bool) {
        return _owner == ECDSA.recover(
            keccak256(
                abi.encodePacked(
                    "\x19Ethereum Signed Message:\n",
                    Strings.toString(bytes(message).length),
                    message
                )
            ),
            signature
        );
    }
}

4
  • 1
    Thank you for this answer... and question! Would you mind adding example Solidity code for validating the signed message in a smart contract?
    – matteo
    Mar 27, 2022 at 16:27
  • 1
    @matteo sure, I edited the anwser Mar 28, 2022 at 21:12
  • 1
    Wouldn't it work just the same if in the PHP code you removed this: $message = "\x19Ethereum Signed Message:\n" . strlen($message) . $message;, and in the Solidity code just did abi.encodePacked(message) ?
    – matteo
    Apr 9, 2022 at 21:13
  • 1
    @matteo yes, it would work the same, although it would be not standard signing. This way you propose is "less correct", although objectively better (less gas usage). And fun fact, contract I deployed when having this problem, actually works this non-standard way Apr 11, 2022 at 13:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.