import ssl import socket from datetime import datetime from typing import Any, Dict, List, Optional import argparse import pprint import sys # added to detect whether --host was passed def fetch_certificate(host: str, port: int = 443, server_hostname: Optional[str] = None, timeout: float = 5.0) -> Dict[str, Any]: """ Fetch the TLS certificate from host:port. This function intentionally uses a non-verifying SSL context to retrieve the certificate even if it doesn't validate, so we can inspect its fields. Parameters: - host: TCP connect target (can be an IP or hostname) - port: TCP port (default 443) - server_hostname: SNI value to send. If None, server_hostname = host. - timeout: socket connect timeout in seconds Returns: Dictionary with the peer certificate (as returned by SSLSocket.getpeercert()) and additional metadata. """ if server_hostname is None: server_hostname = host # Create an SSL context that does NOT verify so we can always fetch the cert. context = ssl.create_default_context() context.check_hostname = False context.verify_mode = ssl.CERT_NONE with socket.create_connection((host, port), timeout=timeout) as sock: with context.wrap_socket(sock, server_hostname=server_hostname) as sslsock: cert = sslsock.getpeercert() peer_cipher = sslsock.cipher() peertime = datetime.utcnow().isoformat() + "Z" info: Dict[str, Any] = {"peer_certificate": cert, "cipher": peer_cipher, "fetched_at": peertime, "server_hostname_used": server_hostname} return info def parse_san(cert: Dict[str, Any]) -> List[str]: """Return list of DNS names from subjectAltName (if any).""" san = [] for typ, val in cert.get("subjectAltName", ()): if typ.lower() == "dns": san.append(val) return san def format_subject(cert: Dict[str, Any]) -> str: """Return a short human-friendly subject string.""" subject = cert.get("subject", ()) parts = [] for rdn in subject: for k, v in rdn: parts.append(f"{k}={v}") return ", ".join(parts) def check_hostname_match(cert: Dict[str, Any], hostname: str) -> bool: """ Check whether the certificate matches hostname using ssl.match_hostname. Returns True if match, False otherwise. """ try: ssl.match_hostname(cert, hostname) return True except Exception: return False def print_report(host: str, port: int, server_hostname: Optional[str]) -> None: """Fetch certificate and print a readable report.""" info = fetch_certificate(host=host, port=port, server_hostname=server_hostname) cert = info["peer_certificate"] print(f"Connected target: {host}:{port}") print(f"SNI sent: {info['server_hostname_used']}") print(f"Cipher: {info['cipher']}") print(f"Fetched at (UTC): {info['fetched_at']}") print() print("Subject:") print(" ", format_subject(cert)) print() print("Issuer:") issuer = cert.get("issuer", ()) issuer_parts = [] for rdn in issuer: for k, v in rdn: issuer_parts.append(f"{k}={v}") print(" ", ", ".join(issuer_parts)) print() sans = parse_san(cert) print("Subject Alternative Names (SANs):") if sans: for n in sans: print(" -", n) else: print(" (none)") not_before = cert.get("notBefore") not_after = cert.get("notAfter") print() print("Validity:") print(" notBefore:", not_before) print(" notAfter: ", not_after) match = check_hostname_match(cert, server_hostname or host) print() print(f"Hostname match for '{server_hostname or host}':", "YES" if match else "NO") # For debugging show the full cert dict if requested # pprint.pprint(cert) def main() -> None: parser = argparse.ArgumentParser(description="Fetch and inspect TLS certificate from a host (SNI-aware).") # make --host optional and default to api.rixdagen.se so running without args works parser.add_argument("--host", "-H", required=False, default="api.rixdagen.se", help="Host or IP to connect to (TCP target). Defaults to api.rixdagen.se") parser.add_argument("--port", "-p", type=int, default=443, help="Port to connect to (default 443).") parser.add_argument("--sni", help="SNI hostname to send. If omitted, the --host value is used as SNI.") args = parser.parse_args() # Notify when using the default host for quick testing if ("--host" not in sys.argv) and ("-H" not in sys.argv): print("No --host provided: defaulting to api.rixdagen.se (you can override with --host or -H)") print_report(host=args.host, port=args.port, server_hostname=args.sni) if __name__ == "__main__": main()