Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 27 additions & 2 deletions node/rustchain_p2p_gossip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=<your-secret>"
)

P2P_SECRET = _P2P_SECRET_RAW
GOSSIP_TTL = 3
SYNC_INTERVAL = 30
MESSAGE_EXPIRY = 300 # 5 minutes
Expand Down
73 changes: 73 additions & 0 deletions node/test_p2p_secret_enforcement.py
Original file line number Diff line number Diff line change
@@ -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()
Loading