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

# 05 — PKI, certificats X.509 et rotation

La **Public Key Infrastructure** (PKI) constitue le socle de confiance de l'internet moderne : elle lie des identités à des clés publiques au moyen de certificats signés par des autorités de certification. Comprendre sa structure, ses failles potentielles et les stratégies de rotation zero-downtime est indispensable pour toute architecture sécurisée.

```{admonition} Prérequis
:class: note
Ce chapitre suppose la maîtrise des primitives cryptographiques (RSA, ECDSA, AES-GCM) vues aux chapitres précédents, ainsi qu'une familiarité avec Linux et les outils réseau TLS.
```

---

## Structure d'un certificat X.509 v3

Un certificat X.509 v3 est encodé en **DER** (binaire) ou **PEM** (base64). Sa structure ASN.1 distingue trois blocs :

### TBSCertificate — la partie signée

`TBSCertificate` ("To Be Signed") contient tous les champs sur lesquels porte la signature de la CA :

| Champ | Rôle |
|---|---|
| `version` | Toujours 2 (v3) pour les certificats modernes |
| `serialNumber` | Identifiant unique au sein de la CA |
| `signature` | Algorithme de signature (ex. `sha256WithRSAEncryption`) |
| `issuer` | DN de la CA émettrice |
| `validity` | `notBefore` / `notAfter` |
| `subject` | DN du titulaire (CN, O, C…) |
| `subjectPublicKeyInfo` | Algorithme + clé publique |
| `extensions` | Extensions v3 (SAN, Key Usage, EKU…) |

### Extensions critiques

Les extensions marquées `critical: true` **doivent** être comprises par le vérificateur (sinon rejet).

**Subject Alternative Name (SAN)** — remplace le CN pour identifier les hôtes depuis RFC 2818 :
```
X509v3 Subject Alternative Name:
    DNS:api.example.com, DNS:*.example.com, IP:192.168.1.1
```

**Key Usage** — usages cryptographiques autorisés de la clé :
```
X509v3 Key Usage: critical
    Digital Signature, Key Encipherment
```

**Extended Key Usage (EKU)** — usages applicatifs :
```
X509v3 Extended Key Usage:
    TLS Web Server Authentication, TLS Web Client Authentication
```

**OCSP / CRL Distribution Points** — comment vérifier la révocation :
```
Authority Information Access:
    OCSP - URI:http://ocsp.example.com/
X509v3 CRL Distribution Points:
    Full Name: URI:http://crl.example.com/ca.crl
```

**Basic Constraints** — distingue CA (`cA: TRUE`) de certificat feuille (`cA: FALSE`) :
```
X509v3 Basic Constraints: critical
    CA:FALSE
```

---

## Chaîne de certification

### Architecture hiérarchique

```
Root CA (offline, HSM)
    └── Intermediate CA (online)
            ├── Certificat feuille A (serveur TLS)
            └── Certificat feuille B (client mTLS)
```

La **Root CA** est gardée hors ligne (air-gap) : sa clé privée ne signe que les certificats d'intermédiaires, rarement. En cas de compromission d'une CA intermédiaire, seule cette branche est révoquée — la racine reste saine.

### Vérification de la chaîne

L'algorithme de vérification (RFC 5280) parcourt la chaîne de bas en haut :

1. Vérifier la signature de chaque certificat avec la clé publique de son émetteur.
2. Vérifier les dates de validité (`notBefore ≤ now ≤ notAfter`).
3. Vérifier que `Basic Constraints: cA:TRUE` est présent sur chaque CA intermédiaire.
4. Vérifier le `pathLen` (profondeur maximale de chaîne autorisée).
5. Vérifier que le certificat feuille n'est pas révoqué (OCSP / CRL).
6. Vérifier que l'émetteur du premier certificat est un trust anchor (Root CA dans le magasin de confiance).

---

## Let's Encrypt et ACME v2

**ACME** (RFC 8555) automatise l'émission et le renouvellement de certificats DV (Domain Validated).

### Challenge HTTP-01

Le client ACME place un token sous `/.well-known/acme-challenge/<token>`. Le serveur ACME vérifie que `http://<domain>/.well-known/acme-challenge/<token>` répond avec la valeur attendue.

