Category: Technology

Viewing *Real* Certificate Chain

Browsers implement AIA which “helps” by repairing the certificate chain and forming a trust even without proper server configuration. Which is great for user experience, but causes a lot of challenges to people troubleshooting SSL connection failures from devices, old equipment, etc. It’s fine when I try it from my laptop!

This python script reports on the real certificate chain being served from an endpoint. Self-signed certificates will show as untrusted

And public certs will show the chain and show as trusted

Code:

import ssl
import socket
import datetime
import select

# Third-party modules (install: pip install pyopenssl cryptography)
try:
    from OpenSSL import SSL, crypto
    from cryptography import x509
    from cryptography.hazmat.primitives import hashes
except ImportError as e:
    raise SystemExit(
        "Missing required modules. Please install:\n"
        "  pip install pyopenssl cryptography\n"
        f"Import error: {e}"
    )

def prompt(text, default=None):
    s = input(text).strip()
    return s if s else default

def check_trust(hostname: str, port: int, timeout=6.0):
    """
    Attempt a TLS connection using system trust store and hostname verification.
    Returns (trusted: bool, message: str).
    """
    try:
        ctx = ssl.create_default_context()
        with socket.create_connection((hostname, port), timeout=timeout) as sock:
            with ctx.wrap_socket(sock, server_hostname=hostname) as ssock:
                # Minimal HTTP GET to ensure we fully complete the handshake
                req = f"GET / HTTP/1.1\r\nHost: {hostname}\r\nConnection: close\r\n\r\n"
                ssock.sendall(req.encode("utf-8"))
                _ = ssock.recv(1)
        return True, "TRUSTED (system trust store)"
    except ssl.SSLCertVerificationError as e:
        return False, f"NOT TRUSTED (certificate verification error): {e}"
    except ssl.SSLError as e:
        return False, f"NOT TRUSTED (SSL error): {e}"
    except Exception as e:
        return False, f"Error connecting: {e}"

def _aware_utc(dt: datetime.datetime) -> datetime.datetime:
    """
    Ensure a datetime is timezone-aware in UTC. cryptography returns naive UTC datetimes.
    """
    if dt.tzinfo is None:
        return dt.replace(tzinfo=datetime.timezone.utc)
    return dt.astimezone(datetime.timezone.utc)

def _cert_to_info(cert: x509.Certificate):
    subj = cert.subject.rfc4514_string()
    issr = cert.issuer.rfc4514_string()
    fp_sha1 = cert.fingerprint(hashes.SHA1()).hex().upper()
    nb = _aware_utc(cert.not_valid_before_utc)
    na = _aware_utc(cert.not_valid_after_utc)
    now = datetime.datetime.now(datetime.timezone.utc)
    delta_days = max(0, (na - now).days)
    return {
        "subject": subj,
        "issuer": issr,
        "sha1": fp_sha1,
        "not_before": nb,
        "not_after": na,
        "days_to_expiry": delta_days
    }

def _do_handshake_blocking(conn: SSL.Connection, sock: socket.socket, timeout: float):
    """
    Drive the TLS handshake, handling WantRead/WantWrite by waiting with select.
    """
    deadline = datetime.datetime.now() + datetime.timedelta(seconds=timeout)
    while True:
        try:
            conn.do_handshake()
            return
        except SSL.WantReadError:
            remaining = (deadline - datetime.datetime.now()).total_seconds()
            if remaining <= 0:
                raise TimeoutError("TLS handshake timed out (WantRead)")
            r, _, _ = select.select([sock], [], [], remaining)
            if not r:
                raise TimeoutError("TLS handshake timed out (WantRead)")
            continue
        except SSL.WantWriteError:
            remaining = (deadline - datetime.datetime.now()).total_seconds()
            if remaining <= 0:
                raise TimeoutError("TLS handshake timed out (WantWrite)")
            _, w, _ = select.select([], [sock], [], remaining)
            if not w:
                raise TimeoutError("TLS handshake timed out (WantWrite)")
            continue

