Québec, Canada

403-1381 1re Avenue

+1 581.849.27.96

bdgouthiere@gmail.com

Clés eMudhra compromises : Go 1.27.3 ne protège que les binaires recompilés

Ou : Pourquoi mettre Go à jour ne répare aucun programme Go déjà compilé

Le 19 octobre 2026, Go 1.27.3 et Go 1.26.10 ont ajouté au paquet crypto/x509 une liste de refus codée en dur : toute chaîne de certificats qui passe par l’un de huit intermédiaires de l’autorité de certification indienne eMudhra est désormais rejetée. Les clés privées de ces intermédiaires devaient être des nombres d’une centaine de chiffres tirés au hasard. Elles tenaient sur un octet, et n’importe qui pouvait les retrouver en un instant1. La faille porte le numéro CVE-2026-111943. Son correctif a une particularité qui change tout : il vit dans la bibliothèque standard, donc dans chaque binaire, figé au moment de la compilation. Installer Go 1.27.3 sur ta machine ne protège aucun programme déjà déployé.

Firefox et Chrome ont réglé l’affaire en quelques heures, par une liste de blocage envoyée à distance. Un binaire Go, lui, n’a que la liste de son compilateur.

Certificats eMudhra refusés par Go : le correctif est dans le binaire, pas sur la machine

Huit clés tirées dans un chapeau de 256 numéros

Le 17 octobre à 20 h 06 UTC, Mozilla reçoit un signalement transmis par le programme de racines de Chrome : un chercheur prouve qu’il détient les clés privées de huit intermédiaires d’eMudhra, six pour le TLS et deux pour la signature de code. Moins de deux heures plus tard, Mozilla retrouve les clés de son côté, sans connaître la méthode. Ces huit clés P-384, générées le 30 avril 2025, étaient des entiers inférieurs à 256. Une clé P-384 normale compte environ 116 chiffres. Celles-ci en avaient trois au plus.

C’est un coffre de banque dont la combinaison serait le numéro de l’étage. Avec un intermédiaire et sa clé, on signe un certificat pour n’importe quel site, et le client le croit. Un participant du fil Mozilla a compté plus de 107 000 certificats signés avec six de ces clés et inscrits les 20 et 21 octobre dans les journaux publics de transparence, presque tous des preuves de concept au nom de google.com2.

Firefox les bloquait le soir même par OneCRL, Chrome par son équivalent, CRLSet. eMudhra les a révoqués le 18 au matin, Sectigo, qui en avait signé des copies en croix, le 193. Go a publié ses deux versions le 19 octobre. L’annonce, envoyée à 2 h 11 UTC le 20, d’où les deux dates qui circulent, ne nomme personne et invoque des « specific security concerns around their operation »4. Une modification fusionnée le 25 octobre dans la branche principale explique ce flou : il ne fallait pas « tip our hand on this trivially exploitable vulnerability ». Elle ajoute aussi, en commentaire, le nom de chaque intermédiaire5.

Ce que Go a ajouté, ligne par ligne

J’ai comparé les sources de Go 1.27.2 et de Go 1.27.3 : hors numéro de version, seuls trois fichiers diffèrent, tous dans crypto/x509. Le cœur tient dans verify.go, ici avec deux empreintes sur huit6 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Empreintes SHA-256 des clés publiques refusées (2 sur 8)
var distrustedSPKIHashes = map[string]struct{}{
	"ebed6475c2c2d80fcb199f3fe8dd6e6b3ce60b77eb15d4e42c3dec0c6f5b5105": {},
	"49dd353dc2b919a3770645117d0e60c16115e7d1bc259eefe734b00ea434fe5f": {},
	// ... six autres
}

// Un seul certificat de la chaîne, à n'importe quelle place, suffit
func containsDistrustedSPKIHash(chain []*Certificate) bool {
	for _, c := range chain {
		h := sha256.Sum256(c.RawSubjectPublicKeyInfo)
		if _, ok := distrustedSPKIHashes[hex.EncodeToString(h[:])]; ok {
			return true
		}
	}
	return false
}

Verify construit toutes les chaînes possibles, puis écarte celles qui contiennent l’une de ces clés. Si aucune ne survit, l’appel échoue avec x509: no valid chains built, précédé de tls: failed to verify certificate: dans une connexion TLS. Le même contrôle est ajouté là où Go délègue la vérification au système, sur macOS et Windows.

Le filtre compare des clés publiques, pas des certificats. Les copies signées en croix par Sectigo ont un autre émetteur et une autre signature, donc une autre empreinte de certificat, mais la même clé. J’ai téléchargé sur crt.sh les huit originaux et six de ces copies : quatorze certificats, huit empreintes de clé, toutes présentes dans la liste de Go.

