"""Server-side certificate generation for agent self-renewal. Agents check certificate expiry on startup and request renewal when < 30 days remaining. Server generates new self-signed certificate, computes fingerprint, updates database, and returns certificate as password-protected PFX to agent. """ import hashlib import secrets from datetime import datetime, timedelta, timezone from flask import current_app from cryptography import x509 from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.backends import default_backend def generate_client_certificate(hostname, validity_days=730): """Generate self-signed client certificate for endpoint agent. Args: hostname: Computer hostname (used as CN) validity_days: Certificate validity period (default 730 = 2 years) Returns: tuple: (pfx_bytes, pfx_password, sha1_fingerprint, expires_at) - pfx_bytes: Password-protected PFX/P12 certificate with private key - pfx_password: Random password for PFX (agent discards after import) - sha1_fingerprint: 40-character lowercase hex SHA1 fingerprint - expires_at: Certificate expiration datetime (UTC) """ # Generate RSA private key (2048-bit for compatibility with Windows) private_key = rsa.generate_private_key( public_exponent=65537, key_size=2048, backend=default_backend() ) # Certificate subject and issuer (self-signed, so both are the same) subject = issuer = x509.Name([ x509.NameAttribute(NameOID.COMMON_NAME, hostname), x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'ShopDB Endpoint Fleet'), x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, 'Endpoint Agents'), ]) # Certificate validity period now = datetime.now(timezone.utc) not_valid_before = now not_valid_after = now + timedelta(days=validity_days) # Build certificate cert = x509.CertificateBuilder().subject_name( subject ).issuer_name( issuer ).public_key( private_key.public_key() ).serial_number( x509.random_serial_number() ).not_valid_before( not_valid_before ).not_valid_after( not_valid_after ).add_extension( # Client authentication extended key usage x509.ExtendedKeyUsage([ExtendedKeyUsageOID.CLIENT_AUTH]), critical=True, ).add_extension( # Subject Alternative Name (hostname as DNS name) x509.SubjectAlternativeName([x509.DNSName(hostname)]), critical=False, ).add_extension( # Basic constraints: not a CA x509.BasicConstraints(ca=False, path_length=None), critical=True, ).sign(private_key, hashes.SHA256(), default_backend()) # Compute SHA1 fingerprint (matches nginx $ssl_client_fingerprint) cert_der = cert.public_bytes(serialization.Encoding.DER) sha1_hash = hashlib.sha1(cert_der).hexdigest() # Generate random PFX password (agent uses once during import, then discards) pfx_password = secrets.token_urlsafe(32) # Serialize to PFX/P12 format with password protection # Windows Certificate Store imports PFX format pfx_bytes = serialization.pkcs12.serialize_key_and_certificates( name=hostname.encode('utf-8'), key=private_key, cert=cert, cas=None, # No CA chain for self-signed encryption_algorithm=serialization.BestAvailableEncryption( pfx_password.encode('utf-8') ) ) current_app.logger.info( f'[cert_renewal] Generated certificate for {hostname}: ' f'fingerprint={sha1_hash[:16]}..., expires={not_valid_after}') return (pfx_bytes, pfx_password, sha1_hash, not_valid_after.replace(tzinfo=None))