Add a method to retrieve the creation time of TLS session tickets used for resumption.

Specifically:
* Adds GetSessionTicketCreationTime() to QuicCryptoClientStreamBase, QuicCryptoClientStream, and the handshaker interfaces.
* Implements GetSessionTicketCreationTime() in TlsClientHandshaker using SSL_SESSION_get_time on the cached TLS session.
* Adds unit tests in tls_client_handshaker_test.cc to verify the creation time is correctly retrieved during resumption and 0-RTT.
PiperOrigin-RevId: 937773796
diff --git a/quiche/quic/core/http/end_to_end_test.cc b/quiche/quic/core/http/end_to_end_test.cc
index 7ded223..0b0a62a 100644
--- a/quiche/quic/core/http/end_to_end_test.cc
+++ b/quiche/quic/core/http/end_to_end_test.cc
@@ -7731,6 +7731,41 @@
   ADD_FAILURE() << "Client should not have 10 resumption tickets.";
 }
 
+TEST_P(EndToEndTest, TlsSessionTicketCreationTime) {
+  QuicWallTime start_time = QuicDefaultClock::Get()->WallNow();
+  SetQuicFlag(quic_disable_server_tls_resumption, false);
+  ASSERT_TRUE(Initialize());
+
+  if (!version_.IsIetfQuic()) {
+    // This test is TLS specific.
+    return;
+  }
+
+  // Send the first request and then disconnect. The client receives a
+  // resumption ticket.
+  SendSynchronousFooRequestAndCheckResponse();
+  QuicSpdyClientSession* client_session = GetClientSession();
+  ASSERT_TRUE(client_session);
+  EXPECT_FALSE(client_session->EarlyDataAccepted());
+  EXPECT_FALSE(client_session->GetCryptoStream()
+                   ->GetSessionTicketCreationTime()
+                   .has_value());
+  client_->Disconnect();
+
+  // Send the second request in 0RTT / Resumption.
+  client_->Connect();
+  SendSynchronousFooRequestAndCheckResponse();
+
+  client_session = GetClientSession();
+  ASSERT_TRUE(client_session);
+  EXPECT_TRUE(client_session->EarlyDataAccepted());
+  std::optional<QuicWallTime> ticket_time =
+      client_session->GetCryptoStream()->GetSessionTicketCreationTime();
+  ASSERT_TRUE(ticket_time.has_value());
+  EXPECT_GE(ticket_time->ToUNIXSeconds(), start_time.ToUNIXSeconds());
+  client_->Disconnect();
+}
+
 TEST_P(EndToEndTest, BlockServerUntilSettingsReceived) {
   SetQuicReloadableFlag(quic_block_until_settings_received_copt, true);
   // Force loss to test data stream being blocked when SETTINGS are missing.
diff --git a/quiche/quic/core/quic_crypto_client_handshaker.h b/quiche/quic/core/quic_crypto_client_handshaker.h
index e0adf83..89371ec 100644
--- a/quiche/quic/core/quic_crypto_client_handshaker.h
+++ b/quiche/quic/core/quic_crypto_client_handshaker.h
@@ -43,6 +43,9 @@
   bool IsResumption() const override;
   bool EarlyDataAccepted() const override;
   ssl_early_data_reason_t EarlyDataReason() const override;
+  std::optional<QuicWallTime> GetSessionTicketCreationTime() const override {
+    return std::nullopt;
+  }
   bool ReceivedInchoateReject() const override;
   int num_scup_messages_received() const override;
   std::string chlo_hash() const override;
diff --git a/quiche/quic/core/quic_crypto_client_stream.cc b/quiche/quic/core/quic_crypto_client_stream.cc
index 356998e..d18f0fa 100644
--- a/quiche/quic/core/quic_crypto_client_stream.cc
+++ b/quiche/quic/core/quic_crypto_client_stream.cc
@@ -85,6 +85,11 @@
   return handshaker_->EarlyDataReason();
 }
 
