miércoles, 17 de julio de 2013

Seguridad PHP - Autenticación HTTP digest

Si la autenticación HTTP básica anterior no se combina con HTTPS, estamos trasmitiendo la información codificada en base-64. Pero no encriptada o usando algoritmos 'hash'. Lo que es muy fácil de decodificar y es prácticamente como si la enviásemos en texto plano.
Por eso el mecanismo que se presenta a continuación, mejora al anterior, transmitiendo la contraseña como un valor resultante de un algoritmo 'hash'. Lo que lo hace algo más seguro. O por lo menos era más seguro hace unos años. Ya que el algoritmo usado es MD5.  Y es bien sabido, que este algoritmo es vulnerable (por fuerza bruta) desde hace tiempo.
El propósito del esquema de autenticación HTTP digest es permitir que los usuarios prueben que ellos conocen una contraseña pero sin revelar dicha contraseña. Al contrarío que la identificación HTTP básica, en esta se crea un hash de diferentes datos usando la contraseña. El hash implica la creación de una cadena basada en datos que tanto el usuario (y por lo tanto el cliente) y el servidor conocen. Pero en definitiva, se envía el hash y no la contraseña. Lo que en caso que alguien consiga interceptar la conexión y suplantar al usuario, no disponga de la contraseña. Que normalmente es requerida para cambios de configuraciones en lo sitios web.

Al igual que en el mecanismo anterior, el navegador hace una petición de entrada a un sitio restringido. Y el servidor contestará nuevamente con una cabecera que contenga HTTP/1.1 401 Authorization Required ya que la primera petición no llevará información de autenticación. Pero a diferencia de la que se devolvía (primera respuesta del servidor) con la autenticación HTTP básico, ahora aparecerá WWW-Authenticate: Digest. Además incluirá nuevos datos necesarios para la comunicación.

Ejemplo de cabeceras devueltas por el servidor requiriendo autenticación tras la primera petición, sin dichos datos, por parte del navegador. El primer ejemplo se trata de autenticación HTTP básica y el segundo de HTTP digest. Podemos apreciar las principales diferencias.

 HTTP/1.1 401 Unauthorized  
 Date: Tue, 22 Jun 2010 03:54:06 GMT   
 Server: Apache/2.2.15 (Unix)   
 WWW-Authenticate: Basic realm="demo"   
 ... 

 HTTP/1.1 401 Unauthorized  
 WWW-Authenticate: Digest realm="Demo",  
       qop="auth",  
       nonce="51af2815a5548",  
       opaque="5ccc069c403ebaf9f0171e9517f40e41"  

Descripción de los datos que se envía al cliente:
- realm: Al igual que en la identificación básica, es el nombre del servicio al que queremos acceder.
- nonce: un valor único generado por el servidor en cada inicio de la comunicación para la identificación. O visto de otra manera, se generará cada vez que el servidor envía una respuesta 401. No debe de ser modificado por el cliente.
- opaque (opcional): Cadena que tiene que volver inalterada al servidor para verificar que no ha habido alteración en la sesión (se podría ver como un id de sesión).
- Stale (optional): El servidor también puede pasar la directiva stale, en la respuesta para el cliente. Y si esta esta a TRUE indica que el valor de nonce fue válido en algún momento pera ya no. Es posible que en el transcurso de la comunicación el valor nonce haya sido regenerado en el servidor. Por lo tanto se avisará al cliente que podrá crear el hash (campo response como se verá a continuación) con el nuevo nonce.
- QOP-(optional): Determina la calidad de la protección en el la comunicación. Los posibles valores son 'auth' para indicar que únicamente queremos identificación o 'auth-int' para utilizar la protección de integridad (el ejemplo se va a realizar con el primer tipo).

El navegador tras recibir la anterior contestación por parte del servidor, mostrará la ventana emergente de identificación donde el cliente introducirá el usuario y contraseña.
Una vez el usuario haya introducido el usuario y la contraseña en la ventana emergente el navegador, automáticamente creará la petición para el servidor. Usando para ello los datos introducidos por el usuario y los recibidos en la primera contestación. Mirar que los campos opaque, qop y nonce permanecen inalterados respecto la primera contestación del servidor.

 Authorization: Digest username="admin",  
       realm="Demo",  
       nonce=51af2815a5548,  
       uri="/",  
       qop=auth, nc=00000001,  
       cnonce="1af289aaac37",  
       response="98ccab4542f284c00a79b5957baaff23",  
       opaque="5ccc069c403ebaf9f0171e9517f40e41"  

Nota: el valor del hash no esta calculado correctamente. Se presenta únicamente a modo de ejemplo.

Descripción de los datos enviados al servidor:
- username: el nombre de usuario.
- realm: igual que el enviado al cliente.
- nonce: igual que el enviado al cliente.
- response: el hash válido creado por el cliente a partir de todos los demás datos.
- uri: uri a la que hemos hecho la petición para identificarnos
- opaque: igual que el enviado al cliente.
- qop: igual que el enviado al cliente.
- nc: número de serie haxadecimal para la respuesta. El cliente debe incrementarlo en uno en cada petición que haga.
- cnonce: id único generado por el cliente.

