一难菌、前言
-
本文延續(xù)google quic tls握手原理(二)對客戶端接收到服務端的
Initial+Handshake
報文后繼續(xù)進行分析珠移。 - 通過前面兩遍關于google quic tls握手相關的分析的畴,目前對google quiche項目握手模塊的代碼分布和設計模式已經(jīng)有較為深入的理解硫眨,本文依然按照前兩遍的分析思路和邏輯對收到服務端的握手包后足淆,客戶端究竟是如何處理的。
二礁阁、服務端握手報文分析
-
首先通過如下流程圖來展示客戶端收到服務端的報文后解析巧号、派發(fā)處理的邏輯關系和代碼所在位置,這樣便于后面的分析姥闭。
000.png -
Quic
數(shù)據(jù)包大致分成三類丹鸿,其一是Initial
包,其次是handeshake
包棚品,再者就是application data
靠欢。 - 對于
Initial
和handshake
相關的如OnCryptoFrame廊敌、OnNewTokenFrame、OnHandshakeDoneFrame
等门怪,在QuicSession
模塊會透過QuicCryptoStream
模塊最終將數(shù)據(jù)轉(zhuǎn)發(fā)到TLS
引擎進行處理骡澈。 - 而對于應用數(shù)據(jù)如
StreamFrame
在QuicSession
模塊會通過GetOrCreateStream()
找到對應的StreamID
,然后派發(fā)到相應的QuicStream
進行處理。 -
最后我們通過抓包文件結(jié)合代碼一步一步進行分析掷空。
005_01.png - 當服務端收到客戶端發(fā)送的
Client Hello
消息后肋殴,服務端會進行配置文件協(xié)商、版本協(xié)商坦弟、各種加密套件疼电、秘鑰算法、應用層協(xié)議等各種擴展協(xié)商减拭,協(xié)商完后和客戶端一樣會驅(qū)動ssl
引擎生成Server Hello
消息,流程和客戶端類似区丑。 - 和客戶端不同的是服務端發(fā)送的首包數(shù)據(jù)會將
Initial
包和HandShake
包進行聚合打包成一個QUIC
報文發(fā)送到客戶端拧粪。 - 客戶端收到該握手報文后會對該報文進行循環(huán)解析,解析成兩個
QuicCryptoFrame
幀沧侥,其中第一個Frame為EncryptionLevel
為ENCRYPTION_INITIAL
,第二個Frame的EncryptionLevel
為ENCRYPTION_HANDSHAKE
可霎。 - 上述報文中包含兩個
QUIC IETF
包,其中Server Hello
對應的是Intial
包宴杀,Server Hello
中包含了服務端采用的加密算法(Cipher Suite
)癣朗、key_share
擴展信息對應服務端的公鑰信息、以及TLS的版本支持1.3旺罢。 - 而另外一個
QUIC IETF
包則是handshake
包,其中包含加密擴展(應用協(xié)議旷余、傳輸參數(shù)和應用設置等等)。 - 緊接著服務端還外發(fā)送一個
handleshake
包扁达,如下圖(2)
005_02.png -
google quic
支持使用聚合包對報文進行打包,服務端收到客戶端的Initial
報文后正卧,連續(xù)回了兩個handleshake
報文,第一個包含Server Hello和加密擴展
跪解,第二個握手包包含了證書和Finished
信息 -
Handshake Protocol:Certificate
(證書本身包含服務器的公鑰炉旷、證書頒發(fā)機構(gòu)(CA)的信息以及其他相關信息)
005_03.png -
Handshake Protocol:Certificate Verify
(用于驗證服務端的身份。這個消息包含了證書簽名算法類型叉讥、服務端使用私鑰對握手消息進行數(shù)字簽名的結(jié)果窘行,客戶端可以通過服務端的證書公鑰來驗證數(shù)字簽名的有效性,從而確定服務端的身份图仓,并建立安全的通信連接)
005_04.png -
Handshake Protocol:Finished
(表示握手完成)
005_05.png - 以上抓包為
QUICK RAW DATA
,也就是不帶http
擴展的報文罐盔。
三、客戶端處理服務端的Initial包
- 本節(jié)分析客戶端是如何處理服務端發(fā)送過來的
Initial
包的,首先簡單看下客戶端收到服務端的包后的簡單處理流程如下圖:
006.png - 無論是服務端還是客戶端收到對端的包后處理流程和上述基本一致救崔,對于握手流程的包都是上述流程翘骂,如果是非握手包那么經(jīng)歷的
QuicStream
模塊會有所變化壁熄。 - 由于本文重點是分析握手,所以本文不分析上述流程中的代碼實現(xiàn)碳竟,圖(4)只是讓大家對Quic報文的讀取處理有一個直觀的認識草丧。
- 本文分析重點是
TlsHandshaker::ProcessInput(…)
之后的流程,在google quic tls握手原理(一)一文中有提到莹桅,TlsHandshaker
模塊由CryptoMessageParser
派生而來昌执,顧名思義CryptoMessageParser
是握手信息解析器,通過其提供的接口ProcessInput()
來完成Server Hello
或Client Hello
的解析,接下來看它的實現(xiàn)如下:
bool TlsHandshaker::ProcessInput(absl::string_view input,
EncryptionLevel level) {
if (parser_error_ != QUIC_NO_ERROR) {
return false;
}
// TODO(nharper): Call SSL_quic_read_level(ssl()) and check whether the
// encryption level BoringSSL expects matches the encryption level that we
// just received input at. If they mismatch, should ProcessInput return true
// or false? If data is for a future encryption level, it should be queued for
// later?
if (SSL_provide_quic_data(ssl(), TlsConnection::BoringEncryptionLevel(level),
reinterpret_cast<const uint8_t*>(input.data()),
input.size()) != 1) {
// SSL_provide_quic_data can fail for 3 reasons:
// - API misuse (calling it before SSL_set_custom_quic_method, which we
// call in the TlsHandshaker c'tor)
// - Memory exhaustion when appending data to its buffer
// - Data provided at the wrong encryption level
//
// Of these, the only sensible error to handle is data provided at the wrong
// encryption level.
//
// Note: the error provided below has a good-sounding enum value, although
// it doesn't match the description as it's a QUIC Crypto specific error.
parser_error_ = QUIC_INVALID_CRYPTO_MESSAGE_TYPE;
parser_error_detail_ = "TLS stack failed to receive data";
return false;
}
AdvanceHandshake();
return true;
}
- 上述函數(shù)通過
ssl
引擎的SSL_provide_quic_data()
將Client Hello
或Server Hello
輸入到ssl
引擎诈泼,隨后調(diào)用AdvanceHandshake()
函數(shù)觸發(fā)雙手握手操作懂拾。 -
上述代碼最終觸發(fā)的流程圖如下:
007.png - 至此為止,客戶端拿到了服務端的公鑰信息以及協(xié)商好的加密算法铐达,基于此為當前
level
創(chuàng)建對應的加密和解密引擎岖赋,注意圖(5)中ProcessInput(..,EncryptionLevel level)
對應Initial
包的level
為ENCRYPTION_INITIAL
,而經(jīng)過ssl
引擎握手處理后需要創(chuàng)建的加密和解密引擎的level
為ENCRYPTION_HANDSHAKE
,也就是此階段創(chuàng)建的解密引擎為Handshake
包解密提供服務。 - 加解密引擎的創(chuàng)建在google quic項目中使用回調(diào)的方式瓮孙,通知上層進行創(chuàng)建,它們最終實現(xiàn)請參考google quic tls握手原理(一)
四唐断、客戶端處理服務端的Handshake證書以及證書驗證
-
Handshake
包的處理流程會比Initial
包的處理流程多一個步驟,就是證書校驗工作杭抠,大致流程如下:
008.png -
在google quic tls握手原理(一)一文中有提到脸甘,客戶端創(chuàng)建
SSL_CTX
的時候通過SSL_CTX_set_custom_verify(ssl_ctx.get(), SSL_VERIFY_PEER, &VerifyCallback)
自定了證書校驗函數(shù),其實現(xiàn)如下:
enum ssl_verify_result_t TlsHandshaker::VerifyCert(uint8_t* out_alert) {
if (verify_result_ != ssl_verify_retry ||
expected_ssl_error() == SSL_ERROR_WANT_CERTIFICATE_VERIFY) {
enum ssl_verify_result_t result = verify_result_;
verify_result_ = ssl_verify_retry;
*out_alert = cert_verify_tls_alert_;
return result;
}
const STACK_OF(CRYPTO_BUFFER)* cert_chain = SSL_get0_peer_certificates(ssl());
if (cert_chain == nullptr) {
*out_alert = SSL_AD_INTERNAL_ERROR;
return ssl_verify_invalid;
}
// TODO(nharper): Pass the CRYPTO_BUFFERs into the QUIC stack to avoid copies.
std::vector<std::string> certs;
for (CRYPTO_BUFFER* cert : cert_chain) {
certs.push_back(
std::string(reinterpret_cast<const char*>(CRYPTO_BUFFER_data(cert)),
CRYPTO_BUFFER_len(cert)));
}
ProofVerifierCallbackImpl* proof_verify_callback =
new ProofVerifierCallbackImpl(this);
cert_verify_tls_alert_ = *out_alert;
QuicAsyncStatus verify_result = VerifyCertChain(
certs, &cert_verify_error_details_, &verify_details_,
&cert_verify_tls_alert_,
std::unique_ptr<ProofVerifierCallback>(proof_verify_callback));
switch (verify_result) {
case QUIC_SUCCESS:
if (verify_details_) {
OnProofVerifyDetailsAvailable(*verify_details_);
}
return ssl_verify_ok;
case QUIC_PENDING:
proof_verify_callback_ = proof_verify_callback;
set_expected_ssl_error(SSL_ERROR_WANT_CERTIFICATE_VERIFY);
return ssl_verify_retry;
case QUIC_FAILURE:
default:
*out_alert = cert_verify_tls_alert_;
QUIC_LOG(INFO) << "Cert chain verification failed: "
<< cert_verify_error_details_;
return ssl_verify_invalid;
}
}
-
SSL_get0_peer_certificates()
用于獲取與當前 SSL/TLS 連接關聯(lián)的對等端證書偏灿。 - 然后調(diào)用
VerifyCertChain(...)
進行證書校驗丹诀,該函數(shù)是個抽象函數(shù),在子類TlsClientHandshaker
中實現(xiàn)翁垂。 - 當證書校驗成功后铆遭,握手成功,繼而繼續(xù)根據(jù)為
ssl_encryption_application
也就是ENCRYPTION_FORWARD_SECURE
類型的leve
創(chuàng)建對應的加密和解密引擎沿猜,為后續(xù)應用數(shù)據(jù)使用疚脐。
QuicAsyncStatus TlsClientHandshaker::VerifyCertChain(
const std::vector<std::string>& certs, std::string* error_details,
std::unique_ptr<ProofVerifyDetails>* details, uint8_t* out_alert,
std::unique_ptr<ProofVerifierCallback> callback) {
const uint8_t* ocsp_response_raw;
size_t ocsp_response_len;
SSL_get0_ocsp_response(ssl(), &ocsp_response_raw, &ocsp_response_len);
std::string ocsp_response(reinterpret_cast<const char*>(ocsp_response_raw),
ocsp_response_len);
const uint8_t* sct_list_raw;
size_t sct_list_len;
SSL_get0_signed_cert_timestamp_list(ssl(), &sct_list_raw, &sct_list_len);
std::string sct_list(reinterpret_cast<const char*>(sct_list_raw),
sct_list_len);
return proof_verifier_->VerifyCertChain(
server_id_.host(), server_id_.port(), certs, ocsp_response, sct_list,
verify_context_.get(), error_details, details, out_alert,
std::move(callback));
}
-
proof_verifier_
為TlsClientHandshaker
的成員變量,在其構(gòu)造函數(shù)中被初始化
class QUIC_EXPORT_PRIVATE TlsClientHandshaker
: public TlsHandshaker,
public QuicCryptoClientStream::HandshakerInterface,
public TlsClientConnection::Delegate {
....
private:
....
// Objects used for verifying the server's certificate chain.
// |proof_verifier_| is owned by the caller of TlsHandshaker's constructor.
ProofVerifier* proof_verifier_;
};
- 對于客戶端可以不對服務端的證書進行校驗邢疙,此時可以使用google quiche項目中默認提供的
FakeProofVerifier
,該模塊實現(xiàn)VerifyCertChain
接口棍弄,并且內(nèi)部什么都不做,直接返回成功疟游。 - 到此為止,從客戶端的角度來看呼畸,握手就已經(jīng)基本完成了,并已為后續(xù)的
application data
數(shù)據(jù)傳輸創(chuàng)建了加解密引擎颁虐。 - 其中
proof_verifier_
成員在客戶端初始化的時候進行實現(xiàn),最終被mark
到QuicCryptoClientConfig
模塊蛮原,在全代碼上下文中使用。 - 到此為止,如果是證書驗證成功,握手就算完成了,接下來看握手完成后做了哪些處理另绩?
五儒陨、客戶端完成握手后處理
- 握手代碼邏輯如下:
void TlsHandshaker::AdvanceHandshake() {
...
int rv = SSL_do_handshake(ssl());
....
if (rv == 1) {
FinishHandshake();
return;
}
....
}
- 首先客戶端調(diào)用
SSL_do_handshake()
函數(shù)觸發(fā)握手花嘶,當收到服務端的Initial+handshake
報文并相關信息驗證通過后,咱門分析假設握手成功蹦漠,那么最后會調(diào)用FinishHandshake()
進行相關的參數(shù)配置處理椭员。
void TlsClientHandshaker::FinishHandshake() {
//1) 填充握手參數(shù)
FillNegotiatedParams();
//2) 處理傳輸參數(shù)
std::string error_details;
if (!ProcessTransportParameters(&error_details)) {
CloseConnection(QUIC_HANDSHAKE_FAILED, error_details);
return;
}
//3) 從加密擴展中選擇應用層協(xié)議(第二節(jié)中的圖1)
const uint8_t* alpn_data = nullptr;
unsigned alpn_length = 0;
SSL_get0_alpn_selected(ssl(), &alpn_data, &alpn_length);
if (alpn_length == 0) {
CloseConnection(QUIC_HANDSHAKE_FAILED, "Server did not select ALPN");
return;
}
//4) 和客戶端已經(jīng)選擇應用協(xié)議進行比較
std::string received_alpn_string(reinterpret_cast<const char*>(alpn_data),
alpn_length);
std::vector<std::string> offered_alpns = session()->GetAlpnsToOffer();
if (std::find(offered_alpns.begin(), offered_alpns.end(),
received_alpn_string) == offered_alpns.end()) {
CloseConnection(QUIC_HANDSHAKE_FAILED, "Client received mismatched ALPN");
return;
}
//5) 通知更上層應用層協(xié)議已選擇
session()->OnAlpnSelected(received_alpn_string);
// Parse ALPS extension.
const uint8_t* alps_data;
size_t alps_length;
SSL_get0_peer_application_settings(ssl(), &alps_data, &alps_length);
if (alps_length > 0) {
// 6) 通知更上層應用設置參數(shù)
auto error = session()->OnAlpsData(alps_data, alps_length);
if (error) {
// Calling CloseConnection() is safe even in case OnAlpsData() has
// already closed the connection.
CloseConnection(
QUIC_HANDSHAKE_FAILED,
absl::StrCat("Error processing ALPS data: ", error.value()));
return;
}
}
state_ = HANDSHAKE_COMPLETE;
// 7)通知QuicSession握手已完成
handshaker_delegate()->OnTlsHandshakeComplete();
}
- 應用協(xié)議和相關的應用設置參數(shù)在服務端的第一個握手包的加密擴展中被攜帶,這里在握手完成后再對這些參數(shù)和客戶端進行適配笛园,如果適配不成功則直接關閉連接隘击。
- 最后通過回調(diào)
OnAlpnSelected()、OnAlpsData()研铆、OnTlsHandshakeComplete()
進一步通知上層進行相應的邏輯處理埋同,本文分析的重點是tls
范疇,所以對上層不做深入分析棵红。 - 到此為止凶赁、
tls
引擎就緒,應用層也就緒逆甜、整個握手過程就算完成了虱肄,同時客戶端也會想服務端發(fā)送handshake(加密擴展+finished)
包,告訴服務端握手完成忆绰,而服務端收到該報文后,會向客戶端發(fā)送一個New Session Ticket
的報文可岂,該報文是0-RTT的基礎错敢,且看下文分析。
六缕粹、客戶端處理服務端的Handshake New Session Ticket
-
首先看一下抓包文件
009.png - 圖(9)為客戶端發(fā)送給服務端的
finished
包
010.png -
New Session Ticket
信息是0-RTT
的基礎稚茅,在創(chuàng)建SSL_CTX
的時候(靜態(tài)的),在google quiche項目中通過如下方法配置了客戶端會緩存SSL_SESSION
平斩。
// static
bssl::UniquePtr<SSL_CTX> TlsClientConnection::CreateSslCtx(
bool enable_early_data) {
bssl::UniquePtr<SSL_CTX> ssl_ctx = TlsConnection::CreateSslCtx();
....
// Configure session caching.
SSL_CTX_set_session_cache_mode(
ssl_ctx.get(), SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL);
SSL_CTX_sess_set_new_cb(ssl_ctx.get(), NewSessionCallback);
....
return ssl_ctx;
}
- 而當客戶端收到服務端發(fā)送過來的
New Session Ticket
報文之后,和上述流程處理一樣亚享,最后經(jīng)過ssl
引擎處理后會觸發(fā)該回調(diào),即NewSessionCallback()..
函數(shù)被回調(diào)。 - 而經(jīng)過一系列的調(diào)用绘面,該函數(shù)的最終實現(xiàn)如下:
void TlsClientHandshaker::InsertSession(bssl::UniquePtr<SSL_SESSION> session) {
//1) 第5節(jié)已經(jīng)解析并處理
if (!received_transport_params_) {
QUIC_BUG(quic_bug_10576_8) << "Transport parameters isn't received";
return;
}
// 2) 初始化的時候配置是否支持緩存
if (session_cache_ == nullptr) {
QUIC_DVLOG(1) << "No session cache, not inserting a session";
return;
}
// 3) has_application_state_是個bool值,在構(gòu)造TlsClientHandshaker的時候會傳入,表示是否有應用狀態(tài)
// received_application_state_當收到服務端的SettingFrame后會被初始化,由于本文分析的是RAW DATA流,所以抓包文件中未能體現(xiàn)
if (has_application_state_ && !received_application_state_) {
// Application state is not received yet. cache the sessions.
if (cached_tls_sessions_[0] != nullptr) {
cached_tls_sessions_[1] = std::move(cached_tls_sessions_[0]);
}
cached_tls_sessions_[0] = std::move(session);
return;
}
// 4) 將SSL_SESSION插入到session_cache_集合進行緩存
session_cache_->Insert(server_id_, std::move(session),
*received_transport_params_,
received_application_state_.get());
}
- 上面的代碼注釋說得已經(jīng)很清晰欺税,需要注意的是第3)點的處理邏輯,本文以
RAW DATA
進行分析,所以在抓包文件中未能體驗0-RTT包,同時在#3會直接return掉,received_application_state_
在https3
demo中當收到Setting Frame
的時候會被實例化揭璃。 - 而對于
https
請求,google demo
可以配置支持0-RTT
晚凿,所以到這里會將SSL_SESSION
插入到session_cache_
。 - 至于
0-RTT
的詳細原理瘦馍,后續(xù)再寫一遍文章進行分析歼秽。 - 服務端發(fā)送
New Session Ticket
包后會繼續(xù)發(fā)送一個HANDSHAKE_DONE
和NEW_TOKEN
的包,接下來我們繼續(xù)進行分析情组。
七燥筷、客戶端處理HANDSHAKE_DONE和NEW_TOKEN箩祥、NEW_CONNECTION_ID
-
HANDSHAKE_DONE
和NEW_TOKEN
是Short Header
包,客戶端收到后依然需要通過QuicClientCryptoStream
進行處理,結(jié)合第2節(jié)中的圖(0)進行分析肆氓。 -
首先我們看一下抓包文件:
011.png
HANDSHAKE_DONE
處理
void QuicSession::OnHandshakeDoneReceived() {
GetMutableCryptoStream()->OnHandshakeDoneReceived();
}
void QuicCryptoClientStream::OnHandshakeDoneReceived() {
handshaker_->OnHandshakeDoneReceived();
}
void TlsClientHandshaker::OnHandshakeDoneReceived() {
if (!one_rtt_keys_available()) {
CloseConnection(QUIC_HANDSHAKE_FAILED,
"Unexpected handshake done received");
return;
}
OnHandshakeConfirmed();
}
void TlsClientHandshaker::OnHandshakeConfirmed() {
QUICHE_DCHECK(one_rtt_keys_available());
if (state_ >= HANDSHAKE_CONFIRMED) {
return;
}
state_ = HANDSHAKE_CONFIRMED;
handshaker_delegate()->DiscardOldEncryptionKey(ENCRYPTION_HANDSHAKE);
handshaker_delegate()->DiscardOldDecryptionKey(ENCRYPTION_HANDSHAKE);
}
-
HANDSHAKE_DONE
的處理比較簡單,對于TLS
引擎來說只是將state_
設置成HANDSHAKE_CONFIRMED
袍祖。 - 當然在
QuicConnection
模塊中也會根據(jù)不同的Frame
做出一些邏輯處理,本文不做分析做院。
NEW_TOKEN
處理
void QuicSession::OnNewTokenReceived(absl::string_view token) {
GetMutableCryptoStream()->OnNewTokenReceived(token);
}
void QuicCryptoClientStream::OnNewTokenReceived(absl::string_view token) {
handshaker_->OnNewTokenReceived(token);
}
void TlsClientHandshaker::OnNewTokenReceived(absl::string_view token) {
if (token.empty()) {
return;
}
if (session_cache_ != nullptr) {
session_cache_->OnNewTokenReceived(server_id_, token);
}
}
- 客戶端對
TOKEN
進行緩存,它是用來干嘛的盲泛?看上去是用于0-RTT
用的。
NEW_CONNECTION_ID
處理
-
NEW_CONNECTION_ID
幀是屬于探測幀键耕,這個連接ID可用于連接遷移使用,本文不做詳細分析寺滚。
總結(jié)
- 本文結(jié)合抓包文件以及配合代碼詳細分析
quic
握手流程,通過本文的學習相信大家對quic
的握手已經(jīng)有了一個比較清晰的流程屈雄。 - 本文都是圍繞客戶端端的代碼進行分析,并沒有對服務端的代碼進行分析村视,在服務端大部分流程是一樣的,只不過部分業(yè)務方面會有差異酒奶。
- 本文也未詳細分析業(yè)務層的代碼蚁孔,只是了解到當握手成功后會觸發(fā)哪些回調(diào)(
OnAlpnSelected()、OnAlpsData()惋嚎、OnTlsHandshakeComplete()
)杠氢,后續(xù)根據(jù)實際應用有需要的時候再進行分析。 - 同時本文還遺留
0-RTT
的實現(xiàn)原理另伍、quic
連接遷移的實現(xiàn)等都未進行分析鼻百。 - 本文在開篇給出了一張客戶端對服務端數(shù)據(jù)報文讀取、解析摆尝、以及各模塊分發(fā)的流程圖温艇,通過該流程圖來清晰的定位
google quiche
項目的各模塊代碼設計模式等。