TLS/SSL : chiffrement et certificats#

TLS (Transport Layer Security) est le protocole qui sécurise l’essentiel du trafic Internet moderne : HTTPS, SMTPS, LDAPS, DoT… Il garantit confidentialité (les données sont chiffrées), intégrité (les données ne peuvent pas être modifiées en transit) et authenticité (vous parlez bien au bon serveur). Comprendre TLS, c’est comprendre pourquoi le petit cadenas de votre navigateur mérite votre confiance — ou méfiance.

Objectifs du chapitre

  • Retracer l’évolution de SSL à TLS 1.3 et comprendre les vulnérabilités corrigées

  • Comprendre la cryptographie asymétrique, l’échange Diffie-Hellman et les certificats X.509

  • Décrypter le handshake TLS 1.3 étape par étape

  • Utiliser le module ssl de Python pour inspecter des connexions TLS

  • Comprendre HSTS, certificate pinning et mTLS

Hide code cell source

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
import numpy as np
import pandas as pd
import seaborn as sns
import ssl
import socket
import datetime

sns.set_theme(style="whitegrid", palette="muted")
plt.rcParams.update({
    "figure.dpi": 110,
    "axes.titlesize": 13,
    "axes.labelsize": 11,
    "font.family": "sans-serif",
})

Historique : de SSL à TLS 1.3#

historique = pd.DataFrame({
    "Version": ["SSL 2.0", "SSL 3.0", "TLS 1.0", "TLS 1.1", "TLS 1.2", "TLS 1.3"],
    "Année": [1995, 1996, 1999, 2006, 2008, 2018],
    "Statut": ["Obsolète", "Obsolète", "Obsolète", "Obsolète", "Déprécié", "Actuel"],
    "Vulnérabilités principales": [
        "DROWN, MD5, pas de protection rejeu",
        "POODLE, RC4 weak, no PFS",
        "BEAST, POODLE-TLS, RC4",
        "BEAST partiel corrigé, CBC IV",
        "Sécurisé si bien configuré",
        "Aucune majeure connue"
    ],
    "RFC": ["N/A", "N/A", "2246", "4346", "5246", "8446"],
})

print(historique.to_string(index=False))
Version  Année   Statut          Vulnérabilités principales  RFC
SSL 2.0   1995 Obsolète DROWN, MD5, pas de protection rejeu  N/A
SSL 3.0   1996 Obsolète            POODLE, RC4 weak, no PFS  N/A
TLS 1.0   1999 Obsolète              BEAST, POODLE-TLS, RC4 2246
TLS 1.1   2006 Obsolète       BEAST partiel corrigé, CBC IV 4346
TLS 1.2   2008 Déprécié          Sécurisé si bien configuré 5246
TLS 1.3   2018   Actuel               Aucune majeure connue 8446
fig, axes = plt.subplots(1, 2, figsize=(14, 4.5))

# ── Timeline ────────────────────────────────────────────────────────────────
ax = axes[0]
versions = historique["Version"].tolist()
annees = historique["Année"].tolist()
statuts = historique["Statut"].tolist()

colors_hist = {
    "Obsolète": "#E87A4C",
    "Déprécié": "#F0C040",
    "Actuel": "#54B87A",
}

ax.set_xlim(1993, 2022)
ax.set_ylim(-1, 1)
ax.axhline(0, color="#AAAAAA", lw=2, zorder=1)
ax.axis("off")
ax.set_title("Chronologie SSL/TLS", fontweight="bold")

for i, (ver, an, stat) in enumerate(zip(versions, annees, statuts)):
    color = colors_hist[stat]
    ax.plot(an, 0, "o", color=color, markersize=14, zorder=3)
    ax.text(an, 0.18 if i % 2 == 0 else -0.25, ver, ha="center",
            fontsize=9, fontweight="bold", color=color)
    ax.text(an, 0.32 if i % 2 == 0 else -0.4, str(an), ha="center",
            fontsize=8, color="#555555")

legend_patches = [
    mpatches.Patch(color=c, label=l)
    for l, c in colors_hist.items()
]
ax.legend(handles=legend_patches, loc="lower right", fontsize=9)

