Как стать автором
Обновить

Подписываем данные: HMAC на практике в API и Web-формах

Время на прочтение8 мин
Количество просмотров70K
HMAC (сокращение от англ. hash-based message authentication code, код проверки подлинности сообщений, использующий односторонние хеш-функции) — в криптографии, один из механизмов проверки целостности информации, позволяющий гарантировать то, что данные, передаваемые или хранящиеся в ненадёжной среде, не были изменены посторонними лицами (атака типа «man in the middle»).

К таким данным могут относиться например данные, передаваемые в запросах 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. Можете форкать и дорабатывать класс на свое усмотрение, комментарии и предложения приветствуются.
Теги:
Хабы:
Всего голосов 18: ↑14 и ↓4+10
Комментарии101

Публикации

Истории

Работа

Ближайшие события

19 августа – 20 октября
RuCode.Финал. Чемпионат по алгоритмическому программированию и ИИ
МоскваНижний НовгородЕкатеринбургСтавропольНовосибрискКалининградПермьВладивостокЧитаКраснорскТомскИжевскПетрозаводскКазаньКурскТюменьВолгоградУфаМурманскБишкекСочиУльяновскСаратовИркутскДолгопрудныйОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
24 – 25 октября
One Day Offer для AQA Engineer и Developers
Онлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
26 октября
ProIT Network Fest
Санкт-Петербург
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань