电报 MTProto 创建授权密钥
查询格式使用二进制数据序列化和TL 语言描述。所有大数都以字符串形式传输,字符串包含按大端序排列的所需字节序列。哈希函数(例如 SHA1)返回的字符串(20 字节)也可以解释为大端序数字。小数(int例如long1、2、3 )通常为小端序;但是,如果它们是 SHA1 的一部分,int128则int256字节不会被重新排列。这样,如果longx是字符串 的 SHA1 低 64 位s,则取20 字节字符串的最后SHA1(s)8 个字节并将其解释为一个 64 位整数。
在发送未加密消息(在这种情况下需要生成授权密钥)之前,客户端必须按如下方式进行 (p,q) 授权。
DH交换启动
1)客户端向服务器发送查询
req_pq_multi#be7e8ef1 nonce:int128 = ResPQ;
nonce值由客户端随机选择(随机数),用于在本次通信中识别客户端。完成此步骤后,所有人都知道该客户端的信息。
2) 服务器发送表单响应
resPQ#05162463 nonce:int128 server_nonce:int128 pq:string server_public_key_fingerprints:Vector long = ResPQ;
这里,字符串 pq 表示一个自然数(二进制大端序格式)。这个数是两个不同的奇素数的乘积。通常情况下,pq 小于或等于 2^63-1。server_nonce 的值由服务器随机选择;此步骤之后,所有人都知道server_nonce的值。
server_public_key_fingerprints是服务器收到的 RSA 公钥指纹列表(SHA1 的 64 位低位(server_public_key);公钥表示为裸类型rsa_public_key n:string e:string = RSAPublicKey,其中,像往常一样,n 和 e 是以大端格式序列化为字节字符串的数字,之后计算 SHA1)。
所有后续消息的明文和加密部分都包含 (nonce, server_nonce) 对,这使得识别“临时会话”成为可能——即本页所述密钥生成协议的一次运行,该运行使用相同的 (nonce, server_nonce) 对。入侵者无法使用相同的参数与服务器创建并行会话,也无法在这样的并行会话中重用服务器端或客户端加密的消息片段用于自身目的,因为服务器会为任何新的“临时会话”选择不同的 server_nonce。
工作证明
3) 客户将 pq 分解为质因数,使得 p < q。
这将启动一轮 Diffie-Hellman 密钥交换。
提供工作量证明;服务器身份验证
4)encrypted_data有效载荷生成
首先,encrypted_data按如下方式生成有效载荷:
-
new_nonce := 客户端生成的另一个(好的)随机数;在此查询之后,客户端和服务器都知道它;
-
数据 := 序列化
p_q_inner_data_dc#a9f55f95 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int = P_Q_inner_data;
或
p_q_inner_data_temp_dc#56fddf88 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int expires_in:int = P_Q_inner_data;
dc我们正在与之通信的 DC 的 ID 在哪里?10000连接到测试服务器时,必须将其添加到 DC ID 中;如果要连接的 DC 是媒体(非 CDN)DC,则必须将其设为负数。
-
encrypted_data := RSA_PAD (data, server_public_key),其中 RSA_PAD 是 RSA 的一个版本,带有如下 4.1 节中解释的 OAEP+ 填充变体。
有人可能会拦截查询并将其替换为自己的查询,从而独立于客户端将 pq 分解为因子。唯一值得修改的字段是 new_nonce,因为入侵者必须重新生成该字段(入侵者无法解密客户端发送的加密数据)。由于所有后续消息都使用 new_nonce 加密或包含 new_nonce_hash,因此客户端不会处理它们(入侵者无法伪造这些消息由服务器生成,因为它们不包含 new_nonce)。因此,这种拦截只会导致入侵者代替客户端完成授权密钥生成协议并创建一个新密钥(与客户端无关);然而,只需以自己的名义创建一个新密钥即可达到同样的效果。
另一种内部数据形式(p_q_inner_data_temp_dc)用于创建临时密钥,这些密钥仅存储在服务器内存中,最多expires_in几秒钟后即被丢弃。服务器可以随时提前丢弃其副本。除此之外,临时密钥生成协议与主密钥生成协议相同。创建临时密钥后,客户端通常使用 ` auth.bindTempAuthKey`方法将其绑定到其主授权密钥,并将其用于所有客户端-服务器通信,直到密钥过期;过期后,系统会生成一个新的临时密钥。由此,客户端-服务器通信中实现了完美前向保密 (PFS)。了解更多关于 PFS 的信息 »
RSA_PAD(data, server_public_key)上述4.1)的具体实现方式如下:
- `data_with_padding := data + random_padding_bytes;` -- 其中 `random_padding_bytes` 的值经过选择,使得 `data_with_padding` 的最终长度恰好为 192 字节,而 `data` 是之前经过 TL 序列化处理后需要加密的数据。需要检查 `data` 的长度是否不超过 144 字节。
- data_pad_reversed := BYTE_REVERSE(data_with_padding); -- 通过反转字节顺序从 data_with_padding 获得。
- 生成一个随机的 32 字节 temp_key。
- data_with_hash := data_pad_reversed + SHA256(temp_key + data_with_padding); -- 经过此赋值后,data_with_hash 正好是 224 字节长。
- aes_encrypted := AES256_IGE(data_with_hash, temp_key, 0); -- 使用零 IV 的 AES256-IGE 加密。
- temp_key_xor := temp_key XOR SHA256(aes_encrypted); -- 调整后的密钥,32 字节
- key_aes_encrypted := temp_key_xor + aes_encrypted; -- 正好 256 字节(2048 位)长
- 将 key_aes_encrypted 的值与 server_pubkey 的 RSA 模数进行比较,比较结果为大端序 2048 位(256 字节)无符号整数。如果 key_aes_encrypted 大于或等于 RSA 模数,则重复之前从生成新的随机 temp_key 开始的步骤。否则,执行最后一步:
- encrypted_data := RSA(key_aes_encrypted, server_pubkey); -- 将 256 字节的大端整数提升到 RSA 公钥模 RSA 模数所需的幂,并将结果存储为由 256 字节组成的大端整数(如果需要,带有前导零字节)。
5) 发送带有生成的 req_DH_params 查询。encrypted_data
req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string = Server_DH_Params
6) 服务器响应:
server_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:string = Server_DH_Params;
如果查询不正确,服务器将返回-404错误,并且必须重新启动握手(任何后续请求-404,即使正确,也会返回错误)。如果在与生产域控制器握手时传递了测试域控制器 ID ,反之亦然,也可能返回
错误。-444p_q_inner_data_(_temp)dc
这里,encrypted_answer 的获取方式如下:
- new_nonce_hash := SHA1 的低 128 位(new_nonce);
-
答案 := 序列化
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;
- answer_with_hash := SHA1(answer) + answer + (0-15 个随机字节);使得长度能被 16 整除;
- tmp_aes_key := SHA1(new_nonce + server_nonce) + substr (SHA1(server_nonce + new_nonce), 0, 12);
- tmp_aes_iv := substr (SHA1(server_nonce + new_nonce), 12, 8) + SHA1(new_nonce + new_nonce) + substr (new_nonce, 0, 4);
- encrypted_answer := AES256_ige_encrypt (answer_with_hash, tmp_aes_key, tmp_aes_iv); 其中,tmp_aes_key 是一个 256 位密钥,tmp_aes_iv 是一个 256 位初始化向量。与所有其他使用 AES 加密的实例一样,加密数据在加密之前会立即用随机字节填充到长度可被 16 整除的位置。
完成此步骤后,new_nonce 仍然只有客户端和服务器知道。客户端可以确定响应来自服务器,并且该响应是专门针对客户端查询 req_DH_params 生成的,因为响应数据已使用 new_nonce 加密。
客户端需要检查p = dh_prime是否为安全的 2048 位素数(即p和(p-1)/2均为素数,且 2^2047 < p < 2^2048),以及g是否生成一个素数阶为(p-1)/2的循环子群,即 g 是否模 p 为二次剩余。由于g始终等于 2、3、4、5、6 或 7,因此可以使用二次互反律轻松完成此操作,从而得到p mod 4g的简单条件——即:当g = 2时,p mod 8 = 7;当g = 3时, p mod 3 = 2;当 g = 4 时,无需额外条件;当g = 5 时,p mod 5 = 1 或 4;当g = 6 时, p mod 24 = 19 或 23;当g = 7时,p mod 7 = 3、5 或 6。客户端检查完g和p之后,缓存结果就很有意义了,这样以后就不用重复冗长的计算了。
如果验证耗时过长(对于较旧的移动设备来说就是这种情况),最初可能只运行 15 次 Miller-Rabin 迭代来验证p和(p - 1)/2的素性,错误概率不超过十亿分之一,然后在后台进行更多迭代。
另一种优化方法是在客户端应用程序代码中嵌入一个包含一些已知“良好”素数对(g, p)(或者仅包含已知安全素数p ,因为g的条件在执行期间很容易验证)的小表,该表在代码生成阶段进行检查,从而完全避免在运行时进行此类验证。服务器很少更改这些值,因此通常需要将服务器的dh_prime的当前值放入该表中。例如,dh_prime的当前值等于(大端字节序)
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
7) 客户端计算一个随机的 2048 位数字b(使用足够的熵),并向服务器发送消息。
set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:string = Set_client_DH_params_answer;
这里,encrypted_data 的获取方式如下:
- g_b := pow(g, b) mod dh_prime;
-
数据 := 序列化
client_DH_inner_data#6643b654 nonce:int128 server_nonce:int128 retry_id:long g_b:string = Client_DH_Inner_Data
- data_with_hash := SHA1(data) + data + (0-15 个随机字节);使得长度能被 16 整除;
- encrypted_data := AES256_ige_encrypt (data_with_hash, tmp_aes_key, tmp_aes_iv);
第一次尝试时,retry_id 字段等于零;否则,它等于先前失败尝试的 auth_key_aux_hash(参见第 9 项)。
8) 此后,auth_key 等于pow(g, {ab}) mod dh_prime;在服务器端,它被计算为pow(g_b, a) mod dh_prime,在客户端,它被计算为(g_a)^b mod dh_prime。
Auth_key_hash 的计算方法为:SHA1(auth_key) 的低 64 位。服务器检查是否已存在具有相同 auth_key_hash 的密钥,并以下列方式之一做出响应。
DH密钥交换完成
9) 服务器响应方式有三种:
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 是通过对 new_nonce 字符串进行 SHA1 校验得到的,校验结果取低 128 位。该字节字符串是通过在 new_nonce 字符串的基础上,添加一个值为 1、2 或 3 的字节,再添加 8 个字节的 auth_key_aux_hash 得到的。这些值必须不同,以防止攻击者将服务器响应 dh_gen_ok 更改为 dh_gen_retry。
- auth_key_aux_hash 是SHA1(auth_key) 的高64位。请勿将其与 auth_key_hash 混淆。
在另一种情况下,客户端会执行步骤 7)并生成一个新的b。在第一种情况下,客户端和服务器协商了 auth_key,之后它们会清除所有其他临时数据,客户端使用 auth_key 创建另一个加密会话。同时,server_salt 被初始设置为substr(new_nonce, 0, 8) XOR substr(server_nonce, 0, 8)。如有需要,客户端会存储步骤 5) 中接收到的 server_time 与其本地时间之间的差值,以便始终能够获得生成正确消息标识符所需的服务器时间的近似值。
重要提示:除了对 Diffie-Hellman 素数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 分钟。如果服务器已忘记响应或所需的临时数据,客户端将需要重新开始请求。
服务器可能会认为,如果客户端已经使用之前服务器对该特定客户端的响应数据发送了下一个查询,则服务器知道客户端已经收到了该响应,因此可能会忘记该响应。
使用示例
生成授权密钥所需的完整查询列表示例显示在单独的页面上。