Je suis Charlie

Autres trucs

Accueil

Seulement les RFC

Seulement les fiches de lecture

Mon livre « Cyberstructure »

Ève

RFC 7616: HTTP Digest Access Authentication

Date de publication du RFC : Septembre 2015
Auteur(s) du RFC : R. Shekh-Yusef (Avaya), D. Ahrens (Independent), S. Bremer (Netzkonform)
Chemin des normes
Réalisé dans le cadre du groupe de travail IETF httpauth
Première rédaction de cet article le 8 octobre 2015


Dans les techniques d'authentification normalisées pour un client HTTP, la plus connue et la plus simple est le mécanisme Basic du RFC 7617. Mais elle a plusieurs failles de sécurité, comme le transmission explicite du mot de passe (et qui se fait en clair si on n'utilise pas HTTPS). D'où cette technique alternative, que je crois peu répandue, car plus complexe (et pas forcément plus sûre) : le mécanisme Digest. Dans ce cas, le serveur HTTP envoie un défi au client, défi auquel le client répondra sans divulguer au serveur aucun secret.

Avec les RFC 7615 et RFC 7617, ce nouveau RFC remplace le très ancien RFC 2617 (qui normalisait tous les mécanismes d'authentification de HTTP). Désormais, l'authentification HTTP comporte un cadre général (le RFC 7235) et un RFC spécifique par mécanisme d'authentification. Celui de notre nouveau RFC, le Digest, a assez peu changé depuis le RFC 2617.

L'essentiel du RFC actuel est dans la section 3. Le client s'authentifie avec un nom et un mot de passe mais le mot de passe n'est pas envoyé tel quel au serveur (quant au nom, ça dépend). Comme son nom l'indique (digest = condensation), les données sont condensées avant d'être envoyées au serveur, empêchant le serveur (ou un indiscret qui écoute) d'accéder à la valeur initiale.

Lorsque le client HTTP tente d'accéder à une ressource protégée, le serveur lui répond avec un code HTTP 401 et un en-tête WWW-Authenticate:, indiquant le mécanisme Digest. Cet en-tête contient le royaume (realm, un identificateur caractérisant le groupe d'utilisateurs attendus, cf. RFC 7235, section 2.2), le domaine (une liste d'URI pour qui cette protection s'applique), un numnique, une chaîne de caractères opaque, que le client renverra telle quelle, une indication de l'algorithme de condensation utilisé et quelques autres détails. Le numnique doit être différent à chaque réponse du serveur et de son mécanisme de génération dépend largement la sécurité de ce mécanisme d'authentification. Un attaquant ne doit pas pouvoir deviner le numnique qui sera utilisé. À part cela, le mécanisme de génération des numniques n'est pas spécifié, c'est une décision locale. Une méthode suggérée par le RFC est que le numnique soit la concaténation d'une estampille temporelle et de la condensation de la concaténation de l'estampille temporelle, de l'ETag de la ressource et d'un secret connu du seul serveur. L'estampille temporelle sert à éviter les attaques par rejeu. Elle est en clair au début du numnique, pour faciliter les comparaisons floues (estampille « pas trop ancienne »). Notez que le RFC ne conseille pas d'inclure l'adresse IP du client dans son calcul, cela défavoriserait trop les clients qui changent d'adresse IP, par exemple parce qu'ils passent par plusieurs relais de sortie possibles. (Alors que la section 5.5 du même RFC donne le conseil opposé...)

Le client HTTP doit répondre à ce défi en envoyant un en-tête Authorization: qui inclut la réponse condensée, le nom de l'utilisateur (qui peut être condensé ou pas, selon la valeur du paramètre userhash, cf. section 3.4.4 pour les détails), un numnique du client, et un compteur du nombre de tentatives d'authentification, qui permet de repérer les rejeux.

La réponse condensée est calculée (section 3.4.1) en condensant la concaténation de plusieurs valeurs, comprenant notamment une condensation de la concaténation du nom de l'utilisateur, de son mot de passe et du royaume, le numnique du serveur et celui du client. Un attaquant ne peut donc pas savoir à l'avance quelle sera la réponse, puisqu'il ne connait pas le numnique.

