Source code for python_template_server.certificate_handler

"""Generate self-signed SSL certificate for local development."""

import ipaddress
import logging
import sys
from datetime import UTC, datetime, timedelta
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 template_python.logging_setup import setup_default_logging

from python_template_server.models import CertificateConfigModel

setup_default_logging()
logger = logging.getLogger(__name__)


[docs] class CertificateHandler: """Handles SSL certificate generation and management."""
[docs] def __init__(self, certificate_config: CertificateConfigModel) -> None: """Initialize the CertificateHandler.""" self.cert_dir = certificate_config.directory self.cert_file = certificate_config.ssl_cert_file_path self.key_file = certificate_config.ssl_key_file_path self.days_valid = certificate_config.days_valid
@property def certificate_subject(self) -> x509.Name: """Define the subject for the self-signed certificate.""" return x509.Name( [ x509.NameAttribute(NameOID.COUNTRY_NAME, "UK"), x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Local"), x509.NameAttribute(NameOID.LOCALITY_NAME, "Local"), x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Development"), x509.NameAttribute(NameOID.COMMON_NAME, "localhost"), ] )
[docs] @staticmethod def new_private_key() -> rsa.RSAPrivateKey: """Generate a new RSA private key.""" return rsa.generate_private_key( public_exponent=65537, key_size=4096, )
@staticmethod def _write_to_file(file_path: Path, data: bytes) -> None: """Write data to a file.""" with file_path.open("wb") as f: f.write(data)
[docs] def write_to_key_file(self, data: bytes) -> None: """Write data to the key file.""" self._write_to_file(self.key_file, data)
[docs] def write_to_cert_file(self, data: bytes) -> None: """Write data to the certificate file.""" self._write_to_file(self.cert_file, data)
[docs] def generate_self_signed_cert(self) -> None: """Generate a self-signed certificate and private key. :raise SystemExit: If certificate directory cannot be created :raise OSError: If certificate files cannot be written :raise PermissionError: If insufficient permissions to write certificate files """ try: # Ensure certificate directory exists and is writable self.cert_file.parent.mkdir(parents=True, exist_ok=True) # Test write permissions if not self.cert_file.parent.exists(): logger.error("Failed to create certificate directory: %s", self.cert_file.parent) sys.exit(1) # Generate private key private_key = self.new_private_key() # Create certificate subject and issuer (self-signed, so they're the same) subject = issuer = self.certificate_subject # Build certificate certificate = ( x509.CertificateBuilder() .subject_name(subject) .issuer_name(issuer) .public_key(private_key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(datetime.now(UTC)) .not_valid_after(datetime.now(UTC) + timedelta(days=self.days_valid)) .add_extension( x509.SubjectAlternativeName( [ x509.DNSName("localhost"), x509.DNSName("127.0.0.1"), x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")), ] ), critical=False, ) .sign(private_key, hashes.SHA256()) ) # Write private key to file self.write_to_key_file( private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption(), ) ) # Write certificate to file self.write_to_cert_file(certificate.public_bytes(serialization.Encoding.PEM)) logger.info("Certificate generated successfully!") logger.info("Saved in directory: %s", self.cert_dir) logger.info("Valid for: %d days", self.days_valid) except PermissionError: logger.exception("Permission denied when writing certificate files: %s", self.cert_file.parent) raise except OSError: logger.exception("Failed to generate certificate files!") raise