# ── Vulnérabilités par version ───────────────────────────────────────────────
ax2 = axes[1]
vuln_score = [9, 8, 6, 4, 2, 0]  # Score de risque arbitraire
colors_v = [colors_hist[s] for s in statuts]
bars = ax2.bar(versions, vuln_score, color=colors_v, edgecolor="white", width=0.6)
ax2.set_ylabel("Score de risque (0 = sûr)")
ax2.set_title("Niveau de risque relatif par version", fontweight="bold")
ax2.set_ylim(0, 11)
ax2.grid(axis="y", alpha=0.4)
for bar, v in zip(bars, vuln_score):
    label = "Critique" if v >= 8 else ("Élevé" if v >= 5 else ("Faible" if v > 0 else "Sûr"))
    ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.2,
             label, ha="center", fontsize=8.5, fontweight="bold")
ax2.tick_params(axis="x", labelsize=9)

plt.tight_layout()
plt.show()
_images/9e7e90191bcc4de61e7fb65b6335873ff98cf177b746dc312fe4f51761ae9e6d.png

Cryptographie asymétrique#

TLS repose sur un mélange savant de cryptographie asymétrique (pour l’authentification et l’échange de clés) et de cryptographie symétrique (pour chiffrer les données en volume).

Échange Diffie-Hellman (DH)#

L’échange DH permet à deux parties de construire un secret partagé sans jamais le transmettre, même sur un canal public. C’est la base du Perfect Forward Secrecy (PFS).

# Illustration mathématique de l'échange DH
# g^a mod p et g^b mod p sont publics ; g^(ab) mod p reste secret

p = 23   # Premier (petit pour l'illustration — en pratique 2048+ bits)
g = 5    # Générateur

# Alice choisit a (secret)
a = 6
# Bob choisit b (secret)
b = 15

# Échange public
A = pow(g, a, p)   # Alice envoie A = g^a mod p
B = pow(g, b, p)   # Bob envoie   B = g^b mod p

# Calcul du secret partagé
secret_alice = pow(B, a, p)   # B^a mod p = g^(ba) mod p
secret_bob   = pow(A, b, p)   # A^b mod p = g^(ab) mod p

print("=== Échange Diffie-Hellman (illustration) ===")
print(f"Paramètres publics : p={p}, g={g}")
print(f"\nAlice : secret a={a}  →  envoie A = g^a mod p = {g}^{a} mod {p} = {A}")
print(f"Bob   : secret b={b}  →  envoie B = g^b mod p = {g}^{b} mod {p} = {B}")
print(f"\nSecret partagé :")
print(f"  Alice : B^a mod p = {B}^{a} mod {p} = {secret_alice}")
print(f"  Bob   : A^b mod p = {A}^{b} mod {p} = {secret_bob}")
print(f"\nLes deux obtiennent le même secret : {secret_alice == secret_bob}")
print("\nUn attaquant intercepte A={A} et B={B} mais ne peut pas calculer ab sans a ou b.")
=== Échange Diffie-Hellman (illustration) ===
Paramètres publics : p=23, g=5

Alice : secret a=6  →  envoie A = g^a mod p = 5^6 mod 23 = 8
Bob   : secret b=15  →  envoie B = g^b mod p = 5^15 mod 23 = 19

Secret partagé :
  Alice : B^a mod p = 19^6 mod 23 = 2
  Bob   : A^b mod p = 8^15 mod 23 = 2

Les deux obtiennent le même secret : True

Un attaquant intercepte A={A} et B={B} mais ne peut pas calculer ab sans a ou b.
fig, axes = plt.subplots(1, 2, figsize=(13, 5))

# ── Illustration DH ──────────────────────────────────────────────────────────
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 7)
ax.axis("off")
ax.set_title("Échange Diffie-Hellman", fontweight="bold")

def box(ax, x, y, w, h, text, color, fs=9):
    r = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.1",
                       linewidth=1.5, edgecolor="#555", facecolor=color, alpha=0.9)
    ax.add_patch(r)
    ax.text(x + w/2, y + h/2, text, ha="center", va="center",
            fontsize=fs, fontweight="bold", color="white", wrap=True)

def arr(ax, x1, y1, x2, y2, label, color="#555"):
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="->", color=color, lw=1.8))
    ax.text((x1+x2)/2, (y1+y2)/2 + 0.2, label, ha="center", fontsize=8.5, color=color)

box(ax, 0.3, 5.5, 2, 0.8, "ALICE\nsecret a=6", "#4C9BE8")
box(ax, 7.7, 5.5, 2, 0.8, "BOB\nsecret b=15", "#54B87A")
box(ax, 3.5, 5.5, 3, 0.8, "CANAL PUBLIC\np=23, g=5", "#888888")

