Aller au contenu

DPoP : attacher un jeton d'accès à une clé que le porteur doit prouver détenir

DPoP : attacher un jeton d’accès à une clé que le porteur doit prouver détenir

Section intitulée « DPoP : attacher un jeton d’accès à une clé que le porteur doit prouver détenir »

Un jeton au porteur fonctionne comme un billet de train : qui l’a, en profite. Toute la sécurité d’OAuth 2.0 repose donc sur le fait qu’il ne fuite pas, hypothèse régulièrement démentie par les faits. DPoP, normalisé par la RFC 9449 en septembre 2023, change la nature du jeton : il le lie à une paire de clés, et exige du client qu’il prouve, à chaque requête, détenir la clé privée. Le billet devient nominatif, et il faut présenter la pièce avec.

→ Lire l’explication complète de la marmotte

Du jeton au porteur au jeton contraint par l’expéditeur

Section intitulée « Du jeton au porteur au jeton contraint par l’expéditeur »

La RFC énonce son objectif sans détour : « empêcher des parties non autorisées ou illégitimes d’utiliser des jetons d’accès fuités ou volés, en liant un jeton à une clé publique à sa délivrance et en exigeant que le client prouve la possession de la clé privée correspondante quand il utilise le jeton ». Le vocabulaire employé mérite d’être retenu, parce qu’il dit la propriété obtenue : un jeton contraint par l’expéditeur, par opposition au jeton au porteur « qui peut être utilisé par n’importe quelle partie en sa possession ».

Pourquoi se donner cette peine, alors que des protections existent pour éviter la divulgation des jetons ? La RFC répond par une liste de cas où elles ont échoué, et cette liste est son meilleur argument : des vecteurs de fuite imprévus sont apparus « à cause de vulnérabilités et de problèmes d’implémentation dans d’autres couches de la pile protocolaire ou logicielle », et elle cite CRIME, BREACH, Heartbleed et le défaut d’analyseur de Cloudflare, avant d’ajouter qu’il y a eu « de nombreuses attaques publiées de vol de jeton sur les implémentations OAuth elles-mêmes ». DPoP est présenté pour ce qu’il est : « une défense en profondeur générale contre l’impact d’une fuite de jeton non anticipée ». Et aussitôt la mise en garde : « DPoP n’est pas un substitut à un transport sécurisé et DOIT toujours être utilisé en conjonction avec HTTPS ».

Le second motif est plus subtil et plus intéressant. Par nature, le client doit divulguer son jeton d’accès à chaque ressource qu’il appelle, et une de ces ressources peut être contrefaite, malveillante ou compromise, puis rejouer le jeton reçu contre une autre ressource. Restreindre l’audience du jeton, par la revendication aud, résoudrait le problème ; mais, écrit la RFC, « faire cela en pratique s’est avéré d’une lourdeur prohibitive pour de nombreux déploiements ». Contraindre l’expéditeur est « un mécanisme plus robuste et plus simple pour empêcher ce rejeu à un autre point d’accès ». C’est un aveu d’échec du modèle d’audience, et il faut l’entendre comme tel : la bonne solution théorique n’a pas été adoptée, donc on en déploie une autre. Le réglage des durées et des portées reste par ailleurs nécessaire, comme l’expose la durée de vie des jetons OAuth, et l’absence de cloisonnement par audience produit les confusions décrites dans l’attaque de confusion de scope OAuth.

Dernier point de cadrage, souvent mal compris : « le mécanisme présenté ici n’est pas une méthode d’authentification du client ». DPoP s’adresse d’abord aux clients publics, ceux qui n’ont pas de secret, et il n’identifie pas le client auprès du serveur d’autorisation. Il atteste seulement que le présentateur du jeton est celui qui détient la clé à laquelle le jeton est lié.

Diagramme de séquence en quatre temps entre un client qui détient une clé privée, un serveur d'autorisation et une ressource protégée. Au temps A, le client envoie sa demande de jeton accompagnée d'une preuve DPoP qui porte sa clé publique. Au temps B, le serveur d'autorisation renvoie un jeton lié à cette clé publique, de type DPoP et non Bearer. Au temps C, le client appelle la ressource protégée en présentant le jeton et une nouvelle preuve fabriquée pour cette requête ; un encadré détaille ce que la preuve couvre, à savoir la méthode HTTP, l'adresse visée sans la partie requête, un condensé du jeton d'accès, l'horodatage de création, un identifiant unique, et un nonce si le serveur en a fourni un. Au temps D, la ressource répond, ou refuse si la preuve ne correspond pas à la requête. Un bandeau final rappelle que ni le corps de la requête ni ses autres en-têtes ne sont couverts, donc que DPoP ne protège pas leur intégrité, et qu'un jeton volé seul devient inutilisable alors que du code hostile exécuté dans le client peut encore fabriquer des preuves.

