Cartouche.Recover (Cartouche v0.2.0)

Copy Markdown View Source

EIP-191 (personal_sign) signature recovery primitives.

Given a message and a 65-byte secp256k1 signature, recover the signer's public key (recover_public_key/2) or Ethereum address (recover_eth/2). When the signature arrived without a recovery bit (e.g. some HSM / KMS backends return only (r, s)), find_recid/3 brute-forces the two valid recids against an expected address.

Signatures may be supplied either as a Curvy.Signature struct (when the recovery bit lives in :recid) or as the raw 65-byte <<r::256, s::256, v::8>> form. The v byte is interpreted as recid 0/1 (raw form), 27/28 (personal_sign), or 35 + 2 * chain_id + recid (EIP-155).

Used internally by Cartouche.Signer for the recover-and-verify sanity check after each sign call, and exposed as the public surface for consumers verifying user-supplied signatures (e.g. personal_sign payloads from MetaMask, WalletConnect, or any wallet implementing EIP-191).

Summary

Functions

Finds the given recid which recovers the given signature for the message to the given Ethereum address. This is a very simple guess-check-revise since there are only four possible values, and we only accept two of those.

Wraps a message in the EIP-191 personal_sign envelope.

Recovers a signer's Ethereum address from a signed message. The message will be digested by keccak first. Note: the rec_id can be embeded in the signature or passed separately.

Recovers a signer's public key from a signed message. The message will be digested by keccak first. Note: the rec_id can be embeded in the signature or passed separately.

Functions

find_recid(message, signature, address)

@spec find_recid(binary(), Curvy.Signature.t(), <<_::160>>) ::
  {:ok, 0..1} | {:error, String.t()}

Finds the given recid which recovers the given signature for the message to the given Ethereum address. This is a very simple guess-check-revise since there are only four possible values, and we only accept two of those.

Examples

iex> use Cartouche.Hex iex> priv_key = ~h[0x800509fa3e80882ad0be77c27505bdc91380f800d51ed80897d22f9fcc75f4bf] iex> {:ok, sig} = Cartouche.Signer.Curvy.sign("test", priv_key) iex> {:ok, recid} = Cartouche.Recover.find_recid("test", sig, ~h[0x63CC7C25E0CDB121ABB0FE477A6B9901889F99A7]) iex> Cartouche.Recover.recover_eth("test", %{sig|recid: recid}) ...> |> to_hex() "0x63cc7c25e0cdb121abb0fe477a6b9901889f99a7"

prefix_eth(msg)

@spec prefix_eth(String.t()) :: String.t()

Wraps a message in the EIP-191 personal_sign envelope.

The returned binary concatenates four parts:

  • 0x19 — the EIP-191 version byte
  • "Ethereum Signed Message:\n" — the literal namespace prefix (newline-terminated)
  • the byte length of msg, formatted as decimal ASCII
  • msg itself

The doctest output below shows \x19 and \n as escape sequences — those are the literal 0x19 byte and 0x0A newline byte in the returned string.

See EIP-191.

Examples

iex> Cartouche.Recover.prefix_eth("hello") "Ethereum Signed Message:\n5hello"

recover_eth(message, signature)

@spec recover_eth(binary(), Curvy.Signature.t() | binary()) :: <<_::160>>

Recovers a signer's Ethereum address from a signed message. The message will be digested by keccak first. Note: the rec_id can be embeded in the signature or passed separately.

Examples

iex> use Cartouche.Hex iex> priv_key = ~h[0x800509fa3e80882ad0be77c27505bdc91380f800d51ed80897d22f9fcc75f4bf] iex> {:ok, sig} = Cartouche.Signer.Curvy.sign("test", priv_key) iex> {:ok, recid} = Cartouche.Recover.find_recid("test", sig, ~h[0x63CC7C25E0CDB121ABB0FE477A6B9901889F99A7]) iex> Cartouche.Recover.recover_eth("test", %{sig|recid: recid}) ...> |> to_hex() "0x63cc7c25e0cdb121abb0fe477a6b9901889f99a7"

recover_public_key(message, signature)

@spec recover_public_key(binary(), Curvy.Signature.t() | binary()) :: binary()

Recovers a signer's public key from a signed message. The message will be digested by keccak first. Note: the rec_id can be embeded in the signature or passed separately.

Examples

iex> use Cartouche.Hex iex> # Decoded Signature iex> priv_key = ~h[0x800509fa3e80882ad0be77c27505bdc91380f800d51ed80897d22f9fcc75f4bf] iex> {:ok, sig} = Cartouche.Signer.Curvy.sign("test", priv_key) iex> {:ok, recid} = Cartouche.Recover.find_recid("test", sig, ~h[0x63CC7C25E0CDB121ABB0FE477A6B9901889F99A7]) iex> Cartouche.Recover.recover_public_key("test", %{sig|recid: recid}) |> to_hex() "0x0480076bfb96955526052b2676dfca87e0b7869ce85e00c5dbce29e76b8429d6dbf0f33b1a0095b2a9a4d9ea2a9746b122995a5b5874ee3161138c9d19f072b2d9"

iex> use Cartouche.Hex iex> # Binary Signature iex> priv_key = ~h[0x800509fa3e80882ad0be77c27505bdc91380f800d51ed80897d22f9fcc75f4bf] iex> {:ok, sig} = Cartouche.Signer.Curvy.sign("test", priv_key) iex> {:ok, recid} = Cartouche.Recover.find_recid("test", sig, ~h[0x63CC7C25E0CDB121ABB0FE477A6B9901889F99A7]) iex> signature = <<sig.r::256, sig.s::256, recid>> iex> Cartouche.Recover.recover_public_key("test", signature) |> to_hex() "0x0480076bfb96955526052b2676dfca87e0b7869ce85e00c5dbce29e76b8429d6dbf0f33b1a0095b2a9a4d9ea2a9746b122995a5b5874ee3161138c9d19f072b2d9"

iex> use Cartouche.Hex iex> # EIP-155 Signature iex> priv_key = ~h[0x800509fa3e80882ad0be77c27505bdc91380f800d51ed80897d22f9fcc75f4bf] iex> {:ok, sig} = Cartouche.Signer.Curvy.sign("test", priv_key) iex> {:ok, recid} = Cartouche.Recover.find_recid("test", sig, ~h[0x63CC7C25E0CDB121ABB0FE477A6B9901889F99A7]) iex> recovery_bit = 35 + 5 * 2 + recid iex> signature = <<sig.r::256, sig.s::256, recovery_bit::8>> iex> Cartouche.Recover.recover_public_key("test", signature) |> to_hex() "0x0480076bfb96955526052b2676dfca87e0b7869ce85e00c5dbce29e76b8429d6dbf0f33b1a0095b2a9a4d9ea2a9746b122995a5b5874ee3161138c9d19f072b2d9"