+std::optional<QuicWallTime>
+QuicCryptoClientStream::GetSessionTicketCreationTime() const {
+  return handshaker_->GetSessionTicketCreationTime();
+}
+
 bool QuicCryptoClientStream::ReceivedInchoateReject() const {
   return handshaker_->ReceivedInchoateReject();
 }
diff --git a/quiche/quic/core/quic_crypto_client_stream.h b/quiche/quic/core/quic_crypto_client_stream.h
index b7d4120..6c38249 100644
--- a/quiche/quic/core/quic_crypto_client_stream.h
+++ b/quiche/quic/core/quic_crypto_client_stream.h
@@ -20,6 +20,7 @@
 #include "quiche/quic/core/quic_crypto_stream.h"
 #include "quiche/quic/core/quic_server_id.h"
 #include "quiche/quic/core/quic_session.h"
+#include "quiche/quic/core/quic_time.h"
 #include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/core/quic_versions.h"
 #include "quiche/quic/platform/api/quic_export.h"
@@ -35,11 +36,14 @@
 class QUICHE_EXPORT QuicCryptoClientStreamBase : public QuicCryptoStream {
  public:
   explicit QuicCryptoClientStreamBase(QuicSession* session);
+  QuicCryptoClientStreamBase(const QuicCryptoClientStreamBase&) = delete;
+  QuicCryptoClientStreamBase& operator=(const QuicCryptoClientStreamBase&) =
+      delete;
 
   ~QuicCryptoClientStreamBase() override {}
 
-  // Performs a crypto handshake with the server. Returns true if the connection
-  // is still connected.
+  // Performs a crypto handshake with the server. Returns true if the
+  // connection is still connected.
   virtual bool CryptoConnect() = 0;
 
   // DEPRECATED: Use IsResumption, EarlyDataAccepted, and/or
@@ -53,9 +57,9 @@
   // Whether TLS resumption was attempted by this client. IETF QUIC only.
   virtual bool ResumptionAttempted() const = 0;
 
-  // Returns true if the handshake performed was a resumption instead of a full
-  // handshake. Resumption only makes sense for TLS handshakes - there is no
-  // concept of resumption for QUIC crypto even though it supports a 0-RTT
+  // Returns true if the handshake performed was a resumption instead of a
+  // full handshake. Resumption only makes sense for TLS handshakes - there is
+  // no concept of resumption for QUIC crypto even though it supports a 0-RTT
   // handshake. This function only returns valid results once the handshake is
   // complete.
   virtual bool IsResumption() const = 0;
@@ -102,10 +106,17 @@
       CachedNetworkParameters /*cached_network_params*/) override {
     QUICHE_DCHECK(false);
   }
+
+  // Returns the creation time of the session ticket used for resumption, if
+  // resumption was attempted.
+  virtual std::optional<QuicWallTime> GetSessionTicketCreationTime() const = 0;
 };
 
 class QUICHE_EXPORT QuicCryptoClientStream : public QuicCryptoClientStreamBase {
  public:
+  // Returns the creation time of the session ticket used for resumption, if
+  // resumption was attempted.
+  std::optional<QuicWallTime> GetSessionTicketCreationTime() const override;
   // kMaxClientHellos is the maximum number of times that we'll send a client
   // hello. The value 4 accounts for:
   //   * One failure due to an incorrect or missing source-address token.
@@ -161,6 +172,11 @@
     // rejected.
     virtual ssl_early_data_reason_t EarlyDataReason() const = 0;
 
+    // Returns the creation time of the session ticket used for resumption, if
+    // resumption was attempted.
+    virtual std::optional<QuicWallTime> GetSessionTicketCreationTime()
+        const = 0;
+
     // Returns true if the client received an inchoate REJ during the handshake,
     // extending the handshake by one round trip. This only applies for QUIC
     // crypto handshakes. The equivalent feature in IETF QUIC is a Retry packet,
diff --git a/quiche/quic/core/tls_client_handshaker.cc b/quiche/quic/core/tls_client_handshaker.cc
index a9829df..9e90268 100644
--- a/quiche/quic/core/tls_client_handshaker.cc
+++ b/quiche/quic/core/tls_client_handshaker.cc
@@ -408,6 +408,19 @@
   return TlsHandshaker::EarlyDataReason();
 }
 
