"""Persistent server identity keys for SSH-style remote host pinning.""" from __future__ import annotations import json from pathlib import Path from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat, load_pem_private_key from browser_cli.constants import CONFIG_DIR SERVER_IDENTITY_PATH = CONFIG_DIR / "server_identity.pem" def load_or_create_server_identity(path: Path = SERVER_IDENTITY_PATH) -> Ed25519PrivateKey: """Load the persistent serve identity key, creating it on first start.""" if path.exists(): key = load_pem_private_key(path.read_bytes(), password=None) if not isinstance(key, Ed25519PrivateKey): raise ValueError(f"server identity key is not Ed25519: {path}") return key path.parent.mkdir(parents=True, exist_ok=True) key = Ed25519PrivateKey.generate() pem = key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()) fd = path.open("xb") try: fd.write(pem) finally: fd.close() path.chmod(0o600) return key def public_key_hex(key: Ed25519PrivateKey) -> str: return key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw).hex() def _signed_challenge_fields(challenge: dict) -> dict: return {key: value for key, value in challenge.items() if key != "server_sig"} def challenge_payload(challenge: dict) -> bytes: """Canonical bytes signed by the server identity key.""" return json.dumps(_signed_challenge_fields(challenge), sort_keys=True, separators=(",", ":")).encode("utf-8") def sign_challenge(challenge: dict, key: Ed25519PrivateKey) -> str: return key.sign(challenge_payload(challenge)).hex() def verify_challenge_signature(challenge: dict) -> bool: pub_hex = challenge.get("server_pubkey") sig_hex = challenge.get("server_sig") if not isinstance(pub_hex, str) or not isinstance(sig_hex, str): return False try: pub = Ed25519PublicKey.from_public_bytes(bytes.fromhex(pub_hex)) pub.verify(bytes.fromhex(sig_hex), challenge_payload(challenge)) return True except (InvalidSignature, ValueError): return False