Le fonctionnement se résume en quatre étapes, telles que la RFC les décrit. Le client demande son jeton en joignant une preuve DPoP dans un en-tête HTTP. Le serveur d’autorisation lie le jeton délivré à la clé publique revendiquée dans cette preuve, et si un jeton de rafraîchissement est délivré à un client public, il est lié de la même manière. Pour utiliser le jeton, le client fabrique une nouvelle preuve, propre à la requête qu’il émet ; la ressource vérifie que la clé publique à laquelle le jeton est lié correspond à celle de la preuve, et que le condensé du jeton figurant dans la preuve correspond au jeton présenté. Enfin, la ressource refuse de servir la requête si la signature échoue ou si les données de la preuve sont fausses, par exemple « si l’adresse visée ne correspond pas à la revendication d’adresse dans le jeton de preuve ».

Deux détails d’ingénierie méritent d’être relevés. La ressource a besoin de connaître la clé à laquelle le jeton est lié : cette information peut être encodée directement dans le jeton, s’il est structuré en JWT, ou obtenue par le point d’introspection. Et le choix de la clé appartient au client, qui peut en avoir une par serveur d’autorisation, voire une par session, ce qui ouvre la porte à des propriétés de non-corrélation qui ne sont pas l’objet de la RFC mais que rien n’interdit.

Ce que contient la preuve, et pourquoi chaque champ est là

Section intitulée « Ce que contient la preuve, et pourquoi chaque champ est là »

La preuve est un jeton JWT signé selon JWS, dont la mécanique est décrite dans JWS. Son en-tête contient trois paramètres obligatoires : typ valant dpop+jwt, qui type explicitement l’objet ; alg, un algorithme de signature asymétrique, et la RFC précise qu’il « NE DOIT PAS être none ni un identifiant d’algorithme symétrique » ; et jwk, la clé publique choisie par le client, qui « NE DOIT PAS contenir de clé privée », précision dont l’existence même en dit long sur ce que les auteurs ont vu passer.

La charge utile contient au minimum quatre revendications, et chacune ferme une porte. jti est un identifiant unique, que la RFC veut tiré sur au moins 96 bits de données pseudo-aléatoires ou sous forme d’identifiant universel de version 4, et dont la fonction est la détection du rejeu. htm porte la méthode HTTP de la requête, htu son adresse cible « sans les parties requête et fragment », de sorte qu’une preuve fabriquée pour un appel en lecture ne serve pas à un appel en écriture ni sur un autre point d’accès. iat est l’horodatage de création. Deux revendications s’ajoutent selon le contexte : ath, le condensé SHA-256 du jeton d’accès encodé en base64url, obligatoire dès que la preuve accompagne un jeton ; et nonce, obligatoire dès que le serveur en a fourni un.

La liaison repose donc sur deux condensés, et la RFC prend soin de justifier le choix de SHA-256 pour les deux : la liaison du jeton à la clé exige une résistance à la seconde préimage suffisante « pour rendre infaisable de trouver ou créer une autre clé produisant la même valeur de condensé », et la liaison de la preuve au jeton exige une résistance aux collisions suffisante pour identifier le jeton de manière fiable. Ces propriétés et leurs conditions de validité sont détaillées dans les propriétés des condensés cryptographiques.

La section de considérations de sécurité de la RFC 9449 est inhabituellement franche, et elle vaut mieux que la plupart des analyses disponibles sur le sujet.

Le rejeu de la preuve. Une preuve interceptée peut être rejouée au même point d’accès et avec la même méthode. La parade imposée est temporelle : les serveurs « NE DOIVENT accepter les preuves DPoP que pendant une durée limitée après leur création, de préférence seulement pendant une période relativement brève, de l’ordre de la seconde ou de la minute ». La parade complète consiste à mémoriser les jti déjà vus pour la fenêtre d’acceptation et à refuser les doublons, ce qui « fournit une protection très forte » mais, note la RFC, « peut ne pas toujours être réalisable en pratique, par exemple quand plusieurs serveurs derrière un même point d’accès n’ont pas d’état partagé ». Au passage, un conseil d’exploitation : un serveur qui suit les jti doit rejeter les valeurs anormalement grandes ou n’en stocker qu’un condensé, sous peine d’offrir une attaque par épuisement mémoire.

