ssl-tls-certificate-verification

code_execution · verified · python3.12/linux · json · download .py

Verify SSL/TLS certificates, check expiry, validate chain, inspect DNS before deployment

import ssl
import sys
import socket
import datetime
import subprocess
import requests as _requests

# ----------------------------------------
# PRE_EXECUTION
# FM-2.2: fetch ground truth for all registry_refs
# ----------------------------------------

REGISTRY_REFS = ["requests", "httpx"]
MAX_RETRIES = 2
registries = {}

for lib in REGISTRY_REFS:
    for attempt in range(MAX_RETRIES):
        try:
            response = _requests.get(
                f"https://checklist.day/api/registry/{lib}",
                timeout=10
            )
            if response.status_code == 200:
                registries[lib] = response.json()
                break
        except _requests.exceptions.RequestException:
            pass

for lib in REGISTRY_REFS:
    assert lib in registries, \
        f"ABORT: registry fetch failed for {lib} after {MAX_RETRIES} attempts"

# FM-2.4: surface breaking warnings
for lib, registry in registries.items():
    breaking = [
        w for w in registry.get("warnings", [])
        if w.get("severity") == "breaking"
    ]
    if breaking:
        print(f"PRE_EXECUTION: {lib} has {len(breaking)} breaking warning(s):")
        for w in breaking:
            print(f"  [!] [{w.get('affected_versions', 'all')}] {w['message'][:120]}")
            print(f"      fix: {w['fix'][:100]}")

print()
print("PRE_EXECUTION: all registry refs verified ✓")

# ----------------------------------------
# KNOWN FAILURE MODES
#
# 1. verify=False left in production — disables all cert validation silently.
#    Never use requests.get(url, verify=False) in production code.
#    Common pattern: dev bypasses SSL for testing, forgets to revert.
#
# 2. No expiry buffer — cert valid today, expires in 3 days, deploy breaks on day 4.
#    Always check expiry with a minimum buffer (30 days recommended).
#
# 3. Self-signed cert in prod — works locally, fails for every external client.
#    Check issuer — must be a trusted CA, not the domain itself.
#
# 4. Incomplete cert chain — leaf cert valid but intermediate not served.
#    Causes failures in strict clients even if browser shows padlock.
#
# 5. DNS mismatch — cert issued for www.example.com, deployed at example.com.
#    CN/SAN must match the hostname being verified.
#
# 6. Wrong port — SSL check on port 443 passes, but service runs on 8443.
#    Always verify against the actual port in use.
# ----------------------------------------

EXPIRY_BUFFER_DAYS = 30   # warn if cert expires within this window
DEFAULT_TIMEOUT = 10
DEFAULT_PORT = 443


def get_cert_info(hostname: str, port: int = DEFAULT_PORT) -> dict:
    """
    Fetch SSL certificate info using stdlib ssl — no extra dependencies.
    FM-1.5: explicit timeout — never hang on unresponsive host.
    """
    context = ssl.create_default_context()

    with socket.create_connection((hostname, port), timeout=DEFAULT_TIMEOUT) as sock:
        with context.wrap_socket(sock, server_hostname=hostname) as ssock:
            cert = ssock.getpeercert()
            # get full chain depth
            chain = ssock.getpeercert(binary_form=False)
            return {
                "cert": cert,
                "protocol": ssock.version(),
                "cipher": ssock.cipher(),
            }


def check_expiry(cert: dict, buffer_days: int = EXPIRY_BUFFER_DAYS) -> dict:
    """
    FM-3.2: check expiry before asserting cert is valid.
    Returns days_remaining and whether within warning buffer.
    """
    not_after = cert.get("notAfter")
    assert not_after, "ABORT: cert missing notAfter field"

    expiry_dt = datetime.datetime.strptime(not_after, "%b %d %H:%M:%S %Y %Z")
    expiry_dt = expiry_dt.replace(tzinfo=datetime.timezone.utc)
    now = datetime.datetime.now(datetime.timezone.utc)
    days_remaining = (expiry_dt - now).days

    return {
        "expiry_date": expiry_dt.strftime("%Y-%m-%d"),
        "days_remaining": days_remaining,
        "within_buffer": days_remaining <= buffer_days,
        "expired": days_remaining < 0,
    }


def check_hostname_match(cert: dict, hostname: str) -> bool:
    """
    FM-3.3: verify cert CN/SAN matches the target hostname.
    SAN (Subject Alternative Names) takes precedence over CN.
    """
    # Check SANs first
    san_list = []
    for san_type, san_value in cert.get("subjectAltName", []):
        if san_type == "DNS":
            san_list.append(san_value.lower())

    if san_list:
        hostname_lower = hostname.lower()
        for san in san_list:
            if san == hostname_lower:
                return True
            # wildcard match: *.example.com matches sub.example.com
            if san.startswith("*."):
                wildcard_domain = san[2:]
                if hostname_lower.endswith("." + wildcard_domain):
                    return True
        return False

    # Fall back to CN
    for rdn in cert.get("subject", []):
        for key, value in rdn:
            if key == "commonName":
                return value.lower() == hostname.lower()

    return False


