Signing Data for Smart Contracts
Smart contracts calls already provide a built-in authentication mechanism as transactions (i.e. call operations) are cryptographically signed by the sender of the transaction. This is a guarantee on which programs can rely.
However, sometimes you may want more involved or flexible authentication schemes. The ones that rely on signature validity checking can be implemented in Michelson, and Liquidity provide a built-in instruction to do so. (You still need to keep in mind that you cannot store unencrypted confidential information on the blockchain).
This instruction is
Crypto.check in Liquidity. Its type can be written as:
Crypto.check: key -> signature -> bytes -> bool
Which means that it takes as arguments a public key, a signature and a sequence of bytes and returns a Boolean.
Crypto.check pub_key signature message is
true if and only if the signature
signature was obtained by signing the Blake2b hash of
message using the private key corresponding to the public key
A small smart contract snippet which implements a signature check (against a predefined public key kept in the smart contract's storage) can be tested online here.
type storage = key let%entry main ((message : string), (signature : signature)) key = let bytes = Bytes.pack message in if not (Crypto.check key signature bytes) then failwith "Wrong signature"; ( : operation list), key
This smart contract fails if the string
message was not signed with the private key corresponding to the public key
key stored. Otherwise it does nothing.
This signature scheme is more flexible than the default transaction/sender one, however it requires that the signature can be built outside of the smart contract. (And more generally outside of the toolset provided by Liquidity and Tezos). On the other hand, signing a transaction is something you get for free if you use the tezos client or any tezos wallet (as is it essentially their base function).
The rest of this blog post will focus on various ways to sign data, and on getting signatures that can be used in Tezos and Liquidity directly.
Signing Using the Tezos Client
One (straightforward) way to sign data is to use the Tezos client directly. You will need to be connected to a Tezos node though as the client makes RPCs to serialize data (this operation is protocol dependent). We can only sign sequences of bytes, so the first thing we need to do is to serialize whichever data we want to sign. This can be done with the command
hash data of the client.
$ ./tezos-client -A alphanet-node.tzscan.io -P 80 hash data '"message"' of type string Raw packed data: 0x0501000000076d657373616765 Hash: exprtXaZciTDGatZkoFEjE1GWPqbJ7FtqAWmmH36doxBreKr6ADcYs Raw Blake2b hash: 0x01978930fd2d04d0db8c2e4ef8a3f5d63b8e732177c8723135ed0dc7d99ebed3 Raw Sha256 hash: 0x32569319f6517036949bcead23a761bfbfcbf4277b010355884a86ba09349839 Raw Sha512 hash: 0xdfa4ea9f77db3a98654f101be1d33d56898df40acf7c2950ca6f742140668a67fefbefb22b592344922e1f66c381fa2bec48aa47970025c7e61e35d939ae3ca0 Gas remaining: 399918 units remaining
This command gives the result of hashing the data using various algorithms but what we're really interested in is the first item
Raw packed data which is the serialized version of our data (
We can now sign these bytes using the Tezos client as well. This step can be performed completely offline, for that we need to use the option
-p of the client to specify the protocol we want to use (the
sign bytes command will not be available without first selecting a valid protocol). Here we use protocol 3, designated by its hash
$ ./tezos-client -p PsddFKi3 sign bytes 0x0501000000076d657373616765 for my_account Signature: edsigto9QHtXMyxFPyvaffRfFCrifkw2n5ZWqMxhGRzieksTo8AQAFgUjx7WRwqGPh4rXTBGGLpdmhskAaEauMrtM82T3tuxoi8
my_account can be any imported account in the Tezos client. In particular, it can be an encrypted key pair (you will need to enter a password to sign) or a hardware Ledger (you will need to confirm the signature on the Ledger). The obtained signature can be used as is with Liquidity or Michelson. This one starts with
edsig because it was obtained using an Ed25519 private key, but you can also get signatures starting with
p2sig depending on the cryptographic curve that you use.
In this second section we detail the necessary steps and provide a Python script to sign string messages using an Ed25519 private key. This can be easily adapted for other signing schemes.
These are the steps that will need to be performed in order to sign a string:
- Assuming that the value you want to sign is a string, you first need to convert its ASCII version to hexa, for the string
- You need to produce the packed version of the corresponding Michelson expression. The binary representation can vary depending on the types of the values you want to pack but for strings it is:
| 0x | 0501 | [size of the string on 4 bytes] | [ascii string in hexa] |
"message" (of length 7), it is
| 0x | 0501 | 00000007 | 6d657373616765 |
- Hash this value using Blake2b (
01978930fd2d04d0db8c2e4ef8a3f5d63b8e732177c8723135ed0dc7d99ebed3) which is 32 bytes long.
- Depending on your public key, you then need to sign it with the corresponding curve (ed25519 for edpk keys), the signature is 64 bytes:
- Optionally convert to base58check. This is not needed because Liquidity and Michelson allow signatures (as well as keys and key hashes) to be given in hex format with a 0x:
The following Python (3) script will do exactly this, entirely offline. Note that this is just an toy example, and should not be used in production. In particular you need to give your private key on the command line so this might not be secure if the machine you run this on is not secure.
$ pip3 install base58check pyblake2 ed25519 > python3 ./sign_string.py "message" edsk2gL9deG8idefWJJWNNtKXeszWR4FrEdNFM5622t1PkzH66oH3r 0x753e013b8515a7d47eaa5424de5efa2f56620ac8be29d08a6952ae414256eac44b8db71f74600275662c8b0c226f3280e9d24e70a5fa83015636b98059b5180c
from pyblake2 import blake2b import base58check import ed25519 import sys message = sys.argv seed_b58 = sys.argv prefix = b'\x05\x01' len_bytes = (len(message)).to_bytes(4, byteorder='big') h = blake2b(digest_size=32) b = bytearray() b.extend(message.encode()) h.update(prefix + len_bytes + b) digest = h.digest() seed = base58check.b58decode(seed_b58)[4:-4] sk = ed25519.SigningKey(seed) sig = sk.sign(digest) print("0x" + sig.hex())
OCamlPro is a R&D lab founded in 2011, with the mission to help industrial users benefit from state-of-the art programming languages like OCaml and Rust.
We design, create and implement custom ad-hoc software for our clients. We also have a long experience in developing and maintaining open-source tooling for OCaml, such as Opam, TryOCaml, ocp-indent, ocp-index and ocp-browser, and we contribute to the core-development of OCaml, notably with our work on the Flambda optimizer branch.
Another area of expertise is that of Formal Methods, with tools such as our SMT Solver Alt-Ergo (check our Alt-Ergo Users'). We also provide vocational trainings in OCaml and Rust, and we can build courses on formal methods on-demand. Do not hesitate to reach out by email: email@example.com.