def fetch_presented_chain(hostname: str, port: int, timeout: float = 12.0):
    """
    Capture the presented certificate chain using pyOpenSSL.
    Returns (chain: list of {subject, issuer, sha1, not_before, not_after, days_to_expiry}, error: str or None).
    """
    # TCP connect
    try:
        sock = socket.create_connection((hostname, port), timeout=timeout)
        sock.settimeout(timeout)
    except Exception as e:
        return [], f"Error connecting: {e}"

    try:
        # TLS client context
        ctx = SSL.Context(SSL.TLS_CLIENT_METHOD)

        # Compatibility tweaks:
        # - Lower OpenSSL security level
        try:
            ctx.set_cipher_list(b"DEFAULT:@SECLEVEL=1")
        except Exception:
            pass

        # - Disable TLS 1.3 and set minimum TLS 1.2
        try:
            ctx.set_options(SSL.OP_NO_TLSv1_3)
        except Exception:
            pass
        try:
            # Ensure TLSv1.2+ (pyOpenSSL exposes set_min_proto_version on some builds)
            if hasattr(ctx, "set_min_proto_version"):
                ctx.set_min_proto_version(SSL.TLS1_2_VERSION)
        except Exception:
            pass

        # - Set ALPN to http/1.1 (some paths work better when ALPN is present)
        try:
            ctx.set_alpn_protos([b"http/1.1"])
        except Exception:
            pass

        conn = SSL.Connection(ctx, sock)
        conn.set_tlsext_host_name(hostname.encode("utf-8"))
        conn.set_connect_state()

        # Blocking mode (best effort)
        try:
            conn.setblocking(True)
        except Exception:
            pass

        # Drive handshake
        _do_handshake_blocking(conn, sock, timeout=timeout)

        # Retrieve chain (some servers only expose leaf)
        chain = conn.get_peer_cert_chain()
        infos = []
        if chain:
            for c in chain:
                der = crypto.dump_certificate(crypto.FILETYPE_ASN1, c)
                cert = x509.load_der_x509_certificate(der)
                infos.append(_cert_to_info(cert))
        else:
            peer = conn.get_peer_certificate()
            if peer is not None:
                der = crypto.dump_certificate(crypto.FILETYPE_ASN1, peer)
                cert = x509.load_der_x509_certificate(der)
                infos.append(_cert_to_info(cert))

        # Cleanup
        try:
            conn.shutdown()
        except Exception:
            pass
        finally:
            try:
                conn.close()
            except Exception:
                pass
            try:
                sock.close()
            except Exception:
                pass

        if not infos:
            return [], "No certificates captured (server did not present a chain and peer cert unavailable)"
        return infos, None

    except Exception as e:
        try:
            sock.close()
        except Exception:
            pass
        etype = type(e).__name__
        emsg = str(e) or "no message"
        return [], f"TLS handshake or chain retrieval error: {etype}: {emsg}"

def main():
    hostname = prompt("Enter hostname to test (e.g., example.domain.com): ")
    if not hostname:
        print("Hostname is required.")
        return
    port_str = prompt("Enter port [default 443]: ", "443")
    try:
        port = int(port_str)
    except ValueError:
        print("Invalid port.")
        return

    print(f"\nTesting TLS chain for {hostname}:{port} ...")
    chain, err = fetch_presented_chain(hostname, port)
    print("\nPresented chain:")
    if err:
        print(f"  [ERROR] {err}")
    elif not chain:
        print("  [No certificates captured]")
    else:
        for i, ci in enumerate(chain, 1):
            print(f"  [{i}] Subject: {ci['subject']}")
            print(f"       Issuer:  {ci['issuer']}")
            print(f"       SHA1:    {ci['sha1']}")
            nb_val = ci.get("not_before")
            na_val = ci.get("not_after")
            nb_str = nb_val.isoformat() if isinstance(nb_val, datetime.datetime) else str(nb_val)
            na_str = na_val.isoformat() if isinstance(na_val, datetime.datetime) else str(na_val)
            print(f"       Not Before: {nb_str}")
            print(f"       Not After:  {na_str}")
            dte = ci.get("days_to_expiry")
            if dte is not None:
                print(f"       Expires In: {dte} days")

    trusted, msg = check_trust(hostname, port)
    print(f"\nTrust result: {'TRUSTED' if trusted else 'NOT TRUSTED'} - {msg}")

if __name__ == "__main__":
    main()

Blender – Box Trim Not Supported in Dynamic Topology Mode

I hand-sculpted bases for my chess set before realizing that I wanted them to be very identical. So I wanted to “cut” the hand-sculpted base off of the figure and replace it with a programmatic one. Except I kept getting this error using box trim — Not supported in dynamic topology mode.

Switch to a drawing brush and uncheck Dynamic Topology box

Then switch back to box trim and it actually trims

And then, of course, you need to remember to turn dynamic topography back on for the drawing tools to draw.

 

DNF5 History *UNDO*?!?

