defmodule Tezex.Crypto do @moduledoc """ Provides cryptographic functions for Tezos blockchain operations. This module includes functions to: - Check Tezos signed messages - Derive public key hashes (pkh) from public keys - Verify that a public key corresponds to a wallet address (public key hash) - Sign messages and operations - Encode and decode various Tezos-specific data formats """ alias Tezex.Crypto.Base58Check alias Tezex.Crypto.BLS 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>> @prefix_blpk <<6, 149, 135, 204>> # public key hash @prefix_tz1 <<6, 161, 159>> @prefix_tz2 <<6, 161, 161>> @prefix_tz3 <<6, 161, 164>> @prefix_tz4 <<6, 161, 166>> # private key @prefix_edsk <<43, 246, 78, 7>> @prefix_spsk <<17, 162, 224, 201>> @prefix_p2sk <<16, 81, 238, 189>> @prefix_blsk <<3, 150, 192, 40>> # sig @prefix_edsig <<9, 245, 205, 134, 18>> @prefix_spsig <<13, 115, 101, 19, 63>> @prefix_p2sig <<54, 240, 44, 52>> @prefix_blsig <<40, 171, 64, 207>> @prefix_sig <<4, 130, 43>> @typedoc """ Represents a private key parameter. It can be either a binary string (the private key itself) or a tuple containing the private key and a passphrase. """ @type privkey_param :: binary() | {privkey :: binary(), passphrase :: binary()} @typedoc """ Represents the result of a signature verification operation. It can be either `:ok` if the signature is valid, or an error tuple with a specific reason. """ @type signature_verification_result :: :ok | {:error, :address_mismatch | :invalid_pubkey_format | :invalid_signature | :bad_signature} @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`. ## Examples iex> address = "tz2BC83pvEAag6r2ZV7kPghNAbjFoiqhCvZx" iex> address_b = "tz1burnburnburnburnburnburnburjAYjjX" iex> signature = "spsig1ZNQaUKNERZSiEiNviqa5EAPkcNASXhfkXtxRatZTDZAnUB4Ra2Jus8b1oEpFnPx8Z6g28pd8vK3R8nPK29JDU5FiSLH5T" iex> message = "05010000007154657a6f73205369676e6564204d6573736167653a207369676e206d6520696e20617320747a32424338337076454161673672325a56376b5067684e41626a466f69716843765a78206f6e206f626a6b742e636f6d20617420323032312d31302d30345431383a35393a31332e3939305a" iex> public_key = "sppk7aBerAEA6tv4wzg6FnK7i5YrGtEGFVvNjWhc2QX8bhzpouBVFSW" iex> check_signature(address, signature, message, public_key) :ok iex> check_signature(address_b, signature, message, public_key) {:error, :address_mismatch} iex> check_signature(address, signature, "", public_key) {:error, :bad_signature} iex> check_signature(address, "x" <> signature, "", public_key) {:error, :invalid_signature} """ @spec check_signature(binary(), binary(), binary(), binary()) :: :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 false -> {:error, :bad_signature} {:error, err} -> {:error, err} end end def 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} {:ok, <<@prefix_blpk, public_key::binary-size(48)>> <> _} -> {: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(signature, message, "ed" <> _ = pubkey) do # tz1… message_hash = hash_message(message) {: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(signature, msg, "sp" <> _ = pubkey) do # tz2… message = :binary.decode_hex(msg) {:ok, <>} = 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(signature, msg, "p2" <> _ = pubkey) do # tz3… message = :binary.decode_hex(msg) {:ok, sig} = decode_signature(signature) <> = sig signature = %Signature{r: r, s: s} {: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) end def verify_signature(signature, msg, "BL" <> _ = pubkey) do # tz4… with {:ok, decoded_sig} <- decode_signature(signature), {:ok, public_key} <- extract_pubkey(pubkey), message <- :binary.decode_hex(msg) do BLS.verify(decoded_sig, message, public_key) else _ -> false end end def verify_signature(_, _, _) do {:error, :invalid_pubkey_format} end defp check_checksum(checksum, data) do computed_checksum = :crypto.hash(:sha256, :crypto.hash(:sha256, data)) |> binary_part(0, 4) if computed_checksum == checksum do :ok else {:error, :invalid_checksum} end end @doc """ Validate an implicit account address ## Examples iex> validate_address("tz1L9r8mWmRpndRhuvMCWESLGSVeFzQ9NAWx") {:error, :invalid_checksum} iex> validate_address("tz3bPFa6mGv8m4Ppn7w5KSD00bEPwbJNpC9p") {:error, :invalid_base58} iex> validate_address("tz1L9r8mWmRpndRhuvMCWESLGSVeFz") {:error, :invalid_length} iex> validate_address("tp3bPFa6mGv8m4Ppn7w5KSDyAbEPwbJNpC9p") {:error, :invalid_prefix} iex> validate_address("tz3bPFa6mGv8m4Ppn7w5KSDyAbEPwbJNpC9p") :ok iex> validate_address("tz4NWRt3aFyFU2Ydah917Eehxv6uf97j8tpZ") :ok """ @spec validate_address(nonempty_binary()) :: :ok | {:error, :invalid_base58 | :invalid_checksum | :invalid_length | :invalid_prefix} def validate_address(address) do with {:ok, decoded} <- Base58Check.decode58(address), <> <- decoded, true <- prefix in [@prefix_tz1, @prefix_tz2, @prefix_tz3, @prefix_tz4], :ok <- check_checksum(checksum, <>) do :ok else :error -> {:error, :invalid_base58} {:error, :invalid_checksum} -> {:error, :invalid_checksum} false -> {:error, :invalid_prefix} b when is_binary(b) -> {:error, :invalid_length} end end @doc """ Verify that `address` is the public key hash of `pubkey`. ## Examples iex> pubkey = "sppk7aBerAEA6tv4wzg6FnK7i5YrGtEGFVvNjWhc2QX8bhzpouBVFSW" iex> check_address("tz2BC83pvEAag6r2ZV7kPghNAbjFoiqhCvZx", pubkey) :ok iex> check_address("tz1burnburnburnburnburnburnburjAYjjX", pubkey) {:error, :address_mismatch} iex> check_address("tz2BC83pvEAag6r2ZV7kPghNAbjFoiqhCvZx", "x" <> pubkey) {:error, :invalid_pubkey_format} """ @spec check_address(nonempty_binary(), nonempty_binary()) :: :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 end end @doc """ Derive public key hash (Tezos wallet address) from public key ## Examples iex> derive_address("edpktsPhZ8weLEXqf4Fo5FS9Qx8ZuX4QpEBEwe63L747G8iDjTAF6w") {:ok, "tz1LKpeN8ZSSFNyTWiBNaE4u4sjaq7J1Vz2z"} iex> derive_address("sppk7aBerAEA6tv4wzg6FnK7i5YrGtEGFVvNjWhc2QX8bhzpouBVFSW") {:ok, "tz2BC83pvEAag6r2ZV7kPghNAbjFoiqhCvZx"} iex> derive_address("p2pk65yRxCX65k6qRPrbqGWvfW5JnLB1p3dn1oM5o9cyqLKPPhJaBMa") {:ok, "tz3bPFa6mGv8m4Ppn7w5KSDyAbEPwbJNpC9p"} iex> derive_address("_p2pk65yRxCX65k6qRPrbqGWvfW5JnLB1p3dn1oM5o9cyqLKPPhJaBMa") {:error, :invalid_pubkey_format} iex> derive_address("p2pk65yRxCX65k6") {:error, :invalid_pubkey_format} """ @spec derive_address(nonempty_binary()) :: {:ok, nonempty_binary} | {:error, :invalid_pubkey_format} def derive_address(pubkey) do case Base58Check.decode58(pubkey) do # tz1 addresses: "edpk" <> _ {:ok, <<@prefix_edpk, public_key::binary-size(32)>> <> _} -> derive_address(public_key, @prefix_tz1) # tz2 addresses: "sppk" <> _ {:ok, <<@prefix_sppk, public_key::binary-size(33)>> <> _} -> derive_address(public_key, @prefix_tz2) # tz3 addresses: "p2pk" <> _ {:ok, <<@prefix_p2pk, public_key::binary-size(33)>> <> _} -> derive_address(public_key, @prefix_tz3) # tz4 addresses: "BLpk" <> _ {:ok, <<@prefix_blpk, public_key::binary-size(48)>> <> _} -> derive_address(public_key, @prefix_tz4) _ -> {:error, :invalid_pubkey_format} end end defp derive_address(pubkey, pkh) do derived = Blake2.hash2b(pubkey, 20) |> Base58Check.encode(pkh) {:ok, derived} end @doc """ Encode a raw public key ## Examples iex> encode_pubkey("tz1LPggcEZincSDQJUXrskwJPif4aJhWxMjd", "52d396892b2489ec91406f83680b172b5ca42f606128cd4c44ea4d09d31aa524") {:ok, "edpkuGhdSSNgqT92Gwoxt9vV3TmpQE93TtQSn5kkyULCh7cfeQFTno"} iex> encode_pubkey("tz1LPggcEZincSDQJUXrskwJPif4aJhWxMjd", "foo") :error """ @spec encode_pubkey(nonempty_binary(), nonempty_binary()) :: :error | {:ok, nonempty_binary()} def encode_pubkey(pkh, hex_pubkey) do prefix = case pkh do "tz1" <> _ -> @prefix_edpk "tz2" <> _ -> @prefix_sppk "tz3" <> _ -> @prefix_p2pk "tz4" <> _ -> @prefix_blpk _ -> :error end with prefix when is_binary(prefix) <- prefix, {:ok, bin_pubkey} <- Base.decode16(String.upcase(hex_pubkey)) do {:ok, Base58Check.encode(bin_pubkey, prefix)} end end defp decode_privkey({privkey, passphrase}) do decode_privkey(privkey, passphrase) end defp decode_privkey(privkey, passphrase \\ nil) do # Check if this is an encrypted key is_encrypted = binary_part(privkey, 2, 1) == "e" if is_encrypted and is_nil(passphrase) do throw("missing passphrase") end # Use the new PrivateKey.from_encoded_key for proper encrypted key support case PrivateKey.from_encoded_key(privkey, passphrase) do {:ok, private_key} -> # Pad to ensure consistent 32-byte length decoded_privkey = Utils.pad(private_key.secret, 32, :leading) {privkey, decoded_privkey} {:error, _} -> # Fallback to old implementation for backward compatibility # This handles cases where the new implementation might not support a specific format prefix = case privkey do "edsk" <> _ -> @prefix_edsk "edes" <> _ -> @prefix_edsk "spsk" <> _ -> @prefix_spsk "spes" <> _ -> @prefix_spsk "p2sk" <> _ -> @prefix_p2sk "p2es" <> _ -> @prefix_p2sk "BLsk" <> _ -> @prefix_blsk end decoded_privkey = Base58Check.decode58!(privkey) |> binary_part(byte_size(prefix), 32) |> Utils.pad(32, :leading) {privkey, decoded_privkey} end 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> sign_message(encoded_private_key, "foo") "spsig1Uyadmsz75zND5qDjSAteir1NGCEuPaxNnT8QmXkwCJkuzWUyxKqKsjSx3nU4uj2nk8t31VFFhQ4YsPKdZ9ghA2d2fe9HF" iex> msg = Tezex.Micheline.pack("foo", :string) "050100000003666f6f" iex> sign_message(encoded_private_key, msg) "spsig1Uyadmsz75zND5qDjSAteir1NGCEuPaxNnT8QmXkwCJkuzWUyxKqKsjSx3nU4uj2nk8t31VFFhQ4YsPKdZ9ghA2d2fe9HF" """ @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.pack(bytes, :string)) 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_edsig) "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 = String.pad_leading(r_bin, 64, "0") s_bin = String.pad_leading(s_bin, 64, "0") signature = :binary.decode_hex(r_bin <> s_bin) Base58Check.encode(signature, @prefix_spsig) "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 = String.pad_leading(r_bin, 64, "0") s_bin = String.pad_leading(s_bin, 64, "0") signature = :binary.decode_hex(r_bin <> s_bin) Base58Check.encode(signature, @prefix_p2sig) "BL" <> _ -> with {:ok, bls_key} <- BLS.deserialize_secret_key(decoded_key), signature <- BLS.sign(bls_key, watermark <> msg) do Base58Check.encode(signature, @prefix_blsig) else {:error, _} -> raise RuntimeError, "Invalid BLS private key" end end end @spec decode_signature!(binary()) :: binary() def decode_signature!(sig) do {:ok, decoded} = decode_signature(sig) decoded end @spec decode_signature(binary()) :: {:error, :invalid_signature} | {:ok, binary()} def decode_signature("edsig" <> _ = sig) do case Base58Check.decode58(sig) do {:ok, <<@prefix_edsig, sig::binary-size(64)>> <> _} -> {:ok, sig} _ -> {:error, :invalid_signature} end end def decode_signature("spsig" <> _ = sig) do case Base58Check.decode58(sig) do {:ok, <<@prefix_spsig, sig::binary-size(64)>> <> _} -> {:ok, sig} _ -> {:error, :invalid_signature} end end def decode_signature("p2sig" <> _ = sig) do case Base58Check.decode58(sig) do {:ok, <<@prefix_p2sig, sig::binary-size(64)>> <> _} -> {:ok, sig} _ -> {:error, :invalid_signature} end end def decode_signature("sig" <> _ = sig) do case Base58Check.decode58(sig) do {:ok, <<@prefix_sig, sig::binary-size(64)>> <> _} -> {:ok, sig} _ -> {:error, :invalid_signature} end end def decode_signature("BLsig" <> _ = sig) do case Base58Check.decode58(sig) do # BLS signatures are: 4-byte prefix + 96-byte signature + 4-byte checksum = 104 bytes total {:ok, <<@prefix_blsig, sig_data::binary-size(96), _checksum::binary-size(4)>>} -> {:ok, sig_data} # Fallback for different encoding formats {:ok, <<@prefix_blsig, sig_data::binary-size(96)>> <> _} -> {:ok, sig_data} _ -> {:error, :invalid_signature} end end def decode_signature(_) do {:error, :invalid_signature} end defp hash_message(message) do iodata = :binary.decode_hex(message) case Blake2.hash2b(iodata, 32) do :error -> "" bin -> bin end end end