arr(ax, 2.3, 5.9, 3.5, 5.9, f"A = g^a mod p = {A}", "#4C9BE8")
arr(ax, 6.5, 5.7, 7.7, 5.7, f"B = g^b mod p = {B}", "#54B87A")

box(ax, 0.3, 3.5, 2, 0.8, f"Secret\nB^a mod p\n= {secret_alice}", "#4C9BE8")
box(ax, 7.7, 3.5, 2, 0.8, f"Secret\nA^b mod p\n= {secret_bob}", "#54B87A")

ax.text(5, 4.2, "✓ Même secret partagé\nsans jamais le transmettre", ha="center",
        fontsize=10, fontweight="bold", color="#E87A4C",
        bbox=dict(facecolor="#FFF3E0", edgecolor="#E87A4C", boxstyle="round,pad=0.3"))

ax.text(3.8, 2.5, "Espion voit A et B\nmais pas ab", ha="center", fontsize=9,
        color="#C96DD8",
        bbox=dict(facecolor="#F9F0FF", edgecolor="#C96DD8", boxstyle="round,pad=0.3"))

# ── Comparaison crypto symétrique vs asymétrique ─────────────────────────────
ax2 = axes[1]
categories = ["Vitesse", "Longueur\nde clé (bits)", "Usage en TLS",
              "PFS possible", "Authentification"]
sym = [10, 256, 8, 5, 3]
asym = [2, 2048, 3, 10, 10]

x_cat = np.arange(len(categories))
width = 0.35
ax2.bar(x_cat - width/2, sym, width, label="Symétrique (AES…)", color="#4C9BE8", alpha=0.85)
ax2.bar(x_cat + width/2, asym, width, label="Asymétrique (RSA/ECDH…)", color="#E87A4C", alpha=0.85)
ax2.set_ylabel("Score relatif (illustratif)")
ax2.set_title("Symétrique vs Asymétrique", fontweight="bold")
ax2.set_xticks(x_cat)
ax2.set_xticklabels(categories, fontsize=8.5)
ax2.legend(fontsize=9)
ax2.grid(axis="y", alpha=0.4)

plt.tight_layout()
plt.show()
_images/931d14816dea28e639f9319260fb3c79234a5d47341c2c2abb07056d3a845bef.png

Certificats X.509#

Un certificat X.509 est un document signé numériquement qui associe une clé publique à une identité.

Structure d’un certificat#

champs_x509 = {
    "Champ": [
        "Version", "Numéro de série", "Algorithme de signature",
        "Émetteur (Issuer)", "Validité (notBefore / notAfter)",
        "Sujet (Subject)", "Clé publique du sujet",
        "SAN (Subject Alternative Names)", "Extensions",
        "Signature CA"
    ],
    "Exemple / Description": [
        "v3 (la plus courante)",
        "Entier unique chez la CA (ex: 0x0F2A...)",
        "sha256WithRSAEncryption / ecdsa-with-SHA256",
        "C=US, O=Let's Encrypt, CN=R3",
        "2024-01-01 → 2025-01-01",
        "CN=example.com (pour les CA DV)",
        "RSA 2048 bits ou EC P-256",
        "DNS:example.com, DNS:www.example.com",
        "KeyUsage, ExtKeyUsage, OCSP, CRL…",
        "Hachage signé avec la clé privée de la CA"
    ]
}

df_x509 = pd.DataFrame(champs_x509)
print(df_x509.to_string(index=False))
                          Champ                       Exemple / Description
                        Version                       v3 (la plus courante)
                Numéro de série    Entier unique chez la CA (ex: 0x0F2A...)
        Algorithme de signature sha256WithRSAEncryption / ecdsa-with-SHA256
              Émetteur (Issuer)                C=US, O=Let's Encrypt, CN=R3
Validité (notBefore / notAfter)                     2024-01-01 → 2025-01-01
                Sujet (Subject)             CN=example.com (pour les CA DV)
          Clé publique du sujet                   RSA 2048 bits ou EC P-256
SAN (Subject Alternative Names)        DNS:example.com, DNS:www.example.com
                     Extensions           KeyUsage, ExtKeyUsage, OCSP, CRL…
                   Signature CA   Hachage signé avec la clé privée de la CA
