Skip to content

Commit

Permalink
feat(crypto): implement sign/3 for tz2
Browse files Browse the repository at this point in the history
  • Loading branch information
vhf committed Mar 15, 2024
1 parent 929b36d commit b37ecaf
Show file tree
Hide file tree
Showing 14 changed files with 568 additions and 61 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
90 changes: 65 additions & 25 deletions lib/crypto.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@ 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}

@prefixes %{
eddsa: <<43, 246, 78, 7>>
}
@prefixes_sig %{
eddsa: <<4, 130, 43>>
}
alias Tezex.Crypto.KnownCurves
alias Tezex.Crypto.Base58Check
alias Tezex.Crypto.ECDSA
alias Tezex.Crypto.PrivateKey
alias Tezex.Crypto.Signature
alias Tezex.Crypto.Utils

# public key
@prefix_edpk <<13, 15, 37, 217>>
@prefix_sppk <<3, 254, 226, 86>>
@prefix_p2pk <<3, 178, 139, 127>>
# private key
@prefix_edsk <<43, 246, 78, 7>>
@prefix_spsk <<17, 162, 224, 201>>
@prefix_p2sk <<16, 81, 238, 189>>
# sig
@prefix_sig <<4, 130, 43>>

@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 Down Expand Up @@ -52,7 +62,7 @@ defmodule Tezex.Crypto do
signature = decode_signature(signature)

# <<0x0D, 0x0F, 0x25, 0xD9>>
<<13, 15, 37, 217, public_key::binary-size(32)>> <> _ = Base58Check.decode58!(pubkey)
<<@prefix_edpk, public_key::binary-size(32)>> <> _ = Base58Check.decode58!(pubkey)

:crypto.verify(:eddsa, :none, message_hash, signature, [public_key, :ed25519])
end
Expand All @@ -65,7 +75,7 @@ defmodule Tezex.Crypto do
message = :binary.decode_hex(msg)

# <<0x03, 0xFE, 0xE2, 0x56>>
<<3, 254, 226, 86, public_key::binary-size(33)>> <> _ = Base58Check.decode58!(pubkey)
<<@prefix_sppk, public_key::binary-size(33)>> <> _ = Base58Check.decode58!(pubkey)

public_key = ECDSA.decode_public_key(public_key, :secp256k1)

Expand All @@ -82,7 +92,7 @@ defmodule Tezex.Crypto do
message = :binary.decode_hex(msg)

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

public_key = ECDSA.decode_public_key(public_key, :prime256v1)

Expand Down Expand Up @@ -140,17 +150,17 @@ 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)>> <> _} ->
{:ok, <<@prefix_edpk, public_key::binary-size(32)>> <> _} ->
pkh = <<6, 161, 159>>
derive_address(public_key, pkh)

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

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

Expand All @@ -162,7 +172,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 @@ -180,29 +190,59 @@ 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("edsk" <> _ = key) do
defp decode_privkey(key, passphrase \\ nil) do
if binary_part(key, 2, 1) == "e" and is_nil(passphrase) do
throw("missing passphrase")
end

prefix =
case key do
"edsk" <> _ -> @prefix_edsk
"spsk" <> _ -> @prefix_spsk
end

key = Base58Check.decode58!(key)
binary_part(key, byte_size(@prefixes.eddsa), 32)

binary_part(key, byte_size(prefix), 32)
|> Utils.pad(32, :leading)
end

def sign("edsk" <> _ = secret_key, bytes, watermark \\ <<>>) do
msg = watermark <> :binary.decode_hex(bytes)
bytes_hash = Blake2.hash2b(msg, 32)
def sign(secret_key, bytes, watermark \\ <<>>) do
msg = :binary.decode_hex(bytes)
decoded_key = decode_privkey(secret_key)
signature = :crypto.sign(:eddsa, :none, bytes_hash, [decoded_key, :ed25519])
Base58Check.encode(signature, @prefixes_sig.eddsa)

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

"spsk" <> _ ->
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)
end
end

defp decode_signature(data) do
Expand Down
14 changes: 14 additions & 0 deletions lib/crypto/curve.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,18 @@ defmodule Tezex.Crypto.Curve do
N: non_neg_integer,
G: Point.t()
}

@doc """
Get the curve length
Parameters:
- `curve` [%Tezex.Crypto.Curve]: curve data
Returns:
- `length` [integer]: curve length
"""
@spec get_length(t()) :: non_neg_integer()
def get_length(curve) do
div(1 + String.length(Integer.to_string(curve."N", 16)), 2)
end
end
70 changes: 61 additions & 9 deletions lib/crypto/ecdsa.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ defmodule Tezex.Crypto.ECDSA do
Decode compressed public key and verify signatures using the Elliptic Curve Digital Signature Algorithm (ECDSA).
"""

alias Tezex.Crypto.{
Curve,
KnownCurves,
Math,
Point,
PublicKey,
Signature,
Utils
}
import Bitwise

alias Tezex.Crypto.Curve
alias Tezex.Crypto.HMACDRBG
alias Tezex.Crypto.KnownCurves
alias Tezex.Crypto.Math
alias Tezex.Crypto.Point
alias Tezex.Crypto.PrivateKey
alias Tezex.Crypto.PublicKey
alias Tezex.Crypto.Signature
alias Tezex.Crypto.Utils

@doc """
Decodes a compressed public key to the EC public key it is representing on EC `curve`.
Expand Down Expand Up @@ -174,4 +176,54 @@ defmodule Tezex.Crypto.ECDSA do
true -> true
end
end

