Skip to content

Commit

Permalink
feat(crypto): sign micheline messages and verify them
Browse files Browse the repository at this point in the history
  • Loading branch information
vhf committed Mar 17, 2024
1 parent 006657f commit 38c5dc2
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 12 deletions.
57 changes: 46 additions & 11 deletions lib/crypto.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ defmodule Tezex.Crypto do
@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 @@ -47,7 +49,9 @@ defmodule Tezex.Crypto do
{: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),
Expand Down Expand Up @@ -85,13 +89,13 @@ defmodule Tezex.Crypto do

def verify_signature(signature, msg, "sp" <> _ = pubkey) do
# tz2…
message = :binary.decode_hex(msg)

{:ok, <<r::unsigned-integer-size(256), s::unsigned-integer-size(256)>>} =
decode_signature(signature)

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

message = :binary.decode_hex(msg)

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

Expand Down Expand Up @@ -132,14 +136,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 Down Expand Up @@ -239,6 +238,42 @@ defmodule Tezex.Crypto do
{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
"""
@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, encode_message(bytes))
end

@doc """
Encode a string to its Micheline representation:
* "05" to indicate that it is a Micheline expression
* "01" to indicate that it is a Micheline string
* byte size encoded on 4 bytes
* hex representation of the string
"""
def encode_message(bytes) do
hex_bytes = :binary.encode_hex(bytes)
padded_bytes_size = String.pad_leading("#{trunc(byte_size(hex_bytes) / 2)}", 8, "0")

"0501" <> padded_bytes_size <> 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)
Expand Down
1 change: 1 addition & 0 deletions lib/crypto/ecdsa.ex
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ defmodule Tezex.Crypto.ECDSA do
- verified [`bool`]: true if message, public key and signature are compatible, false otherwise
"""
@spec verify?(nonempty_binary, Signature.t(), PublicKey.t(), list) :: boolean
@spec verify?(nonempty_binary, Signature.t(), PublicKey.t()) :: boolean
def verify?(message, signature, public_key, options \\ []) do
%{hashfunc: hashfunc} =
Enum.into(options, %{hashfunc: fn msg -> :crypto.hash(:sha256, msg) end})
Expand Down
3 changes: 2 additions & 1 deletion lib/crypto/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ defmodule Tezex.Crypto.Utils do
parsed_int
end

@spec string_from_number(integer(), non_neg_integer()) :: binary()
def string_from_number(number, string_length) do
number
|> Integer.to_string(16)
# pad start
|> fill_number_string(string_length)
|> Base.decode16!()
end
Expand All @@ -64,6 +64,7 @@ defmodule Tezex.Crypto.Utils do
end
end

@spec between(number(), number()) :: number()
def between(minimum, maximum) when minimum < maximum do
range = maximum - minimum + 1
{bytes_needed, mask} = calculate_parameters(range)
Expand Down
20 changes: 20 additions & 0 deletions test/crypto_test.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
defmodule Tezex.Crypto.Test do
use ExUnit.Case, async: true
doctest Tezex.Crypto

alias Tezex.Crypto
alias Tezex.Crypto.Utils

describe "check_signature/4" do
@msg_sig_pubkey [
Expand Down Expand Up @@ -241,6 +243,8 @@ defmodule Tezex.Crypto.Test do

assert Crypto.derive_address(pubkey) == {:ok, pubkeyhash}
assert signature == Crypto.sign(encoded_private_key, bytes, watermark)

assert true == sign_and_verify(encoded_private_key, pubkey)
end

test "Tz1 32 bytes" do
Expand All @@ -260,6 +264,9 @@ defmodule Tezex.Crypto.Test do
assert Crypto.derive_address(pubkey) == {:ok, pubkeyhash}
assert signature == Crypto.sign(encoded_private_key, bytes, watermark)
assert signature == Crypto.sign(secret_key, bytes, watermark)

assert true == sign_and_verify(encoded_private_key, pubkey)
assert true == sign_and_verify(secret_key, pubkey)
end

test "Tz2" do
Expand All @@ -275,6 +282,8 @@ defmodule Tezex.Crypto.Test do

assert Crypto.derive_address(pubkey) == {:ok, pubkeyhash}
assert signature == Crypto.sign(encoded_private_key, bytes, watermark)

assert true == sign_and_verify(encoded_private_key, pubkey)
end

test "Tz2 having 'y' coordinate shorter than 32 bytes" do
Expand All @@ -290,6 +299,8 @@ defmodule Tezex.Crypto.Test do

assert Crypto.derive_address(pubkey) == {:ok, pubkeyhash}
assert signature == Crypto.sign(encoded_private_key, bytes, watermark)

assert true == sign_and_verify(encoded_private_key, pubkey)
end

test "Tz3" do
Expand All @@ -305,6 +316,8 @@ defmodule Tezex.Crypto.Test do

assert Crypto.derive_address(pubkey) == {:ok, pubkeyhash}
assert signature == Crypto.sign(encoded_private_key, bytes, watermark)

assert true == sign_and_verify(encoded_private_key, pubkey)
end

# test "Tz3 Encrypted" do
Expand Down Expand Up @@ -343,6 +356,13 @@ defmodule Tezex.Crypto.Test do
# end
end

defp sign_and_verify(encoded_private_key, pubkey) do
# generate a 10 to 30 random bytes message to sign
msg = Crypto.encode_message(Utils.rand_fun(Enum.random(10..30)))
signature = Crypto.sign_message(encoded_private_key, msg)
Crypto.verify_signature(signature, msg, pubkey)
end

defp replace_nth_character(input, n \\ 20) do
re = Regex.compile!("^(.{0,#{n}})(.{1})(.*)")

Expand Down

0 comments on commit 38c5dc2

Please sign in to comment.