telegtram的通信协议MTproto2.0学习2 之 (认证过程与DH密钥交换)

原文连接: Creating an Authorization Key

创建授权密钥

其实就是使用DH算法以及公私钥机制生成会话密钥的过程:

概述

1)客户端发送一个128位随机数nonce给服务,作为后续会话ID;

2)服务器应答一个128位随机数server_nonce,大素数积g*p,以及选择的公钥的索引;

​ 元组(nonce,server_nonce)将在后续会话中作为标记使用;

3)客户端尝试分解pq;

  1. 客户端生成新的随机数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 SerializationTL 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=3g=4无额外条件p mod 5=1或4,对于g=5****p mod 24=19或23,对于g=6;对于g=7p 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,发送给服务器,服务器返回随机数等,

  1. 返回值就包括了服务器应答的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,

  1. 服务器应答了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; // 标记
}

完毕!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值