NodeJS Https HSM雙向認證實現

githoniel發表於2019-03-07

Intro

工作中需要建立一套HSM的HTTPS雙向認證通道,即通過硬體加密機(Ukey)進行本地加密運算的HTTPS雙向認證,和銀行的UKEY認證類似。

NodeJS可以利用openSSL的HSM plugin方式實現,但是需要編譯C++,太麻煩,作者採用了利用Node Socket介面,純JS自行實現Https/Http協議的方式實現

具體實現可以參考如下node-https-hsm

TLS規範自然是參考RFC文件The Transport Layer Security (TLS) Protocol Version 1.2

概述

本次TLS雙向認證支援以下加密套件(*為建議使用套件):

  • TLS_RSA_WITH_AES_128_CBC_SHA256(TLS v1.2) *
  • TLS_RSA_WITH_AES_256_CBC_SHA256(TLS v1.2) *
  • TLS_RSA_WITH_AES_128_CBC_SHA(TLS v1.1)
  • TLS_RSA_WITH_AES_256_CBC_SHA(TLS v1.1)

四種加密套件流程完全一致,只是部分演算法細節與報文略有差異,體現在

  • AES_128/AES_256的會話AES金鑰長度分別為16/32位元組。
  • TLS 1.1 在計算finish報文資料時,進行的是MD5 + SHA1的HASH演算法,而在TLS v1.2下,HASH演算法變成了單次SHA256。
  • TLS 1.1 處理finish報文時的偽隨機演算法(PRF)需要將種子資料為分兩塊,分別用 MD5 / SHA1 取HASH後異或,TLS 1.2 為單次 SHA256。
  • TLS 1.2 的 CertificateVerify / ServerKeyExchange 報文末尾新增2個位元組的 Signature Hash Algorithm,表示 hash_alg 和 sign_alg。

目前業界推薦使用TLS v1.2, TLS v1.1不建議使用。

流程圖

以下為 TLS 完整握手流程圖

 * =======================FULL HANDSHAKE======================
 * Client                                               Server
 *
 * ClientHello                  -------->
 *                                                 ServerHello
 *                                                 Certificate
 *                                          CertificateRequest
 *                              <--------      ServerHelloDone
 * Certificate
 * ClientKeyExchange
 * CertificateVerify
 * Finished                     -------->
 *                                          change_cipher_spec
 *                              <--------             Finished
 * Application Data             <------->     Application Data
複製程式碼

流程詳解

客戶端發起握手請求

TLS握手始於客戶端發起 ClientHello 請求。

struct {
    uint32 gmt_unix_time; // UNIX 32-bit format, UTC時間
    opaque random_bytes[28]; // 28位長度隨機數
} Random; //隨機數

struct {
    ProtocolVersion client_version; // 支援的最高版本的TLS版本
    Random random; // 上述隨機數
    SessionID session_id; // 會話ID,新會話為空
    CipherSuite cipher_suites<2..2^16-2>; // 客戶端支援的所有加密套件,上述四種
    CompressionMethod compression_methods<1..2^8-1>; // 壓縮演算法
    select (extensions_present) { // 額外外掛,為空
        case false:
            struct {};
        case true:
            Extension extensions<0..2^16-1>;
    };
} ClientHello; // 客戶端傳送支援的TLS版本、客戶端隨機數、支援的加密套件等資訊
複製程式碼

伺服器端回應客戶端握手請求

伺服器端收到 ClientHello 後,如果支援客戶端的TLS版本和演算法要求,則返回 ServerHello, Certificate, CertificateRequest, ServerHelloDone 報文

struct {
    ProtocolVersion server_version; // 服務端最後決定使用的TLS版本
    Random random; // 與客戶端隨機數演算法相同,但是必須是獨立生成,與客戶端毫無關聯
    SessionID session_id; // 確定的會話ID
    CipherSuite cipher_suite; // 最終決定的加密套件
    CompressionMethod compression_method; // 最終使用的壓縮演算法
    select (extensions_present) { // 額外外掛,為空
        case false:
            struct {};
        case true:
            Extension extensions<0..2^16-1>;
    };
} ServerHello; // 伺服器端返回最終決定的TLS版本,演算法,會話ID和伺服器隨機數等資訊

