原文连接: Creating an Authorization Key
创建授权密钥
其实就是使用DH算法以及公私钥机制生成会话密钥的过程:
概述
1)客户端发送一个128位随机数nonce给服务,作为后续会话ID;
2)服务器应答一个128位随机数server_nonce,大素数积g*p,以及选择的公钥的索引;
元组(nonce,server_nonce)将在后续会话中作为标记使用;
3)客户端尝试分解pq;
- 客户端生成新的随机数new_nonce作为后续临时对称加密密钥使用;
编码后,将数据使用公钥加密发给服务器;
客户端生成新的随机数用于做返回数据的对称加密密钥使用,编码后,将数据使用公钥加密发给服务器;
5)服务器计算DH中 的秘钥a,并计算结果g_a发给客户端;
6)客户端使用new_nonce得到的密钥密码收到的数据;根据g_a,p, q 生成随机密钥b,使用DH算法计算出g_b;将g_b发送给服务器,并带上密钥的哈希的低比特(不能发完整哈希,不安全)用于校验;
7)双方都可以计算出密钥了,服务端验证密钥;
8)开始新的正式的通信;
开始
RPC调用的方式是使用一个叫做“Query”方式调用的,数据序列化的定义由 Binary Data Serialization 和 TL Language来描述。
大的数字都是使用大端优先的方式序列化为一个字符串。哈希函数,比如 SHA1就会返回一个20字节的字符串,这个也被理解为大端优先的一个大整数。
小数字(4B, 8B, 16B,32B) (int
, long
, int128
, int256
) 使用小端优先序列化;然而如果他们是SHA1的一部分则不会重新按大头重排,比如有个long(int64)的变量X用来表示SHA1的低64比特,那么这字符串中的最后8字节就可以直接取出来,翻译为64比特的整数。至于string和vector的序列化,在其他的帖子中讨论。
我们再发送消息前,需要使用密钥交换算法来获取授权密钥,如下:
一、 DH 执行前准备工作
1. 客户发送一个请求到服务器
req_pq_multi#be7e8ef1 nonce:int128 = ResPQ;
备注:这里使用了TL语言描述调用以及返回的数据,意思是要求调用签名为be7e8ef1的函数:
ResPQ req_pq_multi(int128 nonce);
客户端随机生成一个128比特随机数,定义为nonce,后续过程中将用于标识此客户端,或者可以理解为会话的session_id。
2. 服务端计算后发回应答
resPQ#05162463 nonce:int128 server_nonce:int128 pq:string server_public_key_fingerprints:Vector long = ResPQ;
这里叫做类型定义,用于数据的序列化,可以理解为返回这样一个结构体的序列化:
struct ResPQ
{
int128 nonce; // 标记会话
int128 server_nonce; // 标记会话
string pq; // 大素数积
Vector<long> server_public_key_fingerprints; // 公钥索引数组
}
在这里,字符串pq是一个自然数(二进制大端格式)。这个数是两个不同的奇数质数(p*q)的乘积;通常,pq小于或等于2^63-1;
服务器随机选择server_nonce的值;
server_public_key_fingerprints
是一组公钥指纹(RSA key fingerprints),也就是 服务器公钥的SHA1的低64比特;
公钥被定义为:
rsa_public_key n:string e:string = RSAPublicKey
理解为:
struct RSAPublicKey
{
string n; // 按照大端序列化为字符串
string e; // 按照大端序列化为字符串
}
所有后续消息都包含明文形式的一对值(nonce,server_nonce),以及加密部分,从而可以识别“临时会话”,即本页中描述的密钥生成协议的一次运行,该协议使用相同的(nonce、server_nonce)对。
入侵者无法使用相同的参数创建与服务器的并行会话,因为服务器将为任何新的“临时会话”选择不同的server_nonce。
3. 客户将pq分解为素因子,使得p<q。
然后启动一轮Diffie-Hellman密钥交换。
**备注:**这里不是很懂,为啥p, q不是直接准备好,而是需要客户端自己算一下?,
官网文档这里叫做工作量证明,那可理解为要求客户提供算力,解决大数分解问题;与后续的使用的p, g并没有太大关系!!但是后续使用的数据可能是之前别人算好的。
二、服务端准备认证
4. 客户发送一个请求到服务器
req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string = Server_DH_Params
理解为客户RPC调用了:
Server_DH_Params req_DH_params(int128 nonce,
int128 server_nonce,
bytes p,
bytes q,
long public_key_fingerprint, // 选择了一个公钥
bytes encrypted_data); // 这里包含临时密钥
其中,加密数据encrypted_data的计算流程如下:
-
new_nonce:客户端生成一个新的随机数,后面放到加密数据中;
-
data:本地执行数据构造函数,计算出一个data,
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;
等价于:
stuct P_Q_inner_data{ string p, string q, int128 nonce, int128 server_nonce, int256 new_nonce, int dc }
或者
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;
-
encrypted_data:获取了data之后,执行函数RSA_PAD (data, server_public_key),这里的 RSA_PAD 带有 OAEP+ 填充的RSA版本,后续4.1)解释;
备注:
这时,可能会有黑客截获了之前的数据,并且自己独立于客户之外,自行分解了pq;黑客无法解码加密数据,这里面有新生成的的随机数(需要私钥解码),所以黑客有可能生成一个新的随机数,也许有些意义;但是后续过程中使用了新的随机数来加密数据,所以,除了服务器,没有人能解密;黑客可以模拟客户端,但是无法模拟服务器应答,所以无法做中间人攻击;重点是在DH密钥交换过程中引入了公钥机制!
另一种形式(p_q_inner_data_temp_dc
)用于创建一个临时密钥,并在在服务器端仅仅存储于内存,过了指定的时间expires_in
秒后,就会失效;在所有其他方面,临时密钥生成协议是相同的。创建临时密钥后,客户端通常通过身份验证将其绑定到其主要授权密钥。通过调用auth.bindTempAuthKey 方法,并将其用于所有客户端-服务器通信,直到其过期;然后生成一个新的临时密钥。从而实现了客户机-服务器通信中的完全前向保密(PFS)。阅读有关PFS的更多信息(https://core.telegram.org/api/pfs)
4.1) RSA_PAD(data, server_public_key)
加密算法描述如下:
-
data_with_padding := data + random_padding_bytes; 之所以要填充随机数据,是要保证长度为192 字节, 加密前data使用TL方式序列化,并且需要保证不超过144字节;
-
data_pad_reversed := BYTE_REVERSE(data_with_padding); 将前边得到的数据按字节倒序;
-
temp_key: 生成一个32-byte的临时加密密钥;
-
data_with_hash := data_pad_reversed + SHA256(temp_key + data_with_padding); 经过这一步骤,数据和HASH一起共224字节;
-
aes_encrypted := AES256_IGE(data_with_hash, temp_key, 0); – AES256-IGE 算法加密;
-
temp_key_xor := temp_key XOR SHA256(aes_encrypted); – 将KEY变形, 32 bytes
-
key_aes_encrypted := temp_key_xor + aes_encrypted; – exactly 256 bytes (2048 bits) long
-
key_aes_encrypted 的值与 server_pubkey 来比较,如果此值更大,则需要从生成临时密钥的步骤重做,否则进行后续流程;
-
encrypted_data := RSA(key_aes_encrypted, server_pubkey); – 生成的结果可以认为是256字节的大整数
**备注:**这里很有意思,不是简单的用公钥加密;而是先用临时密钥执行AES加密,并将密钥执行XOR密文的哈希,异或后的密钥+密文一起执行公钥加密。这里我也不懂为啥要这么麻烦。可能是为了增加强度。
5. 服务端应答随机数a
server_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:string = Server_DH_Params;
如果请求失败了,服务会返回-404代码,握手需要重新开始:
这里加密的应答计算过程如下:
-
new_nonce_hash := SHA1 (new_nonce)的低128比特;
对新的客户端随机数低128比特进行SHA1计算;
-
answer := serialization 将数据序列化,格式定义如下:
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;
等价于
stuct Server_DH_inner_data { int128 nonce, int128 server_nonce, int g, int dh_prime, // pow(g, {a或b}) mod dh_prime string g_a, // a需要自己珍藏 int server_time }
-
answer_with_hash := SHA1(answer) + answer + (0-15 random bytes); 填充后,长度可以被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-bit 的秘钥,而tmp_aes_iv is a 256-bit 初始化向量。 正如其他使用AES加密过程一样,为了方便加密,需要将长度填充到16字节的倍数;
在这一步之后,new_nonce仍然只有客户机和服务器知道。加密数据时用的密钥以及初始向量双方都可以得到;
客户机确信响应的是服务器,并且响应是专门为响应客户机查询req_DH_params而生成的,因为响应数据是使用server_nonce + new_nonce 计算出的密钥再加密的。
客户需要检查p=dh_prime是否是安全的2048位素数(意味着p和**(p-1)/2都是素数,22047<p<22048),并且g生成素数阶(p-1)/2的循环子群,即二次余数模p**。由于g总是等于2、3、4、5、6或7,因此使用二次互易定律可以很容易地做到这一点,从而得到p mod 4g的一个简单条件,即p mod8=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;对于g=7,p mod 7=3、5或6。客户端检查完g和**p后,缓存结果后续使用。
如果验证花费的时间太长(对于较旧的移动设备来说也是如此),一开始可能只运行15次Miller–Rabin迭代来验证p和**(p-1)/2**的素数,错误概率不超过十亿分之一,然后在后台进行更多迭代。
另一个优化是在客户端应用程序代码中嵌入一个小表,其中包含一些已知的“良好”耦合**(g,p)(或者只是已知的安全素数p**,因为g上的条件在执行期间很容易验证),在代码生成阶段进行检查,以避免在运行时完全进行此类验证。服务器很少更改这些值,因此通常必须将服务器的dh_prime的当前值放入这样的表中。
6. 客户端计算随机数b
客户端计算随机2048位数字b(使用足够的熵)并向服务器发送消息
set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:string = Set_client_DH_params_answer;
理解为:
struct {
int128 nonce,
int128 server_nonce,
string encrypted_data
}
加密数据计算方式如下:
-
g_b := pow(g, b) mod dh_prime; 这里就是调用了DH密钥交换中的算法了!
-
data := serialization
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 random bytes); 确信长度为16字节倍数;
-
encrypted_data := AES256_ige_encrypt (data_with_hash, tmp_aes_key, tmp_aes_iv);
The retry_id field is equal to zero at the time of the first attempt; otherwise, it is equal to auth_key_aux_hash from the previous failed attempt (see Item 9).
- encrypted_data := AES256_ige_encrypt (data_with_hash, tmp_aes_key, tmp_aes_iv);
在第一次尝试时,retry_id字段等于零;否则,它等于上一次失败尝试的auth_key_aux_hash。
7. 计算得到auth_key
这里,auth_key 就可以计算出来了,服务端等于
auth_key = pow(g_b, a) mod dh_prime ;
在客户端
auth_key = (g_a)^b mod dh_prime;
8. 双方验证auth_key_hash
auth_key_hash 等于对auth_key计算SAH1后的低64比特。
服务器校验哈希,确信双方获得的是同样的加密KEY。
三、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;
三种应答格式基本一致,就是签名不一样,因为签名是类型声明CRC32得来的;
等价于
struct dh_gen_ok
{
int128 nonce; // 标记会话
int128 server_nonce; // 标记会话
int128 new_nonce_hash1; // 标记
}
new_nonce_hash1、new_nonse_hash2和new_nonze_hash3是通过SHA1计算出来的数据的低128比特;不同的值也是为了防止入侵者将服务器响应dh_gen_ok更改为dh_gen_retry。
临时数据的构造:new_nonce字符串添加1个(值为1、2或3的)字节,然后再添加8个值为auth_key_aux_hash的字节。
这里的auth_key_aux_hash是SHA1(auth_key)的高64比特值,这里需要注意不要与auth_key_hash(低比特位)相混淆!!!
如果需要重试,则需要转到步骤6,
如果应答是情况1,那么密钥交换完成,客户端需要保存密钥,并丢弃临时数据;
此时, server_salt 将被初始化为
substr(new_nonce, 0, 8) XOR substr(server_nonce, 0, 8)
如果必要的话,还需要保存第5步骤中获得的与服务器之间的时间差;
重要提示: 除了Diffie-Hellman算法中,素数dh_prime和生成器g上的条件外,双方都要检查g、g_a和g_b是否大于1且小于dh_prime-1。我们建议检查g_a与g_b也在2{2048-64}和dh_primer-2{2048-164}之间。
四、错误处理 (Lost Queries and Responses)
如果客户机在某个时间间隔内没有收到服务器对其查询的任何响应,它可能只需重新发送查询。如果服务器已经对此查询发送了响应(完全相同的请求,而不仅仅是相似的:重复请求期间的所有参数必须具有相同的值),但它没有到达客户机,那么服务器只需重新发送相同的响应。服务器在收到步骤1)中的查询后会缓存响应长达10分钟。如果服务器已经丢弃了响应或必需的临时数据,则客户端必须从头开始。
过程举例
示例的网页为:(https://core.telegram.org/mtproto/samples-auth_key).
五、 后记总结一下
1)客户端生成随机数作为会话ID,发送给服务器,服务器返回随机数等,
- 返回值就包括了服务器应答的pq, server_n
// RPC调用函数
ResPQ req_pq_multi(int128 nonce);
// 服务器返回
struct ResPQ
{
int128 nonce; // 标记会话
int128 server_nonce; // 标记会话
string pq; // 大素数积
Vector<long> server_public_key_fingerprints; // 公钥索引数组
}
3) 发起新的请求,包含了新的new_nonce,
- 服务器应答了DH中的相关参数,尤其的g_a:
// 其中 p 是 (质数)prime, g 是 原始根模 p
// 整数 g 和 p 是公共的,通常是源代码中的硬编码常量。g可以设置为2,或者5里的g可以设置为2,或者5
// 这里的计算很难的关键是p是个很大的素数
// 这里的g可以设置为2,或者5
Server_DH_Params req_DH_params(int128 nonce,
int128 server_nonce,
bytes p,
bytes q,
long public_key_fingerprint,
bytes encrypted_data);
// 服务端应答
stuct Server_DH_inner_data
{
int128 nonce,
int128 server_nonce,
int g,
int dh_prime, // pow(g, {a或b}) mod dh_prime
string g_a, // a需要自己珍藏
int server_time
}
5)客户计算自己的g_b,发给服务器验证,
// rpc调用
Set_client_DH_params_answer set_client_DH_params(
int128 nonce,
int128 server_nonce,
bytes encrypted_data);
// 服务器校验后应答
struct dh_gen_ok
{
int128 nonce; // 标记会话
int128 server_nonce; // 标记会话
int128 new_nonce_hash1; // 标记
}
完毕!