---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# 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.

```{admonition} Objectifs du chapitre
:class: note
- 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
```

```{code-cell} python3
:tags: [hide-input]
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

```{code-cell} python3
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))
```

```{code-cell} python3
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()
```

## 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).

```{code-cell} python3
# 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.")
```

```{code-cell} python3
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()
```

## Certificats X.509

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

### Structure d'un certificat

```{code-cell} python3
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))
```

```{code-cell} python3
# 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']}")
```

## PKI : chaîne de confiance

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

```{code-cell} python3
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()
```

## 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.

```{code-cell} python3
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()
```

## Inspection TLS avec Python

```{code-cell} python3
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")
```

```{code-cell} python3
# 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']}")
```

## HSTS, Certificate Pinning, mTLS

```{code-cell} python3
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é

```{code-cell} python3
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()
```