- Avantage : simple, pas d'accès DNS requis.
- Limite : ne fonctionne pas pour les wildcards, requiert le port 80 accessible.

### Challenge DNS-01

Le client crée un enregistrement `TXT _acme-challenge.<domain>` avec la valeur du challenge. Permet :
- Les wildcards (`*.example.com`).
- Les machines sans port 80 exposé.
- L'automatisation via API DNS (Route53, Cloudflare…).

### Automatisation avec certbot

```bash
# HTTP-01, renouvellement automatique via systemd timer
certbot certonly --nginx -d api.example.com --non-interactive --agree-tos -m admin@example.com

# DNS-01 wildcard avec plugin Cloudflare
certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
  -d "*.example.com" -d example.com

# Vérification de la chaîne émise
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt \
  /etc/letsencrypt/live/example.com/fullchain.pem
```

---

## PKI interne

### CFSSL (Cloudflare)

```bash
# Initialiser la Root CA
cfssl gencert -initca root-csr.json | cfssljson -bare root-ca

# Émettre un certificat intermédiaire
cfssl gencert -ca root-ca.pem -ca-key root-ca-key.pem \
  -config cfssl-config.json -profile intermediate \
  intermediate-csr.json | cfssljson -bare intermediate-ca

# Émettre un certificat serveur
cfssl gencert -ca intermediate-ca.pem -ca-key intermediate-ca-key.pem \
  -config cfssl-config.json -profile server \
  server-csr.json | cfssljson -bare server
```

### step-ca (Smallstep)

```bash
# Initialiser une PKI
step ca init --name "MyOrg Internal CA" \
  --dns ca.internal --address :9000 \
  --provisioner admin@myorg.com

# Émettre un certificat avec durée courte (24h)
step ca certificate api.internal api.crt api.key \
  --ca-url https://ca.internal:9000 --root root_ca.crt \
  --not-after 24h

# Renouvellement automatique (daemon)
step ca renew --daemon --exec "systemctl reload nginx" api.crt api.key
```

### Vault PKI Engine

```bash
# Activer le moteur PKI
vault secrets enable -path=pki pki
vault secrets tune -max-lease-ttl=87600h pki

# Générer la Root CA interne
vault write pki/root/generate/internal \
  common_name="MyOrg Root CA" ttl=87600h

# Configurer les URLs
vault write pki/config/urls \
  issuing_certificates="https://vault.internal:8200/v1/pki/ca" \
  crl_distribution_points="https://vault.internal:8200/v1/pki/crl"

# Créer un rôle (TTL court = rotation fréquente)
vault write pki/roles/web-server \
  allowed_domains="example.com" allow_subdomains=true \
  max_ttl=72h key_type=ec key_bits=256

# Émettre un certificat
vault write pki/issue/web-server \
  common_name="api.example.com" ttl=24h
```

Les **TTL courts** (24h–72h) dans Vault PKI forcent une rotation fréquente, réduisant drastiquement la fenêtre d'exposition en cas de compromission.

---

## Rotation de certificats et zero-downtime

### Stratégie overlapping

Pour éviter toute interruption, on émet le nouveau certificat **avant** l'expiration de l'ancien :

```
Ancien cert  |=============================|
Nouveau cert               |=============================|
                  ↑ Renouvellement à 2/3 de la durée
```

Pendant la période de chevauchement, les deux certificats sont valides. Le trafic bascule progressivement vers le nouveau (rolling restart Nginx, rotation Kubernetes…).

### cert-manager Kubernetes

```yaml
# Émission automatique via Vault PKI
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: api-tls
  namespace: production
spec:
  secretName: api-tls-secret
  duration: 24h
  renewBefore: 8h          # Renouvellement 8h avant expiration
  subject:
    organizations: ["MyOrg"]
  dnsNames:
    - api.example.com
  issuerRef:
    name: vault-issuer
    kind: ClusterIssuer
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: vault-issuer
spec:
  vault:
    server: https://vault.internal:8200
    path: pki/sign/web-server
    auth:
      kubernetes:
        mountPath: /v1/auth/kubernetes
        role: cert-manager
```

---

## OCSP vs CRL — révocation

### CRL (Certificate Revocation List)

