# Patch 2 of 5 — Config & Wiring > **Context for this agent**: This is part of a patchset adding optional TLS > to the Arweave p2p network. Nodes gossip TLS public keys (SPKI) via a signed > `GET /peers/keys` endpoint. Peers that learn a SPKI connect over TLS with > SPKI pinning. Fully backwards-compatible — nodes without TLS are unaffected. > Ask anything: "why did they do X?", "would approach Y work?", "explain Z differently." ## What This Patch Does Four existing files. No new behaviour is visible without `ar_tls.erl` (patch 3). | File | Change | |------|--------| | `arweave_config.hrl` | Adds `tls_peers = []` field; updates comments on existing `tls_cert_file`/`tls_key_file` | | `ar_cli_parser.erl` | Adds `"generate"` atom clause for `tls_cert_file` and a new `tls_peers` parse clause | | `ar_sup.erl` | Creates `ar_tls` ETS table; calls `ar_tls:init/0` after all ETS tables exist | | `ar_peers.erl` | `get_peer_peers/1` calls `get_tls_peers(Peer)` non-fatally alongside the existing `/peers` fetch | ## `tls_peers` Config Key Mirrors the existing `peers` config key. Format: `tls_peers ip:port:base64url_spki`. ``` peers 1.2.3.4:1984 tls_peers 1.2.3.4:1984:AAAB...base64url_spki... ``` On startup `ar_tls:init/0` pre-populates ETS from this list — first connection to that peer already uses TLS, no gossip bootstrap needed. Also the manual escape hatch for key rotation: clear the entry, restart, re-seed the new SPKI. ## ETS Table Design | ETS Key | Value | Purpose | |---------|-------|---------| | `local_tls_key` | SPKI binary | This node's own public key | | `local_cert_file` | path string | For signing gossip docs | | `local_key_file` | path string | For signing gossip docs | | `{tls_peer_key, Peer}` | SPKI binary | Forward: peer → SPKI | | `{tls_key_peer, SPKI}` | Peer tuple | Reverse: SPKI → peer | ## Norm-Check: ETS table creation in `ar_sup.erl` All tables created in one block at supervisor init. New `ar_tls` table follows the same options as `ar_peers`, `ar_shutdown_manager`, etc.: ```erlang ets:new(ar_shutdown_manager, [set, public, named_table, {read_concurrency, true}]), ets:new(ar_timer, [set, public, named_table, {read_concurrency, true}]), ets:new(ar_peers, [set, public, named_table, {read_concurrency, true}]), ets:new(ar_tls, [set, public, named_table, {read_concurrency, true}]), %% NEW ets:new(ar_http, [set, public, named_table]), ``` ## Norm-Check: `ar_cli_parser.erl` clause ordering More-specific string match must come before generic catch-all: ```erlang parse(["tls_cert_file", "generate" | Rest], C) -> %% specific: atom result parse(Rest, C#config{ tls_cert_file = generate }); parse(["tls_cert_file", CertFilePath | Rest], C) -> %% generic: file path AbsCertFilePath = filename:absname(CertFilePath), ar_util:assert_file_exists_and_readable(AbsCertFilePath), parse(Rest, C#config{ tls_cert_file = AbsCertFilePath }); ``` `tls_peers` splits on the **trailing** colon (`string:split(Entry, ":", trailing)`) to separate `ip:port` from the SPKI — handles any number of colons in the address. ## Norm-Check: `get_peer_peers/1` in `ar_peers.erl` ```erlang %% BEFORE get_peer_peers(Peer) -> case ar_http_iface_client:get_peers(Peer) of unavailable -> []; Peers -> Peers end. %% AFTER get_peer_peers(Peer) -> case ar_http_iface_client:get_peers(Peer) of unavailable -> []; Peers -> %% Also fetch TLS key gossip from the peer (non-fatal). ar_http_iface_client:get_tls_peers(Peer), Peers end. ``` `get_tls_peers` result is intentionally dropped — it's a side-effect that populates ETS. `Peers` is returned unchanged. ## Project File Listings ``` apps/arweave/src/ (~130 .erl files, prefix ar_*) ar_cli_parser.erl ar_http.erl ar_http_iface_client.erl ar_http_iface_middleware.erl ar_http_iface_server.erl ar_peers.erl ar_sup.erl ar_tls.erl <- NEW ar_util.erl ar_wallet.erl apps/arweave/test/ (~50 _tests.erl files) ar_config_tests.erl ar_http_iface_tests.erl ar_http_util_tests.erl ar_semaphore_tests.erl ar_tls_tests.erl <- NEW ar_wallet_tests.erl apps/arweave_config/include/ arweave_config.hrl <- modified arweave_config_spec.hrl ``` ## The Patch ```diff %% CONFIG & WIRING: 4 existing files %% apps/arweave_config/include/arweave_config.hrl — 3 new config fields %% apps/arweave/src/ar_cli_parser.erl — CLI parse clauses for each %% apps/arweave/src/ar_sup.erl — ar_tls ETS table creation %% apps/arweave/src/ar_peers.erl — gossip hook in get_peer_peers/1 %% %% Adds tls_cert_file, tls_key_file (existing fields, new `generate` atom handled), %% and tls_peers [{Peer, SPKIDer}] config list. ar_sup creates the ETS table that %% ar_tls.erl owns. ar_peers fires get_tls_peers/1 non-fatally alongside the %% existing /peers fetch — the return value is intentionally ignored (side-effect only). %% %% No new visible behaviour without ar_tls.erl present. diff --git a/apps/arweave/src/ar_cli_parser.erl b/apps/arweave/src/ar_cli_parser.erl index 4ba78ac..132af57 100644 --- a/apps/arweave/src/ar_cli_parser.erl +++ b/apps/arweave/src/ar_cli_parser.erl @@ -827,6 +827,8 @@ parse(["defragment_module", DefragModuleString | Rest], C) -> {init, stop, [1]} ], C} end; +parse(["tls_cert_file", "generate" | Rest], C) -> + parse(Rest, C#config{ tls_cert_file = generate }); parse(["tls_cert_file", CertFilePath | Rest], C) -> AbsCertFilePath = filename:absname(CertFilePath), ar_util:assert_file_exists_and_readable(AbsCertFilePath), @@ -835,6 +837,22 @@ parse(["tls_key_file", KeyFilePath | Rest], C) -> AbsKeyFilePath = filename:absname(KeyFilePath), ar_util:assert_file_exists_and_readable(AbsKeyFilePath), parse(Rest, C#config{ tls_key_file = AbsKeyFilePath }); +parse(["tls_peers", Entry | Rest], C) -> + %% Format: "ip:port:base64url_spki" + case string:split(Entry, ":", trailing) of + [AddrPart, KeyB64] -> + case ar_util:safe_parse_peer(AddrPart) of + {ok, [Peer | _]} -> + SPKIDer = ar_util:decode(list_to_binary(KeyB64)), + parse(Rest, C#config{ + tls_peers = [{Peer, SPKIDer} | C#config.tls_peers] + }); + _ -> + parse(Rest, C) + end; + _ -> + parse(Rest, C) + end; parse(["http_api.tcp.idle_timeout_seconds", Num | Rest], C) -> parse(Rest, C#config { http_api_transport_idle_timeout = list_to_integer(Num) * 1000 }); parse(["coordinated_mining" | Rest], C) -> diff --git a/apps/arweave/src/ar_peers.erl b/apps/arweave/src/ar_peers.erl index ad173a4..412c392 100644 --- a/apps/arweave/src/ar_peers.erl +++ b/apps/arweave/src/ar_peers.erl @@ -622,7 +622,10 @@ terminate(Reason, _State) -> get_peer_peers(Peer) -> case ar_http_iface_client:get_peers(Peer) of unavailable -> []; - Peers -> Peers + Peers -> + %% Also fetch TLS key gossip from the peer (non-fatal). + ar_http_iface_client:get_tls_peers(Peer), + Peers end. get_or_init_performance(Peer) -> diff --git a/apps/arweave/src/ar_sup.erl b/apps/arweave/src/ar_sup.erl index 188b545..a7c9997 100644 --- a/apps/arweave/src/ar_sup.erl +++ b/apps/arweave/src/ar_sup.erl @@ -33,6 +33,7 @@ init([]) -> ets:new(ar_shutdown_manager, [set, public, named_table, {read_concurrency, true}]), ets:new(ar_timer, [set, public, named_table, {read_concurrency, true}]), ets:new(ar_peers, [set, public, named_table, {read_concurrency, true}]), + ets:new(ar_tls, [set, public, named_table, {read_concurrency, true}]), ets:new(ar_http, [set, public, named_table]), ets:new(ar_rate_limiter, [set, public, named_table, {read_concurrency, true}]), ets:new(ar_blacklist_middleware, [set, public, named_table]), @@ -67,6 +68,9 @@ init([]) -> ets:new(block_index, [ordered_set, public, named_table]), ets:new(node_state, [set, public, named_table]), ets:new(mining_state, [set, public, named_table, {read_concurrency, true}]), + %% Initialise TLS state (resolves/generates cert if TLS is configured). + %% This must happen after ETS tables are created and before any listener starts. + ar_tls:init(), Children = [ ?CHILD(ar_shutdown_manager, worker), ?CHILD(ar_rate_limiter, worker), diff --git a/apps/arweave_config/include/arweave_config.hrl b/apps/arweave_config/include/arweave_config.hrl index 42de946..8c55fa7 100644 --- a/apps/arweave_config/include/arweave_config.hrl +++ b/apps/arweave_config/include/arweave_config.hrl @@ -383,8 +383,13 @@ defragmentation_modules = [], block_throttle_by_ip_interval = ?DEFAULT_BLOCK_THROTTLE_BY_IP_INTERVAL_MS, block_throttle_by_solution_interval = ?DEFAULT_BLOCK_THROTTLE_BY_SOLUTION_INTERVAL_MS, - tls_cert_file = not_set, %% required to enable TLS - tls_key_file = not_set, %% required to enable TLS + %% Path to PEM cert for TLS on the main port; set to 'generate' to auto-generate + %% a self-signed P-256 cert+key in {data_dir}/tls/ + tls_cert_file = not_set, + tls_key_file = not_set, %% path to PEM private key for TLS on the main port + %% Pre-configured peer TLS keys: [{Peer, SPKIDer}] tuples. + %% Populated from 'tls_peers ip:port:base64url_spki' config entries. + tls_peers = [], http_api_transport_idle_timeout = ?DEFAULT_COWBOY_TCP_IDLE_TIMEOUT_SECOND*1000, coordinated_mining = false, cm_api_secret = not_set, ```