def check_issuer(cert: dict) -> dict:
    """
    FM-2.4: surface self-signed cert warning — issuer == subject is a red flag.
    """
    subject = dict(x[0] for x in cert.get("subject", []))
    issuer = dict(x[0] for x in cert.get("issuer", []))

    is_self_signed = subject.get("commonName") == issuer.get("commonName")

    return {
        "issuer_org": issuer.get("organizationName", "unknown"),
        "issuer_cn": issuer.get("commonName", "unknown"),
        "is_self_signed": is_self_signed,
    }


def verify_https_request(url: str) -> int:
    """
    FM-2.6: always use verify=True (default) — never disable SSL verification.
    This will raise SSLError if cert is invalid.
    """
    import requests
    response = requests.get(url, timeout=DEFAULT_TIMEOUT, verify=True)
    return response.status_code


# ----------------------------------------
# EXECUTION
# Test against a known-good public host: checklist.day
# FM-1.1: read-only SSL checks — idempotent, no side effects
# ----------------------------------------

try:
    import requests
except ImportError:
    pkg = registries["requests"]["install"][0]["cmd"].replace("pip install ", "").strip()
    print(f"\nEXECUTION: requests not found — installing {pkg}...")
    subprocess.check_call([sys.executable, "-m", "pip", "install", pkg])
    import requests

TEST_HOST = "checklist.day"
TEST_URL = f"https://{TEST_HOST}"

print()
print(f"EXECUTION: checking SSL certificate for {TEST_HOST}...")

cert_data = get_cert_info(TEST_HOST)
cert = cert_data["cert"]

expiry = check_expiry(cert)
hostname_match = check_hostname_match(cert, TEST_HOST)
issuer = check_issuer(cert)
status_code = verify_https_request(TEST_URL)

print(f"  protocol      : {cert_data['protocol']}")
print(f"  cipher        : {cert_data['cipher'][0]}")
print(f"  expires       : {expiry['expiry_date']}  ({expiry['days_remaining']} days remaining)")
print(f"  hostname match: {hostname_match}")
print(f"  issuer        : {issuer['issuer_org']} ({issuer['issuer_cn']})")
print(f"  self-signed   : {issuer['is_self_signed']}")
print(f"  https status  : {status_code}")

# FM-2.4: surface warnings — do not withhold
if expiry["within_buffer"] and not expiry["expired"]:
    print(f"\n  [!] WARNING: cert expires in {expiry['days_remaining']} days — renew before deployment")
if expiry["expired"]:
    print(f"\n  [!] CRITICAL: cert is EXPIRED — do not deploy")
if issuer["is_self_signed"]:
    print(f"\n  [!] WARNING: self-signed certificate — not trusted by external clients")

# ----------------------------------------
# POST_EXECUTION
# FM-3.2: verify all checks before asserting PASS
# FM-3.3: exact assertions on each check
# ----------------------------------------

assert not expiry["expired"], \
    f"FAIL: certificate is expired as of {expiry['expiry_date']}"

assert expiry["days_remaining"] > EXPIRY_BUFFER_DAYS, \
    f"FAIL: cert expires in {expiry['days_remaining']} days — below {EXPIRY_BUFFER_DAYS}-day buffer"

assert hostname_match, \
    f"FAIL: hostname '{TEST_HOST}' does not match cert CN/SAN"

assert not issuer["is_self_signed"], \
    f"FAIL: self-signed certificate detected — not valid for production"

assert status_code == 200, \
    f"FAIL: HTTPS request returned {status_code}, expected 200"

# Verify protocol is TLS 1.2 or higher
assert cert_data["protocol"] in ("TLSv1.2", "TLSv1.3"), \
    f"FAIL: insecure protocol '{cert_data['protocol']}' — require TLS 1.2+"

print()
print("POST_EXECUTION: cert not expired ✓")
print(f"POST_EXECUTION: expiry buffer ok ✓  ({expiry['days_remaining']} days > {EXPIRY_BUFFER_DAYS}-day minimum)")
print("POST_EXECUTION: hostname match ✓")
print("POST_EXECUTION: trusted CA issuer ✓")
print("POST_EXECUTION: HTTPS request succeeded ✓")
print(f"POST_EXECUTION: protocol verified ✓  ({cert_data['protocol']})")

result = {
    "status": "pass",
    "cert_not_expired": True,
    "expiry_buffer_ok": True,
    "hostname_match": True,
    "trusted_ca": True,
    "https_request_ok": True,
    "tls_protocol_verified": True,
    "days_remaining": expiry["days_remaining"],
    "protocol": cert_data["protocol"],
}
print(result)
print("PASS")