SSL socket 通訊詳解
作者:小馬
最近剛弄完一個ssl socket通訊,整理個筆記。
SSL原理
比如 A要和B互相通訊,為了安全他們希望雙方傳送的資料都是經過加密的。這就要求雙方有一個共同的加解密金鑰(一般加密都是基於對稱加密演算法)。如何才能讓雙方都擁有同一個金鑰呢?有人說由一方生成發給另一方不就行了。這樣就帶來另一個問題,如何保證這個傳送的金鑰是安全的呢?
ssl實現了通過非對稱的RSA加解密原理實現金鑰的交換。(這裡就不細講RSA的原理了,不明白的可以先補充一下這部分知識)。我們假設A是伺服器端,B是客戶端。然後還有一個角色,是一個可信任的第三方CA。A首先生成一個RSA公私鑰對,然後再由這個可信的第三方用自己的私鑰(為了便於描述, 後面叫CA_PRIVATE_KEY)把這個DES金鑰連同RSA公鑰一起簽發一個客戶端證書(後面叫CLIENT_CERT),這個證書中同時也包含一段簽名。
A會把CA證書(證書中包含CA_PRIVATE_KEY 對應的公鑰, 後面叫CA_CERT)連同CLIENT_CERT一起發給客戶端,當然客戶端也可以由其它途徑獲取這兩個證書(比如由專門的安全裝置中匯入到本地)。同時RSA私鑰(後面叫CLIENT_PRIVATE_KEY)也會一起下發到客戶端。
客戶端首先用CA_CERT對CLIENT_CERT做驗籤,以確保資料確實是由A發出來的。如果驗籤能過,其實就已經說明B已經認證了A的身份。前面說到CLIENT_CERT包含伺服器的公鑰,B用這個公鑰對一個對稱的DES金鑰加密,然後可以公開發出去,他不用擔心這個金鑰會被擷取,因為只有A才有CLIENT_PRIVATE_KEY,也只有A才能解密出來這個DES金鑰。這樣就完成了對稱金鑰的交換。
盜個圖,流程基本是下面這樣的,
大部分時候到這裡,SSL通訊前的握手已經完成了,可以進行安全的資料通訊了。不過有時候會有雙向認證的需求,也就是A也想認證B。這個時候CLIENT_PRIVATE_KEY就發生作用了,B會用這個私鑰自己生成簽名,然後發給A來認證。
這些基本就是SSL的原理了。
上面講到的這些流程,如果用程式實現還是有點複雜的。幸運的是,開源庫openssl已經幫我們做了大部分的事情(openssl的實現機制更復雜也更靈活,但基本原理跟上面是一致的)。我們只需要呼叫一些基本的介面就可以完成SSL socket通訊。
openssl 與 socket
基於ssl的socket通訊一般分為幾個步驟。
第一步, 初始化
這一步主要是初始化openssl庫,建立會話上下文等,
SSL_METHOD *method = NULL;
SSL_library_init ();
SSL_load_error_strings();
OpenSSL_add_ssl_algorithms();
method = SSLv3_client_method();
g_ctx = SSL_CTX_new(method); /* Create new context */
SSLv3_client_method 是指定ssl要使用的協議。SSL協議由美國 NetScape公司開發的,V1.0版本從沒有公開發表過;V2.0版本於1995年2月釋出。但是,由於V2.0版本有許多安全漏洞,所以,1996年緊接著就釋出了V3.0版本。微軟從IE 7開始就已經把瀏覽器的預設設定不支援SSL 2.0,但可能是考慮到有些網站還只支援SSL 2.0,所以IE瀏覽器留了一個可以由使用者設定支援SSL 2.0的選項,以便能正常訪問只支援SSL 2.0的網站。IE7/IE8支援SSL 3.0和TLS1.0,而IE9還支援TLS1.1和1.2。
在openssl裡指定協議很簡單,每個協議都有對應的函式,一行程式碼就可以搞定。
SSL_METHOD* TLSv1_client_method(void); TLSv1.0 協議
SSL_METHOD* SSLv2_client_method(void); SSLv2 協議
SSL_METHOD* SSLv3_client_method(void); SSLv3 協議
SSL_METHOD* SSLv23_client_method(void); SSLv2/v3 協議
SSL_CTX_new建立ssl上下文,這裡面很多全域性變數要被各個階段共享。
SSL_load_error_strings(void );
如果想打印出一些方便閱讀的除錯資訊的話,便要在一開始呼叫此函式.
第二步,載入證書和私鑰
前面提到過,證書有CA證書,還是客戶端的證書,私鑰是客戶端私鑰。也是幾個介面就搞定的事情,
int SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *CAfile,const char *CApath);
此函式用來便是載入CA證書檔案的.
int SSL_CTX_use_certificate_file(SSL_CTX *ctx, const char *file, int type);
載入客戶端自己的證書檔案.
int SSL_CTX_use_PrivateKey_file(SSL_CTX *ctx, const char *file, int type);
載入自己的私鑰,以用於簽名.
void SSL_CTX_set_default_passwd_cb_userdata(SSL_CTX *ctx, void *u);
載入私鑰時一般要一個驗證密碼,這個密碼是由生成私鑰的一方來設定的,客戶端得到這個密碼後要通過一個介面傳入驗證:
void SSL_CTX_set_verify(SSL_CTX ctx,int mode,int (*callback)(int, X509_STORE_CTX ));
預設mode是SSL_VERIFY_NONE,如果想要驗證對方的話,便要將此項變成SSL_VERIFY_PEER.SSL/TLS中預設只驗證server,如果沒有設定 SSL_VERIFY_PEER的話,客戶端連證書都不會發過來.
第三步,建立ssl socket連線
首先要建立普通的socket連線,這個就不多說了,連線成功後返回一個socket描述符,假設名稱為socket_fd。然後與ssl進行關聯,三個介面就可以完成:
g_ssl = SSL_new(g_ctx);
SSL_set_fd(g_ssl, socket_fd);
SSL_connect(g_ssl);
這樣後面所以的傳送,接收過程都是基於g_ssl這個ssl的全域性欄位了。
第四步,傳送和接收
收發資料就更簡單了,
傳送,
SSL_write(g_ssl, pData, bytes_left);
接收,
SSL_read(g_ssl, pData, bytes_left);
當然,一般我們在普通的socket收發都會加一些超時處理機制,對於ssl的收發也是一樣的,後面我的示例中會給出來一個較完善的處理過程。
第五步,釋放資源
如果不使用了,就要把所有佔用的資源都釋放掉。
當然首先要把socket關閉,
close(socket_fd);
然後是和ssl相關的資源釋放
SSL_CTX_free(g_ctx);
SSL_shutdown(g_ssl);
SSL_free(g_ssl);
ERR_free_strings();
一個使用示例
初始化,連線等操作都很簡單,這裡只給出一個ssl傳送函式的實現方法,帶超時處理的,供大家參考。
int socket_send_ssl(const char *data,
unsigned int data_len,int time_out)
{
int iRet = -1;
struct timeval tm;
tm.tv_sec = time_out;
tm.tv_usec = 0;
char *pData = data;
int bytes_left = data_len;
int written_bytes = 0;
if((data == NULL) || (data_len <= 0))
{
return -2;
}
iRet = setsockopt(socket_fd,SOL_SOCKET,SO_SNDTIMEO,&tm,sizeof(tm));//設定傳送超時
while(bytes_left > 0)
{
written_bytes = SSL_write(g_ssl, pData, bytes_left);
if(written_bytes <= 0)
{
if(errno == EINTR)
{
written_bytes = 0;
}
else
{
return -1;
}
}
bytes_left -= written_bytes;
pData += written_bytes;
}
return 0;
}
如有錯誤,請不吝賜教。