http signatures more robust, still untested

This commit is contained in:
Moon Man 2024-08-25 17:38:00 +00:00
parent a20dafacde
commit f98bf7b101
3 changed files with 91 additions and 33 deletions

View File

@ -3,30 +3,74 @@ defmodule HTTPSignature do
Implements RFC 9421 HTTP Signatures using the Digest header.
"""
@spec create_signature_header(map(), list(), String.t(), binary()) :: String.t()
def create_signature_header(headers, include_headers, key_id, body) do
# (request-body) is really only for signing not sending as a header, but send
# it anyway for broken implementations.
@post_headers ["(request-body)", "host", "date", "digest"]
@other_headers ["(request-body)", "host", "date"]
@spec create_post_signature(map(), String.t(), String.t(), String.t()) :: String.t()
def create_post_signature(headers, key_id, path, body)
when is_map(headers) and is_binary(key_id) and is_binary(body) and is_binary(path) do
request_target = "post #{path}"
digest = compute_digest(body)
headers = Map.put(headers, "digest", digest)
signing_string = construct_signing_string(headers, include_headers)
date = Timex.format!(Timex.now(:utc), "{RFC1123}")
headers =
Map.put(headers, "digest", digest)
|> Map.put("date", date)
|> Map.put("(request-target)", request_target)
signing_string = construct_signing_string(headers, @post_headers)
signature = sign(signing_string)
build_signature_header(key_id, signature, include_headers)
build_signature_header(key_id, signature, @post_headers)
end
@spec verify_signature(map(), String.t(), list(), binary()) :: boolean()
def verify_signature(headers, signature_header, include_headers, body) do
{_key_id, signature} = parse_signature_header(signature_header)
@spec create_get_signature(map(), String.t(), String.t()) :: String.t()
def create_get_signature(headers, key_id, path)
when is_map(headers) and is_binary(key_id) and is_binary(path) do
request_target = "get #{path}"
date = Timex.format!(Timex.now(:utc), "{RFC1123}")
headers = Map.put(headers, "date", date) |> Map.put("(request-target)", request_target)
signing_string = construct_signing_string(headers, @other_headers)
signature = sign(signing_string)
build_signature_header(key_id, signature, @other_headers)
end
@spec verify_post_signature(map(), String.t()) :: boolean()
def verify_post_signature(headers, body) when is_map(headers) and is_binary(body) do
signature_header = Map.get(headers, "signature")
case parse_signature_header(signature_header) do
{:ok, {_key_id, signature}} ->
digest = compute_digest(body)
headers = Map.put(headers, "digest", digest)
signing_string = construct_signing_string(headers, include_headers)
test_headers = Map.put(headers, "digest", digest)
signing_string = construct_signing_string(test_headers, @post_headers)
expected_signature = sign(signing_string)
SecureCompare.compare(expected_signature, Base.decode64!(signature))
{:error, _error} ->
false
end
end
defp compute_digest(body) do
:crypto.hash(:sha256, body)
|> Base.encode64()
|> (&("SHA-256=" <> &1)).()
@spec verify_get_signature(map()) :: boolean()
def verify_get_signature(headers) when is_map(headers) do
signature_header = Map.get(headers, "signature")
case parse_signature_header(signature_header) do
{:ok, {_key_id, signature}} ->
signing_string = construct_signing_string(headers, @other_headers)
expected_signature = sign(signing_string)
SecureCompare.compare(expected_signature, Base.decode64!(signature))
{:error, _error} ->
false
end
end
defp compute_digest(body) when is_binary(body) do
encoded = :crypto.hash(:sha256, body) |> Base.encode64()
"sha-256=" <> encoded
end
defp construct_signing_string(headers, include_headers) do
@ -37,24 +81,24 @@ defmodule HTTPSignature do
|> Enum.join("\n")
end
defp sign(data) do
:crypto.hash(:sha256, [Vonbraun.KeyAgent.get_private_key(), data])
|> Base.encode64()
|> String.downcase()
defp sign(data) when is_binary(data) do
{:ok, signature} = ExPublicKey.sign(data, Vonbraun.KeyAgent.get_private_key())
Base.encode64(signature)
end
defp build_signature_header(key_id, signature, headers) do
"Signature keyId=\"#{key_id}\",algorithm=\"hmac-sha256\",headers=\"#{Enum.join(headers, " ")}\",signature=\"#{signature}\""
"Signature keyId=\"#{key_id}\",headers=\"#{Enum.join(headers, " ")}\",signature=\"#{signature}\""
end
defp parse_signature_header(header) do
[_, key_id, _, _, _headers, _, signature] =
Regex.scan(
~r/keyId="([^"]+)",algorithm="([^"]+)",headers="([^"]+)",signature="([^"]+)"/,
header
)
|> List.first()
{key_id, signature}
defp parse_signature_header(header_value) when is_binary(header_value) do
with {:prefix, "Signature " <> pairs_string} <- {:prefix, header_value},
{:parse, items} <- {:parse, Regex.scan(~r/([-a-z-A-Z]+)="(.+)"/U, pairs_string)},
{:reduce, %{"keyId" => key_id, "signature" => signature}} <-
{:reduce,
Enum.reduce(items, Map.new(), fn [_, key, value], map -> Map.put(map, key, value) end)} do
{:ok, {key_id, signature}}
else
_ -> {:error, :invalid}
end
end
end

View File

@ -25,7 +25,8 @@ defmodule Vonbraun.MixProject do
{:bandit, "~> 1.5.7"},
{:ecto_sqlite3, "~> 0.17"},
{:secure_compare, "~> 0.1.0"},
{:ex_crypto, "~> 0.10.0"}
{:ex_crypto, "~> 0.10.0"},
{:timex, "~> 3.7.11"}
]
end
end

View File

@ -1,6 +1,8 @@
%{
"bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"ecto": {:hex, :ecto, "3.12.1", "626765f7066589de6fa09e0876a253ff60c3d00870dd3a1cd696e2ba67bfceea", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df0045ab9d87be947228e05a8d153f3e06e0d05ab10c3b3cc557d2f7243d1940"},
@ -8,14 +10,25 @@
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.17.1", "96d08639aa1fb07a087daa2220ebc68d9f4f5dbb8aed930e771d848d8d472d32", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "dbd6a68b5364fe6e9ce5d70336709d62edaebbb4cb4acb5f13ec825f64fa48cc"},
"elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
"ex_crypto": {:hex, :ex_crypto, "0.10.0", "af600a89b784b36613a989da6e998c1b200ff1214c3cfbaf8deca4aa2f0a1739", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "ccc7472cfe8a0f4565f97dce7e9280119bf15a5ea51c6535e5b65f00660cde1c"},
"expo": {:hex, :expo, "1.0.1", "f9e2f984f5b8d195815d52d0ba264798c12c8d2f2606f76fa4c60e8ebe39474d", [:mix], [], "hexpm", "f250b33274e3e56513644858c116f255d35c767c2b8e96a512fe7839ef9306a1"},
"exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"},
"gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
"hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"poison": {:hex, :poison, "6.0.0", "9bbe86722355e36ffb62c51a552719534257ba53f3271dacd20fbbd6621a583a", [:mix], [{:decimal, "~> 2.1", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "bb9064632b94775a3964642d6a78281c07b7be1319e0016e1643790704e739a2"},
"secure_compare": {:hex, :secure_compare, "0.1.0", "01b3c93c8edb696e8a5b38397ed48e10958c8a5ec740606656445bcbec0aadb8", [:mix], [], "hexpm", "6391a49eb4a6182f0d7425842fc774bbed715e78b2bfb0c83b99c94e02c78b5c"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
}