HMAC (сокращение от англ. hash-based message authentication code, код проверки подлинности сообщений, использующий односторонние хеш-функции) — в криптографии, один из механизмов проверки целостности информации, позволяющий гарантировать то, что данные, передаваемые или хранящиеся в ненадёжной среде, не были изменены посторонними лицами (атака типа «man in the middle»).
К таким данным могут относиться например данные, передаваемые в запросах API, когда критически важна целостность передаваемой информации, или же при передаче данных из Web-форм.
Если отойти от научной формулировки, что же такое подпись данных и как это реализуется на практике?
Предположим, мы хотим отправить какие-то данные другому человеку, при этом, и нам и получателю важно убедиться, что данные не будут изменены в процессе передачи.
Например, мы имеем исходный массив данных вида:
Самое простое, что мы можем сделать – это каким-то образом сериализовать массив (привести его в строковое представление), добавить в конец получившейся строки некий секретный ключ – набор символов известный только нам и получателю данных (пусть будет “mysecretkey”), после чего применить к этому какую-нибудь хеш-функцию, скажем, md5.
Какие можно встретить практические решения? В зависимости от того, важны ли значения массива или его ключи, можно встретить, например, такие реализации:
или
Какие есть плюсы и недостатки у данных реализаций?
Самый первый и очевидный плюс, но он же и единственный – это простота реализации. Минусы? Их масса, как минимум в качестве ключевых недостатков, можно привести:
Нужно отметить, что последний недостаток в некоторых случаях, как таковым недостатком и не является – например если мы хотим проверить данные из Web-формы, заполняемой человеком и можем проверить только целостность набора полей, а их значения нам заранее неизвестны.
В частности, таким образом, можно формировать более продвинутые CSRF-токены для форм, используя в качестве секретного ключа какой-нибудь внутренний идентификатор, привязанный к сессии пользователя. Тем самым мы решим сразу две задачи – и защиту от CSRF и контроль целостности набора передаваемых параметров – пользователь уже не сможет «поиграться» с полями формы и попробовать добавить или убрать что-нибудь из параметров.
Остальные три пункта требуют решения. С первым все достаточно просто – используем более современные и стойки хеш-аглоритмы с большей длиной шифрограммы, такие, как SHA256 или SHA512 и спим спокойно.
Второй пункт – тоже решается, если определиться, что элементы массива будут отсортированы по какому-то принципу, скажем, в алфавитном порядке.
Добавлеям перед сериализацией массива:
в результате чего, получаем массив, отсортированный по ключам в алфавитном порядке, и нам становится не важно, в какой последовательности были переданы переменные в массиве.
Следующая проблема решается уже не так просто. Пока мы работали с плоскими массивами все было достаточно просто – отсортировали, в строчку сложили с каким-нибудь разделителем типа ”;” – и готово. Но как же быть с вложенными (многомерными) массивами?
Во-первых функция ksort не рекурсивная, но это, конечно же не большая проблема и решение было найдено достаточно быстро:
Во-вторых, массив с вложенностями линейно в строчку уже не сложишь – нужно придумывать дополнительные правила (то есть, изобретать велосипед), или использовать уже «настоящую» сериализацию, такую как JSON, который учитывал бы все вложенные структуры. Использование JSON также решает и четвертую проблему, так как мы сериализуем сразу весь массив, не ограничиваясь отдельно его ключами или значениями.
Почему именно JSON а не простой serialize PHP? Выбор в пользу JSON упал не случайно, поскольку это очень популярный формат сериализации, с которым будет легко работать не только в PHP, но и в любых других популярных языках программирования, таких как Java. Наша реализация должна быть предельно легко переносима на другие платформы и с использованием JSON-сериализации это будет сделать проще всего.
В этом случае все перечисленные проблемы решаются, но встает вопрос с секретным ключом – делать простую конкатенацию ключа справа конечно можно, но это не очень эстетично, благо в PHP есть реализация HMAC с выбором произвольной хэш-фукции:
HMAC реализует дополнительное XOR данных с ключом и оборачивает сверху указанной хеш-функцией. Сам алгоритм внутри HMAC подробно описан в литературе по криптографии, или в википедии и описывать здесь, каким именно образом происходит шифрование данных на ключе мы не будем. Просто будем пользоваться этим стандартным аглоритмом.
Сводя все изложенное вместе, разработаем простой класс, который реализовывал бы все описанные действия для получения подписи с многомерного массива, вне зависимости от того, в какой последовательности находятся ключи внутри массива.
Итак, получился следующий незамысловатый код:
Вкратце разберем функциональность класса. В свойствах класса объявлены две приватные переменные для ключа и алгоритма, а также переменная $sign_param_name, в которой содержится имя параметра с подписью (по-умолчанию равно “hmac”), который будет использоваться при проверке данных методом check_data_hmac по-умолчанию.
В конструктор передается один обязательный параметр – это секретный ключ. По-умолчанию выбран алгоритм хеш-функции sha256. Можно переопределить алгоритм, передав его вторым параметром в конструктор. В случае, если переданный алгоритм не поддерживается системой, вернется значение константы E_UNSUPPORTED_HASH_ALGO (то есть -1).
Для создания подписи предусмотрен метод:
С ним все довольно просто – обязательный аргумент это данные, можно также использовать для формирования подписи другой секретный ключ, передав его вторым параметром.
Для проверки ранее созданной подписи мы реализовали метод
Метод принимает аргументы:
Сама подпись при этом должна быть внутри $data в параметре с ключом $sign_param_name. Если последний не передан – то будет использовано имя из свойства объекта $this->sign_param_name.
В остальном логика очень проста – собираем подпись, сравниваем регистронезависимо полученную подпись с подписью, переданной в данных.
Метод set_hash_algo, позволяет поменять алгоритм хеш-функции после создания экземпляра объекта. Функция рекурсивной сортировки массива реализована в качестве статического метода, чтобы ее можно было использовать вне экземпляра объекта где-то еще.
Проиллюстрируем работу класса на простом примере:
На выходе получим:
Мы получили простую реализацию, позволяющую нам подписывать любые данные и проверять переданные подписанные данные. Теперь вы можете подписывать данные, передаваемые через HTTP/REST API, или же создавать продвинутые CSRF-токены для форм и быть уверенными в том, что получаемые данные оригинальны и консистентны.
Все исходные коды доступны в репозитории на GitHub: github.com/idsolutions/HMAC_generator
P.S. Можете форкать и дорабатывать класс на свое усмотрение, комментарии и предложения приветствуются.
К таким данным могут относиться например данные, передаваемые в запросах API, когда критически важна целостность передаваемой информации, или же при передаче данных из Web-форм.
Зачем это нужно?
Если отойти от научной формулировки, что же такое подпись данных и как это реализуется на практике?
Предположим, мы хотим отправить какие-то данные другому человеку, при этом, и нам и получателю важно убедиться, что данные не будут изменены в процессе передачи.
Например, мы имеем исходный массив данных вида:
$data = array(
'param1' => 'value1',
'param2' => 'value2',
'param3' => 'sometext',
'a' => 'b'
);
Самое простое, что мы можем сделать – это каким-то образом сериализовать массив (привести его в строковое представление), добавить в конец получившейся строки некий секретный ключ – набор символов известный только нам и получателю данных (пусть будет “mysecretkey”), после чего применить к этому какую-нибудь хеш-функцию, скажем, md5.
Какие можно встретить практические решения? В зависимости от того, важны ли значения массива или его ключи, можно встретить, например, такие реализации:
$hash = md5(implode(";",array_keys($data)).";"."mysecretkey")) = md5("param1;param2;param3;a;mysecretkey")
или
$hash = md5(implode(";",array_values($data)).";"."mysecretkey") = md5("value1;value2;sometext;b;secretkey")
Какие есть плюсы и недостатки у данных реализаций?
Самый первый и очевидный плюс, но он же и единственный – это простота реализации. Минусы? Их масса, как минимум в качестве ключевых недостатков, можно привести:
- MD5 – уже старый алгоритм, который не считается стойким;
- Если нарушится последовательность параметров или значений – подпись не сойдется;
- Как быть с вложенными (многомерными) массивами?
- Либо ключи, либо значения.
Нужно отметить, что последний недостаток в некоторых случаях, как таковым недостатком и не является – например если мы хотим проверить данные из Web-формы, заполняемой человеком и можем проверить только целостность набора полей, а их значения нам заранее неизвестны.
В частности, таким образом, можно формировать более продвинутые CSRF-токены для форм, используя в качестве секретного ключа какой-нибудь внутренний идентификатор, привязанный к сессии пользователя. Тем самым мы решим сразу две задачи – и защиту от CSRF и контроль целостности набора передаваемых параметров – пользователь уже не сможет «поиграться» с полями формы и попробовать добавить или убрать что-нибудь из параметров.
Остальные три пункта требуют решения. С первым все достаточно просто – используем более современные и стойки хеш-аглоритмы с большей длиной шифрограммы, такие, как SHA256 или SHA512 и спим спокойно.
Второй пункт – тоже решается, если определиться, что элементы массива будут отсортированы по какому-то принципу, скажем, в алфавитном порядке.
Добавлеям перед сериализацией массива:
ksort($data);
в результате чего, получаем массив, отсортированный по ключам в алфавитном порядке, и нам становится не важно, в какой последовательности были переданы переменные в массиве.
Следующая проблема решается уже не так просто. Пока мы работали с плоскими массивами все было достаточно просто – отсортировали, в строчку сложили с каким-нибудь разделителем типа ”;” – и готово. Но как же быть с вложенными (многомерными) массивами?
Во-первых функция ksort не рекурсивная, но это, конечно же не большая проблема и решение было найдено достаточно быстро:
function ksort_recursive(&$array, $sort_flags = SORT_REGULAR)
{
if (!is_array($array)) return false;
ksort($array, $sort_flags);
foreach ($array as &$arr) {
ksort_recursive($arr, $sort_flags);
}
return true;
}
Во-вторых, массив с вложенностями линейно в строчку уже не сложишь – нужно придумывать дополнительные правила (то есть, изобретать велосипед), или использовать уже «настоящую» сериализацию, такую как JSON, который учитывал бы все вложенные структуры. Использование JSON также решает и четвертую проблему, так как мы сериализуем сразу весь массив, не ограничиваясь отдельно его ключами или значениями.
Почему именно JSON а не простой serialize PHP? Выбор в пользу JSON упал не случайно, поскольку это очень популярный формат сериализации, с которым будет легко работать не только в PHP, но и в любых других популярных языках программирования, таких как Java. Наша реализация должна быть предельно легко переносима на другие платформы и с использованием JSON-сериализации это будет сделать проще всего.
В этом случае все перечисленные проблемы решаются, но встает вопрос с секретным ключом – делать простую конкатенацию ключа справа конечно можно, но это не очень эстетично, благо в PHP есть реализация HMAC с выбором произвольной хэш-фукции:
hash_hmac(“sha256”,$data,”mysecretkey”);
HMAC реализует дополнительное XOR данных с ключом и оборачивает сверху указанной хеш-функцией. Сам алгоритм внутри HMAC подробно описан в литературе по криптографии, или в википедии и описывать здесь, каким именно образом происходит шифрование данных на ключе мы не будем. Просто будем пользоваться этим стандартным аглоритмом.
Сводя все изложенное вместе, разработаем простой класс, который реализовывал бы все описанные действия для получения подписи с многомерного массива, вне зависимости от того, в какой последовательности находятся ключи внутри массива.
Итак, получился следующий незамысловатый код:
// определим коды ошибок, которые мы будем возвращать.
define("E_UNSUPPORTED_HASH_ALGO",-1);
class HMAC_Generator{
private $key, $algo;
private $sign_param_name = "hmac";
function __construct($key, $algo = "sha256"){
$this->key = $key;
$this->algo = $algo;
}
function make_data_hmac($data, $key = NULL){
// если не задан ключ в параметре - используем из свойств
if(empty($key)) $key = $this->key;
// если параметр с подписью есть в массиве - уберем.
if(isset($data[$this->sign_param_name])) unset($data[$this->sign_param_name]);
// отсортируем по ключам в алфавитном порядке -
// на случай, если последовательность полей изменилась
// например, если данные передавались GET- или POST-запросом.
HMAC_Generator::ksort_recursive($data);
// сформируем JSON (или другую сериализацию - можно переопределить метрд encode_string)
$data_enc = $this->serialize_array($data);
// формируем и возвращаем подпись
return $this->make_signature($data_enc, $key);
}
function check_data_hmac($data, $key = NULL, $sign_param_name = NULL){
// если не задан ключ в аргументах - используем из свойств
if(empty($key)) $key = $this->key;
// если не задано имя параметра с подписью аргументах в параметре - используем из свойств
if(empty($sign_param_name)) $sign_param_name = $this->sign_param_name;
// если в данных нет подписи - сразу вернем false
if(empty($data[$sign_param_name])) return false;
// исходный HMAC нам приходит в том же массиве, что и данные,
// заберем его значение для сверки и выкинем из массива
$hmac = $data[$sign_param_name];
unset($data[$sign_param_name]);
// сформируем контрольный HMAC
$orig_hmap = $this->make_data_hmac($data, $key);
// проверку осуществляем регистронезависимо
if(strtolower($orig_hmap) != strtolower($hmac)) return false;
else return true;
}
// Установка алгоритма хеширования
function set_hash_algo($algo){
// приведем к нижнему регистру
$algo = strtolower($algo);
// проверим, поддерживается ли системой выбранный алгоритм
if(in_array($algo, hash_algos()))
$this->algo = $algo;
else return
E_UNSUPPORTED_HASH_ALGO;
}
//
// сериализацию и хеширование - выносим в отдельные методы, просто перепишите или переопределите их
//
private function serialize_array($data){
// кодируем все в json, в случае если мы будем собирать подпись не только в PHP,
// такой тип сериализации - оптимальный
$data_enc = json_encode($data, JSON_UNESCAPED_UNICODE);
return $data_enc;
}
// переопределите, если будет другой алго формирования подписи, не HASH HMAC
private function make_signature($data_enc, $key){
// сформируем подпись HMAC при помощи выбранного аглоритма
$hmac = hash_hmac($this->algo, $data_enc, $key);
return $hmac;
}
// статический метод для рекурсивной сортировки массива по именам ключей
public static function ksort_recursive(&$array, $sort_flags = SORT_REGULAR) {
// если это не массив - сразу вернем false
if (!is_array($array)) return false;
ksort($array, $sort_flags);
foreach ($array as &$arr) {
HMAC_Generator::ksort_recursive($arr, $sort_flags);
}
return true;
}
}
Вкратце разберем функциональность класса. В свойствах класса объявлены две приватные переменные для ключа и алгоритма, а также переменная $sign_param_name, в которой содержится имя параметра с подписью (по-умолчанию равно “hmac”), который будет использоваться при проверке данных методом check_data_hmac по-умолчанию.
В конструктор передается один обязательный параметр – это секретный ключ. По-умолчанию выбран алгоритм хеш-функции sha256. Можно переопределить алгоритм, передав его вторым параметром в конструктор. В случае, если переданный алгоритм не поддерживается системой, вернется значение константы E_UNSUPPORTED_HASH_ALGO (то есть -1).
Для создания подписи предусмотрен метод:
make_data_hmac($data, [$key])
С ним все довольно просто – обязательный аргумент это данные, можно также использовать для формирования подписи другой секретный ключ, передав его вторым параметром.
Для проверки ранее созданной подписи мы реализовали метод
check_data_hmac($data, [$key], [$sign_param_name])
Метод принимает аргументы:
- $data – массив с данными;
- $key – необязательный аргумент, в котором можно передать секретный ключ. Иначе будет использован ключ из свойств объекта;
- $sign_param_name – имя элемента массива, содержащего контрольную подпись.
Сама подпись при этом должна быть внутри $data в параметре с ключом $sign_param_name. Если последний не передан – то будет использовано имя из свойства объекта $this->sign_param_name.
В остальном логика очень проста – собираем подпись, сравниваем регистронезависимо полученную подпись с подписью, переданной в данных.
Метод set_hash_algo, позволяет поменять алгоритм хеш-функции после создания экземпляра объекта. Функция рекурсивной сортировки массива реализована в качестве статического метода, чтобы ее можно было использовать вне экземпляра объекта где-то еще.
Примеры
Проиллюстрируем работу класса на простом примере:
// данные будут приведены к последовательности param1, param2, param3 в результате работы ksort
$data = array(
'param3' => 'sometext',
'param1' => 'value1',
'param2' => 'value2',
);
// разные алгоритмы, по-умолчанию - SHA256
$hmac_generator = new HMAC_Generator("myprivatekey");
$hmac_generator_md5 = new HMAC_Generator("myprivatekey","md5");
$hmac_generator_sha1 = new HMAC_Generator("myprivatekey","sha1");
echo "SHA256: ".$hmac_generator->make_data_hmac($data)."\n";
echo "MD5: ".$hmac_generator_md5->make_data_hmac($data)."\n";
echo "SHA1: ".$hmac_generator_sha1->make_data_hmac($data)."\n";
На выходе получим:
SHA256: 7f0a656e00d3a17ab0d04170dfcb4583b4e29e184b9a24d7fed869979d0bf7e8
MD5: 4f91a268c5a8fc4eaa19d7d7cf329583
SHA1: 8c4a7288be7a76fa2c1bd7d481718d1c49d6bca0
Вместо заключения
Мы получили простую реализацию, позволяющую нам подписывать любые данные и проверять переданные подписанные данные. Теперь вы можете подписывать данные, передаваемые через HTTP/REST API, или же создавать продвинутые CSRF-токены для форм и быть уверенными в том, что получаемые данные оригинальны и консистентны.
Все исходные коды доступны в репозитории на GitHub: github.com/idsolutions/HMAC_generator
P.S. Можете форкать и дорабатывать класс на свое усмотрение, комментарии и предложения приветствуются.