10.7. Signing a message with PHP

What follows is a simple project to sign the messages with PHP. The following example shows you how to execute a simple signing of transaction with PHP.

10.7.1. Create files

Listing 10.2 composer.json
{
  "name": "cargox/php-signer",
  "description": "Example of singing Ethereum messages with PHP",
  "authors": [
    {
      "name": "CargoX"
    }
  ],
  "minimum-stability": "dev",
  "prefer-stable": true,
  "require": {
    "php": ">=5.5",
    "kornrunner/keccak": ">=1.0.0",
    "kornrunner/secp256k1": ">=0.1.2",
    "pear/console_commandline": ">=1.2.2"
  }
}
Listing 10.3 sign/sign.php
<?
declare(strict_types=1);
error_reporting(E_ALL ^ E_WARNING);
require __DIR__ . '/../vendor/autoload.php';

use kornrunner\Keccak;
use kornrunner\Secp256k1;

// =============================================================================================
// Sanity check
// =============================================================================================
$emptyHashActual   = Keccak::hash("", 256);
$emptyHashExpected = "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470";
if ($emptyHashActual != $emptyHashExpected) {
    throw new Exception('Expected: ' . $emptyHashExpected . '\nGot:      ' . $emptyHashActual);
}


// =============================================================================================
// Actual methods that sign the message
// =============================================================================================
/**
 * Sign the given message using secp256k1 ESCDA and given private key.
 * (Key and message must be given in HEX format).
 */
function Sign($message, $privateKey, $verbose = False) {
    $secp256k1 = new Secp256k1();

    if ($verbose) {
        echo "Sign(message):            $message\r\n";
    }

    // return signature contains r, s and recovery param (v).
    // message and privateKey are hex strings
    $signature = $secp256k1->sign($message, $privateKey, [
        "canonical" => true,
        "n" => null,
    ]);
    $result = "0x" . $signature->toHex();

    // kornrunner library does not add the recovery param (recid) at the end, so we must do it ourselves
    // This hack with 0 is ok, as recovery param is always between 0 and 3
    $result = $result . "0" . dechex($signature->getRecoveryParam());

    return $result;
}

/**
 * Create an Ethereum message signature and sign it using given private key.
 * (Key must be given in HEX format) and message must be in binary format.
 */
function SignHash($message, $privateKey, $verbose = False) {
    if ($verbose) {
        echo "SignHash(message):        " . bin2hex($message) . "\r\n";
    }
    $msglen = strlen($message);
    $msg    = hex2bin("19") . "Ethereum Signed Message:" . hex2bin("0A") . $msglen . $message;
    $hash   = Keccak::hash($msg, 256);
    return    Sign($hash, $privateKey, $verbose);
}

/**
 * Sign the challenge. Takes two parameters -- the challenge (usually a normal, text-based string),
 * and the private key (private key must be in hex format, without the staring `0x`).
 * Returns the message signature in hex-encoded string.
 *
 * Example call:
 * SignChallenge("u3Z5LUDVp31tMtKtwCGr1FCs3kw", "a0f0e1232dae3ce8a0bf968c452602663975005537e37e79d28c23af52b3114e")
 *
 * Returns:
 * "0x2a2785be5d38ba773765df2232d749b01c4ebc97721f9d033a696b7f893ba45d20bccf4341ae6b01a8f697e22e1e4b4697e02e04352363bee1eeaa9494e096cb"
 */
function SignChallenge($challenge, $privateKey, $verbose = False) {
    if ($verbose) {
        echo "SignChallenge(challenge): $challenge \r\n";
    }
    $hash = Keccak::hash($challenge, 256);
    $hash = hex2bin($hash);
    return  SignHash($hash, $privateKey, $verbose);
}

?>
Listing 10.4 sign/demo.php
<?
declare(strict_types=1);
error_reporting(E_ALL ^ E_WARNING);
require_once 'sign.php';


// =============================================================================================
// Example usage
// =============================================================================================

$parser = new Console_CommandLine();
$parser->description = 'A simple tool to sign Ethereum challenges and hashes.';
$parser->version = '1.0.0';

// add an option to make the program verbose
$parser->addOption('verbose', array(
    'short_name'  => '-v',
    'long_name'   => '--verbose',
    'action'      => 'StoreTrue',
    'description' => 'turn on verbose output'
));

$parser->addOption('privateKey', array(
    'short_name'  => '-p',
    'long_name'   => '--private-key',
    'description' => 'Hex-encoded key for signature',
    'help_name'   => 'PRIVATE_KEY',
    'action'      => 'StoreString'
));

$parser->addOption('method', array(
    'short_name'  => '-x',
    'long_name'   => '--method',
    'description' => 'Method to use',
    'list'        => array('challenge', 'hash', 'message'),
    'help_name'   => 'METHOD',
    'action'      => 'StoreString'
));


$parser->addOption('message', array(
    'short_name'  => '-m',
    'long_name'   => '--message',
    'description' => 'Message to sign. Either a free text message (for challenge) or HEX encoded message',
    'help_name'   => 'MESSAGE',
    'action'      => 'StoreString'
));

try {
    $result = $parser->parse();
    $privateKey = $result->options['privateKey'];
    $verbose = $result->options['verbose'];
    $method = $result->options['method'];
    $message = $result->options['message'];
    if ($method) {
        if ($method == 'challenge') {
            echo SignChallenge($message, $privateKey, $verbose) . "\n";
        } else if ($method == 'hash') {
            if (strpos($message, '0x') === 0) {
                // Cut the start off
                $message = substr($message, 2, strlen($message));
             }
            $message = hex2bin($message);
            echo SignHash($message, $privateKey, $verbose) . "\n";
        } else if ($method == 'message') {
            if (strpos($message, '0x') === 0) {
                // Cut the start off
                $message = substr($message, 2, strlen($message));
             }
            echo Sign($message, $privateKey, $verbose) . "\n";
        }
    }
} catch (Exception $exc) {
    $parser->displayError($exc->getMessage());
}

?>

10.7.2. Install dependencies

Execute the following command:

compose require -v

Note

Depending on your OS, you'll also need to install sources for PHP's extract and install PHP's gmp extension.

If you're using the Docker composer image this can be done with the following Dockerfile:

Listing 10.5 Dockerfile
FROM composer:latest

RUN mkdir -p /usr/src/php-signer/sign
COPY composer.json /usr/src/php-signer
RUN true && \
    apk add --update gmp && \
    apk add --virtual build-dependencies --update build-base gmp-dev && \
    docker-php-source extract && \
    docker-php-ext-install gmp && \
    cd /usr/src/php-signer && composer require -v && \
    docker-php-source delete && \
    apk del build-dependencies && (rm -rf /var/cache/apk/* || true)

10.7.3. Test signing

If using Dockerfile simply do:

docker build . -t cargox/php-signer
docker run -it --rm -w /usr/src/php-signer cargox/php-signer \
   php sign/demo.php \
      --verbose \
      --private-key "a0f0e1232dae3ce8a0bf968c452602663975005537e37e79d28c23af52b3114e" \
      --method challenge \
      --message "u3Z5LUDVp31tMtKtwCGr1FCs3kw"

or, without docker:

php sign/demo.php \
   --verbose \
   --private-key "a0f0e1232dae3ce8a0bf968c452602663975005537e37e79d28c23af52b3114e" \
   --method challenge \
   --message "u3Z5LUDVp31tMtKtwCGr1FCs3kw"