mdjohns

mdjohns

Validating Discord Request Headers

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

Marked As Solved

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!

Last Post!

mdjohns

mdjohns

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

Where Next?

Popular in Questions Top

minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 130579 1222
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New

We're in Beta

About us Mission Statement