22
Sep/09
0

Cookies d’authentification

Comme quasiment toute application web, il est nécessaire d’identifier l’utilisateur afin de proposer des traitements et parties de site spécifiques pour cet utilisateur (zones privées, profilage, etc.).

La tendance actuelle, concernant notamment la possibilité de mise à l’échelle ultérieure, est de stocker les éléments de fonctionnement liés à l’utilisateur (données d’état à conserver entre plusieurs requêtes) à l’intérieur de cookie.

Ce mode de fonctionnement permet de s’approcher d’une architecture de type Shared Nothing (SNA), ce qui est une des architectures les plus efficaces. Cela permet également de ne pas introduire d’entrée de jeu des mécanismes (sessions) qui sont fortement structurants et limitatifs.

Le problème de cette solution est la transition des informations complètes (et pas seulement un identifiant de session) sur le réseau et leur utilisation au niveau du poste client. Le protocol SSL étant consommateur et ne pouvant pas être utilisé sur l’ensemble d’un site, cela suppose de bloquer toute manipulation des données par un client.

Fonctionnement

Le fonctionnement est de stocker un message dans le cookie dont certaines parties ne seront connues qu’au niveau du serveur, et de calculer une empreinte de ce message qui fait que toute altération du contenu sera immédiatement détectable ; l’empreinte étant elle même stockée dans le cookie.

Un cookie réellement sécurisé a été décrit par ce document PDF. Par analogie, Rails utilise - par défaut - ce qu’ils appellent le cookiestore et qui est la mise en ouvre du Fu’s cookie protocol.

Ce protocole est suffisant si on ne stocke aucune donnée sensible au sein du cookie, et c’est ce que nous allons mettre en place.

Les principales limitations relevées sur les cookies sont :

  • Expiration des données : la date d’expiration du cookie doit être stockée dans le message lui même car elle n’est plus controllable par le serveur,
  • Bande passante et taille limitée : la taille d’un cookie est limitée en caractères, et elle influe sur l’occupation de la bande passante car le cookie est transmis à chaque requête,
  • Obligation du poste client d’accepter les cookies : mais bon, on peut considérer la population concernée par ce point comme étant négligeable !

Implémentation

Quelques implémentations (cookies vraiment sécurisés ou non) en Erlang peuvent être trouvées sur le oueb, notamment sur le blog de easyErl ou le blog de cestari (trouvé via un commentaire dans le premier blog). Ce dernier a l’air plus professionnel, mais un click sur le fichier zip renvoie une erreur 404 !

Autre référence sur le blog de Jason Davies concernant couchDB.

Pour rappel, le cookie contient les informations suivantes (voir le PDF) :

user name|expiration time|data|HMAC(user name|expiration time|data, server key)

Comme indiqué précedemment, la date d’expiration est contenue dans le cookie ; le HMAC est nécessaire afin de préserver l’intégrité des données et d’empêcher toute altération et ré-exécution des données.

Initialisation

Le service de cryptologie doit être démarré lors du lancement de l’applicatif, donc on ajoute ces lignes dans ercm_app.erl :

start(_Type, _StartArgs) ->
  application:start(crypto),
  application:start(ecouch), ...
 
stop(_State) ->
  application:stop(ecouch),
  application:stop(crypto),
  ok.

Ensuite, on crée un nouveau module src/secure_cookies :

-module(secure_cookies).
-export([test/0]).
 
test() ->
  ok.

Implémentation

On défini alors les constantes suivantes :

-define(SECRET_KEY, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").
-define(SEPARATOR, "-").

La clé secrète du serveur doit être choisie de façon aléatoire et être suffisamment longue. Chaque élément encodé dans le cookie sera séparé par un caractère spécial, ici le tiret.

On va ensuite fournir une première version d’encodage :

encode(UserName, Expiration, Data) ->
  Timeout = integer_to_list(Expiration),
  Hmac = crypto:sha_mac(?SECRET_KEY, [UserName, Timeout, Data]),
  iolist_to_binary([UserName, ?SEPARATOR, Timeout, ?SEPARATOR, base64:encode(Data), ?SEPARATOR, Hmac]).

On transforme l’entier en liste pour le concaténer ultérieurement. On calcule le code MAC selon la formule vu précédemment, puis on concatène tous ces champs en un type binaire. Les données sont encodées en base64 afin d’être transmises de façon sûre au niveau du client.

L’expiration sera retravaillée ultérieurement.

Le décodage du cookie est réalisée en découpant la chaîne de caractères par rapport au séparateur, puis en réadaptant les données obtenues :

decode(Value) ->
  [UserName, Timeout, Data, Hmac] = string:tokens(binary_to_list(Value), ?SEPARATOR),
  {UserName, list_to_integer(Timeout), base64:decode_to_string(Data), iolist_to_binary(Hmac)}.

On doit alors pouvoir faire un premier niveau de test :

test() ->
  Enc = encode("pipo", 300, "UserID"),
  Dec = decode(Enc),
  {"pipo", 300, "UserID", _Hmac} = Dec,
  ok.

Vérification

La vérification se fait d’abord sur la non altération des données. Pour cela, on compare la signature contenue dans le cookie avec une signature recalculée à partir des données de cookie. Si les deux éléments correspondent, le test est concluant, sinon cela signifie une altération des données :

check_cookie(Value) ->
  {UserName, Timeout, Data, CookieMac} = decode(Value),
  CookieMac = crypto:sha_mac(?SECRET_KEY, [UserName, integer_to_list(Timeout), Data]),
  {UserName, Data}.
 
test_inject(Value) ->
  [Head|Tail] = binary_to_list(Value),
  iolist_to_binary([Head + 1] ++ Tail).
 
test() ->
  ...
  {"pipo", "UserID"} = check_cookie(Enc),
  check_cookie(test_inject(Enc)),
  ok.

Dans le cas où la correspondance est avérée on renvoie le tuple utilisateur/données.

Le cas où une altération est trouvée est plus intéressant. En passant, je ne sais pas si le code utilisé pour tester l’altération du cookie est convenable (bien écrite / efficace), mais je ne veux pas passer de temps là dessus.

Tout d’abord, si on lance ce code dans la console, une exception apparaît :

14> secure_cookies:test().
** exception error: no match of right hand side value 
                    <<50,174,25,113,123,229,11,67,200,88,110,191,154,53,141,
                      71,144,111,45,93>>
     in function  secure_cookies:check_cookie/1
     in call from secure_cookies:test/0

Effectivement, si les deux signatures sont différentes le match de la fonction check_cookie/1 ne pourra pas se faire, on n’a pas affaire à une affectation ici.

Et le deuxième point important est qu’on ne teste que le bon cas d’utilisation et que l’on délègue la récupération de l’erreur.

on supprime cette erreur en test via le code suivant :

  {'EXIT', {{badmatch, _},_}} = (catch check_cookie(test_inject(Enc))),

Cette erreur va devoir être traitée quand même : une altération devra nous obliger à diriger l’utilisateur sur une page d’authentification après avoir enregistré une alerte.

Pour l’instant, je ne sais pas si c’est le meilleur moyen de faire, la suite nous indiquera s’il faut changer ce mode de fonctionnement ou pas. De plus, le timeout n’est pas encore géré, mais l’article étant vraiment conséquent, ces points seront traités ultérieurement.

Comments (0) Trackbacks (0)

No comments yet.

Leave a comment

No trackbacks yet.