• HTTPS加密流程超详解(二)


    2.进入正题

    上篇文章介绍了如何简单搭建一个环境帮助我们分析,今天我们就进入正题,开始在这个环境下分析。

    我们使用IE浏览器访问Web服务器根目录的test.txt文件并抓包,可以抓到如下6个包(前面的TCP三次握手在此略过):

    使用服务器私钥解密后的包是这个样子的:

    接下来我们就结合这6个包来分析一下一个完整的HTTPS加解密流程。

    第1包

    Client Hello是TLS握手的第一步,客户端会将一个随机数、支持的加密套件、压缩算法等信息发送给服务器。

    第2包

    1. Server Hello,用来响应客户端的Client Hello,里面同样包含一个32字节的随机数,以及服务端选择的加密套件和压缩算法。
    2. Certificate,服务端会把自己的证书发送给客户端,用来证明自己的身份,证书里面包含一个公钥,供后面的密钥交换使用。(客户端也可以发送证书证明身份,但是比较少见,我们这里就没有客户端证书)。
    3. Server Hello Done,用以表示服务端的密钥交换过程已经结束。

    第3包

    1. Client Key Exchange,包含一个使用服务器公钥加密的预主密钥PreMasterSecret,解密后可以用来生成密钥。
    2. Change Cipher Spec,表明握手协议已经完成。
    3. Finished,表示握手结束,这条消息已经被协商好的密钥加密,也可以起到确认密钥的作用。

    解密后的Finish消息如下,这里面包含一个Verify Data,是利用PRF函数算出来的,这个函数接下来会介绍,这里我们只需要知道函数的输入参数有:(1)两个hash值,是之前所有握手消息的MD5和SHA1;(2)MasterSecret,由PreMasterSecret生成;(3)finished_label,服务端使用“server finished”,客户端使用“client finished”。

    接下来重点介绍下密钥的生成(下面代码中的加解密函数使用了OpenSSL库):

    解密Encrypted PreMasterSecret

    刚才说到PreMasterSecret被服务器的公钥加密了,所以需要使用服务器的私钥解密,直接上代码:

    FILE * priv_fp = fopen("C:\Users\hello\Desktop\server.key","r");//server.key为之前生成的服务器私钥文件
    if (priv_fp == NULL)
    {
    	printf("read key error
    ");
    	return -1;
    }
    
    RSA *rsa = PEM_read_RSAPrivateKey(priv_fp, NULL, NULL, NULL);
    if (rsa == NULL)
    {
    	printf("read key error
    ");
    	return -1;
    }
    
    len = RSA_private_decrypt(128, encrypted_premaster, premaster, rsa, RSA_PKCS1_PADDING);
    

    可以解密出来48字节的PreMasterSecret:

    密钥生成

    密钥生成要使用一个很重要的伪随机函数,Pseudo-random Fuction(PRF),PRF函数原理如下:

    该函数有3个输入,其中Secret相当于密钥;Label是一个标识符,不同场合会使用不同的字符串,比如“server finished”、"master secret"等;Seed是一个种子值,比如客户端和服务器的随机数。

    该函数的代码实现如下:

    static int tls_prf(Data *secret,char *usage,Data *rnd1,Data *rnd2,Data *out)
    {
    	int r,_status;
    	Data *md5_out=0,*sha_out=0;
    	Data *seed;
    	UCHAR *ptr;
    	Data *S1=0,*S2=0;
    	int i,S_l;
    
    	if(r=r_data_alloc(&md5_out,MAX(out->len,16)))
    		return -1;
    	if(r=r_data_alloc(&sha_out,MAX(out->len,20)))
    		return -1;
    	if(r=r_data_alloc(&seed,strlen(usage)+rnd1->len+rnd2->len))
    		return -1;
    	ptr=seed->data;
    	memcpy(ptr,usage,strlen(usage)); ptr+=strlen(usage);
    	memcpy(ptr,rnd1->data,rnd1->len); ptr+=rnd1->len;
    	memcpy(ptr,rnd2->data,rnd2->len); ptr+=rnd2->len;    
    
    	S_l=secret->len/2 + secret->len%2;
    
    	if(r=r_data_alloc(&S1,S_l))
    		return -1;
    	if(r=r_data_alloc(&S2,S_l))
    		return -1;
    
    	memcpy(S1->data,secret->data,S_l);
    	memcpy(S2->data,secret->data + (secret->len - S_l),S_l);
    
    	if(r=tls_P_hash
    		(S1,seed,EVP_get_digestbyname("MD5"),md5_out))
    		return -1;
    	if(r=tls_P_hash(S2,seed,EVP_get_digestbyname("SHA1"),sha_out))
    		return -1;
    
    
    	for(i=0;i<out->len;i++)
    		out->data[i]=md5_out->data[i] ^ sha_out->data[i];
    
    	_status=0;
    abort:
    	r_data_destroy(&md5_out);
    	r_data_destroy(&sha_out);
    	r_data_destroy(&seed);
    	r_data_destroy(&S1);
    	r_data_destroy(&S2);
    	return(_status);
    }
    

    PRF要使用一个扩展函数(P_hash),原理图如下:

    该函数的代码实现如下:

    static int tls_P_hash(Data *secret,Data *seed,const EVP_MD *md,Data *out)
    {
    	UCHAR *ptr=out->data;
    	int left=out->len;
    	int tocpy;
    	UCHAR *A;
    	UCHAR _A[20],tmp[20];
    	unsigned int A_l,tmp_l;
    	HMAC_CTX hm;
    
    	A=seed->data;
    	A_l=seed->len;
    
    	while(left){
    		HMAC_Init(&hm,secret->data,secret->len,md);
    		HMAC_Update(&hm,A,A_l);
    		HMAC_Final(&hm,_A,&A_l);
    		A=_A;
    
    		HMAC_Init(&hm,secret->data,secret->len,md);
    		HMAC_Update(&hm,A,A_l);
    		HMAC_Update(&hm,seed->data,seed->len);
    		HMAC_Final(&hm,tmp,&tmp_l);
    
    		tocpy=MIN(left,tmp_l);
    		memcpy(ptr,tmp,tocpy);
    		ptr+=tocpy;
    		left-=tocpy;
    	}
    
    	HMAC_cleanup(&hm);
    	return 0;
    }
    

    了解了PRF函数后,就可以使用它做密钥生成(密钥扩展)了,下图完整阐述了密钥生成过程:

    密钥生成代码如下:

    tls_prf(&pre_master_secret, "master secret", &random1, &random2, &master_secret);
    
    tls_prf(&master_secret, "key expansion", &random2, &random1, &key_block);
    
    for (int i=0; i<16; i++)
    {
    	client_write_key[i] = key_block.data[40+i];
    }
    

    第一次调用PRF函数,使用PreMasterSecret、"master secret"和两个随机数(上述服务器和客户端各一个)作为输入参数,输出为一个48字节的主密钥MasterSecret:

    第二次调用PRF函数,MasterSecret、"key expansion"和两个随机数作为输入参数,输出为一个Key_block,从41字节开始的16个字节为Client Write key,接下来16个字节为Server Write key,这两个就是接下来双方通信使用的RC4密钥:

    第4包

    1. Change Cipher Spec,表明握手协议已经完成。
    2. Finished,表示握手结束,这条消息已经被协商好的密钥加密。

    第5、6包

    接下来就是传输应用层的信息了,这些信息使用之前协商好的密钥(Client Write key、Server Write key)加密,以客户端为例,解密代码如下:

    EVP_CIPHER_CTX ctx;
    EVP_CIPHER_CTX_init(&ctx);
    int rv, outl;
    rv = EVP_DecryptInit_ex(&ctx, EVP_rc4(), NULL, client_write_key, iv);//初始向量IV为0
    EVP_DecryptUpdate(&ctx, out, &outl, ciphertext, ciphertextlen);
    

    解密后的最后20个字节为MAC校验,这里使用的是SHA1算法。

    解密后的客户端数据:

    同理,解密后的服务端数据:

    至此,一个完整的HTTPS加解密流程就结束了,过程还是比较简单,只是如果自己实现的话一些细节会比较让人头疼,给出代码可以少走一些弯路,至于更复杂的加密套件,这里就不再介绍,流程应该差不太多,有兴趣的朋友可以研究一下。

    参考:http://www.360doc.com/content/16/0320/21/30136251_543905971.shtml

  • 相关阅读:
    dotNet程序保护方案
    网络数据包捕获函数库Libpcap安装与使用(非常强大)
    Objectivec 中 nil, Nil, NULL和NSNull的区别
    对象的相等和恒等
    IOS SDK介绍
    iOS内存管理编程指南
    http权威指南读书笔记(三)——http报文
    http权威指南学习笔记(二)
    http权威指南读书笔记(一)
    CentOS 设置环境变量
  • 原文地址:https://www.cnblogs.com/realwy/p/8185368.html
Copyright © 2020-2023  润新知