@spec sign(iodata(), PrivateKey.t(), list(any())) :: Signature.t()
@spec sign(iodata(), PrivateKey.t()) :: Signature.t()
def sign(message, private_key, options \\ []) do
%{hashfunc: hashfunc} =
Enum.into(options, %{hashfunc: fn msg -> :crypto.hash(:sha256, msg) end})

curve_data = private_key.curve

message = hashfunc.(message)
number_message = Utils.number_from_string(message)

ns1 = :binary.encode_unsigned(curve_data."N" - 1)
nh = curve_data."N" >>> 1

drbg = HMACDRBG.new(private_key.secret, message)

message = Utils.truncate_to_n(number_message, curve_data."N")

Enum.reduce_while(1..1_000_000, drbg, fn _, drbg ->
{k, drbg} = HMACDRBG.generate(drbg)

k =
k
|> :binary.decode_unsigned()
|> Utils.truncate_to_n(curve_data."N", true)

with true <- not (k <= 1 or k >= ns1),
kp = Math.multiply(curve_data."G", k, curve_data."N", curve_data."A", curve_data."P"),
false <- Point.is_at_infinity?(kp),
r <- rem(kp.x, curve_data."N"),
true <- r != 0,
s =
Math.inv(k, curve_data."N") *
(r * :binary.decode_unsigned(private_key.secret) + message),
s = rem(s, curve_data."N"),
true <- s != 0 do
s =
if s > nh do
curve_data."N" - s
else
s
end

{:halt, %Signature{r: r, s: s}}
else
_ -> {:continue, drbg}
end
end)
end
end
105 changes: 105 additions & 0 deletions lib/crypto/hmacdrbg.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
defmodule Tezex.Crypto.HMACDRBG do
@moduledoc """
Pure Elixir implementation of [HMAC-DRBG](https://csrc.nist.gov/csrc/media/events/random-number-generation-workshop-2004/documents/hashblockcipherdrbg.pdf)
Ported from:
> https://github.com/sorpaas/rust-hmac-drbg/blob/master/src/lib.rs
> Apache License, Version 2.0, January 2004
> Copyright {yyyy} {name of copyright owner}
"""
defstruct k: <<>>, v: <<>>, count: 0

def new(entropy, nonce, pers \\ <<>>) do
k = for _ <- 1..32, into: <<>>, do: <<0>>
v = for _ <- 1..32, into: <<>>, do: <<1>>
state = %__MODULE__{k: k, v: v, count: 0}

state
|> update([entropy, nonce, pers])
|> Map.put(:count, 1)
end

def count(state), do: state.count

def reseed(state, entropy, add \\ <<>>) do
update(state, [entropy, add])
end

def generate(state, add \\ <<>>) do
result = for _ <- 1..32, into: <<>>, do: <<0>>
generate_to_slice(state, result, add)
end

def generate_to_slice(state, result, add) do
result = generate_bytes(state, result)

case add do
<<>> -> update(state, nil)
_ -> update(state, [add])
end

{result, %{state | count: state.count + 1}}
end

defp generate_bytes(state, result) do
Enum.reduce_while(1..10000, {0, state.v, result}, fn _, {i, v, result} ->
if i < byte_size(result) do
vmac = hmac(state.k)
vmac = :crypto.mac_update(vmac, v)
vmac = :crypto.mac_final(vmac)
<<ex::binary-size(i), _::binary>> = result
result = ex <> vmac
i = i + byte_size(vmac)
{:cont, {i, v, result}}
else
{:halt, result}
end
end)
end

defp update(state, seeds) do
kmac = hmac(state.k)
kmac = :crypto.mac_update(kmac, state.v)
kmac = :crypto.mac_update(kmac, <<0>>)

kmac =
case seeds do
nil -> kmac
_ -> seeds |> Enum.reduce(kmac, fn seed, acc -> :crypto.mac_update(acc, seed) end)
end

k = :crypto.mac_final(kmac)

vmac = hmac(k)
vmac = :crypto.mac_update(vmac, state.v)

v = :crypto.mac_final(vmac)

case seeds do
nil ->
%__MODULE__{state | k: k, v: v}

_ ->
kmac = hmac(k)
kmac = :crypto.mac_update(kmac, v)
kmac = :crypto.mac_update(kmac, <<1>>)

kmac =
seeds |> Enum.reduce(kmac, fn seed, acc -> :crypto.mac_update(acc, seed) end)

k = :crypto.mac_final(kmac)

vmac = hmac(k)
vmac = :crypto.mac_update(vmac, v)

v = :crypto.mac_final(vmac)

%__MODULE__{state | k: k, v: v}
end
end

defp hmac(k) do
:crypto.mac_init(:hmac, :sha256, k)
end
end
3 changes: 2 additions & 1 deletion lib/crypto/known_curves.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ defmodule Tezex.Crypto.KnownCurves do
Describes the elliptic curves supported by the package
"""

alias Tezex.Crypto.{Curve, Point}
alias Tezex.Crypto.Curve
alias Tezex.Crypto.Point

@secp256k1name :secp256k1
@prime256v1name :prime256v1
Expand Down
Loading

0 comments on commit b37ecaf

Please sign in to comment.