From 45f54cde0d5cd419905c012620b3cec969d468d6 Mon Sep 17 00:00:00 2001 From: createkr <228850445+createkr@users.noreply.github.com> Date: Sat, 4 Apr 2026 14:44:52 +0800 Subject: [PATCH] security: require explicit P2P HMAC secret for gossip --- .env.example | 6 +++ docker-compose.yml | 2 + node/rustchain_p2p_gossip.py | 29 +++++++++++- node/test_p2p_secret_enforcement.py | 73 +++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 node/test_p2p_secret_enforcement.py diff --git a/.env.example b/.env.example index b89de0d1a..4bcc53633 100644 --- a/.env.example +++ b/.env.example @@ -54,6 +54,12 @@ LOG_LEVEL=INFO # Set to 'true' to run container as non-root user RUN_AS_NON_ROOT=true +# === P2P Gossip HMAC Secret (REQUIRED) === +# All nodes in the P2P cluster MUST share the same strong random secret. +# Generate with: openssl rand -hex 32 +# If unset or set to a known placeholder, the node will refuse to start. +RC_P2P_SECRET= + # === GitHub Tip Bot Configuration === # Payout wallet address for bounty distributions TIP_BOT_WALLET=RTC1d48d848a5aa5ecf2c5f01aa5fb64837daaf2f35 diff --git a/docker-compose.yml b/docker-compose.yml index 3f85c26cb..526ed9159 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,8 @@ services: - RUSTCHAIN_DB=/rustchain/data/rustchain_v2.db - DOWNLOAD_DIR=/rustchain/downloads - PYTHONUNBUFFERED=1 + # P2P HMAC secret — MUST be set in .env or docker-compose.override.yml + - RC_P2P_SECRET=${RC_P2P_SECRET:?RC_P2P_SECRET is required — generate with: openssl rand -hex 32} volumes: # Persistent storage for SQLite database - rustchain-data:/rustchain/data diff --git a/node/rustchain_p2p_gossip.py b/node/rustchain_p2p_gossip.py index 5bf69bf45..89db7e76d 100644 --- a/node/rustchain_p2p_gossip.py +++ b/node/rustchain_p2p_gossip.py @@ -26,8 +26,33 @@ import logging import requests -# Configuration -P2P_SECRET = os.environ.get("RC_P2P_SECRET", "rustchain_p2p_secret_2025_decentralized") +# --------------------------------------------------------------------------- +# P2P HMAC secret — MUST be set via the RC_P2P_SECRET environment variable. +# There is NO safe default: every node in a P2P cluster must share the same +# strong, randomly generated secret (≥ 32 hex chars recommended). +# --------------------------------------------------------------------------- +_P2P_SECRET_RAW = os.environ.get("RC_P2P_SECRET", "").strip() + +# Known insecure placeholders that must never be accepted in production. +_INSECURE_DEFAULTS = { + "rustchain_p2p_secret_2025_decentralized", + "changeme", + "secret", + "default", + "default-hmac-secret-change-me", + "", +} + +if not _P2P_SECRET_RAW or _P2P_SECRET_RAW.lower() in _INSECURE_DEFAULTS: + raise SystemExit( + "[P2P] FATAL: RC_P2P_SECRET environment variable is not set or contains " + "an insecure placeholder value. Every node must be configured with the " + "same strong, randomly generated HMAC secret before startup.\n" + " Generate one with: openssl rand -hex 32\n" + " Then export: export RC_P2P_SECRET=" + ) + +P2P_SECRET = _P2P_SECRET_RAW GOSSIP_TTL = 3 SYNC_INTERVAL = 30 MESSAGE_EXPIRY = 300 # 5 minutes diff --git a/node/test_p2p_secret_enforcement.py b/node/test_p2p_secret_enforcement.py new file mode 100644 index 000000000..dd3778122 --- /dev/null +++ b/node/test_p2p_secret_enforcement.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +""" +Tests for RC_P2P_SECRET enforcement in rustchain_p2p_gossip.py. + +Verifies that the module refuses to start when: + 1. RC_P2P_SECRET is unset + 2. RC_P2P_SECRET is an empty string + 3. RC_P2P_SECRET is a known insecure placeholder + 4. RC_P2P_SECRET is set to a valid non-empty value (should import OK) +""" + +import subprocess +import sys +import os +import unittest + +GOSSIP_MODULE = os.path.join(os.path.dirname(__file__), "rustchain_p2p_gossip.py") + + +def _import_with_env(env_vars: dict) -> subprocess.CompletedProcess: + """Attempt to import the gossip module under a given environment.""" + env = os.environ.copy() + env.update(env_vars) + # Remove any pre-existing RC_P2P_SECRET first + env.pop("RC_P2P_SECRET", None) + env.update(env_vars) + + code = ( + "import sys; sys.path.insert(0, '.');" + "import importlib.util;" + "spec = importlib.util.spec_from_file_location('gossip', 'rustchain_p2p_gossip.py');" + "mod = importlib.util.module_from_spec(spec);" + "spec.loader.exec_module(mod)" + ) + return subprocess.run( + [sys.executable, "-c", code], + capture_output=True, text=True, cwd=os.path.dirname(__file__), + env=env, timeout=15 + ) + + +class TestP2PSecretEnforcement(unittest.TestCase): + + def test_unset_secret_causes_fatal_exit(self): + """Module must raise SystemExit when RC_P2P_SECRET is not set.""" + result = _import_with_env({}) + self.assertNotEqual(result.returncode, 0, + f"Expected non-zero exit, got {result.returncode}") + self.assertIn("RC_P2P_SECRET", result.stderr + result.stdout) + + def test_empty_string_secret_causes_fatal_exit(self): + """Module must raise SystemExit when RC_P2P_SECRET is empty.""" + result = _import_with_env({"RC_P2P_SECRET": ""}) + self.assertNotEqual(result.returncode, 0) + self.assertIn("RC_P2P_SECRET", result.stderr + result.stdout) + + def test_known_insecure_default_causes_fatal_exit(self): + """Module must reject the old hardcoded value.""" + result = _import_with_env({ + "RC_P2P_SECRET": "rustchain_p2p_secret_2025_decentralized" + }) + self.assertNotEqual(result.returncode, 0) + self.assertIn("insecure", (result.stderr + result.stdout).lower()) + + def test_valid_secret_allows_import(self): + """Module must load successfully with a strong random secret.""" + result = _import_with_env({"RC_P2P_SECRET": "a" * 64}) + self.assertEqual(result.returncode, 0, + f"Import failed with valid secret: {result.stderr}") + + +if __name__ == "__main__": + unittest.main()