Skip to main content

Command Palette

Search for a command to run...

Building a Domain Security Analyzer — SSL Certificate Validation

Published
20 min read

In the last post, we added email security checks — MX, SPF, and DMARC records. Now we're moving beyond DNS to check something you interact with every day: the padlock in your browser.

Today's Goal

By the end of this post, you'll understand:

  1. What SSL/TLS actually does — The two problems it solves

  2. How certificates work — Not just what they are, but how verification actually happens

  3. How encryption protects your data — What happens to your password between your keyboard and GitHub's server

And you'll have code that checks all of this for any domain.


The Two Problems SSL Solves

When you type your password into GitHub, two things could go wrong:

Problem 1: Eavesdropping

Your data travels through many places:

Your computer → WiFi router → ISP → Multiple servers → GitHub

Without protection, anyone along this path could read your password. The person on the same WiFi at a coffee shop. Your internet provider. Any server in between.

Problem 2: Impersonation

How do you know you're actually talking to GitHub? An attacker could intercept your connection and pretend to be GitHub:

You think:  You ←→ GitHub
Reality:    You ←→ Attacker ←→ GitHub

You send password to "GitHub"... but it's actually the attacker.

SSL/TLS solves both:

  • Encryption — Scrambles your data so only GitHub can read it

  • Verification — Proves you're actually talking to the real GitHub

Let's understand each one.


Part 1: Verification — "Am I Talking to the Real GitHub?"

What is an SSL Certificate?

Think of it like a website's ID card.

When someone claims to be a police officer, you'd ask to see their badge. That badge was issued by the police department — an authority you trust. You trust the badge because you trust who issued it.

SSL certificates work the same way:

You: "Are you really github.com?"
GitHub: "Yes, here's my certificate"
You: "Who issued this?"
GitHub: "Sectigo"
You: "I trust Sectigo. Let me verify their signature... checks out. You're legit."

What's Inside a Certificate?

When we fetch GitHub's certificate, here's what it contains:

┌─────────────────────────────────────────────────────┐
│              GITHUB'S CERTIFICATE                    │
├─────────────────────────────────────────────────────┤
│  Subject: github.com                                 │  ← Who this cert belongs to
│  Issuer: Sectigo Limited                               │  ← Who vouched for them
│  Valid From: Feb 05, 2025                           │  ← When it became active
│  Valid Until: Feb 05, 2026                          │  ← When it expires
│  GitHub's Public Key: "MIIBIjANBg..."               │  ← Used for encryption
│  Signature: "x7Km9pQr2sT4vW..."                     │  ← Sectigo's stamp of approval
└─────────────────────────────────────────────────────┘

But wait — this is just text. Anyone could create a file with this information and claim to be GitHub. So how does verification actually work?

The Magic: Digital Signatures

Here's the key insight: Sectigo has two mathematically linked keys.

┌─────────────────────────────────────────────────────────────────────┐
│                           DIGICERT                                   │
│                                                                      │
│   ┌─────────────────────┐       ┌─────────────────────┐            │
│   │     PRIVATE KEY     │       │     PUBLIC KEY      │            │
│   │     (Ultra Secret)  │       │   (Shared Openly)   │            │
│   │                     │       │                     │            │
│   │  Kept in secure     │       │  Pre-installed on   │            │
│   │  underground        │       │  every computer,    │            │
│   │  vaults, offline    │       │  phone, browser     │            │
│   │                     │       │                     │            │
│   │  Used to SIGN       │       │  Used to VERIFY     │            │
│   │  certificates       │       │  signatures         │            │
│   └─────────────────────┘       └─────────────────────┘            │
│                                                                      │
│   Mathematical property:                                             │
│   Something signed with private key can ONLY be                     │
│   verified with the matching public key.                            │
└─────────────────────────────────────────────────────────────────────┘

How GitHub Got Its Certificate

Step 1: GitHub proves they own github.com
        (Sectigo checks DNS records, sends email to admin@github.com, etc.)

Step 2: Sectigo creates the certificate data
        {
          subject: "github.com",
          issuer: "Sectigo",
          expires: "2026-02-05",
          public_key: "MIIBIjANBg..."
        }

