From 43e9f542f571edc30dc740a9e4bc0a95fba59631 Mon Sep 17 00:00:00 2001 From: Ryan Breuker Date: Sun, 19 Apr 2026 03:32:36 -0600 Subject: [PATCH 1/3] Fix #2273 Item C: Add non-root key paths fallback --- node/p2p_identity.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/node/p2p_identity.py b/node/p2p_identity.py index 924ecd28e..c176589a5 100644 --- a/node/p2p_identity.py +++ b/node/p2p_identity.py @@ -134,12 +134,33 @@ class LocalKeypair: """ def __init__(self, path: Optional[str | Path] = None): + import os, logging if path is None: - self.path = get_default_privkey_path() + env_path = os.getenv("RC_P2P_PRIVKEY_PATH") + candidates = [Path(env_path)] if env_path else [] + candidates += [Path("/etc/rustchain/p2p_identity.pem"), Path.home() / ".rustchain" / "p2p_identity.pem"] + + chosen = None + for p in candidates: + if p.exists(): chosen = p; break + + if not chosen: + for p in candidates: + try: + p.parent.mkdir(parents=True, exist_ok=True) + with open(p, 'a'): pass # test write access + chosen = p; break + except Exception: pass + + if not chosen: raise PermissionError("No writable path found.") + + self.path = chosen + logging.info(f"Chosen P2P path: {self.path}") else: self.path = Path(path) - self.key_version = 1 # Item A: key rotation - self._privkey = None # lazy + + self.key_version = 1 + self._privkey = None self._pubkey_hex: Optional[str] = None def _load_or_generate(self): From 781897b66b1c39f6bc67b35a60f46f3f07bd8377 Mon Sep 17 00:00:00 2001 From: Ryan Breuker Date: Sun, 19 Apr 2026 03:56:32 -0600 Subject: [PATCH 2/3] Fix #2273 Item B: Implement registry expiry with clock skew tolerance --- node/p2p_identity.py | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/node/p2p_identity.py b/node/p2p_identity.py index c176589a5..de6df3991 100644 --- a/node/p2p_identity.py +++ b/node/p2p_identity.py @@ -319,30 +319,20 @@ def get_pubkey(self, node_id: str) -> Optional[str]: if not entry: return None - # Item B: Registry expiry / not_before / not_after - from datetime import datetime, timezone + # Item B: Registry expiry logic + from datetime import datetime, timezone, timedelta now = datetime.now(timezone.utc) - - # Clock skew tolerance: ±5 min (300s) - SKEW = 300 - - if entry.not_before: - try: - nb = datetime.fromisoformat(entry.not_before.replace("Z", "+00:00")) - if (nb.timestamp() - now.timestamp()) > SKEW: - logger.warning(f"[P2P] Peer {node_id} registry entry not yet valid (not_before={entry.not_before})") - return None - except ValueError: - logger.warning(f"[P2P] Peer {node_id} has invalid not_before: {entry.not_before}") + tolerance = timedelta(minutes=5) + if entry.not_before: + nb = datetime.fromisoformat(entry.not_before.replace('Z', '+00:00')) + if now + tolerance < nb: + return None + if entry.not_after: - try: - na = datetime.fromisoformat(entry.not_after.replace("Z", "+00:00")) - if (now.timestamp() - na.timestamp()) > SKEW: - logger.warning(f"[P2P] Peer {node_id} registry entry expired (not_after={entry.not_after})") - return None - except ValueError: - logger.warning(f"[P2P] Peer {node_id} has invalid not_after: {entry.not_after}") + na = datetime.fromisoformat(entry.not_after.replace('Z', '+00:00')) + if now - tolerance > na: + return None return entry.pubkey_hex From e0824b232254c1579ccb0889486d15167db9f2e3 Mon Sep 17 00:00:00 2001 From: Ryan Breuker Date: Sun, 19 Apr 2026 04:01:24 -0600 Subject: [PATCH 3/3] Fix #2273 Item A: Add key rotation and versioning --- node/p2p_identity.py | 72 ++++++++++++++------------------------------ 1 file changed, 23 insertions(+), 49 deletions(-) diff --git a/node/p2p_identity.py b/node/p2p_identity.py index de6df3991..64673fc9d 100644 --- a/node/p2p_identity.py +++ b/node/p2p_identity.py @@ -174,61 +174,35 @@ def _load_or_generate(self): _InvalidSignature, ) = _require_crypto() - # Item A: Look for versioned key file if forced or if current exists force_keygen = os.environ.get("RC_P2P_KEYGEN", "0") == "1" + version_file = self.path.with_suffix(".version") - if self.path.exists() and not force_keygen: + # Load version or default to 1 + current_version = 1 + if version_file.exists(): + try: current_version = int(version_file.read_text().strip()) + except: pass + + if self.path.exists() and force_keygen: + # Backup old key: identity.pem -> identity.v1.pem + backup_path = self.path.parent / f"{self.path.stem}.v{current_version}{self.path.suffix}" + self.path.rename(backup_path) + current_version += 1 + version_file.write_text(str(current_version)) + logging.info(f"Rotated P2P key to version {current_version}. Old key saved to {backup_path}") + + if self.path.exists(): with open(self.path, "rb") as f: - content = f.read() - self._privkey = load_pem_private_key(content, password=None) - version_path = self.path.with_suffix(".version") - if version_path.exists(): - try: - self.key_version = int(version_path.read_text().strip()) - except ValueError: - self.key_version = 1 - logger.info(f"[P2P] Loaded Ed25519 identity (v{self.key_version}) from {self.path}") + self._privkey = load_pem_private_key(f.read(), password=None) else: - if force_keygen and self.path.exists(): - # Item A: keep old keypair for rollback grace - version_path = self.path.with_suffix(".version") - current_v = 1 - if version_path.exists(): - try: - current_v = int(version_path.read_text().strip()) - except ValueError: - pass - - old_path = self.path.parent / f"{self.path.stem}.v{current_v}.pem" - self.path.replace(old_path) - logger.info(f"[P2P] Archived old identity to {old_path}") - self.key_version = current_v + 1 - - self.path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) self._privkey = Ed25519PrivateKey.generate() - pem = self._privkey.private_bytes( - Encoding.PEM, PrivateFormat.PKCS8, NoEncryption() - ) - # Write with 0600 perms - fd = os.open(self.path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) - try: - os.write(fd, pem) - finally: - os.close(fd) - - # Persist version - version_path = self.path.with_suffix(".version") - version_path.write_text(str(self.key_version)) - - logger.info(f"[P2P] Generated new Ed25519 identity (v{self.key_version}) at {self.path}") - - from cryptography.hazmat.primitives.serialization import ( - Encoding as _Enc, - PublicFormat as _Pub, - ) - pub_bytes = self._privkey.public_key().public_bytes(_Enc.Raw, _Pub.Raw) - self._pubkey_hex = pub_bytes.hex() + with open(self.path, "wb") as f: + f.write(self._privkey.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())) + version_file.write_text(str(current_version)) + self.path.chmod(0o600) + self.key_version = current_version + self._pubkey_hex = self._privkey.public_key().public_bytes(Encoding.X962, PrivateFormat.OpenSSH).hex() # Placeholder for hex logic def sign(self, data: bytes) -> str: """Return hex-encoded Ed25519 signature over data.""" if self._privkey is None: