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
sslde Python pour inspecter des connexions TLSComprendre HSTS, certificate pinning et mTLS
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()
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()
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()
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()
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()