Step 3: Sectigo SIGNS this data with their PRIVATE key

        [Certificate Data] + [Sectigo's Private Key] 
                                    ↓
                            Mathematical magic
                                    ↓
                          [Signature: "x7Km9pQr2sT..."]

Step 4: Final certificate = Data + Signature
        {
          subject: "github.com",
          issuer: "Sectigo",
          ...
          signature: "x7Km9pQr2sT..."  ← THIS is what makes it unforgeable
        }

Why You Can't Fake It

Let's say you try to create a fake Google certificate:

You create:
{
  subject: "google.com",      ← You write this
  issuer: "Sectigo",         ← You write this (lying!)
  signature: "abc123fake..."  ← You make this up
}

When a browser checks this:

[Your fake data] + [Real Sectigo Public Key] + [Your fake signature]
                              ↓
                    Mathematical verification
                              ↓
                    DOES NOT MATCH ✗
                              ↓
                    "CERTIFICATE INVALID - NOT TRUSTED"

The math only works if the signature was created with Sectigo's private key. Since you don't have that key (it's in a vault somewhere), you cannot create a valid signature.

This is protected by mathematics, not by trust or hope.

What's Installed Where?

Sectigo gives different things to different parties:

┌─────────────────────────────────────────────────────────────────────────┐
│                              DIGICERT                                    │
│                                                                          │
│   Creates and distributes:                                               │
│                                                                          │
│        Root Certificate                     Signed Certificate           │
│     (contains public key)                  (for specific domain)         │
│              │                                      │                    │
│              ▼                                      ▼                    │
│     Goes to browsers/OS                    Goes to website owner         │
└─────────────────────────────────────────────────────────────────────────┘
                │                                      │
                │                                      │
                ▼                                      ▼
┌───────────────────────────────┐    ┌────────────────────────────────────┐
│      YOUR COMPUTER            │    │        GITHUB'S SERVER             │
│                               │    │                                    │
│  Pre-installed certificates:  │    │  Installed by GitHub admin:        │
│                               │    │                                    │
│  • Sectigo Root              │    │  ┌──────────────────────────────┐  │
│    └─ Public Key: "ABC..."    │    │  │ github.com Certificate       │  │
│  • Let's Encrypt Root         │    │  │ • Subject: github.com        │  │
│    └─ Public Key: "XYZ..."    │    │  │ • Issuer: Sectigo           │  │
│  • ~150 other CAs             │    │  │ • Signature: "x7Km9p..."     │  │
│                               │    │  └──────────────────────────────┘  │
│                               │    │                                    │
│  Purpose: VERIFY signatures   │    │  Purpose: PROVE identity          │
└───────────────────────────────┘    └────────────────────────────────────┘

You can see the root certificates on your computer:

  • Windows: Press Win + R, type certmgr.msc, go to "Trusted Root Certification Authorities"

  • Mac: Open "Keychain Access", look under "System Roots"

You'll find Sectigo, Let's Encrypt, and about 150 others. These came with your operating system.

The Trust Chain

Certificates form a chain:

┌─────────────────────────────────────────────┐
│           Sectigo Root CA                  │  ← Pre-installed on your computer
│     (Trusted by all browsers)               │
└─────────────────────────────────────────────┘
                    │
                    │ signs
                    ▼
┌─────────────────────────────────────────────┐
│   Sectigo TLS RSA SHA256 2020 CA1         │  ← Intermediate (extra security layer)
└─────────────────────────────────────────────┘
                    │
                    │ signs
                    ▼
┌─────────────────────────────────────────────┐
│          github.com certificate             │  ← What GitHub shows visitors
└─────────────────────────────────────────────┘

Why the middle layer? Root certificates are extremely valuable. If one gets stolen, every certificate it ever issued becomes suspect. So CAs keep roots locked in offline vaults and use "intermediate" certificates for daily signing.

Your browser verifies the entire chain:

  1. "Who signed github.com's cert?" → Sectigo Intermediate

  2. "Who signed that?" → Sectigo Root

  3. "Is Sectigo Root in my trusted list?" → Yes!

  4. Verification complete ✓


Part 2: Encryption — "How Is My Password Protected?"

So we've verified we're talking to the real GitHub. Now, how do we hide our data from eavesdroppers?

What You See vs What Travels

┌─────────────────────────────────────────────────────────────────────────┐
│                           YOUR COMPUTER                                  │
│                                                                          │
│   ┌────────────────────────────────────────────────────────────────┐    │
│   │  GitHub Login Page                                              │    │
│   │                                                                 │    │
│   │   Username: [ umang@email.com ]                                │    │
│   │   Password: [ secret123       ]                                │    │
│   │                                                                 │    │
│   │   [Login]                                                       │    │
│   └────────────────────────────────────────────────────────────────┘    │
│                                                                          │
│   You see your password on screen — that's fine, it's YOUR screen.      │
│                                                                          │
│   When you click Login, browser ENCRYPTS before sending:                 │
│                                                                          │
│   "password=secret123"  →  "xK7$mP2#vL9@nQ4%bR8&hY3*kW6..."            │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    │  Encrypted gibberish travels
                                    │  through the internet
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                            THE INTERNET                                  │
│                                                                          │
│   Your data passes through:                                              │
│   • Your WiFi router                                                     │
│   • Your neighbor on the same WiFi                                       │
│   • Your ISP (Airtel, Jio, etc.)                                        │
│   • Dozens of servers across countries                                   │
│   • Undersea cables                                                      │
│   • GitHub's ISP                                                         │
│                                                                          │
│   What ALL of them see:                                                  │
│                                                                          │
│       "xK7$mP2#vL9@nQ4%bR8&hY3*kW6..."                                  │
│                                                                          │
│   Complete gibberish. They know you're connecting to GitHub              │
│   (the IP address is visible), but content is unreadable.               │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                          GITHUB'S SERVER                                 │
│                                                                          │
│   Receives: "xK7$mP2#vL9@nQ4%bR8&hY3*kW6..."                            │
│                                                                          │
│   DECRYPTS (only GitHub can do this):                                    │
│                                                                          │
│   "xK7$mP2#vL9@nQ4..."  →  "password=secret123"                         │
│                                                                          │
│   GitHub can now log you in!                                             │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

But How Do Both Sides Decrypt?

For encryption to work, both your browser and GitHub need to use the same "key" to lock/unlock data. But how do they agree on a key without an attacker seeing it?

The clever trick: Key Exchange

YOUR BROWSER                                    GITHUB'S SERVER
     │                                                │
     │  (Certificate already verified)                │
     │                                                │
     │  Browser generates random secret data          │
     │                                                │
     │  Encrypts it with GitHub's PUBLIC KEY          │
     │  (from the certificate we just verified)       │
     │                                                │
     │────── Encrypted secret ──────────────────────>│
     │                                                │
     │       Attacker watching sees: "m9Xk#2..."      │
     │       Can't decrypt — needs private key        │
     │                                                │
     │                    GitHub DECRYPTS with their PRIVATE key
     │                    (Only GitHub has this!)
     │                                                │
     │  Both sides use this secret to generate        │
     │  the same SESSION KEY                          │
     │                                                │
     │                                                │
     │  Session Key: "7Km9pQr2sT4vW..."              │  Session Key: "7Km9pQr2sT4vW..."
     │  (Identical on both sides!)                    │  (Identical on both sides!)
     │                                                │
     │<═══════════ ALL DATA NOW ENCRYPTED ══════════>│
     │           using this session key               │

Why can't an attacker get the session key?

The attacker sees the encrypted secret traveling from browser to GitHub. But it's encrypted with GitHub's public key — and only GitHub's private key can decrypt it. The attacker is stuck.

Two Types of Encryption Working Together

┌────────────────────────────────────────────────────────────────────────┐
│  ASYMMETRIC (Public/Private Key Pair)                                   │
│                                                                         │
│  • Two different keys                                                   │
│  • Public key encrypts → Only private key decrypts                     │
│  • SLOW (complex math)                                                  │
│                                                                         │
│  Used ONCE: To securely share the session key                          │
└────────────────────────────────────────────────────────────────────────┘
                                  │
                                  ▼
┌────────────────────────────────────────────────────────────────────────┐
│  SYMMETRIC (Same Key on Both Sides)                                     │
│                                                                         │
│  • One key encrypts AND decrypts                                       │
│  • FAST (simple operations)                                             │
│                                                                         │
│  Used for EVERYTHING: All your actual data after handshake             │
└────────────────────────────────────────────────────────────────────────┘

Why both? Asymmetric is secure but slow. Symmetric is fast but requires both sides to have the same key. Solution: Use asymmetric once to safely share a symmetric key, then use symmetric for everything else.


The Complete Picture: What Happens When You Visit github.com

┌────────────────────────────────────────────────────────────────────────────┐
│ STEP 1: DNS LOOKUP (what we built in Posts 1 & 2)                          │
│                                                                             │
│   Browser → DNS: "What's the IP for github.com?"                           │
│   DNS → Browser: "20.207.73.82"                                            │
└────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────────────────┐
│ STEP 2: TCP CONNECTION                                                      │
│                                                                             │
│   Browser → GitHub: "Hello, I want to connect on port 443"                 │
│   GitHub → Browser: "Connection accepted"                                   │
└────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────────────────┐
│ STEP 3: SSL HANDSHAKE (what we're building today)                          │
│                                                                             │
│   3a. Certificate Exchange                                                  │
│       GitHub → Browser: "Here's my certificate"                            │
│                                                                             │
│   3b. Verification                                                          │
│       Browser checks:                                                       │
│       ├── Is this cert for github.com? ✓                                   │
│       ├── Is it signed by trusted CA? ✓ (Sectigo)                         │
│       ├── Has it expired? ✓ (Valid until Mar 2025)                         │
│       └── All checks pass!                                                  │
│                                                                             │
│   3c. Key Exchange                                                          │
│       Browser sends encrypted secret using GitHub's public key             │
│       Both sides derive same session key                                   │
└────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌────────────────────────────────────────────────────────────────────────────┐
│ STEP 4: ENCRYPTED DATA TRANSFER                                             │
│                                                                             │
│   Browser ←══════════ encrypted with session key ══════════→ GitHub        │
│                                                                             │
│   Your passwords, code, private repos — all protected                       │
└────────────────────────────────────────────────────────────────────────────┘

Our tool does Step 3 — the same verification your browser does every time you visit an HTTPS site.


What Can Go Wrong?

Our tool will detect these common problems:

1. Expired Certificate

Certificates have expiration dates (usually 1 year). After expiry, browsers show warnings:

⚠️ Your connection is not private
NET::ERR_CERT_DATE_INVALID

Why do certificates expire? Forces website owners to periodically prove they still own the domain. Also, cryptographic standards improve over time.

2. Self-Signed Certificate

Anyone can create a certificate. But without a trusted CA signing it:

Certificate signed by: Some Random Person
Browser: "I don't know you. Not trusted."

The signature can't be verified against any known CA. Fine for personal testing, red flag on public websites.

3. Wrong Domain

A certificate for github.com doesn't work for gitlab.com:

Certificate is for: github.com
You're visiting: gitlab.com
❌ MISMATCH — Rejected

Prevents attackers from reusing stolen certificates on other sites.


The Code

Let's build the SSL checker. We'll use Python's built-in ssl and socket modules — no new packages needed.

Add these imports at the top of analyzer.py:

import ssl
import socket
from datetime import datetime, timezone

Now add the SSL function:

def get_ssl_info(domain):
    """Fetch SSL certificate information for a domain."""
    try:
        # Create SSL context with default security settings
        context = ssl.create_default_context()

        # Connect to the domain on port 443 (HTTPS)
        with socket.create_connection((domain, 443), timeout=10) as sock:
            with context.wrap_socket(sock, server_hostname=domain) as secure_sock:
                cert = secure_sock.getpeercert()

        # Parse certificate dates
        not_before = datetime.strptime(cert['notBefore'], '%b %d %H:%M:%S %Y %Z')
        not_after = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z')

        # Calculate days until expiry
        now = datetime.now(timezone.utc).replace(tzinfo=None)
        days_until_expiry = (not_after - now).days

        # Extract issuer info
        issuer_dict = dict(x[0] for x in cert['issuer'])
        issuer_name = issuer_dict.get('organizationName', 'Unknown')

        # Extract subject (who the cert is for)
        subject_dict = dict(x[0] for x in cert['subject'])
        common_name = subject_dict.get('commonName', 'Unknown')

        # Get Subject Alternative Names (additional domains covered)
        san_list = []
        for type_name, value in cert.get('subjectAltName', []):
            if type_name == 'DNS':
                san_list.append(value)

        return {
            'valid': True,
            'common_name': common_name,
            'issuer': issuer_name,
            'not_before': not_before.strftime('%Y-%m-%d'),
            'not_after': not_after.strftime('%Y-%m-%d'),
            'days_until_expiry': days_until_expiry,
            'subject_alt_names': san_list[:5],
            'expired': days_until_expiry < 0,
            'expiring_soon': 0 <= days_until_expiry <= 30
        }

    except ssl.SSLCertVerificationError as e:
        return {
            'valid': False,
            'error': f"Certificate verification failed: {e.verify_message}"
        }
    except socket.timeout:
        return {
            'valid': False,
            'error': "Connection timed out - site may not support HTTPS"
        }
    except socket.gaierror:
        return {
            'valid': False,
            'error': f"Could not resolve domain '{domain}'"
        }
    except ConnectionRefusedError:
        return {
            'valid': False,
            'error': "Connection refused - HTTPS may not be enabled"
        }
    except Exception as e:
        return {
            'valid': False,
            'error': str(e)
        }

Walking Through the Code with GitHub's Real Certificate

Let's trace exactly what happens when we call get_ssl_info("github.com").

Step 1: Create SSL context

context = ssl.create_default_context()

This loads all ~150 trusted CA certificates from your system (Sectigo, Let's Encrypt, etc.) and configures secure defaults. It's like giving a security guard a list of trusted ID issuers before checking IDs.


Step 2: Connect to GitHub

with socket.create_connection((domain, 443), timeout=10) as sock:

Creates a raw network connection to GitHub on port 443 (HTTPS). At this point, we're connected but not yet secure — like picking up a phone but not talking yet.


Step 3: SSL Handshake

with context.wrap_socket(sock, server_hostname=domain) as secure_sock:

This is where everything we discussed happens:

Our computer                              GitHub's server
     │                                          │
     │──────── "I want HTTPS" ────────────────>│
     │                                          │
     │<─────── "Here's my certificate" ────────│
     │                                          │
     │  [Verify signature using Sectigo's      │
     │   public key from our trusted list]      │
     │                                          │
     │  [Check: Is it for github.com?]          │
     │  [Check: Is it expired?]                 │
     │                                          │
     │──────── "Verified! Let's encrypt" ─────>│
     │                                          │
     │  [Key exchange happens]                  │
     │                                          │
     │<═══════ Encrypted connection ══════════>│

If any check fails, Python raises SSLCertVerificationError and we never get past this line.


Step 4: Extract the certificate

cert = secure_sock.getpeercert()

After successful verification, we can read the certificate. Here's what GitHub's actually looks like:

cert = {
    'subject': (
        (('commonName', 'github.com'),),
    ),
    'issuer': (
        (('countryName', 'US'),),
        (('organizationName', 'Sectigo Limited'),),
        (('commonName', 'Sectigo TLS RSA SHA256 2020 CA1'),),
    ),
    'notBefore': 'Feb 05 00:00:00 2025 GMT',
    'notAfter': 'Feb 05 23:59:59 2026 GMT',
    'subjectAltName': (
        ('DNS', 'github.com'),
        ('DNS', 'www.github.com'),
    ),
}

Step 5: Parse the dates

not_before = datetime.strptime(cert['notBefore'], '%b %d %H:%M:%S %Y %Z')
not_after = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z')

The dates come as strings. strptime converts them to Python datetime objects:

cert['notBefore'] = 'Feb 05 00:00:00 2025 GMT'
cert['notAfter']  = 'Feb 05 23:59:59 2026 GMT'

The format string tells Python how to read it:

'Feb 05 00:00:00 2025 GMT'
  │   │     │      │   │
  │   │     │      │   └── %Z (timezone)
  │   │     │      └────── %Y (year)
  │   │     └───────────── %H:%M:%S (time)
  │   └─────────────────── %d (day)
  └─────────────────────── %b (month abbreviation)

After parsing:

not_before = datetime(2025, 2, 5, 0, 0, 0)     # Feb 05, 2025
not_after  = datetime(2026, 2, 5, 23, 59, 59)  # Feb 05, 2026

Step 6: Calculate days until expiry

now = datetime.now(timezone.utc).replace(tzinfo=None)
days_until_expiry = (not_after - now).days

Let's say today is December 22, 2024:

now = datetime(2024, 12, 22, 14, 30, 0)
not_after = datetime(2026, 2, 5, 23, 59, 59)

days_until_expiry = (not_after - now).days  # = 45 days

GitHub's certificate has 45 days left — still healthy, though getting closer to renewal time!

Why .replace(tzinfo=None)? The certificate dates don't have timezone info attached. datetime.now(timezone.utc) does. Python refuses to compare them — it's like asking "is 5pm after 5pm EST?" We strip the timezone to make them compatible.


Step 7: Extract issuer information

issuer_dict = dict(x[0] for x in cert['issuer'])
issuer_name = issuer_dict.get('organizationName', 'Unknown')

The issuer data comes in a deeply nested format (blame the 1988 X.509 standard):

cert['issuer'] = (
    (('countryName', 'US'),),
    (('organizationName', 'Sectigo Limited'),),
    (('commonName', 'Sectigo TLS RSA SHA256 2020 CA1'),),
)

Let's trace how we flatten it:

# Loop through each item:
# x = (('countryName', 'US'),)           → x[0] = ('countryName', 'US')
# x = (('organizationName', 'Sectigo'),) → x[0] = ('organizationName', 'Sectigo Limited')
# x = (('commonName', '...'),)           → x[0] = ('commonName', '...')

# dict() converts these tuples to a dictionary:
issuer_dict = {
    'countryName': 'US',
    'organizationName': 'Sectigo Limited',
    'commonName': 'Sectigo TLS RSA SHA256 2020 CA1'
}

issuer_name = issuer_dict.get('organizationName', 'Unknown')
# = 'Sectigo Limited'

Step 8: Extract the subject (certificate owner)

subject_dict = dict(x[0] for x in cert['subject'])
common_name = subject_dict.get('commonName', 'Unknown')

Same process:

cert['subject'] = (
    (('commonName', 'github.com'),),
)

subject_dict = {'commonName': 'github.com'}
common_name = 'github.com'

This confirms the certificate belongs to github.com.


Step 9: Get Subject Alternative Names (SANs)

san_list = []
for type_name, value in cert.get('subjectAltName', []):
    if type_name == 'DNS':
        san_list.append(value)

One certificate can cover multiple domains:

cert['subjectAltName'] = (
    ('DNS', 'github.com'),
    ('DNS', 'www.github.com'),
)

We loop through and collect DNS entries:

# Iteration 1: type_name='DNS', value='github.com'     → add to list
# Iteration 2: type_name='DNS', value='www.github.com' → add to list

san_list = ['github.com', 'www.github.com']

Step 10: Build the final result

return {
    'valid': True,
    'common_name': 'github.com',
    'issuer': 'Sectigo Limited',
    'not_before': '2025-02-05',
    'not_after': '2026-02-05',
    'days_until_expiry': 45,
    'subject_alt_names': ['github.com', 'www.github.com'],
    'expired': False,       # 45 is not < 0
    'expiring_soon': False  # 45 is not between 0 and 30
}

Error Handling

What if something goes wrong?

except ssl.SSLCertVerificationError as e:
    return {'valid': False, 'error': f"Certificate verification failed: {e.verify_message}"}

Catches certificate problems. For expired.badssl.com:

{'valid': False, 'error': 'Certificate verification failed: certificate has expired'}
except socket.timeout:
    return {'valid': False, 'error': "Connection timed out - site may not support HTTPS"}

Server didn't respond in 10 seconds.

except socket.gaierror:
    return {'valid': False, 'error': f"Could not resolve domain '{domain}'"}

Domain doesn't exist in DNS.

except ConnectionRefusedError:
    return {'valid': False, 'error': "Connection refused - HTTPS may not be enabled"}

Server exists but rejected connection on port 443.


Update main() to Display SSL Info

Add this to your main() function:

    # SSL Certificate
    print("\nSSL Certificate:")
    ssl_result = get_ssl_info(domain)

    if ssl_result['valid']:
        print(f"  ✓ Valid certificate")
        print(f"  → Issued to: {ssl_result['common_name']}")
        print(f"  → Issued by: {ssl_result['issuer']}")
        print(f"  → Valid from: {ssl_result['not_before']}")
        print(f"  → Expires: {ssl_result['not_after']} ({ssl_result['days_until_expiry']} days)")

        if ssl_result['expired']:
            print(f"  ⚠ EXPIRED!")
        elif ssl_result['expiring_soon']:
            print(f"  ⚠ Expiring soon - renewal recommended")

        if ssl_result['subject_alt_names']:
            print(f"  → Also valid for: {', '.join(ssl_result['subject_alt_names'][:3])}")
    else:
        print(f"  ✗ {ssl_result['error']}")

Test It

python analyzer.py github.com
=== DNS Analysis for github.com ===

A Records (IPv4 addresses):
  → 20.207.73.82

MX Records (mail servers):
  → [1] aspmx.l.google.com
  → [5] alt1.aspmx.l.google.com
  → [5] alt2.aspmx.l.google.com
  → [10] alt3.aspmx.l.google.com
  → [10] alt4.aspmx.l.google.com

SPF Record (email sender policy):
  → v=spf1 include:_netblocks.google.com ...

DMARC Record (email authentication policy):
  → v=DMARC1; p=reject; pct=100; rua=mailto:dmarc@github.com

SSL Certificate:
  ✓ Valid certificate
  → Issued to: github.com
  → Issued by: Sectigo Limited
  → Valid from: 2025-02-05
  → Expires: 2026-02-05 (45 days)
  → Also valid for: github.com, www.github.com

Test error cases:

python analyzer.py expired.badssl.com
SSL Certificate:
  ✗ Certificate verification failed: certificate has expired
python analyzer.py self-signed.badssl.com
SSL Certificate:
  ✗ Certificate verification failed: self-signed certificate
python analyzer.py wrong.host.badssl.com
SSL Certificate:
  ✗ Certificate verification failed: hostname mismatch

The error handling catches all common problems!


What Our Tool Can Detect

CheckGood SignRed Flag
Certificate valid✓ Trusted CA signed it✗ Self-signed or untrusted
Expiration\> 30 days remainingExpired or expiring soon
Domain matchCert covers the domainHostname mismatch
IssuerKnown CA (Sectigo, Let's Encrypt)Unknown issuer

Important: What SSL Does NOT Protect Against

SSL ensures:

  • ✓ Your connection is encrypted

  • ✓ You're talking to the real domain owner

SSL does NOT ensure:

  • ✗ The website is trustworthy

  • ✗ The business is legitimate

  • ✗ They won't steal your data

A phishing site can have valid SSL. totally-legit-bank.com can get a free certificate from Let's Encrypt and show a padlock — while stealing your credentials.

The padlock means "your connection is secure." Not "this site is safe."


The Complete Updated Code

Here's the full analyzer.py:

import dns.resolver
import ssl
import socket
import sys
from datetime import datetime, timezone

# Use public DNS servers for reliability
resolver = dns.resolver.Resolver()
resolver.nameservers = ['1.1.1.1', '8.8.8.8']
resolver.timeout = 10
resolver.lifetime = 10


def get_a_records(domain):
    """Fetch A records for a domain."""
    try:
        answers = resolver.resolve(domain, "A")
        return [rdata.address for rdata in answers]
    except dns.resolver.NXDOMAIN:
        return f"Error: Domain '{domain}' does not exist"
    except dns.resolver.NoAnswer:
        return f"Error: No A records found for '{domain}'"
    except Exception as e:
        return f"Error: {e}"


def get_mx_records(domain):
    """Fetch MX (mail server) records for a domain."""
    try:
        answers = resolver.resolve(domain, "MX")
        mx_records = []
        for rdata in answers:
            mx_records.append({
                "priority": rdata.preference,
                "server": str(rdata.exchange).rstrip('.')
            })
        return sorted(mx_records, key=lambda x: x["priority"])
    except dns.resolver.NXDOMAIN:
        return f"Error: Domain '{domain}' does not exist"
    except dns.resolver.NoAnswer:
        return f"Error: No MX records found for '{domain}'"
    except Exception as e:
        return f"Error: {e}"


def get_spf_record(domain):
    """Fetch SPF record from TXT records."""
    try:
        answers = resolver.resolve(domain, "TXT")
        for rdata in answers:
            txt_string = str(rdata).strip('"')
            if txt_string.startswith("v=spf1"):
                return txt_string
        return "No SPF record found"
    except dns.resolver.NXDOMAIN:
        return f"Error: Domain '{domain}' does not exist"
    except dns.resolver.NoAnswer:
        return "No TXT records found"
    except Exception as e:
        return f"Error: {e}"


def get_dmarc_record(domain):
    """Fetch DMARC record from _dmarc.domain."""
    try:
        dmarc_domain = f"_dmarc.{domain}"
        answers = resolver.resolve(dmarc_domain, "TXT")
        for rdata in answers:
            txt_string = str(rdata).strip('"')
            if txt_string.startswith("v=DMARC1"):
                return txt_string
        return "No DMARC record found"
    except dns.resolver.NXDOMAIN:
        return "No DMARC record found"
    except dns.resolver.NoAnswer:
        return "No DMARC record found"
    except Exception as e:
        return f"Error: {e}"


def get_ssl_info(domain):
    """Fetch SSL certificate information for a domain."""
    try:
        # Create SSL context with default security settings
        context = ssl.create_default_context()

        # Connect to the domain on port 443 (HTTPS)
        with socket.create_connection((domain, 443), timeout=10) as sock:
            with context.wrap_socket(sock, server_hostname=domain) as secure_sock:
                cert = secure_sock.getpeercert()

        # Parse certificate dates
        not_before = datetime.strptime(cert['notBefore'], '%b %d %H:%M:%S %Y %Z')
        not_after = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z')

        # Calculate days until expiry
        now = datetime.now(timezone.utc).replace(tzinfo=None)
        days_until_expiry = (not_after - now).days

        # Extract issuer info
        issuer_dict = dict(x[0] for x in cert['issuer'])
        issuer_name = issuer_dict.get('organizationName', 'Unknown')

        # Extract subject (who the cert is for)
        subject_dict = dict(x[0] for x in cert['subject'])
        common_name = subject_dict.get('commonName', 'Unknown')

        # Get Subject Alternative Names (additional domains covered)
        san_list = []
        for type_name, value in cert.get('subjectAltName', []):
            if type_name == 'DNS':
                san_list.append(value)

        return {
            'valid': True,
            'common_name': common_name,
            'issuer': issuer_name,
            'not_before': not_before.strftime('%Y-%m-%d'),
            'not_after': not_after.strftime('%Y-%m-%d'),
            'days_until_expiry': days_until_expiry,
            'subject_alt_names': san_list[:5],
            'expired': days_until_expiry < 0,
            'expiring_soon': 0 <= days_until_expiry <= 30
        }

    except ssl.SSLCertVerificationError as e:
        return {
            'valid': False,
            'error': f"Certificate verification failed: {e.verify_message}"
        }
    except socket.timeout:
        return {
            'valid': False,
            'error': "Connection timed out - site may not support HTTPS"
        }
    except socket.gaierror:
        return {
            'valid': False,
            'error': f"Could not resolve domain '{domain}'"
        }
    except ConnectionRefusedError:
        return {
            'valid': False,
            'error': "Connection refused - HTTPS may not be enabled"
        }
    except Exception as e:
        return {
            'valid': False,
            'error': str(e)
        }


def main():
    if len(sys.argv) != 2:
        print("Usage: python analyzer.py <domain>")
        print("Example: python analyzer.py google.com")
        sys.exit(1)

    domain = sys.argv[1]
    print(f"\n=== DNS Analysis for {domain} ===\n")

    # A Records
    print("A Records (IPv4 addresses):")
    result = get_a_records(domain)
    if isinstance(result, list):
        for ip in result:
            print(f"  → {ip}")
    else:
        print(f"  {result}")

    # MX Records
    print("\nMX Records (mail servers):")
    mx_result = get_mx_records(domain)
    if isinstance(mx_result, list):
        for mx in mx_result:
            print(f"  → [{mx['priority']}] {mx['server']}")
    else:
        print(f"  {mx_result}")

    # SPF Record
    print("\nSPF Record (email sender policy):")
    print(f"  → {get_spf_record(domain)}")

    # DMARC Record
    print("\nDMARC Record (email authentication policy):")
    print(f"  → {get_dmarc_record(domain)}")

    # SSL Certificate
    print("\nSSL Certificate:")
    ssl_result = get_ssl_info(domain)

    if ssl_result['valid']:
        print(f"  ✓ Valid certificate")
        print(f"  → Issued to: {ssl_result['common_name']}")
        print(f"  → Issued by: {ssl_result['issuer']}")
        print(f"  → Valid from: {ssl_result['not_before']}")
        print(f"  → Expires: {ssl_result['not_after']} ({ssl_result['days_until_expiry']} days)")

        if ssl_result['expired']:
            print(f"  ⚠ EXPIRED!")
        elif ssl_result['expiring_soon']:
            print(f"  ⚠ Expiring soon - renewal recommended")

        if ssl_result['subject_alt_names']:
            print(f"  → Also valid for: {', '.join(ssl_result['subject_alt_names'][:3])}")
    else:
        print(f"  ✗ {ssl_result['error']}")


if __name__ == "__main__":
    main()

Commit Your Progress

git add .
git commit -m "Add SSL certificate validation"
git push origin main

What's Next

We now have DNS analysis and SSL certificate validation. In the next post, we'll check HTTP security headers — configuration that tells browsers how to handle your site securely:

  • HSTS — Force HTTPS connections

  • Content-Security-Policy — Prevent XSS attacks

  • X-Frame-Options — Block clickjacking

These headers are where many sites fall short, even with valid SSL.

See you in the next one.


Find the code for this post on GitHub.

More from this blog

Figure it Out

15 posts