WIP: Allowed supplying "(created)" pseudoheader #1

Closed
floatingghost wants to merge 1 commit from created-pseudoheader into main
5 changed files with 63 additions and 26 deletions

View file

@ -1,2 +1,2 @@
elixir 1.15.4-otp-26 erlang 26.2.4
erlang 26.0.2 elixir 1.16.2-otp-26

View file

@ -10,6 +10,9 @@ defmodule HTTPSignatures do
require Logger require Logger
defp encode_pseudoheader("created"), do: "(created)"
defp encode_pseudoheader(header), do: header
def split_signature(sig) do def split_signature(sig) do
default = %{"headers" => "date"} default = %{"headers" => "date"}
@ -19,6 +22,7 @@ defmodule HTTPSignatures do
|> String.split(",") |> String.split(",")
|> Enum.reduce(default, fn part, acc -> |> Enum.reduce(default, fn part, acc ->
[key | rest] = String.split(part, "=") [key | rest] = String.split(part, "=")
key = encode_pseudoheader(key)
value = Enum.join(rest, "=") value = Enum.join(rest, "=")
Map.put(acc, key, String.trim(value, "\"")) Map.put(acc, key, String.trim(value, "\""))
end) end)
@ -76,27 +80,26 @@ defmodule HTTPSignatures do
|> Enum.map_join("\n", fn header -> "#{header}: #{headers[header]}" end) |> Enum.map_join("\n", fn header -> "#{header}: #{headers[header]}" end)
end end
# Sort map alphabetically to ensure stability defp maybe_put_created(fields, headers) do
defp stable_sort_headers(headers) when is_map(headers) do case headers[:"(created)"] do
headers nil -> fields
|> Enum.into([]) created -> Keyword.put(fields, :created, created)
|> Enum.sort_by(fn {k, _v} -> k end) end
end end
def sign(private_key, key_id, headers) do def sign(private_key, key_id, headers) do
headers = stable_sort_headers(headers) sigstring = build_signing_string(headers, Enum.sort(Map.keys(headers)))
sigstring = build_signing_string(headers, Keyword.keys(headers))
signature = signature =
:public_key.sign(sigstring, :sha256, private_key) :public_key.sign(sigstring, :sha256, private_key)
|> Base.encode64() |> Base.encode64()
[ []
keyId: key_id, |> maybe_put_created(headers)
algorithm: "rsa-sha256", |> Keyword.put(:signature, signature)
headers: Keyword.keys(headers) |> Enum.join(" "), |> Keyword.put(:headers, Map.keys(headers) |> Enum.sort() |> Enum.join(" "))
signature: signature |> Keyword.put(:algorithm, "rsa-sha256")
] |> Keyword.put(:keyId, key_id)
|> Enum.map_join(",", fn {k, v} -> "#{k}=\"#{v}\"" end) |> Enum.map_join(",", fn {k, v} -> "#{k}=\"#{v}\"" end)
end end
end end

View file