Et rien ne l’éteint. Pas de variable GODEBUG, pas d’option dans VerifyOptions : le filtre s’applique même quand le programme fournit son propre magasin de racines.

La mise à jour qui ne met rien à jour

L’éditeur de liens de Go produit par défaut des binaires statiques, qui embarquent tout leur environnement d’exécution7. Même quand cgo lie le binaire à la libc du système, la bibliothèque standard, crypto/x509 compris, reste copiée dans l’exécutable. Pour le vérifier, j’ai écrit un programme de moins de quarante lignes qui vérifie un intermédiaire eMudhra face à sa seule racine, compilé deux fois sur Linux amd64, avec Go 1.27.2 puis Go 1.27.3 :

1
2
3
4
$ ./verifie-1.27.2 racine-emsign.crt em-dv-tls-g3b.crt
go1.27.2 : chaîne acceptée, 2 certificats
$ ./verifie-1.27.3 racine-emsign.crt em-dv-tls-g3b.crt
go1.27.3 : x509: no valid chains built

Même source, mêmes certificats, deux réponses. La copie signée par Sectigo, avec sa racine AAA Certificate Services, donne le même résultat. Et la liste se voit dans l’exécutable :

1
2
3
4
5
$ go tool nm verifie-1.27.3 | grep -i distrusted
  520ee0 T crypto/x509.containsDistrustedSPKIHash
  73f300 B crypto/x509.distrustedSPKIHashes
$ go tool nm verifie-1.27.2 | grep -ci distrusted
0

Le correctif voyage avec le compilateur, pas avec le système. Une image de construction épinglée sur une version antérieure, un binaire téléchargé tout fait, un service pas recompilé depuis août : tous vérifient les certificats avec le crypto/x509 de leur compilation. Le paquet ca-certificates peut changer sous leurs pieds, pas la liste de refus.

DevOps Dave : J’ai passé toutes les machines de compilation en Go 1.27.3. On est couverts.

Security Sarah : Et l’API de facturation, elle date de quand ?

DevOps Dave : Août. Elle n’a pas bougé, elle tourne parfaitement.

Security Sarah : Donc elle vérifie ses certificats avec le Go d’août.

DevOps Dave : Impossible, je l’ai désinstallé, le Go d’août.

Security Sarah : De ta machine, oui. Pas de l’intérieur de ton binaire.

Retrouver les binaires à recompiler

go version lit la version du compilateur inscrite dans un binaire ; avec un dossier, il le parcourt récursivement, et -m ajoute les modules et les réglages de compilation8 :

1
2
3
4
5
$ go version verifie-1.27.2
verifie-1.27.2: go1.27.2
$ go version -m verifie-1.27.2 | grep -E 'CGO_ENABLED|GOOS'
	build	CGO_ENABLED=1
	build	GOOS=linux

Sur ma machine, Go est en 1.27.3, mais les onze outils installés par go install ont été compilés entre go1.23.3 et go1.26.5. Aucun ne contient la liste.

govulncheck -mode=binary va plus loin : il croise les symboles du binaire avec la base de vulnérabilités de Go, qui enregistre la faille depuis le 21 octobre sous le numéro GO-2026-68099. Sortie abrégée, avec govulncheck v1.9.0 et la base du 2 novembre :

1
2
3
4
5
6
7
8
9
$ govulncheck -mode=binary ./verifie-1.27.2
Vulnerability #1: GO-2026-6809
    Distrust compromised intermediate certificates in crypto/x509
    Found in: crypto/x509@go1.27.2
    Fixed in: crypto/x509@go1.27.3
    Vulnerable symbols found:
      #1: x509.Certificate.Verify
$ govulncheck -mode=binary ./verifie-1.27.3
No vulnerabilities found.

Sur trois de ces outils, il trouve entre 7 et 26 vulnérabilités chacun, GO-2026-6809 comprise. Le mode binaire ne voit que des symboles, pas le graphe d’appels, et peut signaler du code présent mais jamais appelé10. Ici, le doute ne dure pas : tout client TLS qui vérifie ses certificats passe par Certificate.Verify. Même Caddy et son HTTPS automatique n’y échappe pas : c’est un binaire Go, et ses échanges avec l’autorité ACME passent par le crypto/x509 de sa compilation.

Reste la branche 1.25. Go ne maintient que ses deux dernières versions majeures, et Go 1.27 est sorti le 19 août 202611. Un binaire compilé en 1.25 n’aura pas de version corrigée : il faut changer de version majeure. Au 3 novembre 2026, Go 1.27.3 et 1.26.10 restent les dernières versions publiées.

Pourquoi une liste dans le code