# Inspection d'un certificat en direct
def inspect_certificate(hostname: str, port: int = 443) -> dict:
    """Récupère et analyse le certificat TLS d'un serveur."""
    ctx = ssl.create_default_context()
    try:
        with socket.create_connection((hostname, port), timeout=5) as raw_sock:
            with ctx.wrap_socket(raw_sock, server_hostname=hostname) as tls_sock:
                cert = tls_sock.getpeercert()
                cipher = tls_sock.cipher()
                version = tls_sock.version()
                return {
                    "sujet": dict(x[0] for x in cert.get("subject", [])),
                    "émetteur": dict(x[0] for x in cert.get("issuer", [])),
                    "version_tls": version,
                    "chiffrement": cipher[0] if cipher else "?",
                    "bits": cipher[2] if cipher else "?",
                    "valide_jusqu": cert.get("notAfter", "?"),
                    "san": [v for t, v in cert.get("subjectAltName", []) if t == "DNS"],
                    "serial": cert.get("serialNumber", "?"),
                }
    except Exception as e:
        return {"erreur": str(e)}

# Inspecter python.org
print("=== Certificat python.org ===")
info = inspect_certificate("python.org")
if "erreur" not in info:
    for k, v in info.items():
        if isinstance(v, list):
            print(f"  {k:15s} : {', '.join(v[:5])}{'…' if len(v) > 5 else ''}")
        elif isinstance(v, dict):
            print(f"  {k:15s} : {v}")
        else:
            print(f"  {k:15s} : {v}")
else:
    print(f"  Erreur : {info['erreur']}")
=== Certificat python.org ===
  sujet           : {'commonName': 'www.python.org'}
  émetteur        : {'countryName': 'BE', 'organizationName': 'GlobalSign nv-sa', 'commonName': 'GlobalSign Atlas R3 DV TLS CA 2025 Q4'}
  version_tls     : TLSv1.3
  chiffrement     : TLS_AES_128_GCM_SHA256
  bits            : 128
  valide_jusqu    : Feb 14 13:03:45 2027 GMT
  san             : www.python.org, *.python.org, python.org
  serial          : 01FC68FD084537B393B8D6C708974969

PKI : chaîne de confiance#

La PKI (Public Key Infrastructure) est un système hiérarchique de Certificate Authorities (CA) qui établit la confiance.

fig, ax = plt.subplots(figsize=(12, 6))
ax.set_xlim(0, 12)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("PKI — Chaîne de confiance (chain of trust)", fontsize=14, fontweight="bold")

def cert_box(ax, x, y, w, h, title, subtitle, color, fontsize=9):
    rect = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.15",
                          linewidth=2, edgecolor=color, facecolor=color, alpha=0.85)
    ax.add_patch(rect)
    ax.text(x + w/2, y + h*0.65, title, ha="center", va="center",
            fontsize=fontsize, fontweight="bold", color="white")
    ax.text(x + w/2, y + h*0.25, subtitle, ha="center", va="center",
            fontsize=7.5, color="white", alpha=0.9)

def sign_arrow(ax, x1, y1, x2, y2, label):
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="->", color="#E87A4C", lw=2.5))
    ax.text((x1+x2)/2 + 0.4, (y1+y2)/2, label, fontsize=8,
            color="#E87A4C", fontweight="bold")

# CA Racine
cert_box(ax, 4.5, 6.5, 3, 1.0, "CA Racine (Root CA)", "Stockée dans le navigateur\nAutosignée", "#C96DD8")

# CA Intermédiaires
cert_box(ax, 1, 4.5, 3.5, 0.9, "CA Intermédiaire", "Let's Encrypt R3\nISRG Root X1", "#4C9BE8")
cert_box(ax, 7.5, 4.5, 3.5, 0.9, "CA Intermédiaire", "DigiCert Global G2\nDomaine validé", "#4C9BE8")

# Certificats finaux
cert_box(ax, 0.2, 2.3, 2.5, 0.8, "Certificat final", "example.com\nDV", "#54B87A")
cert_box(ax, 3, 2.3, 2.5, 0.8, "Certificat final", "mail.example.com\nDV", "#54B87A")
cert_box(ax, 6.5, 2.3, 2.5, 0.8, "Certificat final", "shop.example.com\nEV", "#54B87A")
cert_box(ax, 9.2, 2.3, 2.5, 0.8, "Certificat final", "api.example.com\nWildcard", "#54B87A")

# Flèches de signature
sign_arrow(ax, 6, 7, 4.5, 5.4, "signe →")
sign_arrow(ax, 6, 7, 9.25, 5.4, "signe →")
sign_arrow(ax, 2.75, 4.5, 1.45, 3.1, "signe →")
sign_arrow(ax, 2.75, 4.5, 4.25, 3.1, "signe →")
sign_arrow(ax, 9.25, 4.5, 7.75, 3.1, "signe →")
sign_arrow(ax, 9.25, 4.5, 10.45, 3.1, "signe →")

