Skip to content

Commit

Permalink
Merge pull request #11 from objkt-com/feat/sign
Browse files Browse the repository at this point in the history
Implement signing
  • Loading branch information
vhf authored Mar 19, 2024
2 parents 86cb611 + bda4341 commit 29cfb8c
Show file tree
Hide file tree
Showing 27 changed files with 1,823 additions and 503 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/elixir.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ jobs:
- uses: erlef/setup-elixir@v1
id: beam
with:
otp-version: 25.2.3
elixir-version: 1.14.3
version-file: .tool-versions
version-type: strict
- name: Cache mix deps
uses: actions/cache@v3
id: mix-cache
Expand Down
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
elixir 1.16.1-otp-26
erlang 26.2.1
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ A collection of utils to work with the Tezos blockchain using Elixir: parsing Mi

Read the documentation on [hexdocs](https://hexdocs.pm/tezex/).

This lib contains a full, pure Elixir implementation of [HMAC-DRBG](https://hexdocs.pm/tezex/Tezex.Crypto.HMACDRBG.html), tested with [NIST CAPV](https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program) fixtures. We might extract it to a separate lib in the future.

Since what this lib provides is so far limited to what we need at [objkt.com](https://objkt.com), external contributions are most welcome.
For instance if you need to sign messages or transactions, or translating Micheline to hex, we'd love to collaborate on a PR.
For instance if you need to sign messages or transactions with a passphrase-protected private key, or translating Micheline to hex, we'd love to collaborate on a PR.

## Installation

Expand Down
249 changes: 201 additions & 48 deletions lib/crypto.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,34 @@ defmodule Tezex.Crypto do
@moduledoc """
A set of functions to check Tezos signed messages, derive a pkh from a pubkey, verify that a public key corresponds to a wallet address (public key hash).
"""
alias Tezex.Crypto.{Base58Check, ECDSA, Signature}

alias Tezex.Crypto.Base58Check
alias Tezex.Crypto.ECDSA
alias Tezex.Crypto.KnownCurves
alias Tezex.Crypto.PrivateKey
alias Tezex.Crypto.Signature
alias Tezex.Crypto.Utils
alias Tezex.Micheline

# public key
@prefix_edpk <<13, 15, 37, 217>>
@prefix_sppk <<3, 254, 226, 86>>
@prefix_p2pk <<3, 178, 139, 127>>
# public key hash
@prefix_tz1 <<6, 161, 159>>
@prefix_tz2 <<6, 161, 161>>
@prefix_tz3 <<6, 161, 164>>
# private key
@prefix_edsk <<43, 246, 78, 7>>
@prefix_spsk <<17, 162, 224, 201>>
@prefix_p2sk <<16, 81, 238, 189>>
# sig
@prefix_edsig <<9, 245, 205, 134, 18>>
@prefix_spsig <<13, 115, 101, 19, 63>>
@prefix_p2sig <<54, 240, 44, 52>>
@prefix_sig <<4, 130, 43>>

@type privkey_param :: binary() | {privkey :: binary(), passphrase :: binary()}

@doc """
Verify that `address` is the public key hash of `pubkey` and that `signature` is a valid signature for `message` signed with the private key corresponding to public key `pubkey`.
Expand All @@ -20,13 +47,17 @@ defmodule Tezex.Crypto do
iex> Tezex.Crypto.check_signature(address, signature, "", public_key)
{:error, :bad_signature}
iex> Tezex.Crypto.check_signature(address, "x" <> signature, "", public_key)
{:error, :invalid_pubkey_format}
{:error, :invalid_signature}
"""
@spec check_signature(binary, binary, binary, binary) ::
:ok | {:error, :address_mismatch | :invalid_pubkey_format | :bad_signature}
:ok
| {:error,
:address_mismatch | :invalid_pubkey_format | :invalid_signature | :bad_signature}

def check_signature("tz" <> _ = address, signature, message, pubkey) do
with :ok <- check_address(address, pubkey),
{:ok, _} <- extract_pubkey(pubkey),
{:ok, _} <- decode_signature(signature),
true <- verify_signature(signature, message, pubkey) do
:ok
else
Expand All @@ -35,48 +66,52 @@ defmodule Tezex.Crypto do
end
end

defp extract_pubkey(pubkey) do
case Base58Check.decode58(pubkey) do
{:ok, <<@prefix_edpk, public_key::binary-size(32)>> <> _} -> {:ok, public_key}
{:ok, <<@prefix_sppk, public_key::binary-size(33)>> <> _} -> {:ok, public_key}
{:ok, <<@prefix_p2pk, public_key::binary-size(33)>> <> _} -> {:ok, public_key}
_ -> {:error, :invalid_pubkey_format}
end
end

@doc """
Verify that `signature` is a valid signature for `message` signed with the private key corresponding to public key `pubkey`
"""
@spec verify_signature(binary, binary, binary) :: boolean
def verify_signature("ed" <> _ = signature, message, pubkey) do
@spec verify_signature(binary, binary, binary) :: boolean()
def verify_signature(signature, message, "ed" <> _ = pubkey) do
# tz1…
message_hash = hash_message(message)
signature = decode_signature(signature)

# <<0x0D, 0x0F, 0x25, 0xD9>>
<<13, 15, 37, 217, public_key::binary-size(32)>> <> _ = Base58Check.decode58!(pubkey)
{:ok, signature} = decode_signature(signature)
{:ok, public_key} = extract_pubkey(pubkey)

:crypto.verify(:eddsa, :none, message_hash, signature, [public_key, :ed25519])
end

def verify_signature("sp" <> _ = signature, msg, pubkey) do
def verify_signature(signature, msg, "sp" <> _ = pubkey) do
# tz2…
<<r::unsigned-integer-size(256), s::unsigned-integer-size(256)>> = decode_signature(signature)
signature = %Signature{r: r, s: s}

message = :binary.decode_hex(msg)

# <<0x03, 0xFE, 0xE2, 0x56>>
<<3, 254, 226, 86, public_key::binary-size(33)>> <> _ = Base58Check.decode58!(pubkey)
{:ok, <<r::unsigned-integer-size(256), s::unsigned-integer-size(256)>>} =
decode_signature(signature)

signature = %Signature{r: r, s: s}

{:ok, public_key} = extract_pubkey(pubkey)
public_key = ECDSA.decode_public_key(public_key, :secp256k1)

ECDSA.verify?(message, signature, public_key, hashfunc: fn msg -> Blake2.hash2b(msg, 32) end)
end

def verify_signature("p2" <> _ = signature, msg, pubkey) do
def verify_signature(signature, msg, "p2" <> _ = pubkey) do
# tz3…
<<54, 240, 44, 52, sig::binary-size(64)>> <> _ = Base58Check.decode58!(signature)
message = :binary.decode_hex(msg)
{:ok, sig} = decode_signature(signature)

<<r::unsigned-integer-size(256), s::unsigned-integer-size(256)>> = sig
signature = %Signature{r: r, s: s}

message = :binary.decode_hex(msg)

# <<0x03, 0xB2, 0x8B, 0x7F>>
<<3, 178, 139, 127, public_key::binary-size(33)>> <> _ = Base58Check.decode58!(pubkey)

{:ok, public_key} = extract_pubkey(pubkey)
public_key = ECDSA.decode_public_key(public_key, :prime256v1)

ECDSA.verify?(message, signature, public_key, hashfunc: fn msg -> Blake2.hash2b(msg, 32) end)
Expand All @@ -102,14 +137,9 @@ defmodule Tezex.Crypto do
:ok | {:error, :address_mismatch | :invalid_pubkey_format}
def check_address(address, pubkey) do
case derive_address(pubkey) do
{:ok, ^address} ->
:ok

{:ok, _derived} ->
{:error, :address_mismatch}

err ->
err
{:ok, ^address} -> :ok
{:ok, _derived} -> {:error, :address_mismatch}
err -> err
end
end

Expand All @@ -133,19 +163,16 @@ defmodule Tezex.Crypto do
def derive_address(pubkey) do
case Base58Check.decode58(pubkey) do
# tz1 addresses: "edpk" <> _
{:ok, <<13, 15, 37, 217, public_key::binary-size(32)>> <> _} ->
pkh = <<6, 161, 159>>
derive_address(public_key, pkh)
{:ok, <<@prefix_edpk, public_key::binary-size(32)>> <> _} ->
derive_address(public_key, @prefix_tz1)

# tz2 addresses: "sppk" <> _
{:ok, <<3, 254, 226, 86, public_key::binary-size(33)>> <> _} ->
pkh = <<6, 161, 161>>
derive_address(public_key, pkh)
{:ok, <<@prefix_sppk, public_key::binary-size(33)>> <> _} ->
derive_address(public_key, @prefix_tz2)

# tz3 addresses: "p2pk" <> _
{:ok, <<3, 178, 139, 127, public_key::binary-size(33)>> <> _} ->
pkh = <<6, 161, 164>>
derive_address(public_key, pkh)
{:ok, <<@prefix_p2pk, public_key::binary-size(33)>> <> _} ->
derive_address(public_key, @prefix_tz3)

_ ->
{:error, :invalid_pubkey_format}
Expand All @@ -155,7 +182,7 @@ defmodule Tezex.Crypto do
defp derive_address(pubkey, pkh) do
derived =
Blake2.hash2b(pubkey, 20)
|> Tezex.Crypto.Base58Check.encode(pkh)
|> Base58Check.encode(pkh)

{:ok, derived}
end
Expand All @@ -173,22 +200,148 @@ defmodule Tezex.Crypto do
def encode_pubkey(pkh, hex_pubkey) do
prefix =
case pkh do
"tz1" <> _ -> <<13, 15, 37, 217>>
"tz2" <> _ -> <<3, 254, 226, 86>>
"tz3" <> _ -> <<3, 178, 139, 127>>
"tz1" <> _ -> @prefix_edpk
"tz2" <> _ -> @prefix_sppk
"tz3" <> _ -> @prefix_p2pk
_ -> :error
end

with prefix when is_binary(prefix) <- prefix,
{:ok, bin_pubkey} <- Base.decode16(String.upcase(hex_pubkey)) do
{:ok, Tezex.Crypto.Base58Check.encode(bin_pubkey, prefix)}
{:ok, Base58Check.encode(bin_pubkey, prefix)}
end
end

defp decode_privkey({privkey, passphrase}) do
throw("not implemented")
decode_privkey(privkey, passphrase)
end

defp decode_privkey(privkey, passphrase \\ nil) do
if binary_part(privkey, 2, 1) == "e" and is_nil(passphrase) do
throw("missing passphrase")
end

prefix =
case privkey do
"edsk" <> _ -> @prefix_edsk
"edes" <> _ -> @prefix_edsk
"spsk" <> _ -> @prefix_spsk
"spes" <> _ -> @prefix_spsk
"p2sk" <> _ -> @prefix_p2sk
"p2es" <> _ -> @prefix_p2sk
end

decoded_privkey =
Base58Check.decode58!(privkey)
|> binary_part(byte_size(prefix), 32)
|> Utils.pad(32, :leading)

{privkey, decoded_privkey}
end

@doc """
Sign an operation using `0x03` as watermark
"""
@spec sign_operation(privkey_param(), binary()) :: nonempty_binary()
def sign_operation(privkey_param, bytes) do
sign(privkey_param, bytes, <<3>>)
end

@doc """
Sign the hexadecimal/Micheline representation of a string, Micheline encoding is done when `bytes` do not start with `"0501"`.
## Examples
iex> encoded_private_key = "spsk24EJohZHJkZnWEzj3w9wE7BFARpFmq5WAo9oTtqjdJ2t4pyoB3"
iex> Tezex.Crypto.sign_message(encoded_private_key, "foo")
"sigm9uJiGjdk2DpuqTmHcjzpAdTSQfqKxFuDKodyNT8JP3UvrfoPFTNkFbFgDP1WfAi2PjJ3dcpZFLTagD7gUBmwVWbPr5mk"
iex> msg = Tezex.Micheline.string_to_micheline_hex("foo")
"050100000003666F6F"
iex> Tezex.Crypto.sign_message(encoded_private_key, msg)
"sigm9uJiGjdk2DpuqTmHcjzpAdTSQfqKxFuDKodyNT8JP3UvrfoPFTNkFbFgDP1WfAi2PjJ3dcpZFLTagD7gUBmwVWbPr5mk"
"""
@spec sign_message(privkey_param(), binary()) :: nonempty_binary()
def sign_message(privkey_param, "0501" <> _ = bytes) do
sign(privkey_param, bytes)
end

def sign_message(privkey_param, bytes) do
sign(privkey_param, Micheline.string_to_micheline_hex(bytes))
end

@spec sign(privkey_param(), binary(), binary()) :: nonempty_binary()
@spec sign(privkey_param(), binary()) :: nonempty_binary()
def sign(privkey_param, bytes, watermark \\ <<>>) do
msg = :binary.decode_hex(bytes)
{privkey, decoded_key} = decode_privkey(privkey_param)

case privkey do
"ed" <> _ ->
bytes_hash = Blake2.hash2b(watermark <> msg, 32)
signature = :crypto.sign(:eddsa, :none, bytes_hash, [decoded_key, :ed25519])
Base58Check.encode(signature, @prefix_sig)

"sp" <> _ ->
pk = %PrivateKey{secret: decoded_key, curve: KnownCurves.secp256k1()}
s = ECDSA.sign(watermark <> msg, pk, hashfunc: fn msg -> Blake2.hash2b(msg, 32) end)

r_bin = Integer.to_string(s.r, 16)
s_bin = Integer.to_string(s.s, 16)

r_bin = Utils.pad(r_bin, 64, :leading)
s_bin = Utils.pad(s_bin, 64, :leading)

signature = :binary.decode_hex(r_bin <> s_bin)

Base58Check.encode(signature, @prefix_sig)

"p2" <> _ ->
pk = %PrivateKey{secret: decoded_key, curve: KnownCurves.prime256v1()}
s = ECDSA.sign(watermark <> msg, pk, hashfunc: fn msg -> Blake2.hash2b(msg, 32) end)

r_bin = Integer.to_string(s.r, 16)
s_bin = Integer.to_string(s.s, 16)

r_bin = Utils.pad(r_bin, 64, :leading)
s_bin = Utils.pad(s_bin, 64, :leading)

signature = :binary.decode_hex(r_bin <> s_bin)

Base58Check.encode(signature, @prefix_sig)
end
end

@spec decode_signature(binary()) :: {:error, :invalid_signature} | {:ok, binary()}
defp decode_signature("edsig" <> _ = sig) do
case Base58Check.decode58(sig) do
{:ok, <<@prefix_edsig, sig::binary-size(64)>> <> _} -> {:ok, sig}
_ -> {:error, :invalid_signature}
end
end

defp decode_signature("spsig" <> _ = sig) do
case Base58Check.decode58(sig) do
{:ok, <<@prefix_spsig, sig::binary-size(64)>> <> _} -> {:ok, sig}
_ -> {:error, :invalid_signature}
end
end

defp decode_signature("p2sig" <> _ = sig) do
case Base58Check.decode58(sig) do
{:ok, <<@prefix_p2sig, sig::binary-size(64)>> <> _} -> {:ok, sig}
_ -> {:error, :invalid_signature}
end
end

defp decode_signature("sig" <> _ = sig) do
case Base58Check.decode58(sig) do
{:ok, <<@prefix_sig, sig::binary-size(64)>> <> _} -> {:ok, sig}
_ -> {:error, :invalid_signature}
end
end

defp decode_signature(data) do
data
|> Base58Check.decode58!()
|> binary_part(5, 64)
defp decode_signature(_) do
{:error, :invalid_signature}
end

defp hash_message(message) do
Expand Down
Loading

0 comments on commit 29cfb8c

Please sign in to comment.