--[ Mécanismes d'authentification HTTP/HTTPS ]-- Cette brève aborde les solutions mises en oeuvre dans les applications de type « web » pour assurer le service d'authentification. Le mécanisme de transmission des accréditations du client vers le serveur est traité essentiellement. Le processus de vérification (auprès d'un fichier « plat », indexé, d'une base de données ou encore d'un annuaire LDAP) ne sera généralement pas abordé. 1. Authentification HTTP Le protocole HTTP est un protocole client/serveur, sans état. La spécification du protocole HTTP dans sa version 1.1 est décrite dans la RFC 2616 [1]. Deux méthodes d'authentification HTTP, dites Basic et Digest, sont décrites dans la RFC 2617 [2]. Les accréditations sont transmises du client vers le serveur via les entêtes HTTP, elles authentifient les requêtes effectuées par le client individuellement (lorsque le keep-alive est utilisé, les différentes requêtes effectuées sur la même connexion TCP sont chacune authentifiées). 1.1. Authentification HTTP Basic 1.1.1 Authentification du client La méthode d'authentification dite « HTTP Basic » ne met en oeuvre aucun service de confidentialité quant à la transmission des accréditations au serveur. Il s'agit d'une méthode communément employée, conjointement au protocole SSL/TLS. Le mécanisme est le suivant : a. Le client émet une requête à destination d'une ressource protégée, le répertoire identifié par l'URI /basic par exemple. La requête simple correspondante est : GET /basic/ HTTP/1.0 b. Le serveur répond avec le code d'erreur 401 (Unauthorized), et sa réponse comporte l'entête WWW-authenticate, spécifiant le type d'authentifcation attendue : HTTP/1.1 401 Authorization Required WWW-Authenticate: Basic realm="Basic realm" c. L'entête renvoyé par le serveur est interprété par le navigateur. Celui-ci présente une fenêtre à l'utilisateur, l'invitant à entrer ses accréditations. Le navigateur réitère la précédente requête, en intégrant l'entête Authorization, suivie de la concaténation du nom d'utilisateur, de ":" et du mot de passe, le tout encodé en base64. Par exemple, pour la paire login/mot de passe dummy:secret : $ echo -n 'dummy:secret' | openssl enc -a -e ZHVtbXk6c2VjcmV0 La requête résultante est donc la suivante : GET /basic/ HTTP/1.0 Authorization: Basic ZHVtbXk6c2VjcmV0 Les accréditations sont donc transmises en clair (encodées) à _chacune_ des requêtes (dans la mesure où le protocole HTTP est dit « sans état »). Il ne s'agit pas de chiffrement ! Les accréditations sont mises en cache par le navigateur, évitant ainsi au client d'avoir à les fournir à chacune des requêtes émises. Cette méthode d'authentification est donc particulièrement sensible à une écoute du trafic. Si un service de confidentialité est mis en oeuvre à travers un mécanisme de chiffrement au niveau transport/session (SSL/TLS par exemple), seul un serveur malveillant pourra accéder à ces accréditations (si l'on exclue la faille des protocoles SSL/TLS permettant, par analyse temporelle pour un attaquant actif, de décrypter un bloc de données chiffré suivant un algorithme symétrique en mode CBC). d. Le serveur renvoie la page protégée (HTTP/1.1 200 OK). Au niveau des journaux, la ligne suivante est générée : 127.0.0.1 - dummy [30/Jan/2003:13:40:50 +0100] "GET /basic/ HTTP/1.0" 200 820 "-" " ""w3m/0.3.1+cvs-1.411"" Cette ligne indique que l'utilisateur 'dummy' a accédé avec succès au répertoire /basic (code 200). Remarques : ·lorsqu'un login apparemment inconnu apparaît dans les journaux, penser à regarder le code de retour correspondant ·le mot de passe peut être conservé dans une base de données, un annuaire LDAP etc. Il peut également s'agir d'un mot de passe à usage unique, de type S/KEY, PAM_OPIE etc. Seule est décrite ici la méthode employée par le client pour transmettre ses accréditations au serveur (usage des entêtes HTTP en l'occurrence). 1.1.2 Suivi de session authentifiée Dans le cas de l'authentification de type « Basic », le « suivi » de session est réalisé par la retransmission systématique des accréditations (à chacune des requêtes). 1.2. Authentification HTTP Digest 1.2.1 Authentification du client La méthode d'authentification dite « HTTP Digest » met en oeuvre un service de confidentialité pour les accréditations, mais pas pour les données transférées (côté client, comme côté serveur). Elle introduit des mécanismes contre le rejeu, et protège le client contre un serveur malveillant, ou potentiellement corrompu, dans la mesure où les accréditations ne sont jamais connues par ce dernier. Il s'agit d'un mécanisme de type « challenge/response », joué pour chaque ressource demandée : très simplement, le serveur envoie un challenge au client, ce dernier répond par une valeur dérivée de ce challenge et d'un secret qu'il partage avec le serveur. Le serveur s'assure alors que le client possède effectivement le secret en calculant à son tour la réponse et vérifiant la cohérence des deux. Les traces effectuées dans cette brève ont été réalisées suite à l'utilisation du module mod_auth_digest d'Apache, et non du module standard mod_digest (qui met en oeuvre un support partiel de l'authentification digest). Le mécanisme, dans le cas le plus simple (authentification sans intégrité), est le suivant : a. Comme précédemment, le client émet une requête à destination d'une ressource protégée, identifiée par son URI : GET /digest/ HTTP/1.0 b. La réponse du serveur, légèrement épurée, est comme suit : HTTP/1.1 401 Authorization Required WWW-Authenticate: Digest realm="Digest Realm", nonce="uD85Pg==a766f996fa716e4d4592943b5762c73958f0378b", algorithm=MD5, domain="/digest", qop="auth" Elle comporte, notamment : - un challenge (« nonce ») - un identifiant d'algorithme (« algorithm ») : la fonction de hachage MD5, dans ce cas Pour chaque page protégée, le client va calculer une réponse à partir de ces paramètres en utilisant son mot de passe. En pratique, le challenge n'est pas renouvelé à chaque requête -- ce qui limite le non-rejeu. c. Le client calcule en réponse l'empreinte qui sera émise via l'entête Authorization, comme lors d'une authentification de type « Basic » : (la méthode de calcul est donnée dans la RFC 2617) : request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) ) <"> avec : KD (secret,data) = H(concat(secret, ":", data) · H étant la fonction de hachage MD5 (et concat signifiant la concaténation des chaînes) A1 = unq(username-value) ":" unq(realm-value) ":" passwd A2 = Method ":" digest-uri-value Quelques remarques : - unq correpond à « unquoted ». - le paramètre cnonce-value est choisi par le client (d'où le préfixe 'c'). Dans ce cas, la valeur choisie par le navigateur donnera un champ unq(cnonce-value) égal à be09d67c532a3a02. Le calcul effectué par le client est donc comme suit (le nom d'utilisateur étant 'dummy', et le mot de passe correspondant 'secret') : * Pour H(A1) : $ echo -n 'dummy:Digest Realm:secret' | openssl md5 0c440e535a0afdc350f3f8ba0aa2f271 Remarque : cette empreinte est celle conservée côté serveur (dans le fichier .htaccess, généré par htdigest(1) pour un serveur Apache). Le serveur n'a donc pas la connaissance du mot de passe de l'utilisateur. La connaissance du mot de passe en clair n'est donc pas nécessaire côté serveur -- au contraire de mécanismes d'authentification similaire de type apop. * Pour H(A2) : $ echo -n 'GET:/digest/' | openssl md5 9942091bc79111e32fecde3962416017 Remarque : la QUERY_STRING (chaîne figurant après '?') est prise en compte dans l'URI pour le calcul de l'empreinte ! Au final, la réponse est la suivante : $ echo -n '0c440e535a0afdc350f3f8ba0aa2f271:uD85Pg==a766f996fa716e4d4592943b5762c7395 8f0378b:00000001:be09d67c532a3a02:auth:9942091bc79111e32fecde3962416017' | openssl md5 Soit : 46f122dedae2a5f8ffbf82d6ad605304 La requête résultante est la suivante, avec la réponse dans le champ 'response' : GET /digest/ HTTP/1.1 Authorization: Digest username="dummy", realm="Digest Realm", nonce="uD85Pg==a766f996fa716e4d4592943b5762c73958f0378b", uri="/digest/", algorithm=MD5, response="46f122dedae2a5f8ffbf82d6ad605304", qop=auth, nc=00000001, cnonce="be09d67c532a3a02" d. Le serveur répond positivement, avec l'entête Authentication-Info : HTTP/1.1 200 OK Authentication-Info: rspauth="1d4d5a9d920fb22197471146c613e767", cnonce="be09d67c532a3a02", nc=00000001, qop=auth L'entête rspauth permet une authentification mutuelle. Le serveur prouve en effet par ce biais qu'il connaît effectivement le mot de passe du client : le calcul effectué est identique à celui effectué par le client, au détail près que le paramètre A2 est pris égal à ':/digest/', la méthode n'est pas prise en compte. Remarque : le non-rejeu est situé au niveau « session HTTP » (par abus de langage, session pendant laquelle est utilisable le « nonce »), et non au niveau « requête HTTP » : dans le cas du module Apache mod_auth_digest, la durée de vie du paramètre nonce est fixée par le paramètre AuthDigestNonceLifetime. Au delà de cette durée, le nonce est renouvelé, et le client doit s'authentifier à nouveau, et explicitement (le serveur lui renvoie un code d'erreur 401). On peut donc raisonnablement considérer que seul un service de confidentialité des accréditations est mis en oeuvre, et non un véritable service de non-rejeu. Outre les indéniables avantages qu'elle présente en terme de confidentialité des accréditations (vis à vis d'un intercepteur comme d'un serveur malveillant, vers lequel le trafic aurait été détourné), la méthode d'authentification HTTP « Digest » est surtout connue, ou notoire, pour l'incompatibilité constatée entre le serveur HTTP le plus répandu, Apache, d'une part, et le navigateur le plus répandu d'autre part, MSIE. Cette incompatibilité tient à une interprétation incorrecte de la RFC 2616 par MSIE (et IIS, par extension). Le court CGI qui suit permet d'illustrer cette constatation : $ cat ./digest/printenv.cgi #!/usr/bin/perl print "Content-Type: text/plain\n\n"; foreach $key (sort keys(%ENV)) { print "$key = $ENV{$key}\n"; } Une requête effectuée par MSIE sur l'URI /digest/printenv.cgi avec la QUERY_STRING param=value donne le résultat, épuré, suivant : GET /digest/printenv.cgi?param=value HTTP/1.0 Authorization: Digest username="dummy", realm="Private Area", qop="auth", algorithm="MD5", uri="/digest/printenv.cgi", nonce="KPw3Pg==08fc61d5b52c87dbda7b038a1c741fd82ae12756", nc=00000007, cnonce="67fa5778e4fac113bc53ca089cf10fa6", response="5045459a94e92c1844e8e1e5a6309c3e" La réponse du serveur se solde par un échec (code d'erreur 400) : HTTP/1.1 400 Bad Request [...]

