Skip to content
Closed
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
131 changes: 58 additions & 73 deletions node/p2p_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -153,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:
Expand Down Expand Up @@ -298,30 +293,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

Expand Down