基於OpenSSL的HTTPS通訊C++實現

  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開發爬蟲應用。html

1. SSL加密通訊

  HTTPS通訊是在TCP通訊層與HTTP應用層之間增長了SSL層,若是應用層不是HTTP協議也是可使用SSL加密通訊的,好比WebSocket協議WS的加上SSL層以後的WSS。Nebula框架能夠經過更換Codec達到不修改代碼變動通信協議目的,Nebula增長SSL支持後,全部Nebula支持的通信協議都有了SSL加密通信支持,基於Nebula的業務代碼無須作任何修改。nginx

https_communication

  Socket鏈接創建後的SSL鏈接創建過程:git

ssl_communication

2. OpenSSL API

  OpenSSL的API不少,但並非都會被使用到,若是須要查看某個API的詳細使用方法能夠閱讀API文檔github

2.1 初始化OpenSSL

  OpenSSL在使用以前,必須進行相應的初始化工做。在創建SSL鏈接以前,要爲Client和Server分別指定本次鏈接採用的協議及其版本,目前可以使用的協議版本包括SSLv二、SSLv三、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函數:shell

//客戶端、服務端都須要調用
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()】對握手進行響應,從而完成握手過程。兩函數原型以下:json

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通訊是如何實現的:

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_test

  查看資源使用狀況,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的Github碼雲給個star,謝謝。

<br/>

參考資料:

相關文章
相關標籤/搜索