+std::optional<QuicWallTime> TlsClientHandshaker::GetSessionTicketCreationTime()
+    const {
+  if (cached_state_ && cached_state_->tls_session) {
+    // SSL_SESSION_get_time returns the time the session was originally
+    // established (i.e. when the ticket was issued in the previous connection).
+    // It remains fixed to that creation time and is not updated when the
+    // session is reused/resumed.
+    return QuicWallTime::FromUNIXSeconds(
+        SSL_SESSION_get_time(cached_state_->tls_session.get()));
+  }
+  return std::nullopt;
+}
+
 bool TlsClientHandshaker::ReceivedInchoateReject() const {
   QUIC_BUG_IF(quic_bug_12736_3, !one_rtt_keys_available());
   // REJ messages are a QUIC crypto feature, so TLS always returns false.
diff --git a/quiche/quic/core/tls_client_handshaker.h b/quiche/quic/core/tls_client_handshaker.h
index 6322ac6..8a5ae74 100644
--- a/quiche/quic/core/tls_client_handshaker.h
+++ b/quiche/quic/core/tls_client_handshaker.h
@@ -48,6 +48,7 @@
   bool IsResumption() const override;
   bool EarlyDataAccepted() const override;
   ssl_early_data_reason_t EarlyDataReason() const override;
+  std::optional<QuicWallTime> GetSessionTicketCreationTime() const override;
   bool ReceivedInchoateReject() const override;
   int num_scup_messages_received() const override;
   std::string chlo_hash() const override;
diff --git a/quiche/quic/core/tls_client_handshaker_test.cc b/quiche/quic/core/tls_client_handshaker_test.cc
index f8bb1b2..826d402 100644
--- a/quiche/quic/core/tls_client_handshaker_test.cc
+++ b/quiche/quic/core/tls_client_handshaker_test.cc
@@ -494,6 +494,7 @@
 }
 
 TEST_P(TlsClientHandshakerTest, Resumption) {
+  QuicWallTime start_time = client_helper_.GetClock()->WallNow();
   // Disable 0-RTT on the server so that we're only testing 1-RTT resumption:
   SSL_CTX_set_early_data_enabled(server_crypto_config_->ssl_ctx(), false);
   // Finish establishing the first connection:
@@ -514,6 +515,10 @@
   EXPECT_TRUE(stream()->one_rtt_keys_available());
   EXPECT_TRUE(stream()->ResumptionAttempted());
   EXPECT_TRUE(stream()->IsResumption());
+  std::optional<QuicWallTime> ticket_time =
+      stream()->GetSessionTicketCreationTime();
+  ASSERT_TRUE(ticket_time.has_value());
+  EXPECT_GE(ticket_time->ToUNIXSeconds(), start_time.ToUNIXSeconds());
 }
 
 TEST_P(TlsClientHandshakerTest, ResumptionRejection) {
@@ -545,6 +550,7 @@
 }
 
 TEST_P(TlsClientHandshakerTest, ZeroRttResumption) {
+  QuicWallTime start_time = client_helper_.GetClock()->WallNow();
   // Finish establishing the first connection:
   CompleteCryptoHandshake();
 
@@ -580,6 +586,10 @@
   EXPECT_TRUE(stream()->IsResumption());
   EXPECT_TRUE(stream()->EarlyDataAccepted());
   EXPECT_EQ(stream()->EarlyDataReason(), ssl_early_data_accepted);
+  std::optional<QuicWallTime> ticket_time =
+      stream()->GetSessionTicketCreationTime();
+  ASSERT_TRUE(ticket_time.has_value());
+  EXPECT_GE(ticket_time->ToUNIXSeconds(), start_time.ToUNIXSeconds());
 }
 
 // Regression test for b/186438140.