目錄
- 前言
- 初始化SSL庫
- 創建SSL 上下文接口(SSL_CTX)
- 安裝證書和私鑰
- 加載證書(客戶端/服務端證書)
- 加載私鑰/公鑰
- 加載CA證書
- 設置對端證書驗證
- 例1 SSL服務端安裝證書
- 例2 客戶端安裝證書
- 創建和安裝SSL結構
- 建立TCP/IP連接
- 客戶端創建socket
- 服務端創建連接
- 創建SSL結構中的BIO
- SSL握手
- 服務端SSL握手
- 客戶端握手
- 通過SSL_read以及SSL_write完成握手(可選)
- 獲取對端證書(可選)
- 數據傳輸
- 發送數據
- 接收數據
- 使用BIOs接口進行數據傳輸(可選)
- 關閉SSL連接
- SSL會話重用
- SSL握手再協商
- 服務端發起再協商
- 客戶端發起再協商
- SSL程序退出
- Linux下c語言實現socket+openssl數據傳輸加密
- 1. Socket連接建立流程
- 2、Socket+SSL的初始化流程
- 3、初始化SSL環境,證書和密鑰
- 4、Socket+SSL 的c語言實現
- 使用tcpdump檢驗
前言
參考:SSL編程指南
地址:https://blog.csdn.net/rzytc/article/details/50647095?spm=1001.2014.3001.5502
本文將介紹如何使用openssl APIs 實現一個簡單的SSL 客戶端和服務端。
雖然SSL客戶端和服務端在創建和配置上有所區別,但它們本質上的步驟可以總結為如下圖,具體步驟將在后面章節介紹:
初始化SSL庫
在SSL應用程序中調用其他Openssl APIs,需要先用下面的APIs進行初始化:
SSL_library_init(); /* 為SSL加載加密和哈希算法 */
SSL_load_error_strings(); /* 為了更友好的報錯,加載錯誤碼的描述字符串 */
SSL_library_init 注冊了所有在SSL APIs中的加密算法和哈希算法,該API加載的加密算法有:DES-CBC, DES-EDE3-CBC, RC2 和 RC4;哈希算法有MD2, MD5, 和 SHA。SSL_library_init正常情況只會返回1;
SSL應用程序需要調用SSL_load_error_strings,該函數為SSL接口和Crypto加密接口加載錯誤描述字符串。之所以SSL 和 Crypto加密錯誤描述字符串需要加載,因為SSL應用程序都會多次調用SSL 和Crypto接口;
創建SSL 上下文接口(SSL_CTX)
初始化的第一步是選擇SSL/TLS協議版本號;通過下面的API創建一個SSL_METHOD結構;SSL_METHOD結構之后會用于通過SSL_CTX_new()創建SSL_CTX結構。
對于每個SSL/TSL來說,有三種APIs可以用來創建一個SSL_METHOD:一個可以用于服務端和客戶端,一個只能用于服務端,另外一個只能由于客戶端。SSLv2,SSLv3以及TLSv1有這和協議名一致的接口函數名,如下表所示:
創建 SSL_METHOD 的函數:
協議號 | 通用 | 服務端專用 | 客戶端專用 |
---|---|---|---|
SSLv2 | SSLv2_method() | SSLv2_server_ method() | SSLv2_client_ method() |
SSLv3 | SSLv3_method() | SSLv3_server_ method() | SSLv3_client_ method() |
TLSv1 | TLSv1_method() | TLSv1_server_ method() | TLSv1_client_ method() |
SSLv23 | SSLv23_method() | SSLv23_server_ method() | SSLv23_client_ method() |
說明: 并沒有SSLv23這個協議號,SSLv23_method將會選擇SSLv2,SSLv3或者TLSv1來匹配對端的版本號。
當開發一個SSL服務端或者客戶端應用程序,需要考慮SSL/TLS版本的不兼容問題;比如,一個TLSv1的服務端不能支持來之SSLv2或者SSLv3客戶端發來的client-hello消息;當需要考慮客戶端的兼容性是,可以使用SSLv23_method和其他變體函數。使用SSLv23的服務器可以支持SSLv2,SSLv3以及TLSv1發來的hello消息。而使用SSLv23 函數的客戶端不能和使用了SSLv3/TLSv1函數的服務端建立連接,因為客戶端發送的SSLv2版本的hello包。
SSL_CTX_new函數以SSL_METHOD結構體作為參數,創建并且返回SSL_CTX結構。
下面的例子中,SSL_METHOD結構既可以用于穿件SSLv3客戶端或者SSLv3服務端;并且SSL_CTX也將會被初始化;
meth = SSLv3_method();
ctx = SSL_CTX_new(meth);
安裝證書和私鑰
“Certificates for SSL Applications” 中介紹了如何安裝SSL客戶端和服務端適合的證書。這個安裝步驟需要加載證書私鑰到SSL_CTX或者SSL結構中。必須安裝以及可選安裝的證書如下:
-
服務端:
服務器自己的證書(必須的)
CA證書(可選的) -
客戶端:
CA證書(強制的)
客戶端自己的證書(可選的)
加載證書(客戶端/服務端證書)
SSL_CTX_use_certificate_file() //函數加載一個證書到一個SSL_CTX結構中SSL_use_certificate_file() //函數加載一個證書到SSL結構中
當創建SSl結構,SSL結構自動加載同樣的證書,并且包含了SSL_CTX結構;因此,當創建SSL結構只需要調用SSL_use_certificate_file(),除非需要加載一個不同的證書,而不是默認證書包含在SSL_CTX結構中。
加載私鑰/公鑰
接下來就是設置一個和服務端或者客戶端證書相關的私鑰。在SSL握手中,公鑰會被傳輸給對端用于加密使用。對端的加密信息只能用本端的私鑰解密。必須加載私鑰,加載時會加載公鑰信息到SSL結構中。
下面的函數用于加載私鑰到SSL結構或者SSL_CTX結構中:
SSL_CTX_use_PrivateKey()
SSL_CTX_use_PrivateKey_ASN1()
SSL_CTX_use_PrivateKey_file()
SSL_CTX_use_RSAPrivateKey()
SSL_CTX_use_RSAPrivateKey_ASN1()
SSL_CTX_use_RSAPrivateKey_file()
SSL_use_PrivateKey()
SSL_use_PrivateKey_ASN1()
SSL_use_PrivateKey_file()
SSL_use_RSAPrivateKey()
SSL_use_RSAPrivateKey_ASN1()
SSL_use_RSAPrivateKey_file()
加載CA證書
為了驗證證書,首先需要加載CA證書(因為對端證書需要用CA證書來驗證)。SSL_CTX_load_verify_locations加載CA證書到SSL_CTX結構中。
函數原型如下:
int SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *CAfile, const char *CApath);
第一個參數ctx,指向一個用于加載CA證書的SSL_CTX結構,第二個參數和第三個參數CAfile、CApatch,用于指向CA證書的路徑;當查找CA證書時,Openssl庫先通過CAfile查找,找不到再通過CApath。
CAfile和CApath參數有以下規則:
如果CAfile已經指定了證書(證書必須存在于SSL應用程序的相同路徑下),CApath可以指定為NULL。如果使用了CApath,CAfile為NULL。必須對CApath指定的路徑下的CA證書進行哈希。使用證書工具(第三章描述的)進行哈希操作
設置對端證書驗證
SSL_CTX中加載的CA證書使用與對端證書驗證的。比如,客戶端的證書驗證是在通過檢查客戶端加載的CA證書和服務端的證書之間的關系。
如果要驗證成功,對端的證書必須通過CA證書直接或者間接的方式的簽名(存在一個正確的證書鏈)。CA證書對對端的證書鏈的檢查深度可以設置到SSL_CTX或者SSL結構的verify_depth 成員中。(如果通過SSL_new創建SSL,SSL中的值可以從SSL_CTX中繼承下來)verify_depth設置為1意味著對端的證書必須被CA證書直接簽名過的。
The SSL_CTX_set_verify() API allows you to set the verification flags in the SSL_CTX structure and a callback function for customized verification as its third argument. (Setting NULL to the callback function means the built-in default verification function is used.) In the second argument of SSL_CTX_set_verify(), you can set the following macros:
翻譯:函數SSL_CTX_set_verify可以用于設置在SSL_CTX結構中的驗證標記,第三個函數可以設置一個指定驗證過程的回調函數。(回調參數如果設置為NULL意味著用內建默認的驗證方式)。SSL_CTX_set_verify中的第二個參數可以指定以下宏:
SSL_VERIFY_NONE
SSL_VERIFY_PEER
SSL_VERIFY_FAIL_IF_NO_PEER_CERT
SSL_VERIFY_CLIENT_ONCE
SSL_VERIFY_PEER 可以指定與客戶端和服務端,用于啟動驗證。但是,后續的行為取決于是服務端還是客戶端所設置。比如;
/* Set a callback function (verify_callback) for peer certificate */
/* verification */
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_callback);
/* Set the verification depth to 1 */
SSL_CTX_set_verify_depth(ctx,1);
驗證對端證書也可以通過一個不常用的方式,使SSL_get_verify_result()。這種方式可以獲取到驗證對端證書的結果,而不需要使用SSL_CTX_set_verify()函數。
調用函數SSL_get_verify_result() 前需要調用以下兩個函數:
調用SSL_connect(客戶端)或者SSL_accept(服務端)完成SSL協商握手。在握手過程會完成證書驗證。在驗證步驟前不能調用SSL_get_verify_result()獲取不到驗證結果。
調用SSL_get_peer_certificate() 不能明確地獲取到對端證書。當對端證書不存在或者驗證成功返回X509_V_OK。
The following code shows how to use SSL_get_verify_result() in the SSL client:
以下代碼展示在客戶端如何使用 SSL_get_verify_result:
SSL_CTX_set_verify_depth(ctx, 1);
err = SSL_connect(ssl);
if (SSL_get_peer_certificate(ssl) != NULL)
{if (SSL_get_verify_result(ssl) == X509_V_OK){BIO_printf(bio_c_out, "client verification with SSL_get_verify_result() succeeded.\n"); } else{BIO_printf(bio_err, "client verification with SSL_get_verify_result() failed.\n");exit(1);}
}
else
{BIO_printf(bio_c_out, -the peer certificate was not presented.\n -);
}
例1 SSL服務端安裝證書
SSL協議要求服務端設置證書和私鑰。如果服務端要驗證客戶端的證書,服務端必須加載一個CA證書,以便于用于驗證客戶端證書。
以下例子展示服務端如何安裝證書:
/* Load server certificate into the SSL context */
if (SSL_CTX_use_certificate_file(ctx, SERVER_CERT,SSL_FILETYPE_PEM) <= 0)
{ERR_print_errors(bio_err);/* ==ERR_print_errors_fp(stderr); */exit(1);
}/* Load the server private-key into the SSL context */
if (SSL_CTX_use_PrivateKey_file(ctx, SERVER_KEY,SSL_FILETYPE_PEM) <= 0)
{ERR_print_errors(bio_err);/* ==ERR_print_errors_fp(stderr); */exit(1);
}/* Load trusted CA. */
if (!SSL_CTX_load_verify_locations(ctx, CA_CERT, NULL))
{ERR_print_errors(bio_err);/* ==ERR_print_errors_fp(stderr); */exit(1);
}/* Set to require peer (client) certificate verification */
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_callback);
/* Set the verification depth to 1 */
SSL_CTX_set_verify_depth(ctx, 1);
例2 客戶端安裝證書
客戶端通常情況在握手過程中驗證服務端的證書。認證過程需要客戶端安裝它的信任的CA證書。服務端的證書必須使用客戶端所加載的CA證書簽名過的證書,以便于服務端驗證可以通過。
以下例子展示了客戶端如何安裝證書:
/*----- Load a client certificate into the SSL_CTX structure -----*/
if (SSL_CTX_use_certificate_file(ctx, CLIENT_CERT,SSL_FILETYPE_PEM) <= 0)
{ERR_print_errors_fp(stderr);exit(1);
}/*----- Load a private-key into the SSL_CTX structure -----*/
if (SSL_CTX_use_PrivateKey_file(ctx, CLIENT_KEY,SSL_FILETYPE_PEM) <= 0)
{ERR_print_errors_fp(stderr);exit(1);
}/* Load trusted CA. */
if (!SSL_CTX_load_verify_locations(ctx, CA_CERT, NULL))
{ERR_print_errors_fp(stderr);exit(1);
}
創建和安裝SSL結構
調用SSL_new創建SSL結構,SSL的連接信息都保存在SSL結構,SSL_new的使用方式如下:
ssl = SSL_new(ctx);
一個新的SSL結構會從SSL_CTX結構中繼承包括,連接類型、選項、驗證方式以及超時。如果SSL_CTX結構已經做過適合的初始化和配置,SSL結構就可以不需要做其他設置了。
SSL相關的API去修改SSL結構中的一些默認值,可以通過多個同名的函數去設置屬性到SSL_CTX結構中。比如,可以使SSL_CTX_use_certificate加載證書到SSL_CTX中,也可以通過SSL_use_certificate加載證書到SSL結構中。
建立TCP/IP連接
SSL需要工作在可靠的協議之上,SSL使用最普遍的傳輸層協議TCP/IP。
下面的章節描述了如何使用SSL API來創建TCP/IP。使用方式和其他TCP/IP應用程序一樣。這里TCP/IP創建使用普通的socket接口,雖然也可以通過OpenVMS系統的接口。
SSL服務端創建監聽端口,SSL服務端和普通的TCP/IP服務器一樣需要兩個sockets,一個用于監聽SSL客戶端發來的請求,一個用于SSL通信。
以下代碼,socket()函數創建監聽socket,使用bind函數綁定了端口和地址后,在調用listen函數后,就可以處理來自客戶端的TCP/IP請求了
listen_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
CHK_ERR(listen_sock, "socket");memset(&sa_serv, 0, sizeof(sa_serv));
sa_serv.sin_family = AF_INET;
sa_serv.sin_addr.s_addr = INADDR_ANY;
sa_serv.sin_port = htons(s_port); /* Server Port number */err = bind(listen_sock, (struct sockaddr*)&sa_serv,sizeof(sa_serv));
CHK_ERR(err, "bind");/* Receive a TCP connection. */
err = listen(listen_sock, 5);
CHK_ERR(err, "listen");
客戶端創建socket
客戶端需要創建一個TCP/IPsocket,然后嘗試去連接服務端。調用connect()函數去連接到指定的服務器。如果成功后,可以使用connect的第一個參數(句柄)用于數據傳輸。
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
CHK_ERR(sock, "socket");memset (&server_addr, '\0', sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(s_port); /* Server Port number */
server_addr.sin_addr.s_addr = inet_addr(s_ipaddr); /* Server IP */err = connect(sock, (struct sockaddr*) &server_addr, sizeof(server_addr));
CHK_ERR(err, "connect");
服務端創建連接
SSL服務端調用accept()來接收一個客戶請求,返回的句柄可以用于和客戶端數據傳輸使用,如下:
sock = accept(listen_sock, (struct sockaddr*)&sa_cli, &client_len);
BIO_printf(bio_c_out, "Connection from %lx, port %x\n",
sa_cli.sin_addr.s_addr, sa_cli.sin_port);
創建SSL結構中的BIO
創建SSL結構和socket后,需要做些關聯,以便于可以使用SSL結構來完成SSL數據傳輸。
下面的代碼片段展示了多種方式將sock和ssl關聯到一起,最簡單的方式是將socket直接設置到SSL結構中,比如
SSL_set_fd(ssl, sock);
一種更好的方式是使用BIO結構,BIO是一種Openssl提供的IO抽象接口。這種方式更好,因為BIO隱藏了底層IO的細節。只要BIO接口設置合理,可以在任何IO之上建立SSL連接。
下面兩個例子展示了如何創建一個BIO以及將BIO設置到SSL結構中:
sbio=BIO_new(BIO_s_socket());
BIO_set_fd(sbio, sock, BIO_NOCLOSE);
SSL_set_bio(ssl, sbio, sbio);
下面的例子中,BIO_new_socket創建了一個tcp/ip的socket BIO,SSL_set_bio()將socket BIO設置到SSL結構中。以下兩行代碼等價于前面的三行:
sbio = BIO_new_socket(socket, BIO_NOCLOSE);
SSL_set_bio(ssl, sbio, sbio);
SSL握手
SSL握手過程是一個復雜過程,涉及到重要的加密秘鑰交換。但是握手過程可以通過服務端調用SSL_accept和客戶度調用SSL_connect完成。
服務端SSL握手
SSL_accept函數會等待客戶端啟動SSL握手。函數成功返回說明SSL握手已經成功完成。
err = SSL_accept(ssl);
客戶端握手
客戶端調用SSL_connect開始SSL握手。握手成功返回1,之后數據就可以通過這個連接安全傳輸了。
err = SSL_connect(ssl);
通過SSL_read以及SSL_write完成握手(可選)
另外,和SSL數據交換一樣也可以通過SSL_write和SSL_read來完成握手。按這種方式,必須先在服務端調用SSL_read前先調用SSL_set_accept_state,在客戶端調用SSL_write前先調用SSL_set_connect_state()。舉例:
/* When SSL_accept() is not called, SSL_set_accept_state() */
/* must be called prior to SSL_read() */
SSL_set_accept_state(ssl);/* When SSL_connect() is not called, SSL_set_connect_state() */
/* must be called prior to SSL_write() */
SSL_set_connect_state(ssl);
獲取對端證書(可選)
另外,在SSL握手完成后,可通過調用SSL_get_peer_certificate來獲取對端的證書。這個函數通常用于直接方式的證書驗證,比如檢查證書信息(comman name和證書過期時間)
peer_cert = SSL_get_peer_certificate(ssl);
數據傳輸
在SSL握手完成后,數據就可以通過已經建立好的連接安全的發送了。SSL_write和SSL_read用于SSL數據傳輸,和write、read、send、recv一樣用在普通的tcp連接上。
發送數據
調用SSL_write在SSL連接上發送數據。被發送的數據存放在一個緩沖區中以第二個參數傳入,比如:
err = SSL_write(ssl, wbuf, strlen(wbuf));
接收數據
調用SSL_read在SSL連接上接收數據,接收數據的緩沖區放在第二個參數傳入,比如
err = SSL_read(ssl, rbuf, sizeof(rbuf)-1);
使用BIOs接口進行數據傳輸(可選)
調用BIO_puts() 和IO_gets(), 和 BIO_write() 和 BIO_read()可以替代SSL_write() 和SSL_read進行數據收發,其中BIO buffer的創建和安裝如下:
BIO *buf_io, *ssl_bio;
char rbuf[READBUF_SIZE];
char wbuf[WRITEBUF_SIZE]buf_io = BIO_new(BIO_f_buffer()); /* create a buffer BIO */
ssl_bio = BIO_new(BIO_f_ssl()); /* create an ssl BIO */
BIO_set_ssl(ssl_bio, ssl, BIO_CLOSE); /* assign the ssl BIO to SSL */
BIO_push(buf_io, ssl_bio); /* add ssl_bio to buf_io */ ret = BIO_puts(buf_io, wbuf);
/* Write contents of wbuf[] into buf_io */
ret = BIO_write(buf_io, wbuf, wlen);
/* Write wlen-byte contents of wbuf[] into buf_io */ret = BIO_gets(buf_io, rbuf, READBUF_SIZE);
/* Read data from buf_io and store in rbuf[] */
ret = BIO_read(buf_io, rbuf, rlen);
/* Read rlen-byte data from buf_io and store rbuf[] */
關閉SSL連接
當關閉SSL連接時,SSL客戶端和服務端需要發送close_notify消息,通知對端SSL將要關閉了。調用SSL_shutdown函數來發送close_notify消息:
關閉過包含以下兩個步驟:
- 發送一個close_notify關閉告警
- 從對端接收一個close_notify的關閉消息
關閉SSL連接有如下規則:
- 任何一方都可以通過發送close_notify消息來發起關閉
- 發送過關閉消息后,接收到任何數據將會被忽略
- 任何一方在關閉寫時,都要先發送close_notify消息
- 收到close_notify的一端也需要應答它自己的close_notify,并且立刻關閉連接,丟棄未寫出的數據。
- 發起關閉的一端,在關閉讀端關閉前,不需要等待響應的close_notify消息。
發起關閉的客戶端或者服務端可以調用SSL_shutdown一次或者兩次。如果調用了兩次,一次調用用于發送close_notify消息,另外一次用于響應對端的。如果只調用一次,發起關閉一端將不會等待對端的響應(發起關閉的一端不需要等待對端的關閉響應)
一旦收到對端關閉消息就要馬上發送關閉響應。
SSL會話重用
可以基于一個已建立連接的SSL會話創建一個新的SSL連接。因為重用了相同的會話秘鑰,SSL握手將會快很多。SSL會話重用將有利于一個并發大的服務器減輕負載。
客戶端可以按以下步驟去重用一個SSL會話:
發起第一個SSL連接,同時會創建一個SSL會話
ret = SSL_connect(ssl)
(Use SSL_read() / SSL_write() for data communication over the SSL connection)
保存SSL會話信息
sess = SSL_get1_session(ssl);
/* sess is an SSL_SESSION, and ssl is an SSL */
關閉第一個SSL連接
SSL_shutdown(ssl);
創建一個新的SSL結構
ssl = SSL_new(ctx);
SSL_connect前將SSL session設置到新的SSL結構中
SSL_set_session(ssl, sess);
使用重用會話啟動新的SSL連接
ret = SSL_connect(ssl)
(Use SSL_read() / SSL_write() for data communication over the SSL connection)
如果SSL客戶端調用了SSL_get1_session和SSL_set_session,SSL服務端將不需要使用其他接口,就可以accept到一個重用之前會話的SSL連接。
服務器的實現步驟將在之前的章節中討論過。
注意:調用了SSL_free會導致SSL sessoion無法重用,即使已經通過SSL_get1_session保存了就的會話信息。
SSL握手再協商
SSL再協商是在一個已經建立了SSL握手的連接上進行一個新的SSL握手。
因為再協商的信息包括加密秘鑰在已加密的連接上進行傳輸的。SSL再協商可以安全地建立另一個SSL連接。如果已經創建了一個普通的SSL連接,SSL再協商可以在以下場景中使用:
- 需要客戶端進行身份認證
- 需要使用不同的加解密秘鑰
- 需要使用不同的加密和哈希的算法
SSL再協商可以由SSL客戶端也可以是服務端發起。當在客戶端發起時需要使用不同接口(發起的客戶端和接收的服務端都需要使用不同函數)
以下章節將討論兩種情況的必要的接口:
注意:SSLv2 不支持SSL再協商,SSLv3或者TLSv3支持這個操作。
服務端發起再協商
服務端發起再協商,需要調用SSL_renegotiate一次和SSL_do_handshake兩次。
SSL_renegotiate為SSL再協商設置標志。SSL_renegotiate實際上并沒有啟動再協商。在SSL_do_handshake時生效,SSL_do_handshake發現設置過標志需要和SSL客戶端需要建立再協商。SSL_do_handshake才真正的執行SSL再協商。第一次調用將會發送一個Server-Hello消息給客戶端。
如果第一次調用成功,表示客戶端已經允許本次的SSL再協商。然后服務端將會設置SSL_ST_ACCEPT 到SSL結構中,并且再調用一次SSL_do_handshake完成再協商剩余的步驟。
以下的代碼片段展示的這些函數的用法:
printf("Starting SSL renegotiation on SSL server (initiating by SSL server)");
if (SSL_renegotiate(ssl) <= 0)
{printf("SSL_renegotiate() failed\n");exit(1);
}if (SSL_do_handshake(ssl) <= 0)
{printf("SSL_do_handshake() failed\n");exit(1);
}ssl->state = SSL_ST_ACCEPT;if (SSL_do_handshake(ssl) <= 0)
{printf("SSL_do_handshake() failed\n");exit(1);
}
以下片段展示服務端發起再協商時,客戶端的調用:
printf("Starting SSL renegotiation on SSL client (initiating by SSL server)");
/* SSL renegotiation */
err = SSL_read(ssl, buf, sizeof(buf)-1);
如上所示,SSL_read除了用于接收數據外,也可以用于處理連接相關的功能,比如再協商;
客戶端發起再協商
SSL客戶端也可以發起SSL再協商,啟動方式與服務端啟動相似。SSL客戶端調用SSL_renegotiate設置一個再協商標志,然后只需要調用一次SSL_do_handshake即可完成再協商。
printf("Starting SSL renegotiation on SSL client (initiating by SSL client)");
if(SSL_renegotiate(ssl) <= 0){printf("SSL_renegotiate() failed\n");exit(1);
}
if(SSL_do_handshake(ssl) <= 0){printf("SSL_do_handshake() failed\n");exit(1);
}
SSL程序退出
當退出SSL程序運行時,首要任務是釋放在程序中創建的相關結構體內存。釋放相關的接口有包含_free后綴的(相反_new后綴則是用于創建結構體的)
必須釋放程序中申請過的結構體內存。通過xxx_new結構申請的內存,通過xxx_free將會自動釋放相關內存。比如,使用SSL_new創建BIO結構通過SSL_free將會釋放相關內存,而不需要調用BIO_free去釋放BIO內部的SSL結構。但是,如果滴啊用了BIO_new申請了BIO結構,就必須通過BIO_free來釋放。
注意:在調用SSL_free前必須先釋放SSL_shutdown.
Linux下c語言實現socket+openssl數據傳輸加密
參考:Linxu下c語言實現socket+openssl數據傳輸加密
地址:https://programtree.blog.csdn.net/article/details/133269452?spm=1001.2014.3001.5502
在進行網絡編程的時候,我們通常使用socket進行數據的傳輸。然而socket作為一個數據傳輸協議,其本身對數據并不會作加密。所以數據傳輸的過程可以很輕松地被監聽并截獲到傳輸的數據。openssl提供了SSL的加密庫,通過 ssl+socket 的方式可以保證連接安全和數據的加密。
1. Socket連接建立流程
在做socket加密之前,還是先與普通的socket做一個對比。
上面的是我們通常在做一個socket連接的時候會涉及到的握手過程。服務端會通過accept去接收客戶端的請求。客戶端通過connect去連接到客戶端。使用send和recv去做數據的傳輸。那么傳輸過程中的參數data也就是我們交互的數據了。當我們建立連接開始發送數據的時候。使用一些抓包工具wireshark,或者tcpdump去監聽socket端口就能很輕松地獲取到傳輸的明文數據了。
2、Socket+SSL的初始化流程
所以為了避免數據的監聽,我們就需要使用SSL去建立一個安全的通道。這里我們先把SSL的建立當作一個子流程:
SSL子過程主要插入在 socket 的 connect()/accept() 和數據交換之間。通過SSL建立完成的所以,一旦SSL握手完成,數據發送和接收的流程與普通的socket通信非常相似,只需要使用SSL_write()和SSL_read()來代替send()和recv()。
替換 send()
為 SSL_write(ssl, buffer, length)
替換recv()
為 SSL_read(ssl, buffer, length)
3、初始化SSL環境,證書和密鑰
openssl 生成證書(分別生成私鑰和自簽名證書),確保環境上已經安裝完成了openssl
openssl genpkey -algorithm RSA -out key.pem
openssl req -new -x509 -key key.pem -out cert.pem -days 365
days為證書的有效期。命令執行完成后在當前目錄下就會生成key.pem
和cert.pem
。記下文件路徑,后續會作為參數傳入到我們的函數中去。
4、Socket+SSL 的c語言實現
4.1 編寫SSL連接函數
在進行SSL初始化的時候需要注意的是,由于連接協商的過程使用的是非對稱加密,因此客戶端和服務端在初始化的時候是使用的不同的算法。因此,我在函數中加入了一個SSL_MODE參數。用來指明當前是服務端還是客戶端的SSL。
SSL* sync_initialize_ssl(const char* cert_path, const char* key_path, SSL_MODE mode, int fd)
{const SSL_METHOD *method;SSL_CTX *ctx;SSL *ssl = NULL;// 初始化OpenSSL庫SSL_library_init();OpenSSL_add_all_algorithms();SSL_load_error_strings();// 根據模式(客戶端/服務端)選擇合適的方法if (mode == SSL_MODE_SERVER) {method = SSLv23_server_method();} else if (mode == SSL_MODE_CLIENT) {method = SSLv23_client_method();} else {// 未知模式printf("Not found method");return NULL;}// 創建SSL上下文ctx = SSL_CTX_new(method);if (!ctx) {printf("Unable to create SSL context");return NULL;}// 配置SSL上下文if (SSL_CTX_use_certificate_file(ctx, cert_path, SSL_FILETYPE_PEM) <= 0 || SSL_CTX_use_PrivateKey_file(ctx, key_path, SSL_FILETYPE_PEM) <= 0 ) {printf("Not found certificate or private key");SSL_CTX_free(ctx);return NULL;}// SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);// 創建SSL對象ssl = SSL_new(ctx);if (!ssl) {printf("Failed to create SSL object.");return NULL;}// 設置文件描述符if (SSL_set_fd(ssl, fd) == 0) {printf("Failed to set fd to SSL object.");return NULL;}// SSL握手if ((mode == SSL_MODE_CLIENT && SSL_connect(ssl) <= 0) ||(mode == SSL_MODE_SERVER && SSL_accept(ssl) <= 0)) {int ssl_result;if (mode == SSL_MODE_CLIENT) {ssl_result = SSL_connect(ssl);} else if (mode == SSL_MODE_SERVER) {ssl_result = SSL_accept(ssl);}int ssl_err = SSL_get_error(ssl, ssl_result);const char *err_str;switch (ssl_err) {case SSL_ERROR_NONE:err_str = "No error";break;case SSL_ERROR_SSL:err_str = "Error in the SSL protocol";break;case SSL_ERROR_WANT_READ:err_str = "SSL read operation did not complete";break;case SSL_ERROR_WANT_WRITE:err_str = "SSL write operation did not complete";break;case SSL_ERROR_WANT_X509_LOOKUP:err_str = "SSL X509 lookup operation did not complete";break;case SSL_ERROR_SYSCALL:err_str = "Syscall error";break;case SSL_ERROR_ZERO_RETURN:err_str = "SSL connection was shut down cleanly";break;case SSL_ERROR_WANT_CONNECT:err_str = "SSL connect operation did not complete";break;case SSL_ERROR_WANT_ACCEPT:err_str = "SSL accept operation did not complete";break;default:err_str = "Unknown error";break;}printf("===============SSL handshake failed. Error: %s========!\n", err_str ? err_str : "Unknown");return NULL;}return ssl;
}
4.2 編寫加密服務端server.c
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <openssl/ssl.h>
#include <openssl/err.h> #define SERVER_PORT 9990
#define MAXLINE 4096
typedef enum
{SSL_MODE_SERVER,SSL_MODE_CLIENT
} SSL_MODE;int main(int argc, char **argv) { int listenfd, connfd; struct sockaddr_in servaddr, cliaddr; char buf[MAXLINE]; // 創建監聽套接字 listenfd = socket(AF_INET, SOCK_STREAM, 0); // 綁定地址和端口 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERVER_PORT); bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)); // 監聽連接 listen(listenfd, 10); printf("Listening on port %d...\n", SERVER_PORT); while (1) { // 接受連接請求 socklen_t len = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &len); printf("Accepted connection from %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); // 創建 SSL 對象并進行握手 SSL *ssl = sync_initialize_ssl("cert.pem", "key.pem", SSL_MODE_SERVER, connfd); // 讀取客戶端發送的數據并回復 memset(buf, 0, MAXLINE); SSL_read(ssl, buf, MAXLINE); printf("Received: %s\n", buf); SSL_write(ssl, "Hello, client!", strlen("Hello, client!")); // 關閉連接和清理資源 close(connfd); SSL_shutdown(ssl); SSL_free(ssl); } return 0;
}
4.3 編寫加密客戶端client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stddef.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <openssl/ssl.h>
#include <openssl/err.h>#define SERVER_IP "127.0.0.1" // 請根據需要更改服務器IP
#define SERVER_PORT 9990
#define MAXLINE 4096
typedef enum
{SSL_MODE_SERVER,SSL_MODE_CLIENT
} SSL_MODE;int main(int argc, char **argv)
{int sockfd;struct sockaddr_in servaddr;char buf[MAXLINE];// 創建 socketsockfd = socket(AF_INET, SOCK_STREAM, 0);// 設置服務器地址和端口memset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);servaddr.sin_port = htons(SERVER_PORT);// 連接到服務器connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr));// 創建 SSL 對象并進行握手SSL *ssl = sync_initialize_ssl("cert.pem", "key.pem", SSL_MODE_CLIENT, sockfd); // 發送數據給服務器SSL_write(ssl, "Hello, server!", strlen("Hello, server!"));// 讀取服務器的回復memset(buf, 0, MAXLINE);SSL_read(ssl, buf, MAXLINE);printf("Received: %s\n", buf);// 關閉連接和清理資源close(sockfd);SSL_shutdown(ssl);SSL_free(ssl);return 0;
}
使用tcpdump檢驗
服務端和客戶端編寫完成后,分別運行起來,這里我運行的端口是9990。使用tcpdump抓取9990端口的傳輸數據
tcpdump -i any port 9990 -A
此時分別運行后,服務端客戶端數據完成交互后,抓包所看到的數據已經是密文傳輸了。