Autenticare richieste HTTP con HMAC

Tempo fa abbiamo realizzato un servizio per chiamare procedure remote tramite JSON-RPC, ma il protocollo non prevede alcun metodo di autenticazione.

Il compito di garantire l’autenticità del messaggio è lasciato al programmatore, che può, ad esempio, scegliere uno dei metodi tradizionali che utilizzano il protocollo HTTP, come Basic access authentication o il poco più sicuro Digest access authentication.

Oggi vedremo come implementare il sistema di autenticazione HMAC.

HMAC

HMAC è un metodo di autenticazione dei messaggi basato su una funzione di hash. L’algoritmo sfrutta una chiave segreta e parte del messaggio (o il messaggio per intero) per generare un codice “firma” che permette di verificare sia l’autenticità che l’integrità del messaggio.

La robustezza del codice generato dipende direttamente dalla funzione di hash utilizzata e dalla dimensione della chiave segreta, ad esempio HMAC-MD5 sarà meno sicuro di HMAC-SHA1 (MD5 e SHA-1 sono le funzioni di hash più usate). Inoltre il rischio di collisione di un codice HMAC è sostanzialmente inferiore rispetto al rischio di collisione della sua sola funzione di hash.

Quale funzione di hash usare?

A questo punto la prima cosa da decidere è la funzione di hash da utilizzare, ma indipendentemente dalla scelta, il procedimento per generare il codice di autenticazione sarà esattamente lo stesso.

La funzione di hash va scelta in base alle richieste di sicurezza dell’applicazione in oggetto, e magari dalla disponibilità di librerie ad alto livello per la generazione dell’hash se si vuole facilitare il compito a sviluppatori esterni.

La wikipedia inglese contiene un’interessante tabella di comparazione degli algoritmi di hashing e della loro robustezza.

La funzione MD5 risulta un po’ troppo debole per gli standard computazionali odierni, mentre la SHA-1 è per ora ancora considerata sufficientemente sicura, anche se c’è stato qualche raro caso di attacco riuscito a creare una collisione con un numero di operazioni molto inferiore rispetto a quelli che dovrebbero essere garantiti dalla lunghezza della chiave e sono presenti dei limiti teorici per i quali è previsto che nei prossimi anni non sarà troppo difficile bucare l’algoritmo.

Sarebbe quindi lungimirante scegliere una funzione di hashing ancora più robusta, come SHA-256/512 (SHA-2).

Implementazione

In PHP esiste già una implementazione completa dell’algoritmo hmac, che è presente in php dalla versione 5.1.2, mentre precedentemente era implementato in un modulo PECL.

Se il modulo non risulta presente, magari perché non abilitato nel php.ini, questa implementazione risulta completamente equivalente ma funziona solo con le funzioni md5 e sha1:

if (!function_exists('hash_hmac')) {
    function hash_hmac($hashfunc, $data, $key) {
        $blocksize = 64;
        if (strlen($key) > $blocksize)
            $key = pack('H*', $hashfunc($key));
        $key = str_pad($key, $blocksize, chr(0x00));
        $ipad = str_repeat(chr(0x36), $blocksize) ^ $key;
        $opad = str_repeat(chr(0x5C), $blocksize) ^ $key;

        $hmac = pack('H*', $hashfunc( $opad.pack('H*', $hashfunc($ipad.$data)) ));
        return bin2hex($hmac);
    }
}

Ora dobbiamo decidere quali sono i dati da firmare e, soprattutto, dove inserire la firma all’interno della richiesta.
Questo metodo si può utilizzare per qualsiasi richiesta HTTP ma riprendiamo l’esempio fatto nell’articolo su JSON-RPC e estendiamolo, firmando la richiesta.

La funzione che generava la richiesta era:

function json_rpc_call($url, $method, $params=null) {
    $request = array(
        'method' => $method,
        'id' => 1
    );
    if (is_array($params))
        $request['params'] = $params;
    $request = json_encode($request);

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
        'Content-Type: application/json',
        'Content-Length: ' . strlen($request) . "\\r\\n",
        $request
    ));
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $output = curl_exec($ch);
    curl_close($ch);

    $output = json_decode($output, true);

    if (!is_null($output['error']))
        throw new Exception($output['error']['message']);

    return $output['result'];
}

I dati da firmare, sono ovviamente i parametri della richiesta, mentre per quanto riguarda il posizionamento della firma, utilizzeremo un header “Authorization: AuthHMAC CODICEBASE64”.
Opzionalmente potremmo inserirci anche l’identificativo utente corrispondente alla firma, in modo da utilizzare una diversa chiave segreta per ogni utente. In questo caso l’header sarà “Authorization: AuthHMAC ID:CODICEBASE64”.

// Variabili d'esempio, queste normalmente arriverebbero da un database, dopo aver fatto il login
$user_id = 1337;
$user_secret = 'mysecret';

function json_rpc_call($url, $method, $params=null) {
    global $user_id, $user_secret;

    $request = array(
        'method' => $method,
        'id' => 1
    );
    if (is_array($params))
        $request['params'] = $params;
    $request = json_encode($request);

    $hmac = hash_hmac('sha1', $request, $user_secret);
    $hmac = base64_encode($hmac);

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
        "Authentication: AuthHMAC $user_id:$hmac",
        'Content-Type: application/json',
        'Content-Length: ' . strlen($request) . "\r\n",
        $request
    ));
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $output = curl_exec($ch);
    curl_close($ch);

    $output = json_decode($output, true);

    if (!is_null($output['error']))
        throw new Exception($output['error']['message']);

    return $output['result'];
}

Mentre lato server, dobbiamo verificare che la richiesta sia autorizzata, quindi prima di lasciare che Zend_Json_Server la elabori, aggiungiamo i dovuti controlli:

// Header Authentication obbligatorio
if (empty($_SERVER['HTTP_AUTHENTICATION']) || strpos($_SERVER['HTTP_AUTHENTICATION'], 'AuthHMAC ') !== 0)
    die("AUTHENTICATION FAILED");

// Separo l'id utente dal codice hmac e lo decodifico da base64
list($user_id, $hmac) = explode(':', substr($_SERVER['HTTP_AUTHENTICATION'], 9));
$hmac = base64_decode($hmac);

function get_secret_by_user($user_id) {
    // normalmente interrogherebbe il database, ma saltiamo l'implementazione che non ci interessa
    if ($user_id == '1337')
        return 'mysecret';
    else
        return '';
}

// Ricalcolo l'hmac e lo confronto con quello associato alla richiesta
if ( hash_hmac('sha1', file_get_contents("php://input"), get_secret_by_user($user_id)) != $hmac )
    die("AUTHENTICATION FAILED");

// Elabora richiesta JSON-RPC
$server->handle();

Ora la richiesta può essere effettuata solo da un client che conosce la chiave segreta. In un’implementazione completa potrebbe essere furbo fare in modo che la chiave venga auto-generata dal server al momento dell’autenticazione dell’utente e salvata in sessione dopo averla comunicata al client. In questo modo ad ogni sessione corrisponde una nuova chiave usa e getta rendendo il tutto ancora più sicuro.

Il sorgente è disponibile per il download.

Lascia un commento

Tutti i campi sono obbligatori.
L'indirizzo email non verrà pubblicato

 

Commenti

  1. Pingback: Rilevare se all’utente piace la tua pagina Facebook « simogrima