spruceid / siwe-ex

Sign-In with Ethereum Implementation for Elixir
Apache License 2.0
12 stars 10 forks source link

Sign-In with Ethereum

Elixir library to enable Sign-In with Ethereum message validation.

Full documentation found at https://hexdocs.pm/siwe.

This library provides functions for parsing and validating SIWE message strings and their corresponding signatures.

Requirements:

Elixir at version 1.10 or higher using OTP 23 or greater. Rustler officially supports the last 3 minor versions of Elixir, which we also suggest for best experience.

A Rust compiler at version 1.56 or higher so that 2021 edition libraries can be compiled.

Installation

The package can be installed by adding siwe to your list of dependencies in mix.exs:

def deps do
  [
    {:siwe, "~> 0.3"}
  ]
end

Example

To see how this works in iex, clone this repository and from the root run:

$ mix deps.get

Then create two files message.txt:

login.xyz wants you to sign in with your Ethereum account:
0xfA151B5453CE69ABf60f0dbdE71F6C9C5868800E

Sign-In With Ethereum Example Statement

URI: https://login.xyz
Version: 1
Chain ID: 1
Nonce: ToTaLLyRanDOM
Issued At: 2021-12-17T00:38:39.834Z

signature.txt:

0x8d1327a1abbdf172875e5be41706c50fc3bede8af363b67aefbb543d6d082fb76a22057d7cb6d668ceba883f7d70ab7f1dc015b76b51d226af9d610fa20360ad1c

then run

$ iex -S mix

Once in iex, you can then run the following to see the result:

iex> {:ok, msg} = File.read("./message.txt")
...
iex> {:ok, sig} = File.read("./signature.txt")
...
iex> Siwe.parse_if_valid(String.trim(msg), String.trim(sig))
{:ok, %{
  __struct__: Siwe,
  address: "0xfA151B5453CE69ABf60f0dbdE71F6C9C5868800E",
  chain_id: "1",
  domain: "login.xyz",
  expiration_time: nil,
  issued_at: "2021-12-17T00:38:39.834Z",
  nonce: "ToTaLLyRanDOM",
  not_before: nil,
  request_id: nil,
  resources: [],
  statement: "Sign-In With Ethereum Example Statement",
  uri: "https://login.xyz",
  version: "1"
}}

Any valid SIWE message and signature pair can be substituted.The functions described below can also be tested with msg, sig, or a value set to the result Siwe.parse_if_valid.

API Overview

This library deals with three different types of input:

1) SIWE message strings. 2) Signatures of SIWE message strings. 3) A parsed SIWE message which is defined as:

  defmodule Message do
    defstruct domain: "",
      address: "",
      statement: "",
      uri: "",
      version: "",
      chain_id: "",
      nonce: "",
      issued_at: "",
      expiration_time: nil, # or a string datetime
      not_before: nil, # or a string datetime
      request_id: nil, # or string
      resources: []
  end

The most basic functions are parse and to_str which translate a SIWE message string to a parsed SIWE message and back (respectively). To simplify using the variables from the above example:

iex> {:ok, parsed} = Siwe.parse(String.trim(msg))
...
iex> {:ok, str2} = Siwe.to_str(parsed) 
iex> str2 == String.trim(msg)
:true

Once parsed, the Message can be verified.

iex> Siwe.verify_sig(parsed, String.trim(sig))
:true
iex> Siwe.verify(parsed, String.trim(sig), "login.xyz", nil, nil)
:true
iex> Siwe.verify(parsed, String.trim(sig), nil, "12341234", nil)
:true
iex> Siwe.verify(parsed, String.trim(sig), nil, nil, "2021-04-04T00:38:39.834Z")
:true
iex> Siwe.verify(parsed, String.trim(sig), nil, nil, nil)
:true

parse_if_valid is an optimized helper function which takes a SIWE message string and a signature then returns a parsed Message only if the signature matches and the current time is after the Message's not_before field (if it exists) and before the Message's expiration_time field (if it exists).

iex> Siwe.generate_nonce()
"EaLc76FkngQ"

Another helper, generate_nonce function is provided to create alphanumeric [a-z, A-Z, 1-9] nonces compliant with SIWE's spec, used like so: This is useful for servers using SIWE for their own authentication systems.

Disclaimer

Our Elixir library for Sign-In with Ethereum has not yet undergone a formal security audit. We welcome continued feedback on the usability, architecture, and security of this implementation.

See Also