WIP: Allowed supplying "(created)" pseudoheader #1
5 changed files with 63 additions and 26 deletions
|
@ -1,2 +1,2 @@
|
||||||
elixir 1.15.4-otp-26
|
erlang 26.2.4
|
||||||
erlang 26.0.2
|
elixir 1.16.2-otp-26
|
||||||
|
|
|
@ -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
|
||||||
|
|
4
mix.exs
4
mix.exs
|
@ -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,
|
||||||
|
|
18
mix.lock
18
mix.lock
|
@ -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"},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
Loading…
Reference in a new issue