Signing
Certain actions – which go to the blockchain – need to be signed by the private key. As described elsewhere, the signer first needs to obtain a challenge:
curl -s -L -X POST "https://cargox.digital/api/v3/blockchain/login-challenge/"
Response:
{ "challenge": "1ZVoa3m5gtZE6veXQQOGKEpRG6M" }
This challenge is then signed using SHA-3 and user's private key. Below are examples in different languages.
- JavaScript
- PHP
- Go
- C#
The easiest way to sign the message is using the Web3 javascript library.
The following command is used to invoke the private key container (e.g. a hardware wallet, MetaMask or in-memory private key):
web3.eth.personal.sign(
web3.utils.sha3(challenge_string),
accounts[0],
function (e, signature) { ... }
)
You can find a working example of signing a message using a Trezor hardware wallet at their GitHub page.
What follows is a simple project to sign the messages with PHP.
Create Files
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"
}
}
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);
}
?>
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());
}
?>
Install Dependencies
Execute the following command:
compose require -v
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:
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)
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"
The following file should give you a quick introduction into how the messages are signed with Go.
Create Files
sign.go
package main
import (
"crypto/ecdsa"
"encoding/hex"
"fmt"
"github.com/ethereum/go-ethereum/crypto"
"golang.org/x/crypto/sha3"
"os"
"strconv"
"strings"
)
// ---- Utility functions ----
// strip0x remove the 0x prefix, which is not important to us
func strip0x(s string) string {
if strings.HasPrefix(s, "0x") {
s = s[2:]
}
return s
}
// ---- Main functions ----
func Sign(hash []byte, privateKey string) (string, error) {
var key *ecdsa.PrivateKey
var bytes []byte
if b, err := hex.DecodeString(strip0x(privateKey)); err != nil {
return "", err
} else {
bytes = b
}
if pk, err := crypto.ToECDSA(bytes); err != nil {
return "", err
} else {
key = pk
}
if sig, err := crypto.Sign(hash, key); err != nil {
return "", err
} else {
return "0x" + hex.EncodeToString(sig), nil
}
}
func SignHash(message []byte, privateKey string) (string, error) {
msglen := []byte(strconv.Itoa(len(message)))
hash := sha3.NewLegacyKeccak256()
hash.Write([]byte{0x19})
hash.Write([]byte("Ethereum Signed Message:"))
hash.Write([]byte{0x0A})
hash.Write(msglen)
hash.Write(message)
buf := hash.Sum([]byte{})
return Sign(buf, privateKey)
}
func SignChallenge(challenge, privateKey string) (string, error) {
hash := sha3.NewLegacyKeccak256()
hash.Write([]byte(challenge))
buf := hash.Sum([]byte{})
return SignHash(buf, privateKey)
}
func main() {
// Remove executable name from arguments
args := os.Args[1:]
// Just take the parameters. In real-live scenario, you would probably want to read the
// private key from stdin and do some parameter validation.
challenge := args[0]
privateKey := args[1]
// Quick and dirty way of doing it. You probably don't want to call panic in your code, though.
if result, err := SignChallenge(challenge, privateKey); err != nil {
panic(err)
} else {
if _, err := fmt.Fprintf(os.Stdout, "%v", result); err != nil {
panic(err)
}
}
}
Install Dependencies
Execute the following command:
go get ./...
Test Signing
Execute the following command:
go run sign.go u3Z5LUDVp31tMtKtwCGr1FCs3kw a0f0e1232dae3ce8a0bf968c452602663975005537e37e79d28c23af52b3114e
Example output:
0x2a2785be5d38ba773765df2232d749b01c4ebc97721f9d033a696b7f893ba45d20bccf4341ae6b01a8f697e22e1e4b4697e02e04352363bee1eeaa9494e096cb01
The following code shows how a challenge may be signed using C#.
Obtaining a Challenge
Firstly, the user needs to obtain a challenge string:
...
static HttpClient client = new HttpClient();
//specify to use TLS 1.2 as default connection
System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
client.BaseAddress = new Uri("https://app.smartbl.io/");
// Add an Accept header for JSON format.
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
...
public static string GetChallenge() {
HttpResponseMessage response = await client.GetAsync("https://app.smartbl.io/api/v1/challenge/");
response.EnsureSuccessStatusCode();
challengeresponse = await response.Content.ReadAsAsync<ChallengeResponse>();
return challengeresponse.challenge_string;
}
Signing the Challenge
This challenge is then signed using the SHA-3 and user's private key. The following method will sign a challenge using C#.
This use case expects the system to have full access to the private key. This means that the user will need to submit the key to the application. This has two security implications:
- The key might be compromised if the application or transfer path is not secure
- It's not possible to use hardware keys as they by default do not allow the private key to be exported in any way
This approach is the most appropriate if you decide to store the private keys on behalf of your users in your database. Either way make sure that private keys are never stored anywhere in plain text -- always encrypt the data before storage.
public static string SignChallenge(string challenge, string privateKey) {
var hasher = new Sha3Keccack();
var hash = hasher.CalculateHash(challenge);
return SignHash(hash, privateKey);
}
public static string SignHash(string hash, string privateKey) {
var signer = new MessageSigner();
var hasher = new Sha3Keccack();
var byteList = new List<byte>();
var bytePrefix = "0x19".HexToByteArray();
var textBytePrefix = Encoding.UTF8.GetBytes("Ethereum Signed Message:\n" + hash.HexToByteArray().Length);
var bytesMessage = hash.HexToByteArray();
byteList.AddRange(bytePrefix);
byteList.AddRange(textBytePrefix);
byteList.AddRange(bytesMessage);
var hashPrefix2 = hasher.CalculateHash(byteList.ToArray()).ToHex();
return signer.Sign(hashPrefix2.HexToByteArray(), privateKey);
}