上篇文章介紹瞭如何簡單搭建一個環境幫助咱們分析,今天咱們就進入正題,開始在這個環境下分析。html
咱們使用IE瀏覽器訪問Web服務器根目錄的test.txt文件並抓包,能夠抓到以下6個包(前面的TCP三次握手在此略過):算法
使用服務器私鑰解密後的包是這個樣子的:瀏覽器
接下來咱們就結合這6個包來分析一下一個完整的HTTPS加解密流程。服務器
Client Hello是TLS握手的第一步,客戶端會將一個隨機數、支持的加密套件、壓縮算法等信息發送給服務器。dom
解密後的Finish消息以下,這裏麪包含一個Verify Data,是利用PRF函數算出來的,這個函數接下來會介紹,這裏咱們只須要知道函數的輸入參數有:(1)兩個hash值,是以前全部握手消息的MD5和SHA1;(2)MasterSecret,由PreMasterSecret生成;(3)finished_label,服務端使用「server finished」,客戶端使用「client finished」。函數
接下來重點介紹下密鑰的生成(下面代碼中的加解密函數使用了OpenSSL庫):加密
剛纔說到PreMasterSecret被服務器的公鑰加密了,因此須要使用服務器的私鑰解密,直接上代碼:3d
FILE * priv_fp = fopen("C:\\Users\\hello\\Desktop\\server.key","r");//server.key爲以前生成的服務器私鑰文件 if (priv_fp == NULL) { printf("read key error\n"); return -1; } RSA *rsa = PEM_read_RSAPrivateKey(priv_fp, NULL, NULL, NULL); if (rsa == NULL) { printf("read key error\n"); return -1; } len = RSA_private_decrypt(128, encrypted_premaster, premaster, rsa, RSA_PKCS1_PADDING);
能夠解密出來48字節的PreMasterSecret:code
密鑰生成要使用一個很重要的僞隨機函數,Pseudo-random Fuction(PRF),PRF函數原理以下:server
該函數有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密鑰:
接下來就是傳輸應用層的信息了,這些信息使用以前協商好的密鑰(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