La base de vulnérabilités de Go s’en explique : certains magasins de racines du système ne se mettent pas à jour sans intervention. Sous Linux, Go lit un fichier ou un dossier de certificats de confiance au format PEM, et ce format n’a pas de case « refusé » : le code ignore tout bloc qui n’est pas un simple CERTIFICATE. Pour écarter un intermédiaire par ce moyen, il faut retirer la racine qui l’a signé, avec tout ce qu’elle a signé d’honnête. Quant à la révocation, la documentation de Verify prévient : « this function doesn’t do any revocation checking »12.

Un serveur bien réglé renouvelle seul ses certificats Let’s Encrypt à 45 jours ; sa liste de refus, elle, ne bouge qu’à la compilation. Go 1.27 venait d’ajouter la signature post-quantique ML-DSA, et sa troisième révision mineure rappelle une évidence : la cryptographie la plus soignée ne protège rien quand la clé privée tient sur un octet.

Firefox a mis moins de trois heures à refuser ces intermédiaires. Go les a inscrits dans son code deux jours après le signalement. Le délai qui compte maintenant, c’est celui qui sépare ce 19 octobre de la prochaine compilation de chacun de tes binaires.



  1. Le commentaire 5 du bogue Mozilla 2083449, posté le 18 octobre 2026 par l’équipe de Mozilla : les huit clés sont des entiers inférieurs à 256, qu’on retrouve en générant simplement les 256 premières clés P-384. Le même commentaire signale que des agents d’IA locaux, à qui l’exercice était présenté comme un jeu de capture du drapeau, ont retrouvé une clé du premier coup. ↩︎

  2. Commentaire 44 du même bogue, posté le 25 octobre par un participant extérieur à Mozilla, qui donne ces chiffres. Il ajoute que presque aucun de ces certificats ne porte d’adresse de révocation : un client qui ne vérifie la révocation que pour le certificat final n’a rien à vérifier. ↩︎

  3. Le premier commentaire du bogue donne la chronologie : signalement reçu à 20 h 06 UTC le 17 octobre, blocage par OneCRL terminé à 22 h 59. La suite du fil date la révocation par eMudhra du 18 octobre à 9 h 38 UTC (commentaire 1) et celle des copies de Sectigo du 19 (commentaire 13). Dans le commentaire 6, Sectigo précise que la racine de ces copies, AAA Certificate Services, a quitté depuis un certain temps les magasins de Chrome et de Mozilla, mais reste reconnue par Apple et Microsoft. Le blocage par CRLSet apparaît dans la chronologie du chercheur (commentaire 2), et le programme de racines de Chrome le confirme dans le commentaire 50. ↩︎

  4. L’annonce de Go 1.27.3 et 1.26.10 sur la liste golang-announce. L’historique officiel des versions date les deux versions du 19 octobre 2026 ; le 20 est la date de l’envoi en temps universel, qui tombait encore le 19 en Amérique du Nord. ↩︎

  5. La modification 852085, fusionnée le 25 octobre 2026 dans la branche principale. Les commentaires qu’elle ajoute nomment six intermédiaires TLS et deux de signature de code, chacun en série G3B (eMudhra BV, Pays-Bas) ou G3C (eMudhra Inc, États-Unis). Au 3 novembre 2026, ces commentaires ne sont dans aucune version publiée : le code de Go 1.27.3 ne contient que les empreintes. ↩︎

  6. La modification 851047, qui porte le correctif sur la branche principale ; son message précise que les chaînes sont identifiées par l’empreinte SHA-256 du champ SubjectPublicKeyInfo, qui contient la clé publique et son algorithme. ↩︎

  7. La FAQ de Go, à la question sur la taille des binaires : tout binaire Go embarque son environnement d’exécution. Mon programme de test, compilé avec cgo, n’est lié dynamiquement qu’à la libc ; avec CGO_ENABLED=0, il est entièrement statique. ↩︎

  8. La documentation de go version, dans l’aide de la commande go. ↩︎

  9. L’entrée GO-2026-6809 de la base de vulnérabilités de Go : versions touchées, toutes celles avant 1.26.10, et les 1.27 avant 1.27.3 ; symbole touché, Certificate.Verify. ↩︎

  10. La documentation de govulncheck décrit le mode binaire : analyse des symboles, pas de pile d’appels, faux positifs possibles pour du code présent mais inaccessible. La commande sort avec un code d’erreur quand elle trouve une vulnérabilité, ce qui suffit pour bloquer une chaîne d’intégration continue. ↩︎

  11. La politique de maintenance de Go : chaque version majeure est maintenue jusqu’à la sortie de la deuxième version qui la suit. La même page date Go 1.27.0 du 19 août 2026. ↩︎

  12. La documentation de Certificate.Verify. Le filtrage des blocs PEM est dans AppendCertsFromPEM, qui saute tout bloc d’un autre type que CERTIFICATE ou muni d’en-têtes. ↩︎