diff --git a/config/prod.exs b/config/prod.exs index 5e4263be1..42967e3ae 100755 --- a/config/prod.exs +++ b/config/prod.exs @@ -125,6 +125,12 @@ config :archethic, ArchEthic.Governance.Pools, uniris: [] ] +config :archethic, ArchEthic.Mining.PendingTransactionValidation, + allowed_node_key_origins: + System.get_env("ARCHETHIC_NODE_ALLOWED_KEY_ORIGINS", "tpm") + |> String.split(";", trim: true) + |> Enum.map(&String.to_existing_atom/1) + config :archethic, ArchEthic.Networking.IPLookup, (case(System.get_env("ARCHETHIC_NETWORKING_IMPL", "NAT")) do diff --git a/lib/archethic/crypto.ex b/lib/archethic/crypto.ex index b857ee72e..b8514833c 100755 --- a/lib/archethic/crypto.ex +++ b/lib/archethic/crypto.ex @@ -9,7 +9,7 @@ defmodule ArchEthic.Crypto do ``` Ed25519 Software Public key | / | - | | |-------| + | | |-------| | | | <<0, 0, 106, 58, 193, 73, 144, 121, 104, 101, 53, 140, 125, 240, 52, 222, 35, 181, 13, 81, 241, 114, 227, 205, 51, 167, 139, 100, 176, 111, 68, 234, 206, 72>> @@ -1070,7 +1070,7 @@ defmodule ArchEthic.Crypto do end @doc """ - Return the Root CA public key for the given versioned public key + Return the Root CA public key for the given versioned public key """ @spec get_root_ca_public_key(key()) :: binary() def get_root_ca_public_key(<<_::8, origin_id::8, _::binary>>) do @@ -1133,4 +1133,29 @@ defmodule ArchEthic.Crypto do """ @spec default_curve() :: supported_curve() def default_curve, do: Application.get_env(:archethic, __MODULE__)[:default_curve] + + @doc """ + Determine if the origin of the key is allowed + + This prevent software keys to be used in prod, as we want secure element to prevent malicious nodes + + ## Examples + + iex> Crypto.authorized_key_origin?(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, [:tpm]) + false + + iex> Crypto.authorized_key_origin?(<<0::8, 1::8, :crypto.strong_rand_bytes(32)::binary>>, [:tpm]) + true + + iex> Crypto.authorized_key_origin?(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, []) + true + """ + @spec authorized_key_origin?(key(), list(supported_origin())) :: boolean() + def authorized_key_origin?(<<_::8, origin_id::8, _::binary>>, allowed_key_origins = [_ | _]) do + ID.to_origin(origin_id) in allowed_key_origins + end + + def authorized_key_origin?(<<_::8, _::8, _::binary>>, []) do + true + end end diff --git a/lib/archethic/mining/pending_transaction_validation.ex b/lib/archethic/mining/pending_transaction_validation.ex index c379e1651..c9fe4ba55 100644 --- a/lib/archethic/mining/pending_transaction_validation.ex +++ b/lib/archethic/mining/pending_transaction_validation.ex @@ -33,12 +33,6 @@ defmodule ArchEthic.Mining.PendingTransactionValidation do require Logger - @validate_connection Application.compile_env( - :archethic, - [__MODULE__, :validate_connection], - false - ) - @doc """ Determines if the transaction is accepted into the network """ @@ -121,21 +115,23 @@ defmodule ArchEthic.Mining.PendingTransactionValidation do previous_public_key: previous_public_key }) do with {:ok, ip, port, _, _, key_certificate} <- Node.decode_transaction_content(content), + true <- + Crypto.authorized_key_origin?(previous_public_key, get_allowed_node_key_origins()), root_ca_public_key <- Crypto.get_root_ca_public_key(previous_public_key), - true <- valid_connection?(@validate_connection, ip, port, previous_public_key), true <- Crypto.verify_key_certificate?( previous_public_key, key_certificate, root_ca_public_key - ) do + ), + true <- valid_connection?(ip, port, previous_public_key) do :ok else :error -> {:error, "Invalid node transaction's content"} false -> - {:error, "Invalid node transaction with invalid key certificate"} + {:error, "Invalid node transaction with invalid key / certificate"} end end @@ -251,6 +247,12 @@ defmodule ArchEthic.Mining.PendingTransactionValidation do defp do_accept_transaction(_), do: :ok + defp get_allowed_node_key_origins do + :archethic + |> Application.get_env(__MODULE__, []) + |> Keyword.get(:allowed_node_key_origins, []) + end + defp get_first_public_key(tx = %Transaction{previous_public_key: previous_public_key}) do previous_address = Transaction.previous_address(tx) @@ -268,15 +270,23 @@ defmodule ArchEthic.Mining.PendingTransactionValidation do end end - defp valid_connection?(true, ip, port, previous_public_key) do - with true <- Networking.valid_ip?(ip), - false <- P2P.duplicating_node?(ip, port, previous_public_key) do - true + defp valid_connection?(ip, port, previous_public_key) do + if should_validate_connection?() do + with true <- Networking.valid_ip?(ip), + false <- P2P.duplicating_node?(ip, port, previous_public_key) do + true + else + _ -> + false + end else - _ -> - false + true end end - defp valid_connection?(false, _, _, _), do: true + defp should_validate_connection? do + :archethic + |> Application.get_env(__MODULE__, []) + |> Keyword.get(:validate_connection, false) + end end diff --git a/test/archethic/mining/distributed_workflow_test.exs b/test/archethic/mining/distributed_workflow_test.exs index 4301052ea..263679756 100644 --- a/test/archethic/mining/distributed_workflow_test.exs +++ b/test/archethic/mining/distributed_workflow_test.exs @@ -42,8 +42,8 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do Enum.each(BeaconChain.list_subsets(), &Registry.register(SubsetRegistry, &1, [])) P2P.add_and_connect_node(%Node{ - ip: {127, 0, 0, 1}, - port: 3000, + ip: {80, 10, 20, 102}, + port: 3001, first_public_key: Crypto.first_node_public_key(), last_public_key: Crypto.last_node_public_key(), authorized?: true, @@ -58,8 +58,8 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do {pub, _} = Crypto.generate_deterministic_keypair("seed") P2P.add_and_connect_node(%Node{ - ip: {127, 0, 0, 1}, - port: 3000, + ip: {80, 10, 20, 102}, + port: 3002, first_public_key: pub, last_public_key: pub, authorized?: true, @@ -76,7 +76,7 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do tx = Transaction.new(:node, %TransactionData{ content: - <<127, 0, 0, 1, 3000::16, 1, 0, 16, 233, 156, 172, 143, 228, 236, 12, 227, 76, 1, 80, + <<80, 10, 20, 102, 3000::16, 1, 0, 16, 233, 156, 172, 143, 228, 236, 12, 227, 76, 1, 80, 12, 236, 69, 10, 209, 6, 234, 172, 97, 188, 240, 207, 70, 115, 64, 117, 44, 82, 132, 186, byte_size(certificate)::16, certificate::binary>> }) @@ -158,7 +158,7 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do ) welcome_node = %Node{ - ip: {127, 0, 0, 1}, + ip: {80, 10, 20, 102}, port: 3005, first_public_key: "key1", last_public_key: "key1", @@ -188,8 +188,8 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do test "should aggregate context and wait enough confirmed validation nodes context building", %{tx: tx, sorting_seed: sorting_seed} do P2P.add_and_connect_node(%Node{ - ip: {127, 0, 0, 1}, - port: 3000, + ip: {80, 10, 20, 102}, + port: 3006, last_public_key: "other_validator_key", first_public_key: "other_validator_key", authorized?: true, @@ -202,8 +202,8 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do }) P2P.add_and_connect_node(%Node{ - ip: {127, 0, 0, 1}, - port: 3000, + ip: {80, 10, 20, 102}, + port: 3007, last_public_key: "other_validator_key2", first_public_key: "other_validator_key2", authorized?: true, @@ -240,7 +240,7 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do end) welcome_node = %Node{ - ip: {127, 0, 0, 1}, + ip: {80, 10, 20, 102}, port: 3005, first_public_key: "key1", last_public_key: "key1", @@ -257,8 +257,8 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do previous_storage_nodes = [ %Node{ - ip: {127, 0, 0, 1}, - port: 3000, + ip: {80, 10, 20, 102}, + port: 3006, first_public_key: "key10", last_public_key: "key10", authorized?: true, @@ -267,8 +267,8 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do network_patch: "AAA" }, %Node{ - ip: {127, 0, 0, 1}, - port: 3002, + ip: {80, 10, 20, 102}, + port: 3007, first_public_key: "key23", last_public_key: "key23", authorized?: true, @@ -333,7 +333,7 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do end) welcome_node = %Node{ - ip: {127, 0, 0, 1}, + ip: {80, 10, 20, 102}, port: 3005, first_public_key: "key1", last_public_key: "key1", @@ -352,8 +352,8 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do previous_storage_nodes = [ %Node{ - ip: {127, 0, 0, 1}, - port: 3000, + ip: {80, 10, 20, 102}, + port: 3006, first_public_key: "key10", last_public_key: "key10", reward_address: :crypto.strong_rand_bytes(32), @@ -363,8 +363,8 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do network_patch: "AAA" }, %Node{ - ip: {127, 0, 0, 1}, - port: 3002, + ip: {80, 10, 20, 102}, + port: 3007, first_public_key: "key23", last_public_key: "key23", reward_address: :crypto.strong_rand_bytes(32), @@ -410,8 +410,8 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do {pub, _} = Crypto.generate_deterministic_keypair("seed3") P2P.add_and_connect_node(%Node{ - ip: {127, 0, 0, 1}, - port: 3000, + ip: {80, 10, 20, 102}, + port: 3008, last_public_key: pub, first_public_key: pub, authorized?: true, @@ -458,7 +458,7 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do end) welcome_node = %Node{ - ip: {127, 0, 0, 1}, + ip: {80, 10, 20, 102}, port: 3005, first_public_key: "key1", last_public_key: "key1", @@ -485,8 +485,8 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do previous_storage_nodes = [ %Node{ - ip: {127, 0, 0, 1}, - port: 3000, + ip: {80, 10, 20, 102}, + port: 3006, first_public_key: "key10", last_public_key: "key10", reward_address: :crypto.strong_rand_bytes(32), @@ -496,8 +496,8 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do network_patch: "AAA" }, %Node{ - ip: {127, 0, 0, 1}, - port: 3002, + ip: {80, 10, 20, 102}, + port: 3007, first_public_key: "key23", last_public_key: "key23", reward_address: :crypto.strong_rand_bytes(32), @@ -639,8 +639,8 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do end) P2P.add_and_connect_node(%Node{ - ip: {127, 0, 0, 1}, - port: 3000, + ip: {80, 10, 20, 102}, + port: 3006, last_public_key: "key10", first_public_key: "key10", network_patch: "AAA", @@ -653,8 +653,8 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do }) P2P.add_and_connect_node(%Node{ - ip: {127, 0, 0, 1}, - port: 3000, + ip: {80, 10, 20, 102}, + port: 3007, last_public_key: "key23", first_public_key: "key23", network_patch: "AAA", @@ -667,7 +667,7 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do }) welcome_node = %Node{ - ip: {127, 0, 0, 1}, + ip: {80, 10, 20, 102}, port: 3005, first_public_key: "key1", last_public_key: "key1", @@ -694,8 +694,8 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do previous_storage_nodes = [ %Node{ - ip: {127, 0, 0, 1}, - port: 3000, + ip: {80, 10, 20, 102}, + port: 3007, first_public_key: "key10", last_public_key: "key10", reward_address: :crypto.strong_rand_bytes(32), @@ -703,8 +703,8 @@ defmodule ArchEthic.Mining.DistributedWorkflowTest do authorization_date: DateTime.utc_now() }, %Node{ - ip: {127, 0, 0, 1}, - port: 3002, + ip: {80, 10, 20, 102}, + port: 3008, first_public_key: "key23", last_public_key: "key23", reward_address: :crypto.strong_rand_bytes(32), diff --git a/test/archethic/mining/pending_transaction_validation_test.exs b/test/archethic/mining/pending_transaction_validation_test.exs index 7a0c67811..4e0ff4651 100644 --- a/test/archethic/mining/pending_transaction_validation_test.exs +++ b/test/archethic/mining/pending_transaction_validation_test.exs @@ -24,11 +24,17 @@ defmodule ArchEthic.Mining.PendingTransactionValidationTest do network_patch: "AAA" }) + on_exit(fn -> + Application.put_env(:archethic, ArchEthic.Mining.PendingTransactionValidation, + allowed_node_key_origins: [] + ) + end) + :ok end describe "validate_pending_transaction/1" do - test "should :ok when a node transaction data content contains node endpoint information" do + test "should return :ok when a node transaction data content contains node endpoint information" do {public_key, _} = Crypto.derive_keypair("seed", 0) certificate = Crypto.get_key_certificate(public_key) @@ -37,9 +43,9 @@ defmodule ArchEthic.Mining.PendingTransactionValidationTest do :node, %TransactionData{ content: - <<127, 0, 0, 1, 3000::16, 1, 0, 4, 221, 19, 74, 75, 69, 16, 50, 149, 253, 24, 115, - 128, 241, 110, 118, 139, 7, 48, 217, 58, 43, 145, 233, 77, 125, 190, 207, 31, 64, - 157, 137, byte_size(certificate)::16, certificate::binary>> + <<80, 20, 10, 200, 3000::16, 1, 0, 4, 221, 19, 74, 75, 69, 16, 50, 149, 253, 24, + 115, 128, 241, 110, 118, 139, 7, 48, 217, 58, 43, 145, 233, 77, 125, 190, 207, 31, + 64, 157, 137, byte_size(certificate)::16, certificate::binary>> }, "seed", 0 @@ -48,6 +54,33 @@ defmodule ArchEthic.Mining.PendingTransactionValidationTest do assert :ok = PendingTransactionValidation.validate(tx) end + test "should return an error when a node transaction public key used on non allowed origin" do + Application.put_env(:archethic, ArchEthic.Mining.PendingTransactionValidation, + allowed_node_key_origins: [:tpm] + ) + + {public_key, private_key} = Crypto.derive_keypair("seed", 0) + {next_public_key, _} = Crypto.derive_keypair("seed", 1) + certificate = Crypto.get_key_certificate(public_key) + + tx = + Transaction.new_with_keys( + :node, + %TransactionData{ + content: + <<80, 20, 100, 50, 3000::16, 1, 0, 4, 221, 19, 74, 75, 69, 16, 50, 149, 253, 24, + 115, 128, 241, 110, 118, 139, 7, 48, 217, 58, 43, 145, 233, 77, 125, 190, 207, 31, + 64, 157, 137, byte_size(certificate)::16, certificate::binary>> + }, + private_key, + public_key, + next_public_key + ) + + assert {:error, "Invalid node transaction with invalid key / certificate"} = + PendingTransactionValidation.validate(tx) + end + test "should return :ok when a node shared secrets transaction data keys contains existing node public keys with first tx" do P2P.add_and_connect_node(%Node{ ip: {127, 0, 0, 1},