Una vez el cliente envíe la nueva petición al servidor, este pasará a comprobar que los datos existen, si no enviará nuevamente una contestación solicitándolos (ventana emergente). Y comprobará que son correctos.
Vamos a ver un pseudocódigo del algoritmo de creación de hash (campo 'response' de la petición al servidor). Evidentemente el cliente utilizará este algoritmo automáticamente para generar los datos de la petición y en el servidor lo implementaremos para comprobación de los mismos.

 A1 = md5(username:realm:password)  
 A2 = md5(metodo_peticion:uri) // metodo petición = GET, POST, etc.  
 Hash = md5(A1:nonce:nc:cnonce:qop:A2)  
 if (Hash == response)  
  //identificación correcta!  
 else  
  //fallo identificación!  

Ejemplo completo (extraido de php.net):

 <?php  
 $realm = 'Restricted area';  
 //user => password  
 $users = array('admin' => 'mypass', 'guest' => 'guest');  
 if (empty($_SERVER['PHP_AUTH_DIGEST'])) {  
   header('HTTP/1.1 401 Unauthorized');  
   header('WWW-Authenticate: Digest realm="' . $realm .  
       '",qop="auth",nonce="' . uniqid() . '",opaque="' . md5($realm) . '"');  
   die('Text to send if user hits Cancel button');  
 }  
 // analyze the PHP_AUTH_DIGEST variable  
 if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) ||  !isset($users[$data['username']]))  
   die('Wrong Credentials!');  
 // generate the valid response  
 $A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]);  
 $A2 = md5($_SERVER['REQUEST_METHOD'] . ':' . $data['uri']);  
 $valid_response = md5($A1 . ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $A2);  
 if ($data['response'] != $valid_response)  
   die('Wrong Credentials!');  
 // ok, valid username & password  
 echo 'You are logged in as: ' . $data['username'];  
 // function to parse the http auth header  
 function http_digest_parse($txt) {  
   // protect against missing data  
   $needed_parts = array('nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1);  
   $data = array();  
   $keys = implode('|', array_keys($needed_parts));  
   preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);  
   foreach ($matches as $m) {  
     $data[$m[1]] = $m[3] ? $m[3] : $m[4];  
     unset($needed_parts[$m[1]]);  
   }  
   return $needed_parts ? false : $data;  
 }  
 ?>  

Hay que destacar el uso de la variable $_SERVER['PHP_AUTH_DIGEST'] para la obtención de la información de la cabecera de autorización.

Vamos a describamos un poco la función http_digest_parse($txt).Supongamos que a la variable $txt le llega la siguiente información:

$txt = 'Authorization: Digest username="admin",  
       realm="Demo",  
       nonce=51af2815a5548,  
       uri="/",  
       qop=auth, nc=00000001,  
       cnonce="1af289aaac37",  
       response="98ccab4542f284c00a79b5957baaff23",  
       opaque="5ccc069c403ebaf9f0171e9517f40e41"';  


Y $keys = "nonce|nc|cnonce|qop|username|uri|response"; que será usado para construir la expresión regular de la siguiente llamada a  preg_match_all(). Que buscará en $txt todas las coincidencias con el patrón (1er parámetro en la función) y las colocará en el array $matches. La bandera PREG_SET_ORDER  ordena los resultados de forma tal que $matches[0] es un array que contiene el primer conjunto de coincidencias (username) de $txt con el patrón, $matches[1] es un array con el segundo conjunto de coincidencias, y así sucesivamente.
El resultado de $matches sería algo así:

 $matches = Array  
 (  
   [0] => Array  
     (  
       [0] => username="admin"  
       [1] => username  
       [2] => "  
       [3] => admin  
     )  
   [1] => Array  
     (  
       [0] => nonce=51af2815a5548  
       [1] => nonce  
       [2] =>   
       [3] =>   
       [4] => 51af2815a5548  
     )  
   [2] => Array  
     (  
       [0] => uri="/"  
       [1] => uri  
       [2] => "  
       [3] => /  
     )  
   [3] => Array  
     (  
       [0] => qop=auth  
       [1] => qop  
       [2] =>   
       [3] =>   
       [4] => auth  
     )  
   [4] => Array  
     (  
       [0] => nc=00000001  
       [1] => nc  
       [2] =>   
       [3] =>   
       [4] => 00000001  
     )  
   [5] => Array  
     (  
       [0] => cnonce="1af289aaac37"  
       [1] => cnonce  
       [2] => "  
       [3] => 1af289aaac37  
     )  
   [6] => Array  
     (  
       [0] => response="98ccab4542f284c00a79b5957baaff23"  
       [1] => response  
       [2] => "  
       [3] => 98ccab4542f284c00a79b5957baaff23  
     )  
 )  

Finalmente mediante el bucle foreach se rellena el array $data. Que será el return de la función y quedaría de la siguiente forma:

 $data = Array  
 (  
   [username] => admin  
   [nonce] => 51af2815a5548  
   [uri] => /  
   [qop] => auth  
   [nc] => 00000001  
   [cnonce] => 1af289aaac37  
   [response] => 98ccab4542f284c00a79b5957baaff23  
 )  


Entradas relacionadas

Seguridad PHP - Autenticación HTTP básica
Seguridad PHP - Autenticación HTTP vs PHP (formularios)

1 comentario: