Aller au contenu
ecdsa.com
← Tous les articles

Épinglage de certificats en 2026 : quand et comment

L’épinglage sacrifie la souplesse de la PKI publique pour se protéger d’une défaillance rare : une AC de confiance qui émet un certificat pour votre nom de domaine au profit de quelqu’un d’autre. Voici ce que coûte ce compromis en 2026, avec des empreintes calculées pour une chaîne réelle et testées dans OkHttp 5.5.0 et dans l’App Transport Security d’Apple sous macOS 27.

En bref. Les navigateurs ont abandonné l’épinglage par en-tête (HPKP) : Chrome l’a retiré dans sa version 72 (janvier 2019) et Firefox dans sa version 72 (janvier 2020), et Certificate Transparency couvre aujourd’hui l’essentiel de ce que HPKP devait détecter. L’épinglage subsiste dans les applications mobiles et les clients d’API qui dialoguent avec des serveurs que vous contrôlez. Si vous épinglez, épinglez le condensat SHA-256 d’une clé publique (SPKI), pas celui d’un certificat ; épinglez des clés d’AC que vous continuerez d’utiliser (un intermédiaire ou une racine) plutôt que la clé de votre certificat feuille, que la plupart des clients ACME remplacent à chaque renouvellement ; livrez toujours au moins une empreinte de secours, pour une autre AC ou pour une clé conservée hors ligne ; et utilisez le mécanisme de la plateforme : la configuration de sécurité réseau d’Android (Network Security Configuration), avec une expiration, ou NSPinnedDomains sur iOS. Google comme Apple déconseillent l’épinglage, à moins que vous ayez une raison concrète d’y recourir.

Ce qu’est une empreinte épinglée

La validation ordinaire d’un certificat accepte toute chaîne qui remonte à n’importe quelle racine du magasin de confiance. Une empreinte épinglée ajoute une condition : au moins une clé de la chaîne validée doit figurer sur votre liste. Cette liste contient des condensats du SubjectPublicKeyInfo (SPKI) : l’identifiant d’algorithme et la clé publique, encodés en DER. Hacher la clé plutôt que le certificat permet à un certificat renouvelé avec la même clé de continuer à correspondre, et une empreinte de clé d’AC correspond à tous les certificats que cette AC émet.

Les empreintes se calculent à partir des certificats avec OpenSSL. Pour la chaîne servie par letsencrypt.org le 20 octobre 2026 (eclipse-temurin:25.0.4.1_1-jdk-noble, OpenSSL 3.0.13) :

Empreinte SPKI de chaque certificat envoyé par un serveur
openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org -showcerts </dev/null 2>/dev/null \
  | awk '/-----BEGIN CERTIFICATE-----/{n++; f=sprintf("c%d.pem", n); p=1} p{print >> f} /-----END CERTIFICATE-----/{p=0; close(f)}'
pin() { openssl x509 -in "$1" -noout -pubkey | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64; }
for f in c*.pem; do printf '%s  %s\n' "$(pin "$f")" "$(openssl x509 -in "$f" -noout -subject | sed 's/^subject=//')"; done
Sortie
x8ltqh+zNPYG7RZS+IjFymndoy6YAWLPaSkptqebzfc=  CN = letsencrypt.org
s/tdAOmUzd8syaTuqfgGvFcn6DzA5Cmb+Vby1ST+U3Y=  C = US, O = Let's Encrypt, CN = YE2
sCkq5UWXjg+7mKu9lMhhYF5bGLsy7VI/UNW3tccdR7w=  C = US, O = ISRG, CN = Root YE
diGVwiVYbubAI3RW4hB9xU8e/CH2GnkuvVFZE8zmgzI=  C = US, O = Internet Security Research Group, CN = ISRG Root X2

Ces quatre candidates se comportent très différemment dans le temps, et choisir parmi elles représente l’essentiel du travail. Le générateur d’empreintes SPKI calcule les mêmes valeurs dans le navigateur et affiche des extraits de configuration.

Pourquoi les navigateurs ont abandonné HPKP

L’épinglage de clé publique HTTP (HTTP Public Key Pinning, RFC 7469, 2015) permettait à un site d’envoyer un en-tête Public-Key-Pins contenant des condensats de clés et une durée max-age ; pendant toute cette durée, le navigateur refusait ensuite toute chaîne qui ne contenait aucune de ces clés. Deux modes de défaillance l’ont condamné :

  • Des pannes auto-infligées. Une clé perdue, un changement d’AC ou une faute de frappe bloquait tout visiteur qui revenait sur le site jusqu’à l’échéance de max-age, sans aucun moyen de revenir en arrière depuis le serveur.
  • L’épinglage hostile. Un attaquant qui contrôlait brièvement un site (ou l’émission de ses certificats) pouvait envoyer des empreintes de clés que lui seul détenait, et transformer ainsi une brève compromission en un long déni de service.

La fiche de Chrome annonçant le retrait invoque une « adoption très faible » et les « risques de déni de service et d’épinglage hostile » (chromestatus) ; la prise en charge a été retirée dans Chrome 72 sur ordinateur et sur Android, et Firefox a suivi dans sa version 72. Les navigateurs embarquent encore une petite liste d’empreintes intégrée pour les domaines de leurs propres éditeurs, mais aucun site ne peut s’y ajouter.

Ce qui l’a remplacé relève de la détection plutôt que de la prévention. Depuis 2018, Chrome et les plateformes d’Apple n’acceptent les certificats TLS de confiance publique qu’accompagnés de Signed Certificate Timestamps issus des journaux Certificate Transparency, si bien que tout certificat émis pour votre nom de domaine est public et peut être surveillé ; les enregistrements CAA indiquent aux AC lesquelles d’entre elles peuvent émettre des certificats pour votre domaine. Ensemble, ces mécanismes rendent visibles en quelques heures les émissions indues que visait HPKP, sans risque de blocage.

Ce qu’il faut épingler

EmpreinteRésiste àÉchoue quand
Clé du certificat feuilleRenouvellements qui conservent la cléChaque renouvellement avec une nouvelle clé, ce qui est le comportement par défaut de certbot
Intermédiaire émetteur (YE2)Renouvellements du certificat feuille et changements de cléL’AC fait tourner ses intermédiaires ; Let’s Encrypt en a plusieurs en rotation, ainsi que des intermédiaires de secours à partir desquels elle « peut commencer à émettre … à tout moment, sans avertissement »
Racine (ISRG Root X2)Rotation des intermédiairesVous changez d’AC, ou l’AC est retirée des magasins de confiance
Clé que vous conservez hors ligneTout, tant qu’elle reste inutiliséeSeulement si vous la perdez

La ligne du certificat feuille mérite qu’on s’y arrête, car c’est le piège par défaut. certbot --help all (v5.8.0) indique que --reuse-key est désactivé par défaut et précise que « ne pas réutiliser les clés privées est le comportement par défaut de Certbot ». Une empreinte de certificat feuille casse donc au renouvellement suivant, et les renouvellements se rapprochent sans cesse : le CA/Browser Forum plafonne la durée des certificats TLS à 200 jours depuis mars 2026, à 100 jours à partir de mars 2027 et à 47 jours à partir de mars 2029, et la durée par défaut de Let’s Encrypt passe à 64 jours en février 2027.

La ligne de l’intermédiaire est la plus subtile. Le certificat de letsencrypt.org a été émis par YE2, mais Let’s Encrypt indique que YE1 et YE2 (ECDSA) ainsi que YR1 et YR2 (RSA) sont en rotation active, avec YE3 et YR3 en secours (chaînes de confiance). Un ensemble d’empreintes qui ne contient que YE2 échoue dès le premier certificat émis par YE1.

Une empreinte de racine résiste à tout cela et n’échoue que lorsque vous changez d’AC. Cela arrive plus souvent qu’on ne le pense : quand Chrome a cessé de faire confiance aux certificats émis par Entrust dont le plus ancien SCT était postérieur au 11 novembre 2024 (Google Security Blog), tous les clients d’Entrust ont dû passer à une autre AC en quelques mois. Une application qui épinglait les racines d’Entrust sans empreinte de secours aurait d’abord dû être mise à jour.

Le compromis habituel : épinglez la racine (ou les deux racines) de votre AC actuelle, plus la racine d’une seconde AC vers laquelle vous pourriez basculer, et, en option, une clé que vous avez générée et que vous conservez hors ligne pour un certificat d’urgence. Avec Let’s Encrypt comme AC actuelle et Google Trust Services comme solution de repli, d’après le paquet ca-certificates 20260816 de Debian :

Empreintes des racines (le même pipeline openssl sur /etc/ssl/certs/*.pem)
C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=  subject=C=US, O=Internet Security Research Group, CN=ISRG Root X1 notAfter=Jun  4 11:04:38 2035 GMT
diGVwiVYbubAI3RW4hB9xU8e/CH2GnkuvVFZE8zmgzI=  subject=C=US, O=Internet Security Research Group, CN=ISRG Root X2 notAfter=Sep 17 16:00:00 2040 GMT
mEflZT5enoR1FuXLgYYGqnVEoZvmf9c2bVBpiOjYQ0c=  subject=C=US, O=Google Trust Services LLC, CN=GTS Root R4 notAfter=Jun 22 00:00:00 2036 GMT
hxqRlPTu1bMS/0DITB1SSu0vd4u/8l8TjPgfaAp63Gc=  subject=C=US, O=Google Trust Services LLC, CN=GTS Root R1 notAfter=Jun 22 00:00:00 2036 GMT

L’empreinte d’ISRG Root X2 correspond au dernier certificat envoyé par letsencrypt.org (X2 avec signature croisée de X1), car il porte la même clé que le X2 auto-signé présent dans les magasins de confiance. Épingler une racine d’une autre AC que vous n’utilisez pas encore ne coûte rien et constitue votre porte de sortie.

Android : Network Security Configuration

Android 7.0 et les versions ultérieures lisent les empreintes dans res/xml/network_security_config.xml, référencé par android:networkSecurityConfig dans le manifeste (documentation) :

res/xml/network_security_config.xml : racines de l’AC actuelle plus une seconde AC, avec une expiration
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">api.example.com</domain>
        <pin-set expiration="2027-10-01">
            <!-- ISRG Root X2 and X1: the current CA, ECDSA and RSA chains -->
            <pin digest="SHA-256">diGVwiVYbubAI3RW4hB9xU8e/CH2GnkuvVFZE8zmgzI=</pin>
            <pin digest="SHA-256">C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=</pin>
            <!-- GTS Root R4 and R1: the CA we would switch to -->
            <pin digest="SHA-256">mEflZT5enoR1FuXLgYYGqnVEoZvmf9c2bVBpiOjYQ0c=</pin>
            <pin digest="SHA-256">hxqRlPTu1bMS/0DITB1SSu0vd4u/8l8TjPgfaAp63Gc=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

Les règles qui comptent, d’après la documentation :

  • Une chaîne est acceptée lorsqu’elle « contient au moins une des clés publiques épinglées » ; le seul algorithme de condensat accepté est SHA-256.
  • Après la date expiration (yyyy-MM-dd), l’épinglage n’est plus appliqué. Cela permet aux anciennes installations de continuer à fonctionner une fois que vous les avez oubliées ; la documentation avertit aussi que cela « peut permettre à des attaquants de contourner vos certificats épinglés » sur les appareils dont l’application n’est plus à jour. Choisissez une date située environ un cycle de publication après celle de votre prochaine mise à jour des empreintes.
  • Les ancres de confiance déclarées dans <debug-overrides> contournent les empreintes lorsque l’application est débogable, ce qui permet d’inspecter le trafic en développement ; overridePins="true" sur un élément <certificates> fait de même dans les builds de production et ne devrait pas être livré.
  • Android 16 a ajouté des vérifications Certificate Transparency dans ce même fichier (<certificateTransparency enabled="true"/>), et à partir d’Android 17, elles sont activées par défaut pour les applications qui ciblent cette version, sauf pour les connexions qui utilisent des ancres de confiance personnalisées. Pour beaucoup d’applications, c’est précisément la protection qu’elles attendaient de l’épinglage.

Les recommandations de Google sont sans détour : l’épinglage « n’est pas recommandé pour les applications Android », et si vous y recourez, prévoyez « plusieurs empreintes de secours, dont au moins une clé entièrement sous votre contrôle, et une période d’expiration suffisamment courte » (sécurité avec les protocoles réseau).

OkHttp

Le CertificatePinner d’OkHttp fonctionne sur Android et sur la JVM, où la Network Security Configuration n’existe pas. Le test épingle letsencrypt.org sur ISRG Root X2, avec X1 en secours, puis sur une clé qu’aucune AC n’a certifiée, ce que voit une application lorsque le serveur passe à une AC non épinglée :

Pin.java (OkHttp 5.5.0, Okio 3.18.1, Kotlin stdlib 2.1.21, Java 25)
// OkHttp CertificatePinner against letsencrypt.org: a correct CA-key pin, a backup pin, and a wrong pin.
import java.io.IOException;
import okhttp3.CertificatePinner;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class Pin {
    static void get(String label, CertificatePinner pinner) {
        OkHttpClient client = new OkHttpClient.Builder().certificatePinner(pinner).build();
        Request request = new Request.Builder().url("https://letsencrypt.org/robots.txt").build();
        try (Response response = client.newCall(request).execute()) {
            System.out.println(label + ": HTTP " + response.code());
        } catch (IOException e) {
            System.out.println(label + ": " + e.getClass().getName() + ": " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        // ISRG Root X2 key, plus a backup pin for ISRG Root X1 (RSA): either satisfies the pin set.
        get("pinned to ISRG Root X2 (+ X1 backup)", new CertificatePinner.Builder()
            .add("letsencrypt.org", "sha256/" + args[0])
            .add("letsencrypt.org", "sha256/" + args[1])
            .build());
        // A key that is not in the chain: what a client sees after the site moves to another CA.
        get("pinned to an unrelated key", new CertificatePinner.Builder()
            .add("letsencrypt.org", "sha256/" + args[2])
            .build());
    }
}
java -Djava.net.preferIPv4Stack=true -cp okhttp-jvm-5.5.0.jar:okio-jvm-3.18.1.jar:kotlin-stdlib-2.1.21.jar Pin.java …
pinned to ISRG Root X2 (+ X1 backup): HTTP 200
pinned to an unrelated key: javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
  Peer certificate chain:
    sha256/x8ltqh+zNPYG7RZS+IjFymndoy6YAWLPaSkptqebzfc=: CN=letsencrypt.org
    sha256/s/tdAOmUzd8syaTuqfgGvFcn6DzA5Cmb+Vby1ST+U3Y=: CN=YE2, O=Let's Encrypt, C=US
    sha256/sCkq5UWXjg+7mKu9lMhhYF5bGLsy7VI/UNW3tccdR7w=: CN=Root YE, O=ISRG, C=US
    sha256/diGVwiVYbubAI3RW4hB9xU8e/CH2GnkuvVFZE8zmgzI=: CN=ISRG Root X2, O=Internet Security Research Group, C=US
  Pinned certificates for letsencrypt.org:
    sha256/VIJvW/e8yv/LvjcOADWb4C9aNclxEiQeW0tr4SvazsA=

Le message d’échec liste les empreintes de la chaîne validée par OkHttp, jusqu’à l’ancre de confiance, et la documentation d’OkHttp elle-même propose exactement cette méthode de mise en place : commencez avec une empreinte erronée sur un réseau de confiance, puis copiez les valeurs attendues depuis l’exception. Les empreintes correspondent à la sortie d’OpenSSL ci-dessus. Ce même commentaire de documentation contient un avertissement qu’il vaut la peine de répéter à la personne qui approuve le changement : « N’utilisez pas l’épinglage de certificat sans la bénédiction de l’administrateur TLS de votre serveur ! » De plus, CertificatePinner ne s’exécute qu’après la validation normale : il ne peut donc pas rendre acceptable un certificat auto-signé.

Un piège de débogage dans lequel nous sommes tombés : le conteneur n’avait pas de route IPv6, et sans -Djava.net.preferIPv4Stack=true, le second appel signalait java.net.SocketException: Network is unreachable. OkHttp 5 essaie les adresses en parallèle (« fast fallback »), et l’échec d’épinglage sur l’adresse IPv4 s’est retrouvé sous la forme d’une exception supprimée, derrière l’erreur de connexion IPv6 ; avec fastFallback(false), l’échec d’épinglage devenait l’exception principale. Quand l’épinglage casse en production, journalisez aussi getSuppressed().

iOS et macOS : NSPinnedDomains

Depuis iOS 14 et macOS 11, App Transport Security épingle les clés déclarées dans Info.plist. L’article d’Apple « Identity Pinning » décrit deux listes par domaine : NSPinnedCAIdentities (la clé doit figurer dans un certificat intermédiaire ou racine de la chaîne) et NSPinnedLeafIdentities (la clé propre du serveur), chaque entrée étant une valeur SPKI-SHA256-BASE64 calculée avec le même pipeline OpenSSL :

Info.plist (extrait) : épingler letsencrypt.org sur la clé d’ISRG Root X2
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSPinnedDomains</key>
    <dict>
        <key>letsencrypt.org</key>
        <dict>
            <key>NSPinnedCAIdentities</key>
            <array>
                <dict>
                    <key>SPKI-SHA256-BASE64</key>
                    <string>diGVwiVYbubAI3RW4hB9xU8e/CH2GnkuvVFZE8zmgzI=</string>
                </dict>
            </array>
        </dict>
    </dict>
</dict>

Nous avons vérifié le comportement sur notre machine de test (macOS 27.0.1, Apple M4 Max, Swift 6.4) : un outil en ligne de commande qui récupère https://letsencrypt.org/robots.txt avec URLSession, compilé trois fois avec swiftc main.swift -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker Info.plist : sans empreinte, avec l’empreinte X2 ci-dessus, et avec la clé sans rapport issue du test OkHttp :

Sortie des trois builds
HTTP 200
HTTP 200
error: NSURLErrorDomain -1200: A TLS error caused the secure connection to fail.

L’application reçoit l’erreur générique NSURLErrorSecureConnectionFailed (-1200), et non un code propre à l’épinglage ; notez donc avec quel ensemble d’empreintes chaque version de l’application a été livrée, sinon une non-correspondance d’empreinte après un changement côté serveur ressemble, dans les rapports de plantage, à n’importe quel autre échec TLS. NSIncludesSubdomains étend une empreinte à un seul niveau de sous-domaines (math.example.org, mais pas advanced.math.example.org). Le conseil d’Apple rejoint celui de Google : « Dans la plupart des cas, l’épinglage n’est pas nécessaire et devrait être évité », et si vous épinglez, épinglez des clés d’AC, et plus d’une.

Faire tourner les empreintes sans bloquer personne

Un ensemble d’empreintes est une promesse sur l’avenir, et l’application qui le contient peut rester un an sans mise à jour. Une rotation qui fonctionne :

  1. Livrez la nouvelle empreinte avant d’en avoir besoin. Ajoutez la racine de la prochaine AC ou la prochaine clé à l’ensemble d’empreintes dans une version, puis attendez que les versions qui ne la contiennent pas ne représentent plus qu’une part d’utilisateurs que vous pouvez vous permettre de bloquer.
  2. Seulement ensuite, modifiez le serveur. Changez d’AC ou de clé ; les anciennes empreintes correspondent toujours sur les anciennes versions de l’application, tant qu’elles incluent au moins une clé de la nouvelle chaîne.
  3. Retirez les empreintes obsolètes dans une version ultérieure.
  4. Gardez une issue de secours. Une clé hors ligne, épinglée mais jamais déployée, vous permet en cas d’urgence d’obtenir auprès de n’importe quelle AC un certificat pour cette clé. Certaines applications récupèrent aussi les mises à jour d’empreintes depuis une configuration distante signée ; ce fichier a alors besoin de sa propre clé de signature, conservée en dehors de l’infrastructure TLS.
  5. Définissez des dates d’expiration sur Android et traitez-les comme l’échéance de la prochaine rotation, pas comme une formalité.
  6. Testez l’échec. Faites passer l’application par un proxy dont le certificat provient de votre magasin de confiance mais n’appartient pas à l’ensemble d’empreintes, et vérifiez qu’elle échoue en bloquant la connexion (fail closed) et qu’elle affiche un message utile.

Les proxys d’inspection TLS d’entreprise cassent par conception les applications avec épinglage : leurs certificats remontent à une racine d’entreprise qui n’est pas épinglée. Sur Android, les applications qui ciblent le niveau d’API 24 ou supérieur ne font pas confiance aux AC installées par l’utilisateur, sauf si la configuration les ajoute, et les empreintes s’appliquent toujours à ces AC, sauf si overridePins est défini.

Erreurs courantes

  • N’épingler que le certificat feuille avec un client ACME qui change de clé. Le renouvellement suivant se traduit par une panne.
  • Épingler un seul intermédiaire d’une AC qui en fait tourner plusieurs. Cela fonctionne jusqu’à ce que l’AC en choisisse un autre.
  • Aucune empreinte de secours, ou une empreinte de secours de la même AC. Quand cette AC est retirée des magasins de confiance, les deux empreintes échouent en même temps.
  • Épingler le condensat du certificat au lieu du SPKI. Chaque renouvellement casse l’empreinte, même avec la même clé.
  • Des gestionnaires de confiance (trust managers) écrits à la main. Remplacer la validation de la plateforme pour « ajouter l’épinglage » supprime souvent par mégarde la vérification du nom d’hôte ou de la chaîne. Utilisez la Network Security Configuration, CertificatePinner ou NSPinnedDomains, qui épinglent en plus de la validation normale.
  • Compter sur les empreintes pour se protéger d’un appareil compromis. Sur un appareil rooté ou jailbreaké, l’utilisateur (ou un logiciel malveillant) peut retirer l’épinglage en modifiant l’application. L’épinglage protège les utilisateurs contre le réseau, pas votre API contre vos utilisateurs.

Sources