Fichier signé par la CA listant les numéros de série révoqués. Problèmes : taille croissante, fréquence de mise à jour limitée (24h–7j), téléchargement complet à chaque vérification.

### OCSP (Online Certificate Status Protocol)

Requête HTTP en temps réel : le client envoie le numéro de série, le répondeur OCSP répond `good` / `revoked` / `unknown`.

**OCSP Stapling** — le serveur TLS inclut une réponse OCSP pré-signée dans le handshake TLS, évitant que le client contacte le répondeur OCSP directement :

```nginx
# Nginx avec OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/certs/chain.pem;
resolver 8.8.8.8 1.1.1.1 valid=300s;
```

```{admonition} OCSP Must-Staple
:class: tip
L'extension `TLS Feature: status_request` (OCSP Must-Staple) force le client à rejeter la connexion si le staple OCSP est absent. Idéal pour les certificats haute sécurité.
```

---

## HSM — Hardware Security Module

Un HSM est un composant matériel conçu pour générer, stocker et utiliser des clés cryptographiques **sans jamais les exposer** en clair à l'extérieur.

### Rôle dans une PKI

- Protège la clé privée de la Root CA (et des CA intermédiaires critiques).
- Opérations cryptographiques effectuées **dans** le HSM, la clé ne quitte jamais l'enveloppe sécurisée.
- Audit log immuable de toutes les opérations.

### FIPS 140-2 / 140-3

Standard NIST définissant les niveaux de sécurité :
- **Niveau 1** : algorithmes validés, logiciel pur acceptable.
- **Niveau 2** : preuves d'altération physique (scellés, revêtements).
- **Niveau 3** : résistance active à l'intrusion physique, effacement des clés si tentative.
- **Niveau 4** : protection complète contre toute intrusion physique ou environnementale.

### SoftHSM2 pour le développement

```bash
# Installation et initialisation
apt install softhsm2
softhsm2-util --init-token --slot 0 --label "DevPKI" \
  --pin 1234 --so-pin 5678

# Générer une clé RSA dans SoftHSM2
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
  --login --pin 1234 --keypairgen \
  --key-type RSA:4096 --id 01 --label "RootCA"

# Utiliser avec OpenSSL via engine
openssl req -engine pkcs11 -keyform engine \
  -key "pkcs11:token=DevPKI;object=RootCA;type=private" \
  -new -x509 -days 3650 -out root-ca.pem
```

---

## Certificate Transparency

CT (RFC 6962) impose que tout certificat DV/OV/EV émis par une CA publique soit enregistré dans des **logs CT** publics et vérifiables avant d'être accepté par Chrome/Firefox.

### Structure des logs

Les logs CT sont des **Merkle trees** append-only :
- Chaque feuille contient un certificat (ou pré-certificat).
- La racine du Merkle tree (STH — Signed Tree Head) est signée périodiquement par le log.
- Toute insertion produit un **SCT** (Signed Certificate Timestamp) que le serveur TLS présente lors du handshake.

### Audit et monitoring

```bash
# Rechercher tous les certificats émis pour un domaine
curl "https://crt.sh/?q=example.com&output=json" | jq '.[].name_value' | sort -u

# Surveiller les nouvelles émissions (defensive monitoring)
# Via certspotter, Facebook CT Monitor, etc.
```

```{admonition} Surveillance CT pour la sécurité
:class: warning
Un attaquant qui obtient frauduleusement un certificat pour votre domaine (via une CA compromise) sera **détecté** par CT monitoring. Configurez des alertes sur crt.sh ou un service équivalent pour votre domaine.
```

---

## Cellules Python

### Génération et parsing d'un certificat X.509 auto-signé

```{code-cell} python3
:tags: [hide-input]
import datetime
import ipaddress

from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import seaborn as sns
import pandas as pd
import numpy as np
```