struct {
    ASN.1Cert certificate_list<0..2^24-1>; // 伺服器證書資訊
} Certificate; // 向客戶端傳送伺服器證書

struct {
    ClientCertificateType certificate_types<1..2^8-1>; // 證書型別,本次握手為 值固定為rsa_sign 
    SignatureAndHashAlgorithm supported_signature_algorithms<2^16-1>; // 支援的HASH 簽名演算法
    DistinguishedName certificate_authorities<0..2^16-1>; // 伺服器能認可的CA證書的Subject列表
} CertificateRequest; // 本次握手為雙向認證,此報文表示請求客戶端傳送客戶端證書

struct {

} ServerHelloDone // 標記伺服器資料末尾,無內容
複製程式碼

客戶端收到伺服器後響應

客戶端應校驗伺服器端證書,通常用當用本地儲存的可信任CA證書校驗,如果校驗通過,客戶端將返回 Certificate, ClientKeyExchange, CertificateVerify, change_cipher_spec, Finished 報文。

CertificateVerify 報文中的簽名為Ukey硬體簽名, 此外客戶端證書也是從Ukey讀取。

struct {
    ASN.1Cert certificate_list<0..2^24-1>; // 伺服器證書資訊
} Certificate; // 向伺服器端傳送客戶端證書

struct {
    select (KeyExchangeAlgorithm) {
        case rsa:
            EncryptedPreMasterSecret; // 伺服器採用RSA演算法,用伺服器端證書的公鑰,加密客戶端生成的46位元組隨機數(premaster secret)
        case dhe_dss:
        case dhe_rsa:
        case dh_dss:
        case dh_rsa:
        case dh_anon:
            ClientDiffieHellmanPublic;
    } exchange_keys;
} ClientKeyExchange; // 用於返回加密的客戶端生成的隨機金鑰(premaster secret)

struct {
    digitally-signed struct {
        opaque handshake_messages[handshake_messages_length]; // 採用客戶端RSA私鑰,對之前所有的握手報文資料,HASH後進行RSA簽名
    }
} CertificateVerify; // 用於伺服器端校驗客戶端對客戶端證書的所有權

struct {
    enum { change_cipher_spec(1), (255) } type; // 固定值0x01
} ChangeCipherSpec; // 通知伺服器後續報文為密文

struct {
    opaque verify_data[verify_data_length];  // 校驗密文,演算法PRF(master_secret, 'client finished', Hash(handshake_messages))
} Finished; // 密文資訊,計算之前所有收到和傳送的資訊(handshake_messages)的摘要,加上`client finished`, 執行PRF演算法
複製程式碼

Finished 報文生成過程中,將產生會話金鑰 master secret,然後生成Finish報文內容。

master_secret = PRF(pre_master_secret, "master secret", ClientHello.random + ServerHello.random)
verify_data = PRF(master_secret, 'client finished', Hash(handshake_messages))
複製程式碼

PRF為TLS v1.2規定的偽隨機演算法, 此例子中,HMAC演算法為 SHA256

PRF(secret, label, seed) = P_<hash>(secret, label + seed)

P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) +
                        HMAC_hash(secret, A(2) + seed) +
                        HMAC_hash(secret, A(3) + seed) + ...
// A(0) = seed
// A(i) = HMAC_hash(secret, A(i-1))
複製程式碼

伺服器完成握手

服務收到請求後,首先校驗客戶端證書的合法性,並且驗證客戶端證書籤名是否合法。根據伺服器端證書私鑰,解密 ClientKeyExchange,獲得pre_master_secret, 用相同的PRF演算法即可獲取會話金鑰,校驗客戶端 Finish 資訊是否正確。如果正確,則伺服器端與客戶端完成金鑰交換。 返回 change_cipher_spec, Finished 報文。

struct {
    enum { change_cipher_spec(1), (255) } type; // 固定值0x01
} ChangeCipherSpec; // 通知伺服器後續報文為密文

struct {
    opaque verify_data[verify_data_length];  // 校驗密文,演算法PRF(master_secret, 'server finished', Hash(handshake_messages))
} Finished; // 密文資訊,計算之前所有收到和傳送的資訊(handshake_messages)的摘要,加上`server finished`, 執行PRF演算法
複製程式碼

客戶端會話開始

客戶端校驗伺服器的Finished報文合法後,握手完成,後續用 master_secret 傳送資料。

相關文章