Files
haproxy-openmanager/backend/utils/ssl_parser.py
T
taylanbakircioglu 6aae0f4309 Initial commit
2025-10-27 12:14:03 +03:00

251 lines
8.4 KiB
Python

"""
SSL Certificate PEM parsing utilities
"""
import re
import logging
from datetime import datetime, timezone
from typing import Optional, Dict, Any
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
logger = logging.getLogger(__name__)
def parse_ssl_certificate(cert_content: str) -> Dict[str, Any]:
"""
Parse SSL certificate PEM content and extract domain, expiry date, and other details
Args:
cert_content: PEM format certificate content
Returns:
Dictionary containing parsed certificate information
"""
try:
# Clean up the certificate content
cert_content = cert_content.strip()
# Ensure proper PEM format
if not cert_content.startswith('-----BEGIN CERTIFICATE-----'):
cert_content = '-----BEGIN CERTIFICATE-----\n' + cert_content
if not cert_content.endswith('-----END CERTIFICATE-----'):
cert_content = cert_content + '\n-----END CERTIFICATE-----'
# Parse the certificate using cryptography library
cert_bytes = cert_content.encode('utf-8')
certificate = x509.load_pem_x509_certificate(cert_bytes)
# Extract domain names (Subject Alternative Names + Common Name)
domains = []
# Get SAN (Subject Alternative Names)
try:
san_extension = certificate.extensions.get_extension_for_oid(x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
san_domains = [name.value for name in san_extension.value]
domains.extend(san_domains)
except x509.ExtensionNotFound:
logger.debug("No SAN extension found in certificate")
# Get Common Name from subject
try:
subject = certificate.subject
cn = subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
if cn:
common_name = cn[0].value
if common_name not in domains:
domains.append(common_name)
except Exception as e:
logger.debug(f"Could not extract common name: {e}")
# Primary domain (first one or common name)
primary_domain = domains[0] if domains else "unknown"
# Extract expiry date - use not_valid_after and force UTC timezone
try:
# Try not_valid_after first (more compatible)
expiry_date = certificate.not_valid_after
logger.info(f"🕐 Using not_valid_after: {expiry_date}, tzinfo: {expiry_date.tzinfo}")
except AttributeError:
# Fallback to not_valid_after_utc
expiry_date = certificate.not_valid_after_utc
logger.info(f"🕐 Using not_valid_after_utc: {expiry_date}, tzinfo: {expiry_date.tzinfo}")
# Force timezone to UTC regardless of what we got
if expiry_date.tzinfo is None:
expiry_date = expiry_date.replace(tzinfo=timezone.utc)
logger.info(f"🔧 Added UTC timezone: {expiry_date}")
else:
# Convert any timezone to UTC
expiry_date = expiry_date.astimezone(timezone.utc)
logger.info(f"🔧 Converted to UTC: {expiry_date}")
# Extract issuer
issuer_name = "Unknown"
try:
issuer = certificate.issuer
org = issuer.get_attributes_for_oid(x509.NameOID.ORGANIZATION_NAME)
if org:
issuer_name = org[0].value
else:
cn = issuer.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
if cn:
issuer_name = cn[0].value
except Exception as e:
logger.debug(f"Could not extract issuer: {e}")
# Calculate certificate status
now = datetime.now(timezone.utc)
logger.debug(f"Now datetime: {now}, tzinfo: {now.tzinfo}")
logger.debug(f"Expiry datetime: {expiry_date}, tzinfo: {expiry_date.tzinfo}")
try:
days_until_expiry = (expiry_date - now).days
logger.debug(f"Days until expiry calculated: {days_until_expiry}")
except Exception as e:
logger.error(f"Error calculating days until expiry: {e}")
# Fallback to 0 if calculation fails
days_until_expiry = 0
if days_until_expiry < 0:
status = "expired"
elif days_until_expiry < 30:
status = "expiring_soon"
else:
status = "valid"
# Get certificate fingerprint
fingerprint = certificate.fingerprint(hashes.SHA256()).hex()
return {
"primary_domain": primary_domain,
"all_domains": domains,
"expiry_date": expiry_date,
"issuer": issuer_name,
"status": status,
"days_until_expiry": days_until_expiry,
"fingerprint": fingerprint,
"serial_number": str(certificate.serial_number),
"version": certificate.version.name
}
except Exception as e:
logger.error(f"Failed to parse SSL certificate: {e}")
return {
"primary_domain": "parse_error",
"all_domains": [],
"expiry_date": None,
"issuer": "Unknown",
"status": "invalid",
"days_until_expiry": 0,
"fingerprint": "",
"serial_number": "",
"version": "unknown",
"error": str(e)
}
def validate_private_key(key_content: str) -> bool:
"""
Validate private key PEM content
Args:
key_content: PEM format private key content
Returns:
Boolean indicating if key is valid
"""
try:
# Clean up the key content
key_content = key_content.strip()
# Try to parse as different key types
key_bytes = key_content.encode('utf-8')
# Try RSA private key
try:
serialization.load_pem_private_key(key_bytes, password=None)
return True
except Exception:
pass
# Try with password (empty password)
try:
serialization.load_pem_private_key(key_bytes, password=b'')
return True
except Exception:
pass
return False
except Exception as e:
logger.error(f"Failed to validate private key: {e}")
return False
def validate_certificate_chain(chain_content: str) -> bool:
"""
Validate certificate chain PEM content
Args:
chain_content: PEM format certificate chain content
Returns:
Boolean indicating if chain is valid
"""
if not chain_content or not chain_content.strip():
return True # Chain is optional
try:
# Clean up the chain content
chain_content = chain_content.strip()
# Split multiple certificates in the chain
cert_pattern = r'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----'
certificates = re.findall(cert_pattern, chain_content, re.DOTALL)
if not certificates:
return False
# Validate each certificate in the chain
for cert_pem in certificates:
try:
cert_bytes = cert_pem.encode('utf-8')
x509.load_pem_x509_certificate(cert_bytes)
except Exception:
return False
return True
except Exception as e:
logger.error(f"Failed to validate certificate chain: {e}")
return False
def format_certificate_info(cert_info: Dict[str, Any]) -> str:
"""
Format certificate information for display
Args:
cert_info: Parsed certificate information
Returns:
Formatted string for display
"""
if cert_info.get("error"):
return f"❌ Invalid Certificate: {cert_info['error']}"
status_emoji = {
"valid": "",
"expiring_soon": "⚠️",
"expired": "",
"invalid": ""
}
emoji = status_emoji.get(cert_info["status"], "")
domain = cert_info["primary_domain"]
expiry = cert_info["expiry_date"]
issuer = cert_info["issuer"]
if expiry:
expiry_str = expiry.strftime("%Y-%m-%d")
days = cert_info["days_until_expiry"]
return f"{emoji} {domain} | Expires: {expiry_str} ({days} days) | Issuer: {issuer}"
else:
return f"{emoji} {domain} | Issuer: {issuer}"