Handle error as specified in https://tools.ietf.org/html/draft-ietf-quic-http-29#section-7.2.4.2 : If a server accepts 0-RTT but then sends a SETTINGS frame that omits a setting value that the client understands (apart from reserved setting identifiers) that was previously specified to have a non-default value, this MUST be treated as a connection error of type H3_SETTINGS_ERROR.

Protected by quic_enable_zero_rtt_for_tls

PiperOrigin-RevId: 316781779
Change-Id: If4e174766bbd65be116f14a04449611963e03c2a
diff --git a/quic/core/http/quic_spdy_client_session_base.cc b/quic/core/http/quic_spdy_client_session_base.cc
index dfef4db..eaa022e 100644
--- a/quic/core/http/quic_spdy_client_session_base.cc
+++ b/quic/core/http/quic_spdy_client_session_base.cc
@@ -234,6 +234,38 @@
 }
 
 bool QuicSpdyClientSessionBase::OnSettingsFrame(const SettingsFrame& frame) {
+  if (!was_zero_rtt_rejected()) {
+    if (max_outbound_header_list_size() != std::numeric_limits<size_t>::max() &&
+        frame.values.find(SETTINGS_MAX_HEADER_LIST_SIZE) ==
+            frame.values.end()) {
+      CloseConnectionWithDetails(
+          QUIC_HTTP_ZERO_RTT_SETTINGS_MISMATCH,
+          "Server accepted 0-RTT but omitted non-default "
+          "SETTINGS_MAX_HEADER_LIST_SIZE");
+      return false;
+    }
+
+    if (qpack_encoder()->maximum_blocked_streams() != 0 &&
+        frame.values.find(SETTINGS_QPACK_BLOCKED_STREAMS) ==
+            frame.values.end()) {
+      CloseConnectionWithDetails(
+          QUIC_HTTP_ZERO_RTT_SETTINGS_MISMATCH,
+          "Server accepted 0-RTT but omitted non-default "
+          "SETTINGS_QPACK_BLOCKED_STREAMS");
+      return false;
+    }
+
+    if (qpack_encoder()->MaximumDynamicTableCapacity() != 0 &&
+        frame.values.find(SETTINGS_QPACK_MAX_TABLE_CAPACITY) ==
+            frame.values.end()) {
+      CloseConnectionWithDetails(
+          QUIC_HTTP_ZERO_RTT_SETTINGS_MISMATCH,
+          "Server accepted 0-RTT but omitted non-default "
+          "SETTINGS_QPACK_MAX_TABLE_CAPACITY");
+      return false;
+    }
+  }
+
   if (!QuicSpdySession::OnSettingsFrame(frame)) {
     return false;
   }
diff --git a/quic/core/http/quic_spdy_client_session_test.cc b/quic/core/http/quic_spdy_client_session_test.cc
index 824ff22..26b40f9 100644
--- a/quic/core/http/quic_spdy_client_session_test.cc
+++ b/quic/core/http/quic_spdy_client_session_test.cc
@@ -16,6 +16,7 @@
 #include "net/third_party/quiche/src/quic/core/http/quic_spdy_client_stream.h"
 #include "net/third_party/quiche/src/quic/core/http/spdy_server_push_utils.h"
 #include "net/third_party/quiche/src/quic/core/quic_constants.h"
+#include "net/third_party/quiche/src/quic/core/quic_error_codes.h"
 #include "net/third_party/quiche/src/quic/core/quic_utils.h"
 #include "net/third_party/quiche/src/quic/core/quic_versions.h"
 #include "net/third_party/quiche/src/quic/core/tls_client_handshaker.h"
@@ -1383,6 +1384,30 @@
   session_->OnSettingsFrame(settings);
 }
 