@ -8,8 +8,8 @@ defmodule HttpSignatures.MixProject do
[ [
app: :http_signatures, app: :http_signatures,
description: "Library for manipulating and validating HTTP signatures", description: "Library for manipulating and validating HTTP signatures",
version: "0.1.1", version: "0.1.2",
elixir: "~> 1.7", elixir: "~> 1.14",
elixirc_options: [warnings_as_errors: true], elixirc_options: [warnings_as_errors: true],
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,

View file

@ -1,14 +1,14 @@
%{ %{
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, "credo": {:hex, :credo, "1.7.6", "b8f14011a5443f2839b04def0b252300842ce7388f3af177157c86da18dfbeea", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "146f347fb9f8cbc5f7e39e3f22f70acbef51d441baa6d10169dd604bfbc55296"},
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ex_doc": {:hex, :ex_doc, "0.30.4", "e8395c8e3c007321abb30a334f9f7c0858d80949af298302daf77553468c0c39", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "9a19f0c50ffaa02435668f5242f2b2a61d46b541ebf326884505dfd3dd7af5e4"}, "ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
} }

View file

@ -33,6 +33,14 @@ defmodule HttpSignaturesTest do
keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="Ef7MlxLXoBovhil3AlyjtBwAL9g4TN3tibLj7uuNB3CROat/9KaeQ4hW2NiJ+pZ6HQEOx9vYZAyi+7cmIkmJszJCut5kQLAwuX+Ms/mUFvpKlSo9StS2bMXDBNjOh4Auj774GFj4gwjS+3NhFeoqyr/MuN6HsEnkvn6zdgfE2i0=" keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="Ef7MlxLXoBovhil3AlyjtBwAL9g4TN3tibLj7uuNB3CROat/9KaeQ4hW2NiJ+pZ6HQEOx9vYZAyi+7cmIkmJszJCut5kQLAwuX+Ms/mUFvpKlSo9StS2bMXDBNjOh4Auj774GFj4gwjS+3NhFeoqyr/MuN6HsEnkvn6zdgfE2i0="
""" """
@created_field_signature """
keyId=\"Test\",algorithm=\"rsa-sha256\",headers=\"(created) (request-target) content-length content-type digest host\",signature=\"AA2GxBzU7U7V3gBQ5RT2WCIK3WvUx89cjQD2HSX3CExZ3sQAlF+msMlquLu6ig977uyHHneXP4TTLGtC84fIJF9KAo4SLS76pR7AINGjxkmSq9nZBdEk1MRSfI18OIleTj4yg0CVK7ofo8JcX2lCWLOilI66rwobHD7wJ+RSj6o=\",created=\"1717961994\"
"""
@created_field_signature_with_wrong_created_value """
keyId=\"Test\",algorithm=\"rsa-sha256\",headers=\"(created) (request-target) content-length content-type digest host\",signature=\"AA2GxBzU7U7V3gBQ5RT2WCIK3WvUx89cjQD2HSX3CExZ3sQAlF+msMlquLu6ig977uyHHneXP4TTLGtC84fIJF9KAo4SLS76pR7AINGjxkmSq9nZBdEk1MRSfI18OIleTj4yg0CVK7ofo8JcX2lCWLOilI66rwobHD7wJ+RSj6o=\",created=\"1717961993\"
"""
test "split up a signature" do test "split up a signature" do
expected = %{ expected = %{
"keyId" => "Test", "keyId" => "Test",
@ -60,6 +68,32 @@ defmodule HttpSignaturesTest do
assert HTTPSignatures.validate(@headers, signature, @public_key) assert HTTPSignatures.validate(@headers, signature, @public_key)
end end
test "validates the (created) pseudo-header case" do
headers = %{
"(request-target)" => "post /foo?param=value&pet=dog",
"host" => "example.com",
"(created)" => "1717961994",
"content-type" => "application/json",
"digest" => "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=",
"content-length" => "18"
}
signature = HTTPSignatures.split_signature(@created_field_signature)
assert HTTPSignatures.validate(headers, signature, @public_key)
end
test "it does not validate a signature with an incorrect pseudo-header" do
headers = %{
"(request-target)" => "post /foo?param=value&pet=dog",
"host" => "example.com",
"content-type" => "application/json",
"digest" => "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=",
"content-length" => "18"
}
signature = HTTPSignatures.split_signature(@created_field_signature_with_wrong_created_value)
refute HTTPSignatures.validate(headers, signature, @public_key)
end
test "it contructs a signing string" do test "it contructs a signing string" do
expected = "date: Thu, 05 Jan 2014 21:31:40 GMT\ncontent-length: 18" expected = "date: Thu, 05 Jan 2014 21:31:40 GMT\ncontent-length: 18"
assert expected == HTTPSignatures.build_signing_string(@headers, ["date", "content-length"]) assert expected == HTTPSignatures.build_signing_string(@headers, ["date", "content-length"])