При описании формата запросов используется двоичная сериализация данных и язык TL. Все большие числа передаются как строки (string), содержащие нужную последовательность байтов в порядке big-endian. Хэш-функции вроде SHA1 возвращают строки (из 20 байтов), которые также могут быть проинтерпретированы как big-endian числа. Маленькие числа (int, long, int128, int256) обычно little-endian; однако если они представляют собой часть SHA1, то байты не переставляются. Таким образом, если long _x_
есть младшие 64 бита от SHA1 от строки s
, то берутся последние 8 байтов 20-байтной строки SHA1(**s**)
, и интерпретируются как 64-битное целое.
Прежде чем отсылать незашифрованные сообщения (в данном случае — необходимые для создания авторизационного ключа), клиент должен пройти «(p,q)-авторизацию» следующим образом:
Клиент шлёт серверу запрос:
req_pq#60469778
nonce:int128
= ResPQ
Значение nonce выбирается клиентом произвольно (случайное число); оно идентифицирует клиента в рамках данного обмена. После этого шага оно известно всем.
Сервер отправляет ответ в виде:
resPQ#05162463
nonce:int128
server_nonce:int128
pq:string
server_public_key_fingerprints:Vector long
= ResPQ
Здесь «строка» pq
представляет собой запись натурального числа (в двоичной системе в формате big-endian). Это число является произведением двух различных нечетных простых чисел. Обычно pq
не превосходит 2^63-1. Значение server_nonce
выбирается сервером произвольно; после этого шага оно известно всем.
server_public_key_fingerprints
— это список отпечатков публичных RSA-ключей (младшие 64 бита SHA1 (server_public_key); при этом публичный ключ представляется как голый тип rsa_public_key n:string e:string = RSAPublicKey
(bare type; подробней про типы см. в статье «сериализация бинарных данных»), где, как обычно, n
и е
— числа в формате big-endian, сериализованные как строки байтов, после чего и вычисляется SHA1), принимаемых сервером.
Отметим, что все последующие сообщения содержат пару (nonce
, server_nonce
) и в открытой, и в зашифрованной части, что позволяет использовать ее для идентификации «временной сессии» (одного запуска обсуждаемого протокола создания ключей). Злоумышленник не в состоянии создать параллельную сессию с сервером с такими же параметрами и переиспользовать зашифрованные клиентом или сервером части их сообщений в своих целях в параллельной сессии, поскольку для новой «временной сессии» сервером будет выбран другой server_nonce
.
Клиент раскладывает число pq на простые множители p < q
.
Теперь начинается раунд обмена ключей по Диффи—Хелману:
Клиент отправляет запрос:
req_DH_params#d712e4be
nonce:int128
server_nonce:int128
p:string
q:string
public_key_fingerprint:long
encrypted_data:string
= Server_DH_Params
Здесь encrypted_data
получается следующим образом:
p_q_inner_data#83c95aec
pq:string
p:string
q:string
nonce:int128
server_nonce:int128
new_nonce:int256
= P_Q_inner_data
или этого
p_q_inner_data_temp#3c6a84d4
pq:string
p:string
q:string
nonce:int128
server_nonce:int128
new_nonce:int256
expires_in:int
= P_Q_inner_data;
Кто-то может перехватить этот запрос и послать вместо него свой, самостоятельно разложив pq на множители вместо клиента. Единственное поле, которое имеет смысл при этом изменить — это new_nonce
, который злоумышленнику как раз придется генерировать заново (поскольку он не в состоянии расшифровать зашифрованные данные, отправленные клиентом). Поскольку все последующие сообщения зашифрованы с помощью new_nonce или содержат new_nonce_hash
, они не будут обработаны клиентом (злоумышленник не сможет сделать вид, что их сгенерировал сервер, поскольку в них не будет new_nonce
). Поэтому результатом такого перехвата будет только то, что злоумышленник вместо клиента завершит протокол создания авторизационного ключа и создаст новый ключ (не имеющий никакого отношения к клиенту); однако он мог бы и так добиться того же эффекта, просто инициировав создание нового ключа от своего имени.
Альтернативная форма внутренних данных (p_q_inner_data_temp
) используется для создания временных ключей, которые сохраняются только в RAM сервера и стираются не более чем через expires_in
секунд. Сервер свободно может стереть эту копию раньше. Во всех остальных отношениях протокол генерации временных ключей такой же. После того как временный ключ создан, клиент обычно привязывает его к основному ключу авторизации посредством метода auth.bindTempAuthKey
и использует его для всех коммуникаций клиент-сервер до того как его срок истечёт; затем генерируется новый временный ключ. Таким образом в коммуникации клиент-сервер достигается Perfect Forward Secrecy.
server_DH_params_fail#79cb045d
nonce:int128
server_nonce:int128
new_nonce_hash:int128
= Server_DH_Params
server_DH_params_ok#d0e8075c
nonce:int128
server_nonce:int128
encrypted_answer:string
= Server_DH_Params
Здесь encrypted_answer
получается таким образом:
server_DH_inner_data#b5890dba
nonce:int128
server_nonce:int128
g:int
dh_prime:string
g_a:string
server_time:int
= Server_DH_inner_data
После этого шага new_nonce всё ещё известно только клиенту и серверу. Клиент уверен, что ответил именно сервер и что ответ сгенерирован специально в ответ на запрос клиента req_DH_params, поскольку данные ответа шифруются с использованием new_nonce.
От клиента ожидается проверка того, является ли p = dh_prime верным 2048-битным простым числом (имеется в виду, что и p и (p-1)/2 являются простыми числами, и что 2^2047 < p < 2^2048), и что g генерирует циклическую подгруппу простого порядка (p-1)/2, т. е. является квадратичным вычетом mod p. Поскольку g всегда равняется 2, 3, 4, 5, 6 или 7, это легко делается с помощью квадратичного закона взаимности; получается простое условие на p mod 4g — именно, p mod 8 = 7 для g = 2; p mod 3 = 2 для g = 3; нет дополнительного условия для g = 4; p mod 5 = 1 или 4 для g = 5; p mod 24 = 19 или 23 для g = 6; и p mod 7 = 3, 5 или 6 для g = 7. После того как g и p проверены клиентом, имеет смысл кэшировать результат, чтобы в будущем не повторять длинные вычисления.
Если верификация занимает слишком много времени (в случае использования более старых мобильных девайсов), она может вначале запустить только 15 итераций Миллера-Рабина для верификации первичности /принадлежности к простым числам p и (p — 1)/2 с вероятностью ошибки не превышающей одну миллиардную, и сделать больше итераций позднее в фоновом режиме.
Другой оптимизацией может быть встраивание в код приложения клиента маленькую таблицу с некоторыми «хорошими» парами (g,p) (или просто известные верные простые числа p, поскольку g легко проверяется во время выполнения), проверяемую во время фазы генерации кода, чтобы избежать выполнения такой верификации во время выполнения. Сервер изменяет эти значения редко, таким образом обычно требуется подставить в такую таблицу текущее значение dh_prime сервера. Например, текущее значение dh_prime равняется (в порядке байт big-endian):
C7 1C AE B9 C6 B1 C9 04 8E 6C 52 2F 70 F1 3F 73 98 0D 40 23 8E 3E 21 C1 49 34 D0 37 56 3D 93 0F
48 19 8A 0A A7 C1 40 58 22 94 93 D2 25 30 F4 DB FA 33 6F 6E 0A C9 25 13 95 43 AE D4 4C CE 7C 37
20 FD 51 F6 94 58 70 5A C6 8C D4 FE 6B 6B 13 AB DC 97 46 51 29 69 32 84 54 F1 8F AF 8C 59 5F 64
24 77 FE 96 BB 2A 94 1D 5B CD 1D 4A C8 CC 49 88 07 08 FA 9B 37 8E 3C 4F 3A 90 60 BE E6 7C F9 A4
A4 A6 95 81 10 51 90 7E 16 27 53 B5 6B 0F 6B 41 0D BA 74 D8 A8 4B 2A 14 B3 14 4E 0E F1 28 47 54
FD 17 ED 95 0D 59 65 B4 B9 DD 46 58 2D B1 17 8D 16 9C 6B C4 65 B0 D6 FF 9C A3 92 8F EF 5B 9A E4
E4 18 FC 15 E8 3E BE A0 F8 7F A9 FF 5E ED 70 05 0D ED 28 49 F4 7B F9 59 D9 56 85 0C E9 29 85 1F
0D 81 15 F6 35 B1 05 EE 2E 4E 15 D0 4B 24 54 BF 6F 4F AD F0 34 B1 04 03 11 9C D8 E3 B9 2F CC 5B
Клиент вычисляет рандомное 2048-битное число b (используя достаточное количество энтропии) и посылает серверу запрос:
set_client_DH_params#f5045f1f
nonce:int128
server_nonce:int128
encrypted_data:string
= Set_client_DH_params_answer
Здесь encrypted_data
получается таким образом:
client_DH_inner_data#6643b654
nonce:int128
server_nonce:int128
retry_id:long
g_b:string
= Client_DH_Inner_Data;
Поле retry_id
равняется нулю во время первой попытки; или равняется auth_key_aux_hash от предыдущей зафейленной попытки (см. пункт 9).
Соответственно, auth_key
равняется pow(g, {ab}) mod dh_prime
; на сервере оно вычисляется как pow(g_b, a) mod dh_prime
, а на клиенте как (g_a)^b mod dh_prime
.
auth_key_hash
вычисляется := 64 бита нижнего порядка SHA1 (auth_key). Сервер проверяет, если ли уже другой ключ с таким же auth_key_hash и отвечает одним из следующих способов.
Сервер отвечает одним из трёх способов:
dh_gen_ok#3bcbf734
nonce:int128
server_nonce:int128
new_nonce_hash1:int128
= Set_client_DH_params_answer
dh_gen_retry#46dc1fb9
nonce:int128
server_nonce:int128
new_nonce_hash2:int128
= Set_client_DH_params_answer
dh_gen_fail#a69dae02
nonce:int128
server_nonce:int128
new_nonce_hash3:int128
= Set_client_DH_params_answer
new_nonce_hash1
, new_nonce_hash2
, и new_nonce_hash3
вычисляются как 128 бит нижнего порядка SHA1 строки байтов, полученной из строки new_nonce добавлением одного байта со значением 1, 2 или 3, с последующими другими 8 байтами с auth_key_aux_hash. Разные значения необходимы, чтобы помешать нарушителю изменить ответ сервера dh_gen_ok
на dh_gen_retry
.auth_key_aux_hash
это 64 бита высшего порядка SHA1(auth_key). Его нельзя путать с auth_key_hash
.В другом случае, клиент обращается к пункту 6) генерируя новое b.
В первом случае, клиент и сервер реализуют auth_key
, после чего они забывают все остальные временные данные, и клиент создаёт другую зашифрованную сессию используя auth_key
. В то же самое время server_salt вначале устанавливается как substr(new_nonce, 0, 8) XOR substr(server_nonce, 0, 8)
. При необходимости клиент сохраняет разницу между server_time полученным в пункте 5) и своим местным временем, чтобы иметь хорошую приблизительную точность к времени сервера, которая нужна для генерации правильных идентификаторов сообщения.
ВАЖНО: Помимо условий простого числа Диффи-Хеллмана dh_prime и генератора g, обе стороны должны проверить что g, g_a и g_b больше 1 и меньше чем dh_prime − 1. Мы рекомендуем также проверять, что g_a и g_b находятся между 2^{2048-64} и dh_prime − 2^{2048-64}.
В том случае, если клиент не получает никакого ответа от сервера на свой запрос в течение какого-то времени, он может просто повторить запрос. Если сервер уже отправлял ответ на этот запрос (ровно на этот запрос, а не на такой же — все параметры при повторе должны принимать то же значение), но он не дошел до клиента, сервер просто повторит тот же ответ. Сервер помнит ответ до 10 минут после момента получения запроса 1). Если же сервер уже забыл ответ или необходимые временные данные, клиенту придется начать все с самого начала.
Сервер вправе считать, что если клиент уже прислал следующий запрос, использующий данные из предыдущего ответа сервера данному клиенту, то этот ответ заведомо получен клиентом и сервер может его забыть.
Пример полного списка запросов, необходимый для генерации ключа авторизации, показан на отдельной странице: «создание ключа авторизации — примеры».