+TEST_P(QuicSpdyClientSessionTest, ServerAcceptsZeroRttButOmitSetting) {
+  if (!session_->version().UsesHttp3()) {
+    return;
+  }
+
+  CompleteFirstConnection();
+
+  CreateConnection();
+  CompleteCryptoHandshake();
+  EXPECT_TRUE(session_->GetMutableCryptoStream()->EarlyDataAccepted());
+
+  EXPECT_CALL(*connection_,
+              CloseConnection(QUIC_HTTP_ZERO_RTT_SETTINGS_MISMATCH, _, _))
+      .WillOnce(testing::Invoke(connection_,
+                                &MockQuicConnection::ReallyCloseConnection));
+  // Let the session receive a different SETTINGS frame.
+  SettingsFrame settings;
+  settings.values[SETTINGS_QPACK_MAX_TABLE_CAPACITY] = 1;
+  // Intentionally omit SETTINGS_MAX_HEADER_LIST_SIZE which was previously sent
+  // with a non-zero value.
+  settings.values[256] = 4;  // unknown setting
+  session_->OnSettingsFrame(settings);
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quic/core/http/quic_spdy_session.cc b/quic/core/http/quic_spdy_session.cc
index 836404f..ddc9f74 100644
--- a/quic/core/http/quic_spdy_session.cc
+++ b/quic/core/http/quic_spdy_session.cc
@@ -888,9 +888,6 @@
 }
 
 bool QuicSpdySession::OnSetting(uint64_t id, uint64_t value) {
-  // TODO(b/158614287): If cached SETTINGS has SETTINGS_QPACK_MAX_TABLE_CAPACITY
-  // and SETTINGS_MAX_HEADER_LIST_SIZE, and the server accepts 0-RTT connection,
-  // make sure the fresh SETTINGS contains the same values.
   if (VersionUsesHttp3(transport_version())) {
     // SETTINGS frame received on the control stream.
     switch (id) {
diff --git a/quic/core/qpack/qpack_encoder.h b/quic/core/qpack/qpack_encoder.h
index 8b75097..202fc6d 100644
--- a/quic/core/qpack/qpack_encoder.h
+++ b/quic/core/qpack/qpack_encoder.h
@@ -99,6 +99,12 @@
     return header_table_.dynamic_table_entry_referenced();
   }
 
+  uint64_t maximum_blocked_streams() const { return maximum_blocked_streams_; }
+
+  uint64_t MaximumDynamicTableCapacity() const {
+    return header_table_.maximum_dynamic_table_capacity();
+  }
+
  private:
   friend class test::QpackEncoderPeer;
 
diff --git a/quic/core/quic_error_codes.cc b/quic/core/quic_error_codes.cc
index 4b2c737..4887525 100644
--- a/quic/core/quic_error_codes.cc
+++ b/quic/core/quic_error_codes.cc
@@ -203,6 +203,7 @@
     RETURN_STRING_LITERAL(QUIC_HTTP_DUPLICATE_SETTING_IDENTIFIER);
     RETURN_STRING_LITERAL(QUIC_HTTP_INVALID_MAX_PUSH_ID);
     RETURN_STRING_LITERAL(QUIC_HTTP_STREAM_LIMIT_TOO_LOW);
+    RETURN_STRING_LITERAL(QUIC_HTTP_ZERO_RTT_SETTINGS_MISMATCH);
     RETURN_STRING_LITERAL(QUIC_HPACK_INDEX_VARINT_ERROR);
     RETURN_STRING_LITERAL(QUIC_HPACK_NAME_LENGTH_VARINT_ERROR);
     RETURN_STRING_LITERAL(QUIC_HPACK_VALUE_LENGTH_VARINT_ERROR);
@@ -570,6 +571,8 @@
     case QUIC_HTTP_STREAM_LIMIT_TOO_LOW:
       return {false, static_cast<uint64_t>(
                          QuicHttp3ErrorCode::GENERAL_PROTOCOL_ERROR)};
+    case QUIC_HTTP_ZERO_RTT_SETTINGS_MISMATCH:
+      return {false, static_cast<uint64_t>(QuicHttp3ErrorCode::SETTINGS_ERROR)};
     case QUIC_HPACK_INDEX_VARINT_ERROR:
       return {true, static_cast<uint64_t>(INTERNAL_ERROR)};
     case QUIC_HPACK_NAME_LENGTH_VARINT_ERROR:
diff --git a/quic/core/quic_error_codes.h b/quic/core/quic_error_codes.h
index f5c5007..60443ce 100644
--- a/quic/core/quic_error_codes.h
+++ b/quic/core/quic_error_codes.h
@@ -431,6 +431,8 @@
   QUIC_HTTP_INVALID_MAX_PUSH_ID = 159,
   // Received unidirectional stream limit is lower than required by HTTP/3.
   QUIC_HTTP_STREAM_LIMIT_TOO_LOW = 160,
+  // Received mismatched SETTINGS frame from HTTP/3 0-RTT connection.
+  QUIC_HTTP_ZERO_RTT_SETTINGS_MISMATCH = 164,
 
   // HPACK header block decoding errors.
   // Index varint beyond implementation limit.
@@ -479,7 +481,7 @@
   QUIC_ZERO_RTT_RESUMPTION_LIMIT_REDUCED = 163,
 
   // No error. Used as bound while iterating.
-  QUIC_LAST_ERROR = 164,
+  QUIC_LAST_ERROR = 165,
 };
 // QuicErrorCodes is encoded as four octets on-the-wire when doing Google QUIC,
 // or a varint62 when doing IETF QUIC. Ensure that its value does not exceed
diff --git a/quic/core/quic_session.h b/quic/core/quic_session.h
index 8328782..e5e794f 100644
--- a/quic/core/quic_session.h
+++ b/quic/core/quic_session.h
@@ -604,6 +604,8 @@
 
   size_t num_static_streams() const { return num_static_streams_; }
 
+  bool was_zero_rtt_rejected() const { return was_zero_rtt_rejected_; }
+
   size_t num_outgoing_draining_streams() const {
     return num_outgoing_draining_streams_;
   }