```{code-cell} python3
# --- Génération d'un certificat X.509 auto-signé ---

# 1. Générer une paire de clés ECDSA P-256
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()

# 2. Construire le sujet / émetteur (auto-signé ⟹ identiques)
subject = issuer = x509.Name([
    x509.NameAttribute(NameOID.COUNTRY_NAME, "FR"),
    x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Alkimya Sécurité"),
    x509.NameAttribute(NameOID.COMMON_NAME, "api.alkimya.fr"),
])

now = datetime.datetime.utcnow()

# 3. Assembler le certificat avec extensions v3
cert = (
    x509.CertificateBuilder()
    .subject_name(subject)
    .issuer_name(issuer)
    .public_key(public_key)
    .serial_number(x509.random_serial_number())
    .not_valid_before(now)
    .not_valid_after(now + datetime.timedelta(days=90))
    # SAN : noms DNS + IP
    .add_extension(
        x509.SubjectAlternativeName([
            x509.DNSName("api.alkimya.fr"),
            x509.DNSName("*.alkimya.fr"),
            x509.IPAddress(ipaddress.IPv4Address("192.168.1.10")),
        ]),
        critical=False,
    )
    # Key Usage
    .add_extension(
        x509.KeyUsage(
            digital_signature=True, key_encipherment=False,
            content_commitment=False, key_agreement=True,
            key_cert_sign=False, crl_sign=False,
            data_encipherment=False, encipher_only=False, decipher_only=False,
        ),
        critical=True,
    )
    # Extended Key Usage
    .add_extension(
        x509.ExtendedKeyUsage([
            ExtendedKeyUsageOID.SERVER_AUTH,
            ExtendedKeyUsageOID.CLIENT_AUTH,
        ]),
        critical=False,
    )
    # Basic Constraints : pas une CA
    .add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
    .sign(private_key, hashes.SHA256())
)

# 4. Afficher les champs principaux
print("=== Certificat X.509 v3 ===")
print(f"Sujet       : {cert.subject.rfc4514_string()}")
print(f"Émetteur    : {cert.issuer.rfc4514_string()}")
print(f"Numéro série: {cert.serial_number}")
print(f"Algorithme  : {cert.signature_algorithm_oid.dotted_string}")
print(f"Valide du   : {cert.not_valid_before_utc}")
print(f"Valide au   : {cert.not_valid_after_utc}")
print(f"Courbe      : {cert.public_key().curve.name}")

# Extensions
san = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
print(f"\nSAN         : {[str(n) for n in san.value]}")

ku = cert.extensions.get_extension_for_class(x509.KeyUsage)
print(f"Key Usage   : digital_signature={ku.value.digital_signature}, key_agreement={ku.value.key_agreement}")

eku = cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
print(f"EKU         : {[o.dotted_string for o in eku.value]}")

bc = cert.extensions.get_extension_for_class(x509.BasicConstraints)
print(f"Basic Const.: cA={bc.value.ca}")
```

### Simulation d'une chaîne PKI complète