La pré-génération. Celle-ci est délicieuse, parce que l’attaquant qu’elle vise peut être l’utilisateur légitime. Quiconque contrôle le client peut fabriquer à l’avance des preuves pour un point d’accès donné, en choisissant un horodatage arbitrairement lointain. La RFC donne l’exemple : « un employé de banque pourrait pré-générer des preuves DPoP sur un ordinateur de la banque puis les copier sur une autre machine pour un usage futur, contournant ainsi les contrôles d’audit de la banque ». Et elle en tire la conclusion qui dégonfle la promesse : « quand les preuves DPoP peuvent être pré-générées et exfiltrées, tout ce qui est réellement prouvé dans les interactions du protocole est la possession d’une preuve DPoP, et non de la clé ». La parade est le nonce fourni par le serveur, imprévisible, qui ramène la démonstration à ce qu’elle prétend être. À défaut, la RFC recommande de ne pas délivrer de jetons DPoP à longue durée de vie.

Le repli sur l’absence de nonce. Une phrase, et elle est impérative : « un serveur NE DOIT PAS accepter de preuve DPoP sans la revendication nonce dès lors qu’un nonce DPoP a été fourni au client ». Une négociation qui accepte le mode faible après avoir annoncé le mode fort n’est pas une négociation, c’est une porte.

Le code hostile dans le contexte du client. C’est la limite la plus importante en pratique, et la RFC l’énonce sans ménagement : « si un adversaire est capable d’exécuter du code dans le contexte d’exécution du client, la sécurité de DPoP n’est plus garantie ». Elle détaille utilement ce que la clé non exportable gagne et ne gagne pas. Si la clé est stockée de façon non exportable, dans un module matériel ou logiciel, l’adversaire ne peut pas l’exfiltrer ; en revanche, « il peut créer de nouvelles preuves DPoP tant que le client est en ligne », et les utiliser depuis la machine de la victime ou la sienne. Il peut également pré-calculer des preuves horodatées dans le futur pour agir hors ligne, ou lancer un octroi d’autorisation silencieux dans un cadre pour associer ses propres clés aux jetons délivrés. La conclusion est sobre : « protéger les clients contre l’exécution de code non fiable est extrêmement important même si DPoP est utilisé », et la politique de sécurité de contenu est citée comme seconde ligne de défense contre l’injection de script. Le mécanisme de cette injection est l’objet de la page sur le XSS, et le cloisonnement sur lequel reposent ces défenses, de la politique de même origine.

L’intégrité de la requête. Dernière limite, et elle est assumée comme un choix : « DPoP n’assure pas l’intégrité de la charge utile ni des en-têtes des requêtes. La preuve ne contient que des revendications pour l’adresse et la méthode, mais pas le corps du message ni les en-têtes généraux. » La RFC qualifie cela de « décision de conception intentionnelle destinée à garder DPoP simple d’usage », tout en reconnaissant que cela le rend « potentiellement susceptible à des attaques de rejeu où un attaquant est capable de modifier le contenu et les en-têtes du message ». Dans la plupart des déploiements, ajoute-t-elle, l’intégrité et la confidentialité fournies par TLS suffisent. Traduction : DPoP ne remplace pas une signature de requête, il la complète ou l’ignore selon ce qu’on signe par ailleurs.

Liaison du code d’autorisation, et comparaison avec mTLS

Section intitulée « Liaison du code d’autorisation, et comparaison avec mTLS »

Une dernière pièce complète le dispositif, et elle résout un problème que PKCE ne couvre pas entièrement. Le code d’autorisation peut lui-même être lié à la clé DPoP, par un paramètre dpop_jkt transmis dans la requête d’autorisation. Cette liaison « empêche les attaques dans lesquelles l’attaquant capture le code d’autorisation et crée une preuve DPoP en utilisant une clé autre que celle détenue par le client, puis échange le code en utilisant cette preuve ». La RFC explique pourquoi cela compte, et le passage est instructif pour qui exploite un serveur d’autorisation : les codes se récupèrent « dans les endroits où les messages HTTP qui les contiennent sont journalisés », et même quand des efforts sont faits pour les rendre à usage unique, « en pratique, il existe souvent une fenêtre de temps pendant laquelle les attaquants peuvent les rejouer », notamment quand le serveur d’autorisation est un service répliqué dont certaines répliques n’ont pas encore l’information nécessaire. La façon la plus propre de transmettre ce paramètre est la requête d’autorisation poussée, traitée dans PAR et JAR ; le reste du flux, et PKCE en particulier, est décrit dans les pièges OAuth 2.1.

