一、SSL協議的設計思想
上一篇文章通過三個例子說明了HTTP協議存在的三個安全性問題:
- 通信內容可以被竊聽
- 通信內容可以被篡改
- 通信對象可以被冒充
或許有人會想在WEB應用層面解決這個問題,但是這樣做有幾個缺點:
- 大大增加了WEB應用的實現難度,除了WEB應用的業務邏輯,還必須為WEB應用單獨設計編寫一套加密和驗證的方案;
- WEB應用本身無法對整個HTTP數據包進行加密,只能對數據包里面的關鍵內容進行加密,其他信息仍然有被竊取的風險,所以基于WEB應用的安全解決方案并不可靠;
- 基于WEB應用的解決方案不具備通用性。
其實不光HTTP協議存在這個問題,幾乎大部分應用層的協議都會存在這些問題,因為應用層的數據包是直接遞送給傳輸控制層明文傳送的。
上個世紀90年代中期,網景公司為了解決HTTP協議明文傳送的安全性問題,設計了SSL(Secure Sockets Layer 安全套接層)協議。
SSL協議的思想是基于傳輸控制層協議(例如TCP)建立一個安全的網絡連接層。
簡單來說,就是應用層和傳輸層之間多了一個安全套接層,應用層的數據先遞送給安全套接層,安全套接層對應用層的數據進行分段、壓縮、添加消息認證碼和加密之后,再往下遞送給傳輸層進行傳送。同樣,傳輸層把接收到的數據先傳給安全套接層,由安全套接層解密、驗證消息完整性、解壓并且組裝之后再傳給應用層。
這樣一來,傳輸層負責提供可靠的網絡連接,應用層負責處理業務,中間的數據安全由一個單獨的層面來負責,大家各司其職,分工明確,安全套接層替代傳輸層為應用層直接提供安全且可靠的數據傳輸。這樣,所有應用層的協議都可以配合SSL協議實現安全的數據傳輸,相對于基于應用的安全解決方案,這個方案更加通用可靠,也使得應用開發者可以專心處理業務邏輯而無需為應用的數據安全作過多的思考。
二、SSL協議的組成
SSL協議由以下四個子協議組成:
-
記錄協議(Record Protocol)
記錄協議工作在SSL的底層,主要職責是接收上層協議或下層協議的消息并進行一系列的處理,然后再將處理后的消息繼續向下或向上傳遞。對于從上層協議接收的消息,記錄層的處理步驟是:將消息分段、壓縮、添加消息認證碼以及加密;對于從下層協議接收的消息,記錄層的處理步驟是:解密、驗證消息完整性、解壓以及重新組裝消息。其實,記錄層的角色就像一個郵遞員,只負責按照記錄協議的規則進行搬磚工作。另外,記錄層的上層協議除了應用層協議之外,還有SSL的其他子協議。 -
握手協議(Handshake Protocol)
SSL的握手協議又是干嘛的呢?跟TCP的握手協議有什么區別?
SSL協議的主要功能是為應用層提供數據加密等安全服務,那在開始對應用層的數據進行傳輸之前,通信雙方必須得知道該使用什么算法加密數據以及對應的加密密鑰是什么吧?因此,在開始對應用層的數據進行傳輸之前,通信雙方必須得有一個協商的過程,SSL的握手協議就是對這個過程該協商什么以及怎么協商的一個規定。
SSL的握手協議是建立在TCP握手協議之上的。TCP握手協議的作用是建立一個可靠的網絡連接,在TCP連接建立之后,SSL層就可以利用建立的TCP連接傳輸數據,此時進入SSL的握手協商階段。SSL的握手協議主要目的是為通信雙方確立安全連接所需要的安全參數,通常也會在此階段對通信雙方身份的真實性進行驗證。 -
警告協議(Alert Protocol)
無論是在握手階段還是在對應用層數據的傳輸階段,都有可能出現差錯。警告協議規定了在SSL協議工作過程中可能出現的差錯、錯誤的嚴重等級以及相應的處理方式。 -
密碼規范改變協議(Change Cipher Protocol)
在SSL握手剛開始的時候,由于加密參數還沒確定,消息都是明文傳送的;當雙方在協商好加密參數之后,通信雙方在發送握手結束消息之前,需要發送一個密碼規范改變消息(Change Cipher Message)來通知對方隨后的消息都使用剛剛協商好的加密算法和加密密鑰進行加密。
對于握手協議和警告協議,它們包含許多種消息類型,而對于密碼規范改變協議,它只有包含一種消息,該消息只是一個簡單的通知。也許你會覺得奇怪,為什么不將這個如此簡單的通知消息作為握手消息的一個子類型,而需要單獨為它設置一個消息類型呢?后面會對此作出解釋。
SSL協議的組成如下圖所示:
三、SSL會話(Session)和連接(Connection)
會話是一個虛擬概念,在很多地方我們都會碰到這個術語。那在SSL協議中,會話代表什么意思呢?
前面我們提到,在應用層的數據開始傳輸之前,通信雙方需要通過握手協議來協商一些安全參數。但是,握手過程的開銷比較大,如果每次建立連接都要完整的走一遍握手過程來重新協商一遍安全參數,那么客戶端每一次建立連接都會很慢,而且還會占用過多的網絡帶寬資源。為了避免這種情況發生,客戶端和服務器可以把協商好的結果緩存起來并賦予它唯一的標識,當客戶端在一定期限內再次發起連接請求的時候,服務器和客戶端就可以重用之前協商好的結果而不用走完整個握手流程。因此,會話可以理解為:
在特定的時間內,特定的客戶端和特定的服務器之間的所有通信內容的總和
也就是說,一次會話,可能會包含多次連接、多次消息往返。一次會話內的所有連接,可以共享一些信息。比如,壓縮方法、加密算法、哈希算法等。此外,SSL會話還必須保存一個稱之為主秘密(master secret)的信息,客戶端和服務器可以根據這個信息導出本次連接所需要的密鑰及其他一些重要參數。
以下是SSL會話(Session)的屬性:
-
會話標識(session identifier)
服務器端生成的隨機字節序列。 -
對端證書(peer certificate)
對端的X509.v3證書,該狀態可能為null。 - 壓縮方法(compression method)
-
密碼規范(cipher spec)
指定數據加密算法(例如null,DES等)和MAC算法(例如MD5或者SHA),密碼規范還定義了一些加密屬性,例如hash_size。 -
主秘密(master secret)
服務端和客戶端共享的48字節秘密。根據它可以導出每個連接所需的安全參數。 -
是否可重用(is resumable)
標志位,用來表明當前會話是否還可以用來初始化新的連接。
以下則是SSL連接(Connection)所必備的一些屬性:
-
服務器端和客戶端隨機數(server and client random)
服務器和客戶端為每次連接生成的隨機字節序列。要這個有什么用呢?這個兩個隨機數會和會話的主秘密(master secret)一起經過一定的運算生成連接所需的加密密鑰和MAC秘密。這兩個隨機數保證了每次連接都會生成不同的加密密鑰和MAC秘密,保證了連接的安全性。 -
服務端寫MAC秘密(server write MAC secret)
服務端用于生成消息認證碼(Message Authentication Code)的秘密。 -
客戶端寫MAC秘密(client write MAC secret)
客戶端用于生成消息認證碼(Message Authentication Code)的秘密。 -
服務端寫密鑰(server write key)
服務端加密數據用的密鑰和客戶端解密數據用的密鑰。 -
客戶端寫密鑰(client write key)
客戶端加密數據用的密鑰和服務端解密數據用的密鑰。 -
初始向量(initialization vectors)
當使用CBC模式的分組密碼算法時,對應每一個加密密鑰都要維持一個初始化向量(IV)。也就是說,對應于服務端寫密鑰(server write key)要有一個IV,對應于客戶端寫密鑰(client write key)也要有一個。初始向量的作用是避免多次對同一文本加密產生相同的密文,它保證了密文的隨機性。初始向量的值由握手協議初始化,此后每一條記錄的最后一個密文分組作為下一條記錄的初始化向量。 -
序列號(sequence numbers)
通信雙方會各自為發送出去和接收到的消息維護自己的序列號。
注:上面提到的消息認證碼MAC的作用和計算方法后文會有介紹
四、記錄層(Record Layer)
前面已經對記錄層的職能作了描述,這里就不再重復了。接下來主要詳細介紹一下記錄層的三個工作階段:分段和組裝、記錄壓縮和解壓、記錄保護。
4.1 分段和組裝
記錄層把從上層接收到的數據塊分成小于或者等于2^14
字節的SSLPlaintext
記錄。SSLPlaintext
的數據結構如下:
struct {
uint8 major, minor;
} ProtocolVersion;
enum {
change_cipher_spec(20),
alert(21),
handshake(22),
application_data(23),
(255)
} ContentType;
struct {
ContentType type;
ProtocolVersion version;
uint16 length;
opaque fragment[SSLPlaintext.length];
} SSLPlaintext;
- type:消息類型,記錄層傳輸的消息類型有4種:密碼規范改變消息、警告消息、握手消息和應用數據消息;
- version: 協議版本,本文介紹的是SSL 3.0;
-
length:消息長度,即
SSLPlaintext.fragment
的長度,不能超過2^14
字節; - fragment:消息內容。
相反,解壓后的記錄可能會被重新組裝成更大的數據塊再往上層傳送。
注意1:如果上層傳遞下來的消息體積比較小,那么多個同種類型的上層消息有可能會被合并成一個SSLPlaintext
記錄。密碼規范改變消息被設計成一個單獨的消息類型而不是作為握手消息的一部分跟這個特性有關,后面會講具體原因。
注意2:不同類型數據的傳輸可能不是嚴格按先后順序的,有可能是交叉的。通常應用層數據的傳輸優先級要低于其他類型的。
4.2 記錄壓縮解壓
所有記錄都使用會話狀態中定義的壓縮算法進行壓縮。初始的時候壓縮算法的值被定義成CompressionMethod.null
,此時壓縮是一個恒等操作,即壓縮前后的數據是一樣的。壓縮操作將記錄從SSLPlaintext
轉換成SSLCompressed
。如下:
struct {
ContentType type; /* same as SSLPlaintext.type */
ProtocolVersion version;/* same as SSLPlaintext.version */
uint16 length;
opaque fragment[SSLCompressed.length];
} SSLCompressed;
-
length:
SSLCompressed.fragment
的長度,不能超過2^14 + 1024字節; -
fragment:
SSLPlaintext.fragment
的壓縮形式。
壓縮必須是無損壓縮,并且壓縮后的體積增長不能超過1024字節。如果解壓函數遇到一個解壓后體積會超過2^14字節的SSLCompressed.fragment
,它會發出一個錯誤等級為嚴重的decompression_failure
警告消息。
解壓操作則是反過來,將記錄從SSLCompressed
轉換成SSLPlaintext
。
4.3 記錄保護
所有記錄都使用當前會話狀態中的密碼規范CipherSpec
中定義的加密算法和MAC算法實現數據保護。密碼規范的初始值是SSL_NULL_WITH_NULL_NULL
,此時不提供任何安全保護。
一旦握手階段結束,通信雙方就有了共享的秘密(secrets)來加密記錄并且計算信息內容的Message Authentication Code(MAC)。進行加密操作和MAC操作的方法在密碼規范CipherSpec
中定義并且受密碼類型CipherSpec.cipher_type
的影響。加密函數和MAC函數將記錄從SSLCompressed
轉換成SSLCiphertext
,解密函數則相反。
struct {
ContentType type; /* same as SSLCompressed.type */
ProtocolVersion version; /* same as SSLCompressed.version */
uint16 length;
select (CipherSpec.cipher_type) {
case stream: GenericStreamCipher;
case block: GenericBlockCipher;
} fragment;
} SSLCiphertext;
-
length
:SSLCiphertext.fragment
的長度,不能超過2^14+2048字節; -
fragment
:SSLCompressed.fragment
的加密形式,包括MAC。
如上密文結構所示,密碼類型不同,得到的加密數據的結構也不同。密碼類型有兩種:序列密碼stream cipher
和分組密碼block cipher
。下面簡單介紹一下這兩種類型的密碼。
4.3.1 Null 或者標準序列密碼(standard stream cipher)
序列密碼算法(包括BulkCipherAlgorithm.null
)把SSLCompressed.fragment
結構體轉換成序列密碼形式的SSLCiphertext.fragment
結構體,如下。
stream-ciphered struct {
opaque content[SSLCompressed.length];
opaque MAC[CipherSpec.hash_size];
} GenericStreamCipher;
MAC的計算方法如下:
hash(MAC_write_secret + pad_2 +
hash(MAC_write_secret + pad_1 + seq_num +
SSLCompressed.type + SSLCompressed.length +
SSLCompressed.fragment));
-
pad_1:如果
hash
函數是MD5
,pad_1
為字符0x36
重復48次,如果hash
函數是SHA
,pad_1
為字符0x36
重復40次; -
pad_2:如果
hash
函數是MD5
,pad_2
為字符0x5c
重復48次,如果hash
函數是SHA
,pad_2
為字符0x5c
重復40次; - seq_num:該消息的序列號;
- hash:密碼套件中指定的哈希算法。
4.3.2 分組密碼(block cipher)
對于分組密碼算法(例如RC2或DES),加密函數和MAC函數會把SSLCompressed.fragment
結構體轉換成分組密碼形式的結構體SSLCiphertext.fragment
,如下。
block-ciphered struct {
opaque content[SSLCompressed.length];
opaque MAC[CipherSpec.hash_size];
uint8 padding[GenericBlockCipher.padding_length];
uint8 padding_length;
} GenericBlockCipher;
MAC的計算方法同上;
-
padding
:為了讓被加密內容長度等于分組密碼長度整數倍而填充的字符; -
padding_length
:填充字符的長度,不超過分組密碼的長度,也可能是0。
注意:如果分組密碼采用的是CBC模式,第一條傳送紀錄的初始化向量(IV)是由握手協議初始化的,而接下來的記錄的初始化向量則是上一條記錄的最后一個密文分組。
五、密碼規范改變協議(Change Cipher Spec Protocol)
密碼規范改變協議是用來通知密碼策略變化的。該協議只包含一個消息,該消息由值為1的單字節組成,如下。
struct {
enum { change_cipher_spec(1), (255) } type;
} ChangeCipherSpec;
改變密碼規范消息由客戶端或者服務器發送,來通知接收方接下來的消息記錄將會由剛剛協商好的密碼規范和密鑰進行加密保護。
六、警告協議(Alert Protocol)
警告類型是SSL記錄層支持的消息類型之一。警告消息的內容包含警告消息的嚴重程度和關于警告消息的一個描述。如下所示。
enum { warning(1), fatal(2), (255) } AlertLevel;
enum {
close_notify(0),
unexpected_message(10),
bad_record_mac(20),
decompression_failure(30),
handshake_failure(40),
no_certificate(41),
bad_certificate(42),
unsupported_certificate(43),
certificate_revoked(44),
certificate_expired(45),
certificate_unknown(46),
illegal_parameter (47) (255)
} AlertDescription;
struct {
AlertLevel level;
AlertDescription description;
} Alert;
一個fatal
等級的警告消息將導致連接的立即中斷。跟其他消息類型一樣,警告消息同樣會經過記錄層進行壓縮和加密。
6.1 關閉警告(Closure Alerts)
通信的雙方,在關閉連接的寫入端之前,要求發出一個close_notify
警告,對方也要響應一個close_notify
警告并且立即關閉連接,丟棄正在寫入的內容。連接關閉的發起方并不需要等到對方的close_notify
響應才關閉讀出端。如果一個連接中斷了但是沒有發出close_notify
警告,那么與該連接相關聯的會話將不能重用。
6.2 錯誤警告(Error Alerts)
握手協議中的錯誤處理非常簡單。當檢測到錯誤的時候,錯誤的檢測方發送一個消息個對方。當發出或者接收到一個fatal
級別的警告消息時,雙方立即關閉連接。服務器和客戶端要丟棄跟此連接相關聯的會話ID(session identifiers)、密鑰(keys)和秘密(secrets)。
七、握手協議(Handshake Protocol)
在開始介紹握手協議之前,首先要明確握手協議的目標,即在開始傳輸應用層數據之前協商出安全通信所需的安全參數。這些參數主要包括:采用的協議版本、壓縮算法、加密算法、哈希算法以及密鑰等。此外,還可能會對服務器和客戶端的真實身份進行認證。
握手過程的消息時序圖如下:
握手消息的數據結構如下:
enum {
hello_request(0),
client_hello(1),
server_hello(2),
certificate(11),
server_key_exchange (12),
certificate_request(13),
server_hello_done(14),
certificate_verify(15),
client_key_exchange(16),
finished(20), (255)
} HandshakeType;
struct {
HandshakeType msg_type; /* handshake type */
uint24 length; /* bytes in message */
select (HandshakeType) {
case hello_request: HelloRequest;
case client_hello: ClientHello;
case server_hello: ServerHello;
case certificate: Certificate;
case server_key_exchange: ServerKeyExchange;
case certificate_request: CertificateRequest;
case server_hello_done: ServerHelloDone;
case certificate_verify: CertificateVerify;
case client_key_exchange: ClientKeyExchange;
case finished: Finished;
} body;
} Handshake;
7.1 ClientHello
ClientHello
消息的結構如下:
struct {
ProtocolVersion client_version;
Random random;
SessionID session_id;
CipherSuite cipher_suites<2..2^16-1>;
CompressionMethod compression_methods<1..2^8-1>;
} ClientHello;
- client_version:客戶端希望使用的協議版本,一般填寫客戶端支持的最新版本;
- random:客戶端生成的32字節的隨機數,后面導出SSL連接所需的密鑰和秘密時需要用到它。客戶端隨機數與后面介紹到的服務器隨機數一起保證了SSL連接密鑰的隨機性。它的數據結構如下:
struct {
uint32 gmt_unix_time;
opaque random_bytes[28];
} Random;
- session_id:客戶端希望重用的會話ID。如果客戶端沒有可用的會話或者客戶端希望生成新的會話,那么這個值為空;
-
cipher_suites:客戶端支持的密碼套件列表,客戶端希望優先使用的密碼套件排在前面。如果
session_id
的值不為空,那么該向量至少要包含對應會話的密碼套件。密碼套件的內容主要包括:密鑰交換算法、對稱加密算法、哈希算法等。 -
compression_methods:客戶端支持的壓縮方法列表,客戶端希望優先使用的壓縮方法排在前面。如果
session_id
的值不為空,那么該向量至少要包含對應會話的壓縮方法。
7.2 ServerHello
struct {
ProtocolVersion server_version;
Random random;
SessionID session_id;
CipherSuite cipher_suite;
CompressionMethod compression_method;
} ServerHello;
- server_version:服務器會從自己支持的協議的最高版本和客戶端支持的協議的最高版本中選擇一個較低版本的協議;
- random:服務端生成的32字節隨機數,與客戶端的隨機數結構相同;
-
session_id:本次會話ID。如果
ClientHello.session_id
不為空,服務器會查詢它的session
緩存。如果查到了匹配的會話信息并且服務器愿意使用指定的會話建立新的連接,服務器會返回一樣的會話ID。否則,服務器會新建一個會話并且新建會話的ID。 - cipher_suite:服務端從客戶端支持的密碼套件列表里面選擇的一個密碼套件;
- compression_method:服務端從客戶端支持的壓縮方法列表里面選擇的一個壓縮方法。
7.3 Server Certificate
如果服務器要求被認證(通常情況都是這樣),在ServerHello
消息之后,服務器會立即發送它的證書給客戶端。證書的類型必須與選擇的密碼套件的密鑰交換算法相匹配。通常都是X.509.V3證書,當密鑰交換算法為FORTEZZA
算法時,則是經過修改的X.509證書。
證書的內容一般可以分為三部分:
- 服務器的身份信息,例如域名、公司信息等;
- 服務器的公鑰;
- CA的簽名。
客戶端接收到服務器的證書后,首先驗證證書的簽名,如果沒有問題,則可進一步認證服務器的信息并可放心使用證書上附帶的服務器公鑰。
服務器的公鑰可能有兩種用法。最直接的用法就是將服務器公鑰用于密鑰交換。但是,在某些情況下,服務器的公鑰并不直接用于密鑰交換。服務器會根據選擇的密鑰交換算法發送額外的密鑰交換參數給客戶端。為了防止中間人攻擊,服務器會使用自己的私鑰給這些密鑰交換參數進行簽名,此時客戶端可利用服務器證書中的公鑰驗證密鑰交換參數消息的簽名。
Certificate
消息的結構如下:
struct {
ASN.1Cert certificate_list<1..2^24-1>;
} Certificate;
- certificate_list:證書鏈。證書鏈的概念請參考 SSL教程:什么是SSL證書鏈?。
7.4 Server Key Exchange
一般情況下,服務器是不用發送密鑰交換信息的,直接使用服務器證書中附帶的公鑰進行密鑰交換即可。但是,在以下幾種情況下,服務器需要發送ServerKeyExchange
消息用于密鑰交換:
- 服務器沒有證書;
- 服務器有證書,但是證書中的公鑰長度過長,不能用來進行密鑰交換;
- 密鑰交換算法為Diffie-Hellman算法且服務器證書中不包含Diffie-Hellman參數;
- 密鑰交換算法為
FORTEZZA KEA
算法。
注:根據美國當前的出口法,在從美國出口的軟件中,模數大于512位的RSA公鑰不能用來進行密鑰交換。
根據選擇的密鑰交換算法的不同,ServerKeyExchange
消息要發送的參數是不同的。SSL使用的密鑰交換算法有三種:RSA
、Diffie-Hellman
和FORTEZZA KEA
。
ServerKeyExchange
消息的結構如下:
struct {
select (KeyExchangeAlgorithm) {
case diffie_hellman:
ServerDHParams params;
Signature signed_params;
case rsa:
ServerRSAParams params;
Signature signed_params;
case fortezza_kea:
ServerFortezzaParams params;
};
} ServerKeyExchange;
對于RSA算法:
struct {
opaque rsa_modulus<1..2^16-1>;
opaque rsa_exponent<1..2^16-1>;
} ServerRSAParams;
對于DH算法:
struct {
opaque dh_p<1..2^16-1>;
opaque dh_g<1..2^16-1>;
opaque dh_Ys<1..2^16-1>;
} ServerDHParams;
對于FORTEZZA KEA算法:
struct {
opaque r_s [128];
} ServerFortezzaParams;
7.5 Certificate Request
當服務器要對客戶端的身份進行認證時,需要發送此消息。
7.6 ServerHello Done
ServerHelloDone
消息表示Server Hello階段的結束。發出該消息后,服務器會等待客戶端的響應。如果客戶端需要對服務器的身份進行認證,那么在客戶端收到ServerHelloDone
消息時,就需要認證服務器發過來的證書。
ServerHelloDone
的消息結構如下:
struct { } ServerHelloDone;
7.7 Client Certificate
當服務器要對客戶端的身份進行認證時,需要發送此消息。
7.8 Client Key Exchange
客戶端密鑰交換消息的內容取決于選擇了哪一種密鑰交換算法,如下:
struct {
select (KeyExchangeAlgorithm) {
case rsa: EncryptedPreMasterSecret;
case diffie_hellman: ClientDiffieHellmanPublic;
case fortezza_kea: FortezzaKeys;
} exchange_keys;
} ClientKeyExchange;
-
RSA
當使用RSA密鑰交換算法時,由客戶端生成一個48字節的預備主秘密(premaster secret)并用服務器證書中提供的公鑰或者ServerKeyExchange
消息中提供的臨時RSA公鑰加密后發送給服務器。 -
Diffie-Hellman
當使用Diffie-Hellman密鑰交換算法時,客戶端是需要把它的DH公開值傳回給服務器。這時分兩種情況。如果客戶端證書中已經包含了DH公開值,則客戶端密鑰交換消息不用再傳送DH公開值,而是傳送一個空消息;如果客戶端證書沒有包含DH公開值,則客戶端密鑰交換消息需要傳送DH公開值。這樣,根據DH算法,客戶端和服務器都有足夠的參數來生成共同的預備主秘密(premaster secret)。 -
FORTEZZA KEA
當使用FORTEZZA KEA密鑰交換算法時,客戶端會根據FORTEZZA KEA算法首先計算出一個令牌加密密鑰TEK。然后,由客戶端生成客戶端寫密鑰client_write_key
、服務端寫密鑰server_write_key
、客戶端寫密鑰對應的初始向量IVclient_write_iv
、服務端寫密鑰對應的初始向量IVserver_write_iv
以及48字節的預備主秘密(premaster secret)并用TEK進行加密后傳給服務器。當然了,除了這幾個參數外,還要傳送其他的參數來讓服務器能夠產生根客戶端相同的TEK。
到此,應該說客戶端和服務器已經完成了密鑰的交換。因為利用預備主秘密(premaster secret)可以導出主秘密(master secret),利用主秘密(master secret)最后可以導出最后使用的各種secret、key和IV。
主秘密(master secret)的計算方式如下:
master_secret =
MD5(pre_master_secret + SHA(’A’ + pre_master_secret +
ClientHello.random + ServerHello.random)) +
MD5(pre_master_secret + SHA(’BB’ + pre_master_secret +
ClientHello.random + ServerHello.random)) +
MD5(pre_master_secret + SHA(’CCC’ + pre_master_secret +
ClientHello.random + ServerHello.random));
最終使用的密鑰、MAC秘密和初始向量IV的生成方式如下:
key_block =
MD5(master_secret + SHA(‘A’ + master_secret +
ServerHello.random +
ClientHello.random)) +
MD5(master_secret + SHA(‘BB’ + master_secret +
ServerHello.random +
ClientHello.random)) +
MD5(master_secret + SHA(‘CCC’ + master_secret +
ServerHello.random +
ClientHello.random)) + [...];
依次類推,直到有足夠的輸出為止。然后,key_block
會被分割成所需的參數。
client_write_MAC_secret[CipherSpec.hash_size]
server_write_MAC_secret[CipherSpec.hash_size]
client_write_key[CipherSpec.key_material]
server_write_key[CipherSpec.key_material]
client_write_IV[CipherSpec.IV_size] /* non-export ciphers */
server_write_IV[CipherSpec.IV_size] /* non-export ciphers */
key_block
中多余的數據會被丟棄。
對于出口加密算法(CipherSpec.is_exportable
為true
),還需經過以下步驟才能得出最終的密鑰和初始向量。
final_client_write_key = MD5(client_write_key +
ClientHello.random +
ServerHello.random);
final_server_write_key = MD5(server_write_key +
ServerHello.random +
ClientHello.random);
client_write_IV = MD5(ClientHello.random + ServerHello.random);
server_write_IV = MD5(ServerHello.random + ClientHello.random);
注:對于FORTEZZA KEA密鑰交換算法,主秘密(master secret)只用來生成MAC秘密。
7.9 Certificate Verify
CertificateVerify
消息的作用是證明客戶端擁有與剛剛所發送證書對應的私鑰。它是一個簽名消息,服務器接收到此消息之后,只要使用客戶端證書中提供的公鑰驗證簽名即可。
7.10 Finished
結束消息總是在ChangeCipherSpec
消息之后立即發送。結束消息是用來確認密鑰交換和認證過程的成功結束的。結束消息是第一個使用剛剛協商好的加密算法、密鑰和秘密進行加密保護和完整性保護的消息。通信雙方在發送結束消息之后就可以開始傳送應用層數據了。結束消息的接收方要驗證結束消息的正確性。結束消息的結構如下:
enum {
client(0x434C4E54), server(0x53525652)
} Sender;
struct {
opaque md5_hash[16];
opaque sha_hash[20];
} Finished;
- md5_hash
MD5(master_secret + pad2 + MD5(handshake_messages + Sender + master_secret + pad1));
- sha_hash
SHA(master_secret + pad2 + SHA(handshake_messages + Sender + master_secret + pad1));
其中,handshake_messages
包括除結束消息外的所有握手消息。ChangeCipherSpec
消息不算在內,因為它不屬于握手消息的一部分。那為什么不把ChangeCipherSpec
消息作為握手消息的一部分而要把它獨立出去呢?因為記錄層有可能會把幾個類型相同的消息合并成一個消息記錄傳送呢,如果ChangeCipherSpec
作為握手消息的一部分,那么ChangeCipherSpec
消息很有可能會和其它握手消息合并在一起傳送。然而,我們希望看到的結果是,通信的雙方在收到ChangeCipherSpec
消息之后,立即將協商好的密碼規范應用到ChangeCipherSpec
之后的消息。如果是將ChangeCipherSpec
作為握手消息的一部分的話,就有可能存在問題。因此,需要將它獨立出去。詳細的解析請參考 Why is change cipher spec an independent protocol content type and not part of Handshake Messages?
另外,當客戶端和服務器重用會話而非新建會話時,握手消息的時序如下圖所示:
在
ServerHello
消息之后,服務器直接發送ChangeCipherSpec
消息和Finished
消息;客戶端緊接著也直接發送ChangeCipherSpec
消息和Finished
消息。握手過程結束。在重用會話的情況下,客戶端和服務器不用走完整個握手流程,因為通信雙方可以根據會話狀態中保存的主秘密(master secret)、ClientHello.random以及ServerHello.random直接導出加密密鑰、MAC秘密和初始向量IV。
八、總結
- SSL協議在傳輸控制層的基礎上建立了安全的連接,它作為一種通用可靠的安全解決方案,可與多種應用層協議結合使用,實現應用數據的安全傳輸。我們常見的https即為http協議與SSL協議(或者TLS協議)的結合;
- SSL協議是一個分層協議,由記錄協議(Record Protocol)、警告協議(Alert Protocol)、密碼規范改變協議(Change Cipher Protocol)和握手協議(Handshake Protocol)組成,其中記錄協議工作在最底層;
- SSL協議在進行應用數據傳輸之前,需要通過握手協議來協商安全通信所需的安全參數。
另外,此文對于SSL協議的解析是基于 RFC6101。個人水平有限,文中所述難免有誤,歡迎批評指正。