# Légende
ax.text(6, 1.4, "Vérification : le navigateur remonte la chaîne jusqu'à une CA racine de confiance",
        ha="center", fontsize=9.5, color="#333333",
        bbox=dict(facecolor="#F0F4F8", edgecolor="#AAAAAA", boxstyle="round,pad=0.3"))

# CRL / OCSP
for x, y, txt in [(1.45, 0.4, "CRL\n(liste révocation)"), (6, 0.4, "OCSP\n(vérification en ligne)")]:
    ax.text(x, y, txt, ha="center", fontsize=8, color="#888888",
            bbox=dict(facecolor="#F8F8F8", edgecolor="#CCCCCC", boxstyle="round,pad=0.2"))

plt.tight_layout()
plt.show()
_images/37e394f060f301f1772c3dc60d7efcac3769b7bec5dd3d69a87d1e190fcd6cba.png

Le handshake TLS 1.3#

TLS 1.3 (RFC 8446) a profondément simplifié le handshake : 1-RTT en connexion initiale, 0-RTT pour les reconnexions.

fig, ax = plt.subplots(figsize=(13, 8))
ax.set_xlim(0, 14)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title("Handshake TLS 1.3 — 1-RTT", fontsize=14, fontweight="bold")

CLIENT_X, SERVER_X = 2, 12

# Colonnes
def col_box(ax, x, label, color):
    r = FancyBboxPatch((x - 1, 9.3), 2, 0.6, boxstyle="round,pad=0.1",
                       linewidth=2, edgecolor=color, facecolor=color, alpha=0.9)
    ax.add_patch(r)
    ax.text(x, 9.6, label, ha="center", va="center", fontsize=11,
            fontweight="bold", color="white")

col_box(ax, CLIENT_X, "CLIENT", "#4C9BE8")
col_box(ax, SERVER_X, "SERVEUR", "#54B87A")

# Lignes de vie
for x in [CLIENT_X, SERVER_X]:
    ax.plot([x, x], [0.5, 9.3], "--", color="#BBBBBB", lw=1.2)

def msg_arrow(ax, src_x, dst_x, y, label, sublabel, color, lw=2):
    ax.annotate("", xy=(dst_x, y), xytext=(src_x, y),
                arrowprops=dict(arrowstyle="-|>", color=color, lw=lw,
                                mutation_scale=15))
    mx = (src_x + dst_x) / 2
    ax.text(mx, y + 0.22, label, ha="center", fontsize=9.5,
            fontweight="bold", color=color)
    if sublabel:
        ax.text(mx, y - 0.18, sublabel, ha="center", fontsize=8,
                color="#555555", style="italic")

def note(ax, x, y, text, color, align="right"):
    ha = "right" if align == "right" else "left"
    ax.text(x, y, text, ha=ha, fontsize=8, color=color,
            bbox=dict(facecolor="white", edgecolor=color, alpha=0.85,
                      boxstyle="round,pad=0.2"))

# Étapes
msg_arrow(ax, CLIENT_X, SERVER_X, 8.5,
          "ClientHello",
          "version=TLS1.3, cipher_suites, key_share (ECDH), random",
          "#4C9BE8")

msg_arrow(ax, SERVER_X, CLIENT_X, 7.2,
          "ServerHello + {Chiffré}",
          "key_share, Extensions, Certificate, CertificateVerify, Finished",
          "#54B87A")

note(ax, SERVER_X + 1.2, 7.2,
     "Clés de session\ndérivées ici\n(HKDF)", "#54B87A", align="left")

note(ax, CLIENT_X - 1.2, 6.5,
     "Client déchiffre\net vérifie le cert.", "#4C9BE8", align="right")

msg_arrow(ax, CLIENT_X, SERVER_X, 5.8,
          "{Finished}",
          "Confirmation du handshake",
          "#4C9BE8")

# Zone données chiffrées
from matplotlib.patches import Rectangle
rect = Rectangle((CLIENT_X - 0.3, 4.4), SERVER_X - CLIENT_X + 0.6, 1.1,
                 linewidth=2, edgecolor="#E87A4C", facecolor="#FFF3E0", alpha=0.7)
