mdjohns

mdjohns

Hey all, working on my first Elixir/Phoenix project, which includes a Discord bot.

Per the Discord documentation, applications must validate each request. In order to add an an interactions endpoint URL, the application must validate the request, and respond with the expected response.

In particular, Discord includes two headers:

  • X-Signature-Ed25519
  • X-Signature-Timestamp

The expectation, as I understand, is to concatenate the timestamp and the raw request body, hash them using my application’s public key, and compare the result with the signature included in Discord’s header.

I’ve created a plug for this purpose, and the code is below. I’m assuming the issue is with the way that I’m creating the hash to compare. When I do that, my hash result is much shorter than the signature included in the header.

Does anything stick out in the code below? Thanks!

defmodule MyApp.DiscordPlug do
  @behaviour Plug
  import Plug.Conn
  require Logger

  @impl true
  def init(opts), do: opts

  @impl true
  def call(conn, _) do
    with {:ok, signature} <- get_header(conn, "x-signature-ed25519"),
         {:ok, timestamp} <- get_header(conn, "x-signature-timestamp"),
         {:ok, raw_body} <- get_raw_body(conn),
         {:ok, key} <- get_public_key(),
         :ok <- valid_request?(signature, timestamp, raw_body, key) do
      conn
    else
      {:error, message} ->
        Logger.error("Discord request validation failed with reason: #{message}")

        conn
        |> send_resp(401, "invalid request signature")
        |> halt()
    end
  end

  defp valid_request?(expected_signature, timestamp, body, key) do
    payload = "#{timestamp}#{body}"

    Logger.info("Constructed payload: #{payload}")
    Logger.info("Expected signature: #{expected_signature}")
    Logger.info("Signing key: #{key}")

    actual_signature =
      :crypto.mac(:hmac, :sha256, key, payload)
      |> Base.encode16(case: :lower)
      |> IO.inspect(label: "Hash result")

    case Plug.Crypto.secure_compare(actual_signature, expected_signature) do
      true -> :ok
      false -> {:error, "Signatures do not match"}
    end
  end

  defp get_header(conn, key) do
    case Plug.Conn.get_req_header(conn, key) do
      [value] -> {:ok, value}
      _ -> {:error, "No header for key #{key}"}
    end
  end

  defp get_raw_body(conn) do
    case conn.assigns[:raw_body] do
      nil -> {:error, "No raw body present"}
      [raw_body] -> {:ok, raw_body} |> IO.inspect(label: "raw body")
    end
  end

  defp get_public_key() do
    case Application.get_env(:chronicle, :discord_public_key) do
      nil -> {:error, "No public key set"}
      value -> {:ok, value}
    end
  end
end

Showing Posts 1 to 2

rjk

rjk

AFAICS you’re using a SHA256 HMAC signature (verifier) where it should be an Ed25519 signature/verifier.
I also looked into a couple other language implementations and they all seem to use NaCL/Libsodium or Ed25519 library. In (pure) elixir this could be done with ed25519 | Hex
These crypto things are pretty hard to get right so I always try to get a baseline test case with some parameters of which I know should work. Here I’ve put them in a basic unit test below. To be able to test this easily from a unit test perspective I’ve extracted the core “valid_request?” function into a seperate module.

Something like this seems to work;

defmodule DiscordSignatureVerifierTest do
  use ExUnit.Case

  test "valid_request? with valid signature" do
    expected_signature = "f31a129c4e06d93e195ea019392fc568fa7d63c9b43beb436d75f6826d5e5d36270763ee438f13ad5686ed310e8fa3253426af798927bf69cee2ff21be589109"
    timestamp = "1625603592"
    body = "this should be a json."
    key = "e421dceefff3a9d008b7898fcc0974813201800419d72f36d51e010d6a0acb71"

    assert DiscordSignatureVerifier.valid_request?(expected_signature, timestamp, body, key) == :ok
  end
end

where the valid_request? plug function itself is then

defmodule DiscordSignatureVerifier do
  def valid_request?(expected_signature, timestamp, body, key) do
    payload = "#{timestamp}#{body}"

    case Ed25519.valid_signature?(from_hex(expected_signature), payload, from_hex(key)) do
      true -> :ok
      false -> {:error, "Signatures do not match"}
    end
  end

  def from_hex(<<>>), do: ""

  def from_hex(s) do
    size = div(byte_size(s), 2)
    {n, ""} = s |> Integer.parse(16)
    zero_pad(:binary.encode_unsigned(n), size)
  end

  def zero_pad(s, size) when byte_size(s) == size, do: s
  def zero_pad(s, size) when byte_size(s) < size, do: zero_pad(<<0>> <> s, size)
end

(from_hex/zero_pad comes from ed25519_ex/test/test_helper.exs at master · mwmiller/ed25519_ex · GitHub )

Hopefully this helps you enough to get on with your project!

mdjohns

mdjohns OP

That did the trick! Thanks for the explanation and a great point about the baseline test case.

— All posts loaded —

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
roeland
Kia ora, We have been using elixir-google-api to connect to Google Drive. However, with the updates to Tesla due to CVEs this is now bro...
New
subsaharancoder
I’ve followed the Phoenix LiveView file upload code here Uploads — Phoenix LiveView v1.0.0-rc.7 and so far everything works just fine wit...
New
mohsen
I’m using an Umbrella project for a Phoenix application, and I want to have one Ecto Repo and one PostgreSQL database shared by all apps....
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews