OpenSSL 編程指南

目錄

  • 前言
  • 初始化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 的函數:

協議號通用服務端專用客戶端專用
SSLv2SSLv2_method()SSLv2_server_ method()SSLv2_client_ method()
SSLv3SSLv3_method()SSLv3_server_ method()SSLv3_client_ method()
TLSv1TLSv1_method()TLSv1_server_ method()TLSv1_client_ method()
SSLv23SSLv23_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做一個對比。

Client Server connect() accept() send(data) recv(data) send(response) recv(response) close() close() Client Server

上面的是我們通常在做一個socket連接的時候會涉及到的握手過程。服務端會通過accept去接收客戶端的請求。客戶端通過connect去連接到客戶端。使用send和recv去做數據的傳輸。那么傳輸過程中的參數data也就是我們交互的數據了。當我們建立連接開始發送數據的時候。使用一些抓包工具wireshark,或者tcpdump去監聽socket端口就能很輕松地獲取到傳輸的明文數據了。

2、Socket+SSL的初始化流程

所以為了避免數據的監聽,我們就需要使用SSL去建立一個安全的通道。這里我們先把SSL的建立當作一個子流程:

Client Server conn_fd=connect() accept_fd=accept() SSL Sub-Process Begins Initialize SSL Environment(準備證書,私鑰) Initialize SSL Environment(準備證書,私鑰) Create new SSL session (綁定conn_fd) Create new SSL session (綁定accept_fd) SSL_connect() (傳入證書,私鑰,和conn_fd進行握手) SSL_accept() (傳入證書,私鑰,和accept_fd進行握手) SSL Sub-Process Ends SSL_write(data) SSL_read(data) SSL_write(response) SSL_read(response) SSL Shutdown Sub-Process Begins SSL_shutdown() SSL_shutdown() SSL Shutdown Sub-Process Ends close() close() Client Server

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.pemcert.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

在這里插入圖片描述
此時分別運行后,服務端客戶端數據完成交互后,抓包所看到的數據已經是密文傳輸了。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/211081.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/211081.shtml
英文地址,請注明出處:http://en.pswp.cn/news/211081.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Scrum

Scrum是一個用于開發和維持復雜產品的框架&#xff0c;是一個增量的、迭代的開發過程。在這個框架中&#xff0c;整個開發過程由若干個短的迭代周期組成&#xff0c;一個短的迭代周期稱為一個Sprint&#xff0c;每個Sprint的建議長度是2到4周(互聯網產品研發可以使用1周的Sprin…

【Linux】輸出緩沖區和fflush刷新緩沖區

目錄 一、輸出緩沖區 1.1 輸出緩沖區的使用 1.2 緩沖區的刷新 1.3 輸出緩沖區的作用 二、回車換行 一、輸出緩沖區 C/C語言&#xff0c;當調用輸出函數&#xff08;如printf()、puts()、fwrite()等&#xff09;時&#xff0c;會給我們提供默認的緩沖區。這些數據先存…

虛擬機安裝 hyper—v 沙盒

一、下載系統鏡像 1、確認電腦內存在8G及以上并提前準備完整的系統鏡像 安裝Hyper-V并重啟電腦后打開程序選擇虛擬機 選擇安裝位置并設置保留第一代的虛擬參數即可開始分配內存&#xff0c;根據自己的需求進行設置 右鍵虛擬機啟動并開始運行&#xff0c;進行鏡像系統的安裝便完…

【Flutter】創建應用頂級組件,應用根組件 (學習記錄)

前言 在 Flutter 中&#xff0c;應用的頂級組件或根組件通常是在 main() 函數中通過 runApp() 方法創建的。這個組件通常是一個 MaterialApp、CupertinoApp、GetMaterialApp 或其他類似的應用框架組件。 以下是一個創建 MaterialApp 作為根組件的示例&#xff1a; void main()…

牛客算法心得——環形數組的連續子數組最大和(dp)

大家好&#xff0c;我是晴天學長&#xff0c; 一個找連續子數組最大和的變形題&#xff0c;需要的小伙伴可以關注支持一下哦&#xff01;后續會繼續更新的。&#x1f4aa;&#x1f4aa;&#x1f4aa; 1) .環形數組的連續子數組的最大和 描述 給定一個長度為 nn 的環形整數數組&…

『 MySQL數據庫 』聚合統計

文章目錄 前言 &#x1f951;&#x1f95d; 聚合函數&#x1f353; COUNT( ) 查詢數據數量&#x1f353; SUM( ) 查詢數據總和&#x1f353; AVG( ) 查詢數據平均值&#x1f353; MAX( ) 查詢數據最大值&#x1f353; MIN( ) 查詢數據最小值 &#x1f95d; 數據分組GROUP BY子句…

湖科大計網:計算機網絡概述

一、計算機網絡的性能指標 一、速率 有時候數據量也認為是以10為底的&#xff0c;看怎么好算。&#xff08;具體吉大考試用什么待商榷&#xff09; 二、帶寬 在模擬信號系統中帶寬的含義&#xff0c;本課程中用到的地方是&#xff1a;香農定理和奈奎斯特定理公式的應用之中。 …

全面高壓化與全面超快充,破解新能源汽車的時代難題

是什么讓新能源車主感到疲憊與焦慮&#xff1f;是什么阻擋更多消費者選擇新能源汽車&#xff1f;我們在身邊進行一個簡單的調查就會發現&#xff0c;問題的答案非常一致&#xff1a;充電。 充電難&#xff0c;充電慢的難題&#xff0c;始終是困擾新能源汽車產業發展&#xff0c…

vue,uniapp的pdf等文件在線預覽

vue&#xff0c;uniapp文件在線預覽方案&#xff0c;用了個稍微偏門一點的方法實現了 通過后端生成文件查看頁面&#xff0c;然后前端只要展示這個網頁就行&#xff0c;uniapp就用web-view來展示&#xff0c;后臺系統就直接window.open()打開就行 示例查看PDF文件&#xff0c;…

每日一練【四數之和】

一、題目描述 18. 四數之和 給你一個由 n 個整數組成的數組 nums &#xff0c;和一個目標值 target 。請你找出并返回滿足下述全部條件且不重復的四元組 [nums[a], nums[b], nums[c], nums[d]] &#xff08;若兩個四元組元素一一對應&#xff0c;則認為兩個四元組重復&#x…

基于ssm社區管理與服務的設計與實現論文

目錄 摘 要 1 Abstract 2 第一章 緒論 3 1.1研究背景 3 1.2 研究現狀 3 1.3 研究內容 4 第二章 系統關鍵技術 5 2.1 Java簡介 5 2.2 MySql數據庫 5 2.3 B/S結構 6 2.4 Tomcat服務器 6 第三章 系統分析 7 3.1可行性分析 7 3.1.1技術可行性 7 3.1.2經濟可行性 7 3.1.3運行可行性…

uniapp自定義的日歷(純手寫)

效果圖&#xff1a; html&#xff1a; <!-- 年月 --><view class"box"><view class"box_time"><view class"time"><image click"lefts" :src"url/uploads/20231206/9d1fb520b12383960dca3c214d84fa0…

vue獲取主機id和IP地址

獲取主機id和IP地址 在vue.config.js const os require(“os”); function getNetworkIp() { let needHost “”; // 打開的host try { // 獲得網絡接口列表 let network os.networkInterfaces(); for (let dev in network) { let iface network[dev]; for (let i 0; i …

LLM之Agent(五)| AgentTuning:清華大學與智譜AI提出AgentTuning提高大語言模型Agent能力

?論文地址&#xff1a;https://arxiv.org/pdf/2310.12823.pdf Github地址&#xff1a;https://github.com/THUDM/AgentTuning 在ChatGPT帶來了大模型的蓬勃發展&#xff0c;開源LLM層出不窮&#xff0c;雖然這些開源的LLM在各自任務中表現出色&#xff0c;但是在真實環境下作…

【Android】Glide的簡單使用(下)

文章目錄 緩存設置內存緩存硬盤緩存自定義磁盤緩存行為圖片請求優先級縮略圖旋轉圖片Glide的回調:TargetsBaseTargetTarget注意事項設置具體尺寸的Target 調試及Debug獲取異常信息 配置第三方網絡庫自定義緩存 緩存設置 GlideApp .with(context).load(gifUrl).asGif().error(…

MySQL_7.索引概述

1.什么是索引 在關系數據庫中&#xff0c;索引是一種單獨的、物理的數對數據庫表中一列或多列的值進行排序的一種存儲結構。 它是某個表中一列或若干列值的集合和相應的指向表中物理標識這些值的數據頁的邏輯指針清單 2.索引的優點 (1)通過創建唯一性索引,可以保證數據庫表中每…

編寫Yaml文件當Poc,利用Nuclei掃描器去掃描漏洞

編寫Yaml文件當Poc,利用Nuclei掃描器去掃描漏洞 YAML是一種數據序列化語言&#xff0c;它的基本語法規則注意如下&#xff1a; -大小寫敏感 -使用縮進表示層級關系 -縮進時不允許使用Tab鍵&#xff0c;只允許使用空格。 -縮進的空格數目不重要&#xff0c;只要相同層級的元…

VSCode如何設置Vue前端的debug調試

vscode在調試vue.代碼時&#xff0c;如何進行debug? 1.安裝Chrome Debug插件。 2.在launch.json中&#xff0c;將url修改成你前端項目的路徑&#xff1a; 1 {2 // Use IntelliSense to learn about possible attributes.3 // Hover to view descriptions of existing att…

redis 三主三從高可用集群docker swarm

由于數據量過大&#xff0c;單個Master復制集難以承擔&#xff0c;因此需要對多個復制集進行集群&#xff0c;形成水平擴展每個復制集只負責存儲整個數據集的一部分&#xff0c;這就是Redis的集群&#xff0c;其作用是提供在多個Redis節點間共享數據的程序集。 官網介紹地址 re…

Elasticsearch:向量數據庫的真相

通過工作示例了解什么是向量數據庫、它們如何實現 “相似性” 搜索以及它們可以在明顯的 LLM 空間之外的哪些地方使用。除非你一直生活在巖石下&#xff0c;否則你可能聽說過諸如生成式人工智能和大型語言模型&#xff08;LLM&#xff09;之類的術語。 除此之外&#xff0c;你很…