""" Self-signed TLS certificate management for local-identity serve. Certificate and key are generated on first run and stored at: ~/.local-identity/tls/server.crt (mode 644, 10-year validity) ~/.local-identity/tls/server.key (mode 600) The certificate covers localhost and 127.0.0.1 via SubjectAltName. """ import datetime import ipaddress import os from pathlib import Path from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID from .store import _store_dir def _tls_dir() -> Path: return _store_dir() / "tls" def ensure_tls_cert() -> tuple[Path, Path]: """Ensure a self-signed TLS cert exists. Returns (cert_path, key_path).""" tls_dir = _tls_dir() tls_dir.mkdir(mode=0o700, parents=True, exist_ok=True) cert_path = tls_dir / "server.crt" key_path = tls_dir / "server.key" if not cert_path.exists() or not key_path.exists(): _generate_cert(cert_path, key_path) return cert_path, key_path def _generate_cert(cert_path: Path, key_path: Path) -> None: tls_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) key_path.write_bytes( tls_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption(), ) ) os.chmod(key_path, 0o600) subject = x509.Name([ x509.NameAttribute(NameOID.COMMON_NAME, "localhost"), x509.NameAttribute(NameOID.ORGANIZATION_NAME, "local-identity"), ]) now = datetime.datetime.now(datetime.timezone.utc) cert = ( x509.CertificateBuilder() .subject_name(subject) .issuer_name(subject) .public_key(tls_key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(now) .not_valid_after(now + datetime.timedelta(days=3650)) .add_extension( x509.SubjectAlternativeName([ x509.DNSName("localhost"), x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")), ]), critical=False, ) .sign(tls_key, hashes.SHA256()) ) cert_path.write_bytes(cert.public_bytes(serialization.Encoding.PEM)) os.chmod(cert_path, 0o644)