Packages
message_signatures
0.1.0
RFC 9421 HTTP Message Signatures for Elixir: create and verify signatures over HTTP message components with strict signature-base canonicalization.
Current section
Files
Jump to
Current section
Files
lib/message_signatures/plug.ex
if Code.ensure_loaded?(Plug) do
defmodule MessageSignatures.Plug do
@moduledoc """
Verifies RFC 9421 signatures on incoming requests.
plug MessageSignatures.Plug,
key_resolver: MyApp.KeyResolver,
policy: [required_components: ["@method", "@target-uri", "content-digest"]]
On success the `MessageSignatures.VerifyResult` lands in
`conn.assigns[:message_signature]`. On failure the connection halts
with 400 for malformed input or 401 for other verification errors;
override the response with `:on_failure`.
This plug verifies the signature, including a covered `content-digest`
header. Verifying that the digest matches the request body is a separate
layer. Compose `HTTPDigest.Plug` from the `http_digest` package before
this plug so each layer verifies its own concern:
plug Plug.Parsers,
parsers: [:json],
json_decoder: JSON,
body_reader: {HTTPDigest.Plug, :read_body, []}
plug HTTPDigest.Plug, required: true
plug MessageSignatures.Plug,
key_resolver: MyApp.KeyResolver,
policy: [required_components: ["@method", "@target-uri", "content-digest"]]
"""
@behaviour Plug
import Plug.Conn
alias MessageSignatures.{Error, Message}
@malformed [
:malformed_signature_input,
:malformed_signature,
:malformed_component,
:invalid_message
]
@impl true
def init(opts) do
opts =
Keyword.validate!(opts,
key_resolver: nil,
label: nil,
tag: nil,
policy: [],
nonce_checker: nil,
sf_types: %{},
on_failure: nil
)
opts[:key_resolver] || raise ArgumentError, "MessageSignatures.Plug requires :key_resolver"
validate_on_failure!(opts[:on_failure])
opts
end
@impl true
def call(conn, opts) do
verify_opts =
opts
|> Keyword.take([:key_resolver, :label, :tag, :policy, :nonce_checker, :sf_types])
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
case MessageSignatures.verify(message_from_conn(conn), verify_opts) do
{:ok, result} -> assign(conn, :message_signature, result)
{:error, error} -> fail(conn, opts, error)
end
end
defp validate_on_failure!(nil), do: :ok
defp validate_on_failure!(fun) when is_function(fun, 2), do: :ok
defp validate_on_failure!(_),
do: raise(ArgumentError, ":on_failure must be an arity-2 function")
defp message_from_conn(conn) do
url =
%URI{
scheme: Atom.to_string(conn.scheme),
host: conn.host,
port: conn.port,
path: conn.request_path,
query: empty_query_to_nil(conn.query_string)
}
|> URI.to_string()
Message.from_request(method: conn.method, url: url, headers: conn.req_headers)
end
defp empty_query_to_nil(""), do: nil
defp empty_query_to_nil(query), do: query
defp fail(conn, opts, %Error{} = error) do
case opts[:on_failure] do
fun when is_function(fun, 2) ->
fun.(conn, error)
nil ->
conn
|> send_resp(
status_for(error),
"message signature verification failed: #{error.reason}"
)
|> halt()
end
end
defp status_for(%Error{reason: reason}) when reason in @malformed, do: 400
defp status_for(_error), do: 401
end
end