一、前言
-
本文延续google quic tls握手原理(二)对客户端接收到服务端的
Initial+Handshake
报文后继续进行分析。 - 通过前面两遍关于google quic tls握手相关的分析,目前对google quiche项目握手模块的代码分布和设计模式已经有较为深入的理解,本文依然按照前两遍的分析思路和逻辑对收到服务端的握手包后,客户端究竟是如何处理的。
二、服务端握手报文分析
-
首先通过如下流程图来展示客户端收到服务端的报文后解析、派发处理的逻辑关系和代码所在位置,这样便于后面的分析。
-
Quic
数据包大致分成三类,其一是Initial
包,其次是handeshake
包,再者就是application data
。 - 对于
Initial
和handshake
相关的如OnCryptoFrame、OnNewTokenFrame、OnHandshakeDoneFrame
等,在QuicSession
模块会透过QuicCryptoStream
模块最终将数据转发到TLS
引擎进行处理。 - 而对于应用数据如
StreamFrame
在QuicSession
模块会通过GetOrCreateStream()
找到对应的StreamID
,然后派发到相应的QuicStream
进行处理。 -
最后我们通过抓包文件结合代码一步一步进行分析。
- 当服务端收到客户端发送的
Client Hello
消息后,服务端会进行配置文件协商、版本协商、各种加密套件、秘钥算法、应用层协议等各种扩展协商,协商完后和客户端一样会驱动ssl
引擎生成Server Hello
消息,流程和客户端类似。 - 和客户端不同的是服务端发送的首包数据会将
Initial
包和HandShake
包进行聚合打包成一个QUIC
报文发送到客户端。 - 客户端收到该握手报文后会对该报文进行循环解析,解析成两个
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
包,其中包含加密扩展(应用协议、传输参数和应用设置等等)。 - 紧接着服务端还外发送一个
handleshake
包,如下图(2)
-
google quic
支持使用聚合包对报文进行打包,服务端收到客户端的Initial
报文后,连续回了两个handleshake
报文,第一个包含Server Hello和加密扩展
,第二个握手包包含了证书和Finished
信息 -
Handshake Protocol:Certificate
(证书本身包含服务器的公钥、证书颁发机构(CA)的信息以及其他相关信息)
-
Handshake Protocol:Certificate Verify
(用于验证服务端的身份。这个消息包含了证书签名算法类型、服务端使用私钥对握手消息进行数字签名的结果,客户端可以通过服务端的证书公钥来验证数字签名的有效性,从而确定服务端的身份,并建立安全的通信连接)
-
Handshake Protocol:Finished
(表示握手完成)
- 以上抓包为
QUICK RAW DATA
,也就是不带http
扩展的报文。
三、客户端处理服务端的Initial包
- 本节分析客户端是如何处理服务端发送过来的
Initial
包的,首先简单看下客户端收到服务端的包后的简单处理流程如下图:
- 无论是服务端还是客户端收到对端的包后处理流程和上述基本一致,对于握手流程的包都是上述流程,如果是非握手包那么经历的
QuicStream
模块会有所变化。 - 由于本文重点是分析握手,所以本文不分析上述流程中的代码实现,图(4)只是让大家对Quic报文的读取处理有一个直观的认识。
- 本文分析重点是
TlsHandshaker::ProcessInput(…)
之后的流程,在google quic tls握手原理(一)一文中有提到,TlsHandshaker
模块由CryptoMessageParser
派生而来,顾名思义CryptoMessageParser
是握手信息解析器,通过其提供的接口ProcessInput()
来完成Server Hello
或Client Hello
的解析,接下来看它的实现如下:
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;
}
- 上述函数通过
ssl
引擎的SSL_provide_quic_data()
将Client Hello
或Server Hello
输入到ssl
引擎,随后调用AdvanceHandshake()
函数触发双手握手操作。 -
上述代码最终触发的流程图如下:
- 至此为止,客户端拿到了服务端的公钥信息以及协商好的加密算法,基于此为当前
level
创建对应的加密和解密引擎,注意图(5)中ProcessInput(..,EncryptionLevel level)
对应Initial
包的level
为ENCRYPTION_INITIAL
,而经过ssl
引擎握手处理后需要创建的加密和解密引擎的level
为ENCRYPTION_HANDSHAKE
,也就是此阶段创建的解密引擎为Handshake
包解密提供服务。 - 加解密引擎的创建在google quic项目中使用回调的方式,通知上层进行创建,它们最终实现请参考google quic tls握手原理(一)
四、客户端处理服务端的Handshake证书以及证书验证
-
Handshake
包的处理流程会比Initial
包的处理流程多一个步骤,就是证书校验工作,大致流程如下:
-
在google quic tls握手原理(一)一文中有提到,客户端创建
SSL_CTX
的时候通过SSL_CTX_set_custom_verify(ssl_ctx.get(), SSL_VERIFY_PEER, &VerifyCallback)
自定了证书校验函数,其实现如下:
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 连接关联的对等端证书。 - 然后调用
VerifyCertChain(...)
进行证书校验,该函数是个抽象函数,在子类TlsClientHandshaker
中实现。 - 当证书校验成功后,握手成功,继而继续根据为
ssl_encryption_application
也就是ENCRYPTION_FORWARD_SECURE
类型的leve
创建对应的加密和解密引擎,为后续应用数据使用。
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
的成员变量,在其构造函数中被初始化
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
,该模块实现VerifyCertChain
接口,并且内部什么都不做,直接返回成功。 - 到此为止,从客户端的角度来看,握手就已经基本完成了,并已为后续的
application data
数据传输创建了加解密引擎。 - 其中
proof_verifier_
成员在客户端初始化的时候进行实现,最终被mark
到QuicCryptoClientConfig
模块,在全代码上下文中使用。 - 到此为止,如果是证书验证成功,握手就算完成了,接下来看握手完成后做了哪些处理?
五、客户端完成握手后处理
- 握手代码逻辑如下:
void TlsHandshaker::AdvanceHandshake() {
...
int rv = SSL_do_handshake(ssl());
....
if (rv == 1) {
FinishHandshake();
return;
}
....
}
- 首先客户端调用
SSL_do_handshake()
函数触发握手,当收到服务端的Initial+handshake
报文并相关信息验证通过后,咱门分析假设握手成功,那么最后会调用FinishHandshake()
进行相关的参数配置处理。
void TlsClientHandshaker::FinishHandshake() {
//1) 填充握手参数
FillNegotiatedParams();
//2) 处理传输参数
std::string error_details;
if (!ProcessTransportParameters(&error_details)) {
CloseConnection(QUIC_HANDSHAKE_FAILED, error_details);
return;
}
//3) 从加密扩展中选择应用层协议(第二节中的图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) 和客户端已经选择应用协议进行比较
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) 通知更上层应用层协议已选择
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) 通知更上层应用设置参数
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();
}
- 应用协议和相关的应用设置参数在服务端的第一个握手包的加密扩展中被携带,这里在握手完成后再对这些参数和客户端进行适配,如果适配不成功则直接关闭连接。
- 最后通过回调
OnAlpnSelected()、OnAlpsData()、OnTlsHandshakeComplete()
进一步通知上层进行相应的逻辑处理,本文分析的重点是tls
范畴,所以对上层不做深入分析。 - 到此为止、
tls
引擎就绪,应用层也就绪、整个握手过程就算完成了,同时客户端也会想服务端发送handshake(加密扩展+finished)
包,告诉服务端握手完成,而服务端收到该报文后,会向客户端发送一个New Session Ticket
的报文,该报文是0-RTT的基础,且看下文分析。
六、客户端处理服务端的Handshake New Session Ticket
-
首先看一下抓包文件
- 图(9)为客户端发送给服务端的
finished
包
-
New Session Ticket
信息是0-RTT
的基础,在创建SSL_CTX
的时候(静态的),在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;
}
- 而当客户端收到服务端发送过来的
New Session Ticket
报文之后,和上述流程处理一样,最后经过ssl
引擎处理后会触发该回调,即NewSessionCallback()..
函数被回调。 - 而经过一系列的调用,该函数的最终实现如下:
void TlsClientHandshaker::InsertSession(bssl::UniquePtr<SSL_SESSION> session) {
//1) 第5节已经解析并处理
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值,在构造TlsClientHandshaker的时候会传入,表示是否有应用状态
// received_application_state_当收到服务端的SettingFrame后会被初始化,由于本文分析的是RAW DATA流,所以抓包文件中未能体现
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());
}
- 上面的代码注释说得已经很清晰,需要注意的是第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
的详细原理,后续再写一遍文章进行分析。 - 服务端发送
New Session Ticket
包后会继续发送一个HANDSHAKE_DONE
和NEW_TOKEN
的包,接下来我们继续进行分析。
七、客户端处理HANDSHAKE_DONE和NEW_TOKEN、NEW_CONNECTION_ID
-
HANDSHAKE_DONE
和NEW_TOKEN
是Short Header
包,客户端收到后依然需要通过QuicClientCryptoStream
进行处理,结合第2节中的图(0)进行分析。 -
首先我们看一下抓包文件:
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
模块中也会根据不同的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可用于连接迁移使用,本文不做详细分析。
总结
- 本文结合抓包文件以及配合代码详细分析
quic
握手流程,通过本文的学习相信大家对quic
的握手已经有了一个比较清晰的流程。 - 本文都是围绕客户端端的代码进行分析,并没有对服务端的代码进行分析,在服务端大部分流程是一样的,只不过部分业务方面会有差异。
- 本文也未详细分析业务层的代码,只是了解到当握手成功后会触发哪些回调(
OnAlpnSelected()、OnAlpsData()、OnTlsHandshakeComplete()
),后续根据实际应用有需要的时候再进行分析。 - 同时本文还遗留
0-RTT
的实现原理、quic
连接迁移的实现等都未进行分析。 - 本文在开篇给出了一张客户端对服务端数据报文读取、解析、以及各模块分发的流程图,通过该流程图来清晰的定位
google quiche
项目的各模块代码设计模式等。