google quic tls 握手原理(三)

一、前言

  • 本文延续google quic tls握手原理(二)对客户端接收到服务端的Initial+Handshake报文后继续进行分析。
  • 通过前面两遍关于google quic tls握手相关的分析,目前对google quiche项目握手模块的代码分布和设计模式已经有较为深入的理解,本文依然按照前两遍的分析思路和逻辑对收到服务端的握手包后,客户端究竟是如何处理的。

二、服务端握手报文分析

  • 首先通过如下流程图来展示客户端收到服务端的报文后解析、派发处理的逻辑关系和代码所在位置,这样便于后面的分析。


    000.png
  • Quic数据包大致分成三类,其一是Initial包,其次是handeshake包,再者就是application data
  • 对于Initialhandshake相关的如OnCryptoFrame、OnNewTokenFrame、OnHandshakeDoneFrame等,在QuicSession模块会透过QuicCryptoStream模块最终将数据转发到TLS引擎进行处理。
  • 而对于应用数据如StreamFrameQuicSession模块会通过GetOrCreateStream()找到对应的StreamID,然后派发到相应的QuicStream进行处理。
  • 最后我们通过抓包文件结合代码一步一步进行分析。


    005_01.png
  • 当服务端收到客户端发送的Client Hello消息后,服务端会进行配置文件协商、版本协商、各种加密套件、秘钥算法、应用层协议等各种扩展协商,协商完后和客户端一样会驱动ssl引擎生成Server Hello消息,流程和客户端类似。
  • 和客户端不同的是服务端发送的首包数据会将Initial包和HandShake包进行聚合打包成一个QUIC报文发送到客户端。
  • 客户端收到该握手报文后会对该报文进行循环解析,解析成两个QuicCryptoFrame帧,其中第一个Frame为EncryptionLevelENCRYPTION_INITIAL,第二个Frame的EncryptionLevelENCRYPTION_HANDSHAKE
  • 上述报文中包含两个QUIC IETF包,其中Server Hello对应的是Intial包,Server Hello中包含了服务端采用的加密算法(Cipher Suite)、key_share 扩展信息对应服务端的公钥信息、以及TLS的版本支持1.3。
  • 而另外一个QUIC IETF包则是handshake包,其中包含加密扩展(应用协议、传输参数和应用设置等等)。
  • 紧接着服务端还外发送一个handleshake包,如下图(2)
    005_02.png
  • google quic支持使用聚合包对报文进行打包,服务端收到客户端的Initial报文后,连续回了两个handleshake报文,第一个包含Server Hello和加密扩展,第二个握手包包含了证书和Finished信息
  • Handshake Protocol:Certificate(证书本身包含服务器的公钥、证书颁发机构(CA)的信息以及其他相关信息)
    005_03.png
  • Handshake Protocol:Certificate Verify(用于验证服务端的身份。这个消息包含了证书签名算法类型、服务端使用私钥对握手消息进行数字签名的结果,客户端可以通过服务端的证书公钥来验证数字签名的有效性,从而确定服务端的身份,并建立安全的通信连接)
    005_04.png
  • Handshake Protocol:Finished(表示握手完成)
    005_05.png
  • 以上抓包为QUICK RAW DATA,也就是不带http扩展的报文。

三、客户端处理服务端的Initial包

  • 本节分析客户端是如何处理服务端发送过来的Initial包的,首先简单看下客户端收到服务端的包后的简单处理流程如下图:
    006.png
  • 无论是服务端还是客户端收到对端的包后处理流程和上述基本一致,对于握手流程的包都是上述流程,如果是非握手包那么经历的QuicStream模块会有所变化。
  • 由于本文重点是分析握手,所以本文不分析上述流程中的代码实现,图(4)只是让大家对Quic报文的读取处理有一个直观的认识。
  • 本文分析重点是TlsHandshaker::ProcessInput(…)之后的流程,在google quic tls握手原理(一)一文中有提到,TlsHandshaker模块由CryptoMessageParser派生而来,顾名思义CryptoMessageParser是握手信息解析器,通过其提供的接口ProcessInput()来完成Server HelloClient 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(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 HelloServer Hello输入到ssl引擎,随后调用AdvanceHandshake()函数触发双手握手操作。
  • 上述代码最终触发的流程图如下:


    007.png
  • 至此为止,客户端拿到了服务端的公钥信息以及协商好的加密算法,基于此为当前level创建对应的加密和解密引擎,注意图(5)中ProcessInput(..,EncryptionLevel level)对应Initial包的levelENCRYPTION_INITIAL,而经过ssl引擎握手处理后需要创建的加密和解密引擎的levelENCRYPTION_HANDSHAKE,也就是此阶段创建的解密引擎为Handshake包解密提供服务。
  • 加解密引擎的创建在google quic项目中使用回调的方式,通知上层进行创建,它们最终实现请参考google quic tls握手原理(一)

四、客户端处理服务端的Handshake证书以及证书验证

  • Handshake包的处理流程会比Initial包的处理流程多一个步骤,就是证书校验工作,大致流程如下:
    008.png
  • 在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 certs;
  for (CRYPTO_BUFFER* cert : cert_chain) {
    certs.push_back(
        std::string(reinterpret_cast(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(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& certs, std::string* error_details,
    std::unique_ptr* details, uint8_t* out_alert,
    std::unique_ptr 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(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(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_成员在客户端初始化的时候进行实现,最终被markQuicCryptoClientConfig模块,在全代码上下文中使用。
  • 到此为止,如果是证书验证成功,握手就算完成了,接下来看握手完成后做了哪些处理?

五、客户端完成握手后处理

  • 握手代码逻辑如下:
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(alpn_data),
                                   alpn_length);
  std::vector 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

  • 首先看一下抓包文件


    009.png
  • 图(9)为客户端发送给服务端的finished
    010.png
  • New Session Ticket信息是0-RTT的基础,在创建SSL_CTX的时候(静态的),在google quiche项目中通过如下方法配置了客户端会缓存SSL_SESSION
// static
bssl::UniquePtr TlsClientConnection::CreateSslCtx(
    bool enable_early_data) {
  bssl::UniquePtr 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 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_DONENEW_TOKEN的包,接下来我们继续进行分析。

七、客户端处理HANDSHAKE_DONE和NEW_TOKEN、NEW_CONNECTION_ID

  • HANDSHAKE_DONENEW_TOKENShort Header包,客户端收到后依然需要通过QuicClientCryptoStream进行处理,结合第2节中的图(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模块中也会根据不同的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项目的各模块代码设计模式等。

参考文献

  • 一文读懂 QUIC 协议:更快、更稳、更高效的网络通信

  • 跟我一起学QUIC(一):初识“连接”

  • RFC9000

你可能感兴趣的:(google quic tls 握手原理(三))