Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement signing #11

Merged
merged 11 commits into from
Mar 19, 2024
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
Loading