This is cool – I’ve only tested with a package I didn’t need and didn’t matter if it got mucked up. No idea if there’s an undo for, say, kernels. Or if undo on an update would roll back to the previous version. That’s the sort of testing to do on a sandbox that you don’t want running 30 minutes from now!

[root@fedora log]# dnf5 install bvi
Updating and loading repositories:
Repositories loaded.
Package Arch Version Repository Size
Installing:
bvi x86_64 1.5.0-1.fc43 fedora 157.1 KiB

Transaction Summary:
Installing: 1 package

Total size of inbound packages is 83 KiB. Need to download 83 KiB.
After this operation, 157 KiB extra will be used (install 157 KiB, remove 0 B).
Is this ok [y/N]: y
[1/1] bvi-0:1.5.0-1.fc43.x86_64 100% | 351.6 KiB/s | 83.0 KiB | 00m00s
———————————————————————————————————————————————————————————————————————————————
[1/1] Total 100% | 164.9 KiB/s | 83.0 KiB | 00m01s
Running transaction
[1/3] Verify package files 100% | 142.0 B/s | 1.0 B | 00m00s
[2/3] Prepare transaction 100% | 2.0 B/s | 1.0 B | 00m00s
[3/3] Installing bvi-0:1.5.0-1.fc43.x86_64 100% | 243.9 KiB/s | 160.0 KiB | 00m01s
Complete!
[root@fedora log]# dnf5 history list
ID Command line Date and time Action(s) Altered
29 dnf install bvi 2026-02-06 04:50:43 1
28 dnf remove kernel-core-6.18.3-200.fc43.x86_64 kernel-modules-6.18.3-200.fc43.x86_64 kernel-modules-extra-6.18.3-200.fc43.x86_64 2026-01-14 05:07:22 4
27 dnf update 2026-01-14 05:01:58 70
26 dnf remove kernel-core-6.17.9-300.fc43.x86_64 kernel-modules-6.17.9-300.fc43.x86_64 kernel-modules-extra-6.17.9-300.fc43.x86_64 2026-01-14 05:01:28 4
25 dnf update 2026-01-09 19:19:48 42
24 dnf update 2026-01-08 19:57:05 618
23 dnf5 remove kernel-core-6.11.10-300.fc41.x86_64 kernel-modules-6.11.10-300.fc41.x86_64 kernel-modules-core-6.11.10-300.fc41.x86_64 2026-01-08 19:53:42 3
22 dnf remove kernel-6.11.10-300.fc41 2025-12-26 02:06:47 1
21 dnf install speedtest-cli 2025-12-19 23:06:12 1
20 yum install chromedriver 2025-12-08 00:20:24 3
19 dnf system-upgrade download –releasever=43 2025-12-06 18:40:14 3792
18 dnf upgrade –refresh 2025-12-06 08:22:19 1422
17 dnf install -y cloud-utils-growpart 2025-12-06 08:16:00 1
16 dnf install xmlsec1 xmlsec1-openssl 2025-10-27 20:44:39 1
15 yum install xmlsec1 2025-10-27 20:42:29 1
14 dnf install mod_md 2025-07-03 19:22:36 1
13 yum install npm 2025-06-23 20:05:13 5
12 dnf update 2025-02-23 19:56:36 482
11 dnf update 2025-01-31 16:01:33 626
10 dnf5 install dnf5-plugin-automatic 2025-01-31 15:59:29 9
9 yum install xxd 2025-01-15 21:38:38 1
8 dnf install mosquitto 2025-01-04 23:40:52 3
7 dnf update 2025-01-03 04:51:41 14
6 dnf update 2025-01-01 00:19:22 358
5 dnf update 2024-12-06 04:30:32 18
4 dnf update 2024-12-06 04:14:41 404
3 dnf update 2024-11-22 17:21:43 65
2 dnf update 2024-11-18 18:33:18 116
1 dnf update 2024-11-14 18:58:09 54

[root@fedora log]# dnf5 history undo 29
Updating and loading repositories:
Repositories loaded.
Package Arch Version Repository Size
Removing:
bvi x86_64 1.5.0-1.fc43 fedora 157.1 KiB

Transaction Summary:
Removing: 1 package

After this operation, 157 KiB will be freed (install 0 B, remove 157 KiB).
Is this ok [y/N]: y
Running transaction
[1/2] Prepare transaction 100% | 4.0 B/s | 1.0 B | 00m00s
[2/2] Removing bvi-0:1.5.0-1.fc43.x86_64 100% | 35.0 B/s | 20.0 B | 00m01s
Complete!