"""TLS certificate generation and fingerprint helpers for node-to-node comms.""" from __future__ import annotations import datetime import hashlib import ipaddress import json import os import socket import ssl import stat from pathlib import Path _CERT_PATH = Path.home() / ".config" / "meshnet" / "node_cert.pem" _KEY_PATH = Path.home() / ".config" / "meshnet" / "node_key.pem" def generate_self_signed_cert( cert_path: Path | None = None, key_path: Path | None = None, common_name: str | None = None, ) -> tuple[Path, Path]: """Generate a self-signed RSA-2048 cert valid for 10 years. Returns (cert_path, key_path). Skips generation if both files already exist. """ from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID cert_p = cert_path or _CERT_PATH key_p = key_path or _KEY_PATH if cert_p.exists() and key_p.exists(): return cert_p, key_p cert_p.parent.mkdir(parents=True, exist_ok=True) key = rsa.generate_private_key(public_exponent=65537, key_size=2048) cn = common_name or socket.getfqdn() subject = issuer = x509.Name([ x509.NameAttribute(NameOID.COMMON_NAME, cn), x509.NameAttribute(NameOID.ORGANIZATION_NAME, "meshnet-node"), ]) san_list: list = [x509.DNSName(cn)] try: san_list.append(x509.IPAddress(ipaddress.IPv4Address(socket.gethostbyname(cn)))) except (socket.gaierror, ValueError): pass san_list.append(x509.IPAddress(ipaddress.IPv4Address("127.0.0.1"))) cert = ( x509.CertificateBuilder() .subject_name(subject) .issuer_name(issuer) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(datetime.datetime.now(datetime.timezone.utc)) .not_valid_after(datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=365 * 10)) .add_extension(x509.SubjectAlternativeName(san_list), critical=False) .sign(key, hashes.SHA256()) ) key_pem = key.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption(), ) cert_pem = cert.public_bytes(serialization.Encoding.PEM) key_p.write_bytes(key_pem) cert_p.write_bytes(cert_pem) try: os.chmod(key_p, stat.S_IRUSR | stat.S_IWUSR) except OSError: pass return cert_p, key_p def cert_fingerprint(cert_path: Path | None = None) -> str: """Return sha256 fingerprint of the cert as 'sha256:'.""" from cryptography import x509 from cryptography.hazmat.primitives import hashes p = cert_path or _CERT_PATH cert = x509.load_pem_x509_certificate(p.read_bytes()) fp = cert.fingerprint(hashes.SHA256()).hex() return f"sha256:{fp}" def make_server_ssl_context( cert_path: Path | None = None, key_path: Path | None = None, ) -> ssl.SSLContext: """Return an ssl.SSLContext for a server using our self-signed cert.""" cert_p = cert_path or _CERT_PATH key_p = key_path or _KEY_PATH ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ctx.load_cert_chain(certfile=str(cert_p), keyfile=str(key_p)) return ctx def make_client_ssl_context(verify: bool = False) -> ssl.SSLContext: """Return a client SSLContext. verify=False for self-signed TOFU connections.""" ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) if not verify: ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE return ctx