```{code-cell} python3
# --- Chaîne PKI : Root CA → CA intermédiaire → Certificat feuille ---

def create_ca(name: str, issuer_cert=None, issuer_key=None, path_length: int = 0):
    """Crée une CA (auto-signée si issuer_cert est None)."""
    key = ec.generate_private_key(ec.SECP256R1())
    subject = x509.Name([
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Alkimya PKI"),
        x509.NameAttribute(NameOID.COMMON_NAME, name),
    ])
    actual_issuer = issuer_cert.subject if issuer_cert else subject
    actual_issuer_key = issuer_key if issuer_key else key

    now = datetime.datetime.utcnow()
    cert = (
        x509.CertificateBuilder()
        .subject_name(subject)
        .issuer_name(actual_issuer)
        .public_key(key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(now)
        .not_valid_after(now + datetime.timedelta(days=3650))
        .add_extension(x509.BasicConstraints(ca=True, path_length=path_length), critical=True)
        .add_extension(
            x509.KeyUsage(
                digital_signature=True, key_cert_sign=True, crl_sign=True,
                key_encipherment=False, content_commitment=False,
                key_agreement=False, data_encipherment=False,
                encipher_only=False, decipher_only=False,
            ),
            critical=True,
        )
        .add_extension(x509.SubjectKeyIdentifier.from_public_key(key.public_key()), critical=False)
        .sign(actual_issuer_key, hashes.SHA256())
    )
    return cert, key


def create_leaf(cn: str, dns_names: list, ca_cert, ca_key):
    """Crée un certificat feuille signé par la CA fournie."""
    key = ec.generate_private_key(ec.SECP256R1())
    now = datetime.datetime.utcnow()
    cert = (
        x509.CertificateBuilder()
        .subject_name(x509.Name([
            x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Alkimya"),
            x509.NameAttribute(NameOID.COMMON_NAME, cn),
        ]))
        .issuer_name(ca_cert.subject)
        .public_key(key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(now)
        .not_valid_after(now + datetime.timedelta(days=90))
        .add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
        .add_extension(
            x509.SubjectAlternativeName([x509.DNSName(d) for d in dns_names]),
            critical=False,
        )
        .add_extension(
            x509.KeyUsage(
                digital_signature=True, key_encipherment=False,
                content_commitment=False, key_agreement=True,
                key_cert_sign=False, crl_sign=False,
                data_encipherment=False, encipher_only=False, decipher_only=False,
            ),
            critical=True,
        )
        .sign(ca_key, hashes.SHA256())
    )
    return cert, key


# Construire la chaîne
root_cert, root_key = create_ca("Alkimya Root CA", path_length=1)
inter_cert, inter_key = create_ca("Alkimya Intermediate CA", root_cert, root_key, path_length=0)
leaf_cert, leaf_key = create_leaf("api.alkimya.fr", ["api.alkimya.fr", "www.alkimya.fr"], inter_cert, inter_key)

# Vérification manuelle de la chaîne
def verify_chain(leaf, intermediate, root):
    """Vérification basique de la chaîne de certification."""
    results = {}

    # 1. Le certificat feuille est signé par l'intermédiaire
    try:
        intermediate.public_key().verify(
            leaf.signature, leaf.tbs_certificate_bytes,
            ec.ECDSA(hashes.SHA256())
        )
        results["feuille ← intermédiaire"] = "✓ Signature valide"
    except Exception as e:
        results["feuille ← intermédiaire"] = f"✗ {e}"

    # 2. L'intermédiaire est signé par la Root CA
    try:
        root.public_key().verify(
            intermediate.signature, intermediate.tbs_certificate_bytes,
            ec.ECDSA(hashes.SHA256())
        )
        results["intermédiaire ← root"] = "✓ Signature valide"
    except Exception as e:
        results["intermédiaire ← root"] = f"✗ {e}"

    # 3. La Root est auto-signée
    try:
        root.public_key().verify(
            root.signature, root.tbs_certificate_bytes,
            ec.ECDSA(hashes.SHA256())
        )
        results["root ← auto-signée"] = "✓ Signature valide"
    except Exception as e:
        results["root ← auto-signée"] = f"✗ {e}"

    # 4. Basic Constraints
    def get_bc(cert):
        return cert.extensions.get_extension_for_class(x509.BasicConstraints).value

    bc_root = get_bc(root)
    bc_inter = get_bc(intermediate)
    bc_leaf = get_bc(leaf)
    results["Root CA cA=True"]  = "✓" if bc_root.ca else "✗"
    results["Intermediate cA=True"] = "✓" if bc_inter.ca else "✗"
    results["Feuille cA=False"] = "✓" if not bc_leaf.ca else "✗"

    return results

results = verify_chain(leaf_cert, inter_cert, root_cert)
print("=== Vérification de la chaîne PKI ===")
for check, status in results.items():
    print(f"  {check:40s} {status}")

print("\n=== Résumé de la chaîne ===")
for label, cert in [("Root CA", root_cert), ("Intermediate CA", inter_cert), ("Feuille", leaf_cert)]:
    bc = cert.extensions.get_extension_for_class(x509.BasicConstraints).value
    validity = cert.not_valid_after_utc - cert.not_valid_before_utc
    print(f"  {label:18s} CN={cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value:35s} "
          f"cA={bc.ca}  durée={validity.days}j")
```

### Timeline de rotation de certificats avec fenêtres overlapping

