diff --git a/lib/nodelix/node_downloader.ex b/lib/nodelix/node_downloader.ex index c32db31..008a238 100644 --- a/lib/nodelix/node_downloader.ex +++ b/lib/nodelix/node_downloader.ex @@ -7,7 +7,7 @@ defmodule Nodelix.NodeDownloader do @checksums_signature_base_url "https://nodejs.org/dist/v$version/SHASUMS256.txt.sig" @signing_keys_list_url "https://raw.githubusercontent.com/nodejs/release-keys/main/keys.list" - @signing_keys_dir_url "https://raw.githubusercontent.com/nodejs/release-keys/main/keys" + @signing_key_base_url "https://raw.githubusercontent.com/nodejs/release-keys/main/keys/$key_id.asc" require Logger @@ -22,9 +22,7 @@ defmodule Nodelix.NodeDownloader do - [X] fetch checksums file signature (https://nodejs.org/dist/v20.10.0/SHASUMS256.txt.sig) - [X] fetch Node.js signing keys list (https://raw.githubusercontent.com/nodejs/release-keys/main/keys.list) - [X] fetch keys (https://raw.githubusercontent.com/nodejs/release-keys/main/keys/4ED778F539E3634C779C87C6D7062848A1AB005C.asc) - - [ ] convert keys to PEM (https://stackoverflow.com/questions/10966256/erlang-importing-gpg-public-key) - - [ ] check signature of the checksums file with each key until there's a match - - [ ] match the hash for the archive filename + - [X] verify signature of the checksums file - [ ] check integrity of the downloaded archive - [ ] decompress archive (delete destination first, see https://github.com/phoenixframework/tailwind/pull/67) """ @@ -71,11 +69,15 @@ defmodule Nodelix.NodeDownloader do def install(archive_base_url \\ @default_archive_base_url) do fetch_archive(archive_base_url) fetch_checksums_and_signature() - _signing_keys = fetch_signing_keys() + %{checksums: checksums_path, signature: signature_path} = paths() + + verify_signature!(checksums_path, signature_path) + + Logger.debug("Signature ok!") end - defp fetch_signing_keys() do - Logger.debug("Downloading signing keys from #{@signing_keys_list_url}") + defp verify_signature!(file_path, signature_path) do + Logger.debug("Downloading signing keys list from #{@signing_keys_list_url}") signing_key_ids = @signing_keys_list_url @@ -83,9 +85,30 @@ defmodule Nodelix.NodeDownloader do |> String.trim() |> String.split("\n") - Enum.map(signing_key_ids, fn key_id -> - HttpUtils.fetch_body!("#{@signing_keys_dir_url}/#{key_id}.asc") + Logger.debug("Using GPG to retrieve signing keys") + + %{path: keystore_path} = keystore = GPGex.Keystore.get_keystore_temp() + + signing_key_ids + |> Enum.map(fn key_id -> + GPGex.cmd!(["--recv-keys", key_id], keystore: keystore) end) + + case GPGex.cmd( + [ + "--verify", + signature_path, + file_path + ], + keystore: keystore + ) do + {:ok, _, _} -> + File.rm_rf!(keystore_path) + :ok + + {:error, _, _, _} -> + raise "invalid signature" + end end defp fetch_archive(archive_base_url) do diff --git a/mix.exs b/mix.exs index 6aab2bb..74e0337 100644 --- a/mix.exs +++ b/mix.exs @@ -53,6 +53,7 @@ defmodule Nodelix.MixProject do defp deps do [ + {:gpg_ex, "1.0.0-alpha.3"}, {:castore, "~> 1.0"}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} ] diff --git a/mix.lock b/mix.lock index 76333f3..2829057 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,9 @@ %{ "castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"}, "earmark_parser": {:hex, :earmark_parser, "1.4.38", "b42252eddf63bda05554ba8be93a1262dc0920c721f1aaf989f5de0f73a2e367", [:mix], [], "hexpm", "2cd0907795aaef0c7e8442e376633c5b3bd6edc8dbbdc539b22f095501c1cdb6"}, + "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, "ex_doc": {:hex, :ex_doc, "0.30.9", "d691453495c47434c0f2052b08dd91cc32bc4e1a218f86884563448ee2502dd2", [: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", "d7aaaf21e95dc5cddabf89063327e96867d00013963eadf2c6ad135506a8bc10"}, + "gpg_ex": {:hex, :gpg_ex, "1.0.0-alpha.3", "d5860bb91b8c833441770f38707ad0556074ab5772f8e1dfc71ddedf7cae9e78", [:mix], [{:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}], "hexpm", "748d4283f0a62836afa07d27944f54c29cd6f02bd238ab1f72534f5273745630"}, "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, "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_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},