HTTPS是以安全為目标的HTTP通道,簡單講是HTTP的安全版。即HTTP下加入SSL層,HTTPS的安全基礎是SSL,是以加密的詳細内容就需要SSL。 Nebula 是一個為開發者提供一個快速開發高并發網絡服務程式或搭建高并發分布式服務叢集的高性能事件驅動網絡架構。Nebula作為通用網絡架構提供HTTPS支援十分重要,Nebula既可用作https伺服器,又可用作https用戶端。本文将結合Nebula架構的https實作詳細講述基于openssl的SSL程式設計。如果覺得本文對你有用,幫忙到Nebula的 Github 或 碼雲 給個star,謝謝。Nebula不僅是一個架構,還提供了一系列基于這個架構的應用,目标是打造一個高性能分布式服務叢集解決方案。Nebula的主要應用領域:即時通訊(成功應用于一款 IM )、消息推送平台、資料實時分析計算( 成功案例 )等,Bwar還計劃基于Nebula開發爬蟲應用。
1. SSL加密通信
HTTPS通信是在TCP通信層與HTTP應用層之間增加了SSL層,如果應用層不是HTTP協定也是可以使用SSL加密通信的,比如WebSocket協定WS的加上SSL層之後的WSS。Nebula架構可以通過更換Codec達到不修改代碼變更通訊協定目的,Nebula增加SSL支援後,所有Nebula支援的通訊協定都有了SSL加密通訊支援,基于Nebula的業務代碼無須做任何修改。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicGcq5yY2czY4MjNlhTN1MjZzIGOxI2M1UTZ2kTOjdTM2EzYyE2M38CX0VmbjN3bvwFdl5mLh5WaoN2cv5yZtl2Yz92Lc9CX6MHc0RHaiojIsJye.jpg)
Socket連接配接建立後的SSL連接配接建立過程:
2. OpenSSL API
OpenSSL的API很多,但并不是都會被使用到,如果需要檢視某個API的詳細使用方法可以閱讀
API文檔。
2.1 初始化OpenSSL
OpenSSL在使用之前,必須進行相應的初始化工作。在建立SSL連接配接之前,要為Client和Server分别指定本次連接配接采用的協定及其版本,目前能夠使用的協定版本包括SSLv2、SSLv3、SSLv2/v3和TLSv1.0。SSL連接配接若要正常建立,則要求Client和Server必須使用互相相容的協定。 下面是Nebula架構SocketChannelSslImpl::SslInit()函數初始化OpenSSL的代碼,根據OpenSSL的不同版本調用了不同的API進行初始化。
#if OPENSSL_VERSION_NUMBER >= 0x10100003L
if (OPENSSL_init_ssl(OPENSSL_INIT_LOAD_CONFIG, NULL) == 0)
{
pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__, "OPENSSL_init_ssl() failed!");
return(ERR_SSL_INIT);
}
/*
* OPENSSL_init_ssl() may leave errors in the error queue
* while returning success
*/
ERR_clear_error();
#else
OPENSSL_config(NULL);
SSL_library_init(); // 初始化SSL算法庫函數( 加載要用到的算法 ),調用SSL函數之前必須調用此函數
SSL_load_error_strings(); // 錯誤資訊的初始化
OpenSSL_add_all_algorithms();
#endif
2.2 建立CTX
CTX是SSL會話環境,建立連接配接時使用不同的協定,其CTX也不一樣。建立CTX的相關OpenSSL函數:
//用戶端、服務端都需要調用
SSL_CTX_new(); //申請SSL會話環境
//若有驗證對方證書的需求,則需調用
SSL_CTX_set_verify(); //指定證書驗證方式
SSL_CTX_load_verify_location(); //為SSL會話環境加載本應用所信任的CA憑證清單
//若有加載證書的需求,則需調用
int SSL_CTX_use_certificate_file(); //為SSL會話加載本應用的證書
int SSL_CTX_use_certificate_chain_file();//為SSL會話加載本應用的證書所屬的證書鍊
int SSL_CTX_use_PrivateKey_file(); //為SSL會話加載本應用的私鑰
int SSL_CTX_check_private_key(); //驗證所加載的私鑰和證書是否相比對
2.3 建立SSL套接字
在建立SSL套接字之前要先建立Socket套接字,建立TCP連接配接。建立SSL套接字相關函數:
SSL *SSl_new(SSL_CTX *ctx); //建立一個SSL套接字
int SSL_set_fd(SSL *ssl, int fd); //以讀寫模式綁定流套接字
int SSL_set_rfd(SSL *ssl, int fd); //以隻讀模式綁定流套接字
int SSL_set_wfd(SSL *ssl, int fd); //以隻寫模式綁定流套接字
2.4 完成SSL握手
在這一步,我們需要在普通TCP連接配接的基礎上,建立SSL連接配接。與普通流套接字建立連接配接的過程類似:Client使用函數SSL_connect()【類似于流套接字中用的connect()】發起握手,而Server使用函數SSL_ accept()【類似于流套接字中用的accept()】對握手進行響應,進而完成握手過程。兩函數原型如下:
int SSL_connect(SSL *ssl);
int SSL_accept(SSL *ssl);
握手過程完成之後,Client通常會要求Server發送證書資訊,以便對Server進行鑒别。其實作會用到以下兩個函數:
X509 *SSL_get_peer_certificate(SSL *ssl); //從SSL套接字中擷取對方的證書資訊
X509_NAME *X509_get_subject_name(X509 *a); //得到證書所用者的名字
2.5 資料傳輸
經過前面的一系列過程後,就可以進行安全的資料傳輸了。在資料傳輸階段,需要使用SSL_read( )和SSL_write( )來代替普通流套接字所使用的read( )和write( )函數,以此完成對SSL套接字的讀寫操作,兩個新函數的原型分别如下:
int SSL_read(SSL *ssl,void *buf,int num); //從SSL套接字讀取資料
int SSL_write(SSL *ssl,const void *buf,int num); //向SSL套接字寫入資料
2.6 會話結束
當Client和Server之間的通信過程完成後,就使用以下函數來釋放前面過程中申請的SSL資源:
int SSL_shutdown(SSL *ssl); //關閉SSL套接字
void SSl_free(SSL *ssl); //釋放SSL套接字
void SSL_CTX_free(SSL_CTX *ctx); //釋放SSL會話環境
3. SSL 和 TLS
HTTPS 使用 SSL(Secure Socket Layer) 和 TLS(Transport LayerSecurity)這兩個協定。 SSL 技術最初是由浏覽器開發商網景通信公司率先倡導的,開發過 SSL3.0之前的版本。目前主導權已轉移到 IETF(Internet Engineering Task Force,Internet 工程任務組)的手中。
IETF 以 SSL3.0 為基準,後又制定了 TLS1.0、TLS1.1 和 TLS1.2。TSL 是以SSL 為原型開發的協定,有時會統一稱該協定為 SSL。目前主流的版本是SSL3.0 和 TLS1.0。
由于 SSL1.0 協定在設計之初被發現出了問題,就沒有實際投入使用。SSL2.0 也被發現存在問題,是以很多浏覽器直接廢除了該協定版本。
4. Nebula中的SSL通訊實作
Nebula架構同時支援SSL服務端應用和SSL用戶端應用,對openssl的初始化隻需要初始化一次即可(SslInit()隻需調用一次)。Nebula架構的SSL相關代碼(包括用戶端和服務端的實作)都封裝在
SocketChannelSslImpl這個類中。Nebula的SSL通信是基于異步非阻塞的socket通信,并且不使用openssl的BIO(因為沒有必要,代碼還更複雜了)。
SocketChannelSslImpl是
SocketChannelImpl的派生類,在SocketChannelImpl正常TCP通信之上增加了SSL通信層,兩個類的調用幾乎沒有差異。SocketChannelSslImpl類聲明如下:
class SocketChannelSslImpl : public SocketChannelImpl
{
public:
SocketChannelSslImpl(SocketChannel* pSocketChannel, std::shared_ptr<NetLogger> pLogger, int iFd, uint32 ulSeq, ev_tstamp dKeepAlive = 0.0);
virtual ~SocketChannelSslImpl();
static int SslInit(std::shared_ptr<NetLogger> pLogger);
static int SslServerCtxCreate(std::shared_ptr<NetLogger> pLogger);
static int SslServerCertificate(std::shared_ptr<NetLogger> pLogger,
const std::string& strCertFile, const std::string& strKeyFile);
static void SslFree();
int SslClientCtxCreate();
int SslCreateConnection();
int SslHandshake();
int SslShutdown();
virtual bool Init(E_CODEC_TYPE eCodecType, bool bIsClient = false) override;
// 覆寫基類的Send()方法,實作非阻塞socket連接配接建立後繼續建立SSL連接配接,并收發資料
virtual E_CODEC_STATUS Send() override;
virtual E_CODEC_STATUS Send(int32 iCmd, uint32 uiSeq, const MsgBody& oMsgBody) override;
virtual E_CODEC_STATUS Send(const HttpMsg& oHttpMsg, uint32 ulStepSeq) override;
virtual E_CODEC_STATUS Recv(MsgHead& oMsgHead, MsgBody& oMsgBody) override;
virtual E_CODEC_STATUS Recv(HttpMsg& oHttpMsg) override;
virtual E_CODEC_STATUS Recv(MsgHead& oMsgHead, MsgBody& oMsgBody, HttpMsg& oHttpMsg) override;
virtual bool Close() override;
protected:
virtual int Write(CBuffer* pBuff, int& iErrno) override;
virtual int Read(CBuffer* pBuff, int& iErrno) override;
private:
E_SSL_CHANNEL_STATUS m_eSslChannelStatus; //在基類m_ucChannelStatus通道狀态基礎上增加SSL通道狀态
bool m_bIsClientConnection;
SSL* m_pSslConnection;
static SSL_CTX* m_pServerSslCtx; //當打開ssl選項編譯,啟動Nebula服務則自動建立
static SSL_CTX* m_pClientSslCtx; //預設為空,當打開ssl選項編譯并且第一次發起了對其他SSL服務的連接配接時(比如通路一個https位址)建立
};
SocketChannelSslImpl類中帶override關鍵字的方法都是覆寫基類SocketChannelImpl的同名方法,也是實作SSL通信與非SSL通信調用透明的關鍵。不帶override關鍵字的方法都是SSL通信相關方法,這些方法裡有openssl的函數調用。不帶override的方法中有靜态和非靜态之分,靜态方法在程序中隻會被調用一次,與具體Channel對象無關。SocketChannel外部不需要調用非靜态的ssl相關方法。
因為是非阻塞的socket,SSL_do_handshake()和SSL_write()、SSL_read()傳回值并不完全能判斷是否出錯,還需要SSL_get_error()擷取錯誤碼。SSL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE都是正常的。
網上的大部分openssl例子程式是按順序調用openssl函數簡單實作同步ssl通信,在非阻塞IO應用中,ssl通信要複雜許多。SocketChannelSslImpl實作的是非阻塞的ssl通信,從該類的實作上看整個通信過程并非完全線性的。下面的SSL通信圖更清晰地說明了Nebula架構中SSL通信是如何實作的:
SocketChannelSslImpl中的靜态方法在程序生命期内隻需調用一次,也可以了解成SSL_CTX_new()、SSL_CTX_free()等方法隻需調用一次。更進一步了解SSL_CTX結構體在程序内隻需要建立一次(在Nebula中分别為Server和Client各建立一個)就可以為所有SSL連接配接所用;當然,為每個SSL連接配接建立獨立的SSL_CTX也沒問題(Nebula 0.4中實測過為每個Client建立獨立的SSL_CTX),但一般不這麼做,因為這樣會消耗更多的記憶體資源,并且效率也會更低。
建立SSL連接配接時,用戶端調用SSL_connect(),服務端調用SSL_accept(),許多openssl的demo都是這麼用的。Nebula中用的是SSL_do_handshake(),這個方法同時适用于用戶端和服務端,在兼具client和server功能的服務更适合用SSL_do_handshake()。注意調用SSL_do_handshake()前,如果是client端需要先調用SSL_set_connect_state(),如果是server端則需要先調用SSL_set_accept_state()。非阻塞IO中,SSL_do_handshake()可能需要調用多次才能完成握手,具體調用時機需根據SSL_get_error()擷取錯誤碼SSL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE判斷需監聽讀事件還是寫事件,在對應事件觸發時再次調用SSL_do_handshake()。詳細實作請參考SocketChannelSslImpl的Send和Recv方法。
關閉SSL連接配接時先調用SSL_shutdown()正常關閉SSL層連接配接(非阻塞IO中SSL_shutdown()亦可能需要調用多次)再調用SSL_free()釋放SSL連接配接資源,最後關閉socket連接配接。SSL_CTX無須釋放。整個SSL通信順利完成,Nebula 0.4在開多個終端用shell腳本死循環調用curl簡單壓測中SSL client和SSL server功能一切正常:
while :
do
curl -v -k -H "Content-Type:application/json" -X POST -d '{"hello":"nebula ssl test"}' https://192.168.157.168:16003/test_ssl
done
測試方法如下圖:
檢視資源使用情況,SSL Server端的記憶體使用一直在增長,疑似有記憶體洩漏,不過pmap -d檢視某一項anon記憶體達到近18MB時不再增長,說明可能不是記憶體洩漏,隻是部分記憶體被openssl當作cache使用了。這個問題網上沒找到解決辦法。從struct ssl_ctx_st結構體定義發現端倪,再從nginx源碼中發現了SSL_CTX_remove_session(),于是在SSL_free()之前加上SSL_CTX_remove_session()。session複用可以提高SSL通信效率,不過Nebula暫時不需要。
這種測試方法把NebulaInterface作為SSL服務端,NebulaLogic作為SSL用戶端,同時完成了Nebula架構SSL服務端和用戶端功能測試,簡單的壓力測試。Nebula架構的SSL通信測試通過,也可以投入生産應用,在後續應用中肯定還會繼續完善。openssl真的難用,難怪被吐槽那麼多,或許不久之後的Nebula版本将用其他ssl庫替換掉openssl。
5. 結束
加上SSL支援的Nebula架構測試通過,雖然不算太複雜,但過程還是蠻曲折,耗時也挺長。這裡把Nebula使用openssl開發SSL通信分享出來,希望對準備使用openssl的開發者有用。如果覺得本文對你有用,别忘了到Nebula的
給個star,謝謝。
本文來自雲栖社群合作夥伴“開源中國”
本文作者:Bwar
https://my.oschina.net/cqcbw 原文連結