```{code-cell} python3
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, ax = plt.subplots(figsize=(13, 5))

# Paramètres : durée 90 jours, renouvellement à 2/3 (jour 60)
duree = 90
renouvellement_fraction = 2 / 3
renewal_day = int(duree * renouvellement_fraction)  # Jour 60

certs = [
    {"label": "Cert v1", "start": 0,   "end": 90,  "y": 2},
    {"label": "Cert v2", "start": 60,  "end": 150, "y": 3},
    {"label": "Cert v3", "start": 120, "end": 210, "y": 4},
]

colors = sns.color_palette("muted", 3)

for i, c in enumerate(certs):
    width = c["end"] - c["start"]
    rect = FancyBboxPatch(
        (c["start"], c["y"] - 0.35), width, 0.70,
        boxstyle="round,pad=0.05",
        facecolor=colors[i], edgecolor="white", linewidth=1.5, alpha=0.85,
    )
    ax.add_patch(rect)
    mid = (c["start"] + c["end"]) / 2
    ax.text(mid, c["y"], c["label"], ha="center", va="center",
            fontsize=10, fontweight="bold", color="white")

# Zones d'overlap
overlap_zones = [(60, 90, "Overlap 1\n(v1 + v2)"), (120, 150, "Overlap 2\n(v2 + v3)")]
for x0, x1, lbl in overlap_zones:
    ax.axvspan(x0, x1, alpha=0.15, color="gold", zorder=0)
    ax.text((x0 + x1) / 2, 4.7, lbl, ha="center", va="center",
            fontsize=8, color="goldenrod", fontstyle="italic")

# Lignes de renouvellement
for day, label in [(60, "Renouvellement\nv1→v2"), (120, "Renouvellement\nv2→v3")]:
    ax.axvline(x=day, color="crimson", linestyle="--", linewidth=1.5)
    ax.text(day, 1.3, label, ha="center", va="top", fontsize=8, color="crimson")

# Ligne "aujourd'hui"
ax.axvline(x=75, color="steelblue", linestyle=":", linewidth=2)
ax.text(75, 4.85, "Aujourd'hui", ha="center", va="bottom", fontsize=8, color="steelblue")

ax.set_xlim(-5, 215)
ax.set_ylim(0.8, 5.3)
ax.set_xlabel("Jours depuis l'émission de Cert v1", fontsize=11)
ax.set_title(
    "Rotation zero-downtime de certificats TLS\n"
    "Renouvellement à 2/3 de la durée de validité (jour 60 sur 90)",
    fontsize=12, fontweight="bold",
)
ax.set_yticks([])
ax.xaxis.set_major_locator(matplotlib.ticker.MultipleLocator(30))
ax.grid(axis="x", linestyle="--", alpha=0.4)

# Légende
legend_handles = [
    mpatches.Patch(facecolor=colors[i], label=f"Cert v{i+1}") for i in range(3)
]
legend_handles.append(mpatches.Patch(facecolor="gold", alpha=0.4, label="Fenêtre d'overlap"))
ax.legend(handles=legend_handles, loc="lower right", fontsize=9)

plt.savefig("rotation_certs.png", dpi=120, bbox_inches="tight")
plt.show()
print("Timeline de rotation générée.")
```

---

## Résumé

1. **Structure X.509 v3** : le certificat est un objet ASN.1 signé par une CA. Les extensions (SAN, Key Usage, EKU, Basic Constraints) définissent précisément l'identité et les usages autorisés.

2. **Chaîne de certification** : la Root CA reste hors ligne. La vérification RFC 5280 remonte la chaîne en validant signatures, dates, contraintes de base et révocation.

3. **Let's Encrypt / ACME v2** : automatise l'émission DV. HTTP-01 pour les serveurs publics, DNS-01 pour les wildcards et les environnements internes.

4. **PKI interne** : CFSSL, step-ca et Vault PKI permettent d'émettre des certificats à TTL court (24h–72h), réduisant la surface d'exposition en cas de compromission.

5. **Rotation zero-downtime** : les fenêtres d'overlap garantissent qu'aucune interruption de service n'accompagne le renouvellement. cert-manager automatise ce processus dans Kubernetes.

6. **Révocation** : OCSP (temps réel) préféré à CRL (fichier volumeux). OCSP Stapling déplace la charge de vérification côté serveur, améliorant performance et confidentialité.

7. **HSM** : seule solution garantissant l'inviolabilité des clés privées des CA racines. SoftHSM2 réplique le comportement pour le développement et les tests.

8. **Certificate Transparency** : les logs CT forment un registre public append-only permettant de détecter toute émission frauduleuse de certificat pour un domaine surveillé.