Bad Request

Your browser sent a request that this server could not understand.

Digest: uri mismatch - does not match request-uri </digest/printenv.cgi?param=value>

Ainsi, MSIE calcule sa réponse en prenant comme valeur pour le paramètre digest-uri-value la variable REQUEST_URI (/digest/printenv.cgi?param=value), mais _sans_ la partie QUERY_STRING (param=value). Des CGI dont l'accès est restreint et requèrent une authentification de type digest doivent par conséquent recevoir leurs paramètres sur leur entrée standard (passés dans le corps de la requête, via POST), et non plus via l'environnement par la variable QUERY_STRING. Une autre méthode suggérée [3] : utiliser la variable PATH_INFO. Cette variable d'environnement est initialisée lorsqu'un « chemin » est concaténé au CGI à exécuter. Par exemple, la requête GET /digest/printenv.cgi/chemin/supplementaire?param=value va initialiser les variables PATH_INFO (/chemin/supplementaire) et QUERY_STRING (param=blah). 1.2.2 Suivi de session authentifiée Comme précédemment, le suivi de session est réalisé par la transmission, à chaque requête, des accréditations. Ces dernières sont transmises et calculées pour chaque requête, dérivées des paramètres d'authentification mis en cache par le navigateur. 2. Authentification HTTPS 2.1 Authentification du client Le protocole HTTPS met en oeuvre des services d'authentification des parties (unilatérale ou mutuelle, à partir du protocole SSLv3), d'authenticité et de confidentialité des données pendant toute une session SSL/TLS (la session « multiplexant » les connexions TCP et requêtes HTTP associées). La méthode d'authentification HTTP de type Basic est le plus souvent utilisée conjointement à SSL/TLS, afin d'assurer la confidentialité des accréditations. Le protocole HTTPS introduit, par l'intermédiaire des protocoles SSLv3/TLS, une méthode d'authentification du client par certificat X.509v3. Cette méthode a été très brièvement abordée dans une précédente brève présentant la bibliothèque OpenSSL (http://www.hsc.fr/ressources/breves/ssl_configuration.html ), le détail du mécanisme ne sera donc pas abordé. Dans ce cas, le serveur et l'application peuvent s'appuyer sur la session SSL/TLS mutuellement authentifiée pour assurer un suivi de session authentifiée de l'utilisateur, au niveau applicatif par conséquent et non plus au niveau HTTP. 2.2 Suivi de session authentifiée Dans le cas d'une authentification du client par certificat X.509, un certain nombre de paramètres discriminants permettent d'identifier l'utilisateur authentifié. Ces paramètres discriminants sont, à titre d'exemple, les champs DN (distinguishedName), CN (commonName) ou encore l'adresse de courrier électronique dérivés du certificat présenté lors de la négociation des paramètres de la session SSL/TLS. La liste des variables d'environnement initialisées par mod_ssl, dérivant du certificat client, figurent à l'URL suivante : http://www.modssl.org/docs/2.8/ssl_reference.html Dans le cas où le client n'est pas authentifié par certificat X.509, le paramètre discriminant, dont pourrait être dérivé un identifiant de session au niveau de l'application, ne peut plus plus provenir du certificat client. Les seuls paramètres « candidats » sont ceux négociés lors du handshake SSL/TLS, et plus particulièrement le session_id sur 32 bits (variable d'environnement SSL_SESSION_ID pour mod_ssl, initialisé par le serveur). Le standard RFC2246 (« The TLS Protocol Version 1.0 ») [4] stipule, à propos de la durée de vie de cet identifiant : « An upper limit of 24 hours is suggested for session ID lifetimes, since an attacker who obtains a master_secret may be able to impersonate the compromised party until the corresponding session ID is retire ». La limite supérieure de durée de vie de cet identifiant est donc proposée à 24 heures - ce qui est a priori largement suffisant pour suivre une session authentifiée. Cependant, certaines versions d'Internet Explorer (IE 5.0/5.01(SP1)/5.5(SP1) 9x/NT4) renégocient ce paramètre toutes les 2 minutes (Q265369) [5]. Ce comportement peut être modifié dans la base de registres : HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SecurityProviders\SCHANNEL La clef ClientCacheTime est positionnée à 120000 (ms) par défaut. Le suivi de session fondé sur le session_id SSL/TLS uniquement est donc, malheureusement, inexploitable en pratique. La solution, lorsque l'authentification par certificat client n'est pas mise en oeuvre, repose donc sur les mécanismes usuels utilisés dans HTTP - qui seront brièvement abordés par la suite, ave la notion d'identifiant de session authentifiée (cookie ou autre). Remarque : outre le suivi de session authentifiée au niveau « applicatif », ce renouvellement fréquent du session_id SSL/TLS pose le problème de l'équilibrage de charge sur des flux HTTPS (pour s'appuyer sur d'autres paramètres que l'adresse IP notamment). À cet effet, la bibliothèque OpenSSL 0.9.7 introduit la fonction SSL_CTX_set_generate_session_id(), permettant au serveur HTTPS de générer des sessions id SSL dont le préfixe est statique et exploitable au niveau de l'équipement de répartition de charge, pour peu que celui-ci permette un tel paramétrage. 3. Authentification « applicative » 3.1 Authentification du client L'authentification entendue au sens « applicatif » consiste en la vérification par l'application elle-même des accréditations présentées par le client. Il ne s'agit plus, pour cette dernière, de s'appuyer sur une authentification réalisée au niveau HTTP ou SSL/TLS, par l'intermédiaire des variables d'environnement initialisées par le serveur par exemple : REMOTE_USER pour une authentification HTTP, SSL_CLIENT_DN pour une authentification par certificat client etc. Les accréditations ne sont donc plus transmises via les entêtes HTTP (méthodes d'authentification HTTP précédemment décrites), mais par un mécanisme indiqué par la « requête simple » de la requête. La méthode utilisée peut être GET (via la QUERY_STRING, concaténée à l'URI) ou POST (dans le corps de la requête), indifféremment. Remarque : il est clair que les langages de scripts, comme PHP par exemple, permettent également de mettre en oeuvre des mécanismes d'authentification de type HTTP « Basic », dans la mesure où ils peuvent également lire/écrire les requêtes/réponses. Les accréditations sont généralement transmises via un tunnel chiffré/authentifié unilatéralement (SSL/TLS avec authentification du serveur par certificat), afin d'assurer la confidentialité des accréditations transmises. Remarque : une solution alternative, et rarement employée, consiste à utiliser un mécanisme local au client, de type javascript, pour générer des accréditations à « usage unique » à partir des paramètres d'authentification, et envoyer ces accréditations au serveur (à travers un canal non chiffré). À titre d'exemple, la mise en oeuvre d'une authentification de type CHAP, par un javascript côté client et des scripts ASP côté serveur [6]. 3.2 Suivi de session authentifiée Le suivi de session est à la charge de l'application. Dans les cas HTTP « Basic » et « Digest » (avec ou sans surcouche SSL/TLS), il y a authentification à chaque requête, avec une gestion d'état (ensemble de variables associées à une « session » applicative, à un ensemble de requête provenant d'un même utilisateur) côté serveur. Le même principe peut être adopté pour une application. Mais le processus est tout autre, en pratique : l'utilisateur commence par s'authentifier au niveau d'un formulaire, les paramètres sont transmis (via la méthode GET ou la méthode POST) pour validation, puis un identifiant de session est attribué et sera utilisé pour lier les requêtes suivantes au contexte initialisé côté serveur -- d'où la notion de session applicative. Les méthodes de passage d'identifiant de session sont multiples. À titre d'exemple, une liste non exhaustive de techniques rencontrées : - cookie - géré par le serveur HTTP : · Fixé côté serveur, par l'entête Set-Cookie ou via javascript : Set-Cookie: SESSIONID=e3e5f57ca9adbbb19a21b1c2a22be987; path=/ · Émis par le client, par l'entête Cookie : Cookie: SESSIONID=e3e5f57ca9adbbb19a21b1c2a22be987 Le cookie est intéressant d'un point de vue « sécurité », avec l'introduction de fonctionnalités via les paramètres « secure » (permettant la transmission du cookie via un tunnel SSL/TLS uniquement, afin d'éviter son interception « passive » par écoute du trafic) ou encore « httponly » (introduit par le Service Pack 1 d'IE 6, protégeant ce navigateur contre les attaques de type XSS en interdisant la manipulation du cookie par des scripts, type javascript notamment -- tout en demeurant vulnérable à un usage détourné de la méthode HTTP TRACE [7] qu'il convient donc de désactiver au niveau des serveurs HTTP). - méthode GET : GET /page.php?SESSIONID=e3e5f57ca9adbbb19a21b1c2a22be987 - méthode POST : POST /page.php Content-type: application/x-www-form-urlencoded Content-length: 42 SESSIOND=e3e5f57ca9adbbb19a21b1c2a22be987 - insertion dans la REQUEST_URI (mod_rewrite + PHP4 [8] ) : GET /SESSIOND=e3e5f57ca9adbbb19a21b1c2a22be987/index.php - réécriture d'URL GET /index.php Host: e3e5f57ca9adbbb19a21b1c2a22be987.webserver.tld La sécurité relative à la génération d'un cookie (ou d'un identifiant de session plus généralement) d'une part, et à son suivi d'autre part sont deux sujets non triviaux qui débordent du cadre de cette brève. Cependant, des règles de bonnes pratiques existent : le projet OWASP [9] émet un certain nombre de recommandations concernant ces aspects. Le point essentiel est qu'il convient d'identifier les requêtes (indépendantes les unes des autres, dans la mesure où HTTP est « sans état ») d'un même utilisateur authentifié, et les associer à un contexte unique géré côté serveur et initialisé suite à une phase d'authentification. Certaines suites logicielles de SSO, par exemple, mettent en oeuvre des mécanismes tout à fait satisfaisants, en générant par exemple un nouvel identifiant de session à chaque requête et permettant de se prémunir contre les attaques de type XSS. La gestion correcte de cet identifiant assurera la sécurité du service d'authentification, mais cet identifiant devra par la suite être correctement corrélé avec les habilitations des utilisateurs, afin de mettre un place une véritable gestion des autorisations, et non une simple « personnalisation » des menus affichés à l'utilisateur... Le manque de cloisonnement entre les contextes utilisateurs au niveau applicatif sont aussi dangereux que les vols de session par XSS, et leur résolution ne passe pas forcément par l'ajout d'options diverses à un cookie, ou encore à un filtrage systématique du contenu des pages renvoyées au client, mais à une conception sécurisée de l'application même, qu'une solution de filtrage ou de SSO ou autre, aussi évoluée soit-elle, ne remplacera pas. 5. Ressources [1] RFC2616 -- « Hypertext Transfer Protocol -- HTTP/1.1 » http://www.ietf.org/rfc/rfc2616.txt [2] RFC2617 -- « HTTP Authentication: Basic and Digest Access Authentication » http://www.ietf.org/rfc/rfc2617.txt [3] ApacheWeek: Issue 317, 20th December 2002 http://www.apacheweek.com/issues/02-12-20 [4] RFC2246 -- « The TLS Protocol -- Version 1.0 » http://www.ietf.org/rfc/rfc2246.txt [5] « Microsoft Knowledge Base Article - 265369 » http://support.microsoft.com/default.aspx?scid=kb;EN-US;q265369 [6] « MD5-based login scheme in Javascript » http://builder.cnet.com/webbuilding/pages/Programming/Scripter/013100/ss02.html [7] « Cross-Site Tracing -- XST » http://www.cgisecurity.com/whitehat-mirror/WhitePaper_screen.pdf [8] Gestion des sessions dans PHP4 http://lxr.php.net/source/php4/ext/session/session.c [9] « The Open Web Application Security Project » http://www.owasp.org $Id: http_digest.tip,v 1.6 2003/04/30 16:29:54 davy Exp $