ax.add_patch(rect)
ax.text(7, 5.0, "Application Data chiffrées (AES-GCM / ChaCha20-Poly1305)", ha="center",
        fontsize=9.5, fontweight="bold", color="#E87A4C")
ax.annotate("", xy=(SERVER_X - 0.3, 4.8), xytext=(CLIENT_X + 0.3, 4.8),
            arrowprops=dict(arrowstyle="<->", color="#E87A4C", lw=2))

# Timeline RTT
ax.annotate("", xy=(CLIENT_X - 1.5, 5.8), xytext=(CLIENT_X - 1.5, 8.5),
            arrowprops=dict(arrowstyle="<->", color="#C96DD8", lw=2))
ax.text(CLIENT_X - 2.5, 7.15, "1 RTT", ha="center", fontsize=11,
        color="#C96DD8", fontweight="bold",
        bbox=dict(facecolor="white", edgecolor="#C96DD8", boxstyle="round"))

# Perfect Forward Secrecy
ax.text(7, 3.6, "Perfect Forward Secrecy : clés ephémères ECDH — \n"
        "même si la clé privée du serveur est compromise, les sessions passées restent secrètes.",
        ha="center", fontsize=9, color="#333333",
        bbox=dict(facecolor="#F0FFF4", edgecolor="#54B87A", boxstyle="round,pad=0.3"))

# 0-RTT
ax.text(7, 2.5, "0-RTT (Early Data) : le client peut envoyer des données dès le premier message\n"
        "en utilisant un ticket de session précédent. Attention : pas de protection contre le rejeu.",
        ha="center", fontsize=8.5, color="#555555",
        bbox=dict(facecolor="#F8F8F8", edgecolor="#AAAAAA", boxstyle="round,pad=0.3"))

plt.tight_layout()
plt.show()
_images/64a97a71789321ad50d1315535544ebad402d25f1f806716f73341906078b388.png

Inspection TLS avec Python#

import ssl, socket, datetime

def inspect_tls(hostname: str, port: int = 443) -> None:
    """Inspecte et affiche les détails TLS d'une connexion."""
    ctx = ssl.create_default_context()

    print(f"=== Connexion TLS à {hostname}:{port} ===\n")
    try:
        with socket.create_connection((hostname, port), timeout=6) as raw:
            with ctx.wrap_socket(raw, server_hostname=hostname) as tls:
                # Version et chiffrement
                print(f"Version TLS      : {tls.version()}")
                cipher = tls.cipher()
                if cipher:
                    print(f"Suite de chiffrement : {cipher[0]}")
                    print(f"  Protocole    : {cipher[1]}")
                    print(f"  Bits de clé  : {cipher[2]}")

                # Certificat
                cert = tls.getpeercert()
                print(f"\nSujet            : {dict(x[0] for x in cert.get('subject', []))}")
                print(f"Émetteur         : {dict(x[0] for x in cert.get('issuer', []))}")

                not_after = cert.get("notAfter", "?")
                print(f"Valide jusqu'au  : {not_after}")

                # Jours restants
                try:
                    exp = datetime.datetime.strptime(not_after, "%b %d %H:%M:%S %Y %Z")
                    jours = (exp - datetime.datetime.utcnow()).days
                    print(f"Jours restants   : {jours}")
                except Exception:
                    pass

                # SAN
                san = [v for t, v in cert.get("subjectAltName", []) if t == "DNS"]
                print(f"SAN DNS          : {san[:5]}{'…' if len(san) > 5 else ''}")

                # Obtenir le DER et afficher quelques infos
                der = tls.getpeercert(binary_form=True)
                print(f"\nTaille certificat (DER) : {len(der)} octets")

    except ssl.SSLCertVerificationError as e:
        print(f"Erreur vérification certificat : {e}")
    except Exception as e:
        print(f"Erreur : {e}")

inspect_tls("python.org")
=== Connexion TLS à python.org:443 ===

Version TLS      : TLSv1.3
Suite de chiffrement : TLS_AES_128_GCM_SHA256
  Protocole    : TLSv1.3
  Bits de clé  : 128

Sujet            : {'commonName': 'www.python.org'}
Émetteur         : {'countryName': 'BE', 'organizationName': 'GlobalSign nv-sa', 'commonName': 'GlobalSign Atlas R3 DV TLS CA 2025 Q4'}
Valide jusqu'au  : Feb 14 13:03:45 2027 GMT
Jours restants   : 324
SAN DNS          : ['www.python.org', '*.python.org', 'python.org']