En recevant cette réponse à son défi, le serveur doit récupérer le mot de passe de l'utilisateur dans sa base de données, et refaire les mêmes opérations de concaténation et de condensation que le client, puis vérifier qu'il trouve le même résultat. (Notez qu'il n'y a pas besoin de stocker le mot de passe en clair, et que c'est même déconseillé, il suffit d'avoir accès à son condensat.) Par exemple, avec Apache, l'outil htdigest gère tout cela.

Le même mécanisme peut s'utiliser pour s'authentifier auprès d'un relais, mais dans ce cas les en-têtes se nomment Proxy-Authenticate: et Proxy-Authorization:.

Bon, c'est bien compliqué tout cela, place aux exemples. Un client veut accéder à http://www.example.org/dir/index.html (section 3.9.1 du RFC). Le royaume est http-auth@example.org, le nom d'utilisateur Mufasa et le mot de passe Circle of Life (n'oubliez pas les deux espaces entre les mots). Le serveur envoie le défi :

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Digest
       realm="http-auth@example.org",
       qop="auth, auth-int",
       algorithm=SHA-256,
       nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
       opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS"      
    

Le serveur demande qu'on condense avec SHA-256. Le client répond au défi :

Authorization: Digest username="Mufasa",
       realm="http-auth@example.org",
       uri="/dir/index.html",
       algorithm=SHA-256,
       nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v",
       nc=00000001,
       cnonce="f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ",
       qop=auth,
       response="753927fa0e85d155564e2e272a28d1802ca10daf4496794697cf8db5856cb6c1",
       opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS"

Le nom de l'utilisateur était en clair (non condensé). Le numnique et la chaîne opaque sont renvoyés tels quels. La réponse a été calculée en suivant l'algorithme de la section 3.4.1. Si vous voulez toutes les étapes (condensat calculé avec la commande Unix sha256sum) :

  • A1 = Mufasa:http-auth@example.org:Circle of Life
  • H(A1) = 7987c64c30e25f1b74be53f966b49b90f2808aa92faf9a00262392d7b4794232 (avec le nom de l'utilisateur, qui sert de clé d'accès, c'est la seule information à garder sur le serveur, cela évite des mots de passe stockés en clair)
  • A2 = GET:/dir/index.html (la réponse dépend de la partie locale de l'URI, ce qui limite l'ampleur d'un éventuelle attaque par rejeu)
  • H(A2) = 9a3fdae9a622fe8de177c24fa9c070f2b181ec85e15dcbdc32e10c82ad450b04
  • data = 7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v:00000001:f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ:auth:9a3fdae9a622fe8de177c24fa9c070f2b181ec85e15dcbdc32e10c82ad450b04
  • secret:data = 7987c64c30e25f1b74be53f966b49b90f2808aa92faf9a00262392d7b4794232:7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v:00000001:f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ:auth:9a3fdae9a622fe8de177c24fa9c070f2b181ec85e15dcbdc32e10c82ad450b04
  • KD (réponse) = 753927fa0e85d155564e2e272a28d1802ca10daf4496794697cf8db5856cb6c1

Parmi les points qui peuvent compliquer ce mécanisme (qui est ennuyeux mais simple à mettre en œuvre), l'internationalisation (section 4). Si le nom ou le mot de passe ne se limitent pas à ASCII, le serveur a intérêt à utiliser le paramètre charset pour indiquer le jeu de caractères dont le client devra se servir. La seule valeur légale de ce caractère est UTF-8, et les chaînes de caractères doivent être normalisées en NFC (cf. RFC 5198, notamment la section 3). Un exemple se trouve dans la section 3.9.2.

Maintenant que ce mécanisme est décrit, analysons sa sécurité (section 5 du RFC). D'abord, les mots de passe utilisés peuvent être trop faibles. Ce mécanisme Digest permet les attaques par dictionnaire (on essaie tous les mots d'un dictionnaire comme mots de passe) et, si le mot de passe figure dans les dictionnaires habituels (c'est le cas de azertyuipop et de 123456789), il finira par être trouvé. Cette attaque marche quelles que soient les qualités de l'algorithme de condensation employé. Il faut donc essayer de s'assurer qu'on n'utilise que des mots de passe forts. (Si la communication n'utilise pas HTTPS, et qu'un attaquant écoute passivement les défis et les réponses, il peut également les tester contre un dictionnaire, sans avoir besoin de faire des essais qui peuvent donner l'alarme.)

Ces mots de passe doivent être stockés sur le serveur. Bien sûr, on peut stocker uniquement leur forme condensée mais, comme c'est elle qui sert d'entrée à l'algorithme de calcul de la réponse, un attaquant qui met la main sur cette base a un accès complet aux ressources du serveur, sans étape de décryptage (comme cela serait le cas avec un attaquant mettant la main sur un /etc/passwd ou /etc/shadow Unix). Notez que le mécanisme Digest ne permet pas de saler les mots de passe. (À titre personnel, c'est pour cela que je ne trouve pas forcément ce mécanisme forcément plus sûr que Basic, contrairement à ce que dit le RFC en 5.13. Aujourd'hui, les piratages de bases des serveurs sont fréquents. La documentation d'Apache est sceptique aussi, disant « this [Digest authentication] does not lead to a significant security advantage over basic authentication » et « the password storage on the server is much less secure with digest authentication than with basic authentication »). C'est en raison de cette faiblesse que le royaume est inclus dans la condensation qu'on stocke dans la base : au moins l'attaquant ne pourra pas attaquer les autres sites, même si on a le même nom d'utilisateur et mot de passe.

Notez au passage que, comme le mécanisme Basic du RFC 7617, ce Digest permet au serveur d'authentifier le client, mais pas le contraire. Si on veut plus fort, il va falloir utiliser l'authentification permise par TLS (avec des certificats).

Et les attaques par rejeu ? La principale protection est fournie par le numnique. S'il est stupidement généré (par exemple, un simple compteur incrémental, trivial à deviner), les attaques par rejeu deviennent possibles. Avec l'algorithme conseillé, les attaques par rejeu sont plus difficiles mais pas impossibles : comme on peut réutiliser le numnique pendant un certain temps, un attaquant rapide peut faire plusieurs essais. Si c'est intolérable, la seule solution est d'avoir des numniques qui ne sont absolument pas réutilisables (par exemple, des numniques aléatoires, en prenant les précautions du RFC 4086). L'inconvénient est qu'il faut que le serveur les mémorise, au lieu de simplement les recalculer.

Il y a aussi les attaques classiques de l'Homme du Milieu (par exemple dans un relais Web). Un tel attaquant peut, par exemple, remplacer l'algorithme dans le défi par un algorithme plus faible, voir remplacer le défi de Digest par un appel à utiliser un mécanisme plus faible, tel que Basic. Le client HTTP peut prendre quelques mesures (se souvenir de l'authentification utilisée et avertir l'utilisateur si un site qui utilisait Digest passe à Basic) mais aucune n'est parfaite.

Le RFC recommande aussi de n'utiliser l'authentification qu'au-dessus de TLS. Même si le mot de passe n'est pas transmis, l'observation de l'authentification peut donner des informations (et donner accès au contenu des pages Web alors que, si on authentifie, cela peut être parce qu'il est confidentiel). Et TLS avec authentification du serveur protège contre les attaques de l'Homme du Milieu.

Le mécanisme Digest est dans le registre IANA des mécanismes d'authentification. Un nouveau registre est créé pour stocker les algorithmes de condensation.

Les changements depuis le RFC 2617 sont décrits dans l'annexe A. Les principaux ? SHA-256 et SHA-512 sont ajoutés aux algorithmes de condensation. MD5 est conservé, pour des raisons de compatibilité mais très déconseillé (RFC 6151). La possibilité de condenser le nom de l'utilisateur est ajoutée. L'internationalisation est ajoutée.

Si vous utilisez curl pour vous connecter au serveur HTTP, l'option --digest vous permettra d'utiliser le mécanisme de ce RFC (ou bien vous mettez l'option --anyauth et vous laissez curl se débrouiller pour trouver). Côté serveur, pour Apache, c'est bien documenté. Notez la directive Apache AuthDigestDomain pour spécifier le domaine, c'est-à-dire la liste des URI protégés. Pour Nginx, il n'y a apparement pas de solution standard, il faut utiliser un module supplémentaire (ou utiliser le mécanisme Basic + TLS, ce qui est probablement meilleur).

Deux bons textes à lire si vous voulez creuser la question, avec des exemples, un chez Microsoft, et sur le blog perso de Chua Hock-Chuan.


Téléchargez le RFC 7616

Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)

Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)