Reste à situer DPoP par rapport à l’autre manière de contraindre l’expéditeur. La RFC le fait elle-même, et sans flatterie : « DPoP a une nature de protection quelque peu différente des méthodes fondées sur TLS, telles qu’OAuth mutual TLS », parce que « les mécanismes fondés sur TLS peuvent exploiter une intégration étroite entre la couche TLS et la couche applicative pour obtenir une forte intégrité, authenticité et protection contre le rejeu des messages ». Autrement dit, la liaison au certificat client, décrite dans les jetons liés au certificat client, est techniquement supérieure ; DPoP existe pour les clients qui ne peuvent pas la déployer, les applications dans un navigateur et les applications mobiles au premier chef. Le choix entre les deux n’est donc pas un arbitrage de sécurité pure, mais de faisabilité, et c’est exactement ainsi que les profils de sécurité exposés dans FAPI 2.0 le traitent : ils exigent l’un ou l’autre, et interdisent le jeton au porteur nu.

Sans contrainte sur l’expéditeur, un jeton d’accès intercepté, extrait d’un journal, obtenu par une fuite de mémoire ou exfiltré par une injection de script s’utilise tel quel par n’importe qui, depuis n’importe où, jusqu’à son expiration. Sans liaison du jeton à une clé, une ressource contrefaite ou compromise rejoue contre d’autres ressources les jetons qu’elle a reçus, et la restriction d’audience qui l’empêcherait est trop lourde pour être déployée partout. Sans fenêtre d’acceptation courte ni suivi des identifiants uniques, une preuve interceptée se rejoue au même point d’accès. Sans nonce fourni par le serveur, un utilisateur ou un code hostile pré-génère des preuves horodatées dans le futur, et ce qui est prouvé n’est plus la détention de la clé mais la détention d’un fichier. Sans refus strict des preuves dépourvues de nonce après qu’un nonce a été fourni, toute cette protection se contourne en omettant un champ. Et sans liaison du code d’autorisation à la clé, un code capturé dans un journal reste échangeable contre un jeton lié à la clé de l’attaquant.

Quand une application accède à vos données chez un service, elle présente un jeton d’accès, c’est-à-dire une chaîne de caractères que le service a délivrée. Le problème est que ce jeton fonctionne comme un ticket au porteur : quiconque l’obtient peut s’en servir, et les occasions de fuite ne manquent pas, qu’il s’agisse d’un journal mal configuré, d’un défaut dans une bibliothèque ou d’un script hostile injecté dans une page.

DPoP change la nature du jeton. Au moment de le demander, l’application fabrique une paire de clés, en garde une secrète et montre l’autre. Le service note : « ce jeton n’est valable que présenté avec une signature de cette clé. » Ensuite, à chaque appel, l’application signe un petit message qui dit quelle méthode elle utilise, à quelle adresse elle s’adresse, à quelle heure, avec un numéro unique et une empreinte du jeton. Le service vérifie la signature et refuse si quelque chose ne correspond pas. Un jeton volé tout seul ne vaut donc plus rien.

Il faut connaître trois limites, et le document de référence les énonce lui-même. D’abord, cette signature ne couvre que la méthode et l’adresse, pas le contenu envoyé : ce n’est pas une signature de la requête entière, c’est une preuve qu’on détient la clé. Ensuite, si un programme hostile s’exécute à l’intérieur de l’application, il peut demander lui-même des signatures tant que l’application tourne, et même en fabriquer à l’avance en les datant dans le futur. La parade consiste à ce que le service fournisse une valeur imprévisible à inclure dans chaque signature, ce qui rend ces préparatifs inutiles. Enfin, DPoP ne remplace pas le chiffrement du transport, qui reste obligatoire.

Un dernier mot sur le choix de la méthode. Il existe une façon plus solide de rattacher un jeton à son détenteur, qui consiste à utiliser un certificat client au niveau du transport. Elle est préférable quand on peut la déployer, ce qui est rarement le cas pour une application dans un navigateur ou sur un téléphone. DPoP a été conçu pour ces cas-là, et il est largement meilleur que le jeton au porteur qu’il remplace.

À lire aussi

Pages voisines

: les deux pages se citent l’une l’autre.