Taille certificat (DER) : 1670 octets
/tmp/ipykernel_30425/1199849089.py:30: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
  jours = (exp - datetime.datetime.utcnow()).days
# Créer un contexte TLS manuellement avec options avancées
def create_tls_context(verify: bool = True,
                        min_version: ssl.TLSVersion = ssl.TLSVersion.TLSv1_2
                        ) -> ssl.SSLContext:
    """
    Crée un contexte TLS client configuré.
    """
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

    # Version minimale
    ctx.minimum_version = min_version

    # Vérification du certificat
    if verify:
        ctx.verify_mode = ssl.CERT_REQUIRED
        ctx.check_hostname = True
        ctx.load_default_certs()
    else:
        ctx.verify_mode = ssl.CERT_NONE
        ctx.check_hostname = False

    # Désactiver les suites faibles
    ctx.set_ciphers("ECDH+AESGCM:ECDH+CHACHA20:!aNULL:!MD5:!RC4")

    return ctx

# Afficher la configuration du contexte
ctx = create_tls_context()
print("=== Configuration du contexte TLS ===")
print(f"Version minimale     : {ctx.minimum_version}")
print(f"Vérification cert.   : {ctx.verify_mode}")
print(f"Check hostname       : {ctx.check_hostname}")
print(f"Protocole            : {ctx.protocol}")

# Lister les suites de chiffrement disponibles
ciphers = ctx.get_ciphers()
print(f"\nSuites disponibles   : {len(ciphers)}")
print("Premières suites :")
for c in ciphers[:6]:
    print(f"  {c['name']:<45} bits={c['bits']}")
=== Configuration du contexte TLS ===
Version minimale     : 771
Vérification cert.   : 2
Check hostname       : True
Protocole            : 16

Suites disponibles   : 9
Premières suites :
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[11], line 40
     38 print("Premières suites :")
     39 for c in ciphers[:6]:
---> 40     print(f"  {c['name']:<45} bits={c['bits']}")

KeyError: 'bits'

HSTS, Certificate Pinning, mTLS#

fig, axes = plt.subplots(1, 3, figsize=(14, 5))

# ── HSTS ────────────────────────────────────────────────────────────────────
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("HSTS\n(HTTP Strict Transport Security)", fontweight="bold")

steps = [
    (5, 7, "Navigateur", "#4C9BE8"),
    (5, 1.5, "Serveur HTTPS", "#54B87A"),
]
for x, y, lbl, c in steps:
    r = FancyBboxPatch((x-2, y-0.35), 4, 0.7, boxstyle="round,pad=0.1",
                       linewidth=1.5, edgecolor=c, facecolor=c, alpha=0.85)
    ax.add_patch(r)
    ax.text(x, y, lbl, ha="center", va="center", fontsize=10,
            fontweight="bold", color="white")

ax.annotate("", xy=(5, 2.2), xytext=(5, 6.65),
            arrowprops=dict(arrowstyle="->", color="#4C9BE8", lw=2))
ax.text(6.5, 4.5, "GET / HTTP (1ère\nfois)", ha="center", fontsize=8.5, color="#4C9BE8")

ax.annotate("", xy=(5, 6.65), xytext=(5, 2.2),
            arrowprops=dict(arrowstyle="->", color="#54B87A", lw=2))
ax.text(3.5, 4.0, "Strict-Transport-\nSecurity:\nmax-age=31536000;\nincludeSubDomains",
        ha="center", fontsize=7.5, color="#54B87A",
        bbox=dict(facecolor="#F0FFF4", edgecolor="#54B87A", boxstyle="round,pad=0.2"))

ax.text(5, 3.1, "→ Navigateur retient :\nHTTP toujours redirigé\nvers HTTPS pendant 1 an",
        ha="center", fontsize=8, color="#E87A4C",
        bbox=dict(facecolor="#FFF3E0", edgecolor="#E87A4C", boxstyle="round,pad=0.2"))

# ── Certificate Pinning ──────────────────────────────────────────────────────
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 8)
ax2.axis("off")
ax2.set_title("Certificate Pinning", fontweight="bold")

ax2.text(5, 7.5, "Application mobile / client", ha="center", fontsize=9,
         fontweight="bold", color="white",
         bbox=dict(facecolor="#4C9BE8", boxstyle="round,pad=0.3"))

ax2.text(5, 5.5, "Clé publique connue\nat compile-time :\nSHA256(pubkey) =\nABCDEF1234...",
         ha="center", fontsize=8, color="#333",
         bbox=dict(facecolor="#E8F4FD", edgecolor="#4C9BE8", boxstyle="round,pad=0.3"))

ax2.text(5, 3.5, "✓ Certificat reçu\ncorrespond au pin\n→ Connexion autorisée", ha="center",
         fontsize=9, color="#54B87A", fontweight="bold",
         bbox=dict(facecolor="#F0FFF4", edgecolor="#54B87A", boxstyle="round,pad=0.3"))

ax2.text(5, 1.8, "✗ Certificat différent\n(même CA valide)\n→ Connexion BLOQUÉE",
         ha="center", fontsize=9, color="#E87A4C", fontweight="bold",
         bbox=dict(facecolor="#FFF0F0", edgecolor="#E87A4C", boxstyle="round,pad=0.3"))

# ── mTLS ─────────────────────────────────────────────────────────────────────
ax3 = axes[2]
ax3.set_xlim(0, 10)
ax3.set_ylim(0, 8)
ax3.axis("off")
ax3.set_title("mTLS\n(Mutual TLS — authentification mutuelle)", fontweight="bold")

ax3.text(2, 7.2, "CLIENT\n+ certificat client", ha="center", fontsize=8.5,
         color="white", fontweight="bold",
         bbox=dict(facecolor="#4C9BE8", boxstyle="round,pad=0.3"))
ax3.text(8, 7.2, "SERVEUR\n+ certificat serveur", ha="center", fontsize=8.5,
         color="white", fontweight="bold",
         bbox=dict(facecolor="#54B87A", boxstyle="round,pad=0.3"))

for y, label, color in [
    (6.0, "→ ClientHello + cert client", "#4C9BE8"),
    (5.1, "← ServerHello + cert serveur", "#54B87A"),
    (4.2, "→ CertificateVerify (client)", "#4C9BE8"),
    (3.3, "← Finished", "#54B87A"),
]:
    ax3.text(5, y, label, ha="center", fontsize=8.5, color=color,
             bbox=dict(facecolor="white", edgecolor=color, boxstyle="round,pad=0.2"))

ax3.text(5, 2.2, "Use cases :\n• API machine-à-machine (M2M)\n"
         "• Service mesh (Istio, Linkerd)\n• Zero Trust Network Access",
         ha="center", fontsize=8, color="#333",
         bbox=dict(facecolor="#F8F9FA", edgecolor="#888", boxstyle="round,pad=0.3"))

plt.tight_layout()
plt.show()

Résumé#

fig, ax = plt.subplots(figsize=(12, 5.5))
ax.axis("off")
ax.set_title("Récapitulatif — TLS/SSL", fontsize=14, fontweight="bold", pad=15)

resume = [
    ["SSL 2.0/3.0", "Historiques, multiples vulnérabilités (POODLE, DROWN) — ne jamais utiliser"],
    ["TLS 1.3 (RFC 8446)", "Standard actuel : 1-RTT, 0-RTT, PFS intégré, suites modernes uniquement"],
    ["Diffie-Hellman (ECDH)", "Échange de clés sans transmettre le secret — base du PFS"],
    ["Certificat X.509 v3", "Sujet, émetteur, clé publique, SAN, extensions, signature CA"],
    ["PKI — Chaîne de confiance", "CA racine → CA intermédiaire → certificat final"],
    ["OCSP / CRL", "Révocation : Online Certificate Status Protocol / Certificate Revocation List"],
    ["ssl.create_default_context()", "Contexte Python sécurisé par défaut (TLS 1.2+, vérification cert.)"],
    ["HSTS", "Forcer HTTPS pendant N secondes via header Strict-Transport-Security"],
    ["Certificate Pinning", "Vérifier une empreinte de clé connue — protection contre CA compromise"],
    ["mTLS", "Authentification mutuelle — client ET serveur présentent un certificat"],
]

table = ax.table(
    cellText=resume,
    colLabels=["Concept", "Description"],
    cellLoc="left",
    loc="center",
    colWidths=[0.28, 0.62]
)
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1, 1.7)

for j in range(2):
    table[0, j].set_facecolor("#2C3E50")
    table[0, j].set_text_props(color="white", fontweight="bold")
for i in range(1, len(resume) + 1):
    for j in range(2):
        if i % 2 == 0:
            table[i, j].set_facecolor("#F5F7FA")
        if j == 0:
            table[i, j].set_text_props(fontweight="bold", color="#2C3E50", fontsize=8.5)

plt.tight_layout()
plt.show()