Do QUIC Key Update when approaching AEAD Confidentiality Limit, and close connection if reached.

Protected by FLAGS_quic_reloadable_flag_quic_enable_aead_limits.

PiperOrigin-RevId: 337581109
Change-Id: Ie021fd42be174cfb1c069159ecca0bf34bd99731
diff --git a/quic/core/crypto/aes_base_encrypter.cc b/quic/core/crypto/aes_base_encrypter.cc
index 436b7bd..f4a152f 100644
--- a/quic/core/crypto/aes_base_encrypter.cc
+++ b/quic/core/crypto/aes_base_encrypter.cc
@@ -35,4 +35,13 @@
   return out;
 }
 
+QuicPacketCount AesBaseEncrypter::GetConfidentialityLimit() const {
+  // For AEAD_AES_128_GCM and AEAD_AES_256_GCM, the confidentiality limit is
+  // 2^25 encrypted packets.
+  // https://quicwg.org/base-drafts/draft-ietf-quic-tls.html#name-limits-on-aead-usage
+  static_assert(kMaxOutgoingPacketSize < 16384,
+                "This key limit requires limits on encryption payload sizes");
+  return 33554432U;
+}
+
 }  // namespace quic
diff --git a/quic/core/crypto/aes_base_encrypter.h b/quic/core/crypto/aes_base_encrypter.h
index 7339640..6b1c98c 100644
--- a/quic/core/crypto/aes_base_encrypter.h
+++ b/quic/core/crypto/aes_base_encrypter.h
@@ -20,6 +20,7 @@
 
   bool SetHeaderProtectionKey(absl::string_view key) override;
   std::string GenerateHeaderProtectionMask(absl::string_view sample) override;
+  QuicPacketCount GetConfidentialityLimit() const override;
 
  private:
   // The key used for packet number encryption.
diff --git a/quic/core/crypto/chacha20_poly1305_encrypter.cc b/quic/core/crypto/chacha20_poly1305_encrypter.cc
index 37b899f..f66f31c 100644
--- a/quic/core/crypto/chacha20_poly1305_encrypter.cc
+++ b/quic/core/crypto/chacha20_poly1305_encrypter.cc
@@ -27,4 +27,11 @@
 
 ChaCha20Poly1305Encrypter::~ChaCha20Poly1305Encrypter() {}
 
+QuicPacketCount ChaCha20Poly1305Encrypter::GetConfidentialityLimit() const {
+  // For AEAD_CHACHA20_POLY1305, the confidentiality limit is greater than the
+  // number of possible packets (2^62) and so can be disregarded.
+  // https://quicwg.org/base-drafts/draft-ietf-quic-tls.html#name-limits-on-aead-usage
+  return std::numeric_limits<QuicPacketCount>::max();
+}
+
 }  // namespace quic
diff --git a/quic/core/crypto/chacha20_poly1305_encrypter.h b/quic/core/crypto/chacha20_poly1305_encrypter.h
index 7200788..98f6ff0 100644
--- a/quic/core/crypto/chacha20_poly1305_encrypter.h
+++ b/quic/core/crypto/chacha20_poly1305_encrypter.h
@@ -29,6 +29,8 @@
   ChaCha20Poly1305Encrypter& operator=(const ChaCha20Poly1305Encrypter&) =
       delete;
   ~ChaCha20Poly1305Encrypter() override;
+
+  QuicPacketCount GetConfidentialityLimit() const override;
 };
 
 }  // namespace quic
diff --git a/quic/core/crypto/chacha20_poly1305_tls_encrypter.cc b/quic/core/crypto/chacha20_poly1305_tls_encrypter.cc
index c650b27..a03a14c 100644
--- a/quic/core/crypto/chacha20_poly1305_tls_encrypter.cc
+++ b/quic/core/crypto/chacha20_poly1305_tls_encrypter.cc
@@ -27,4 +27,11 @@
 
 ChaCha20Poly1305TlsEncrypter::~ChaCha20Poly1305TlsEncrypter() {}
 
+QuicPacketCount ChaCha20Poly1305TlsEncrypter::GetConfidentialityLimit() const {
+  // For AEAD_CHACHA20_POLY1305, the confidentiality limit is greater than the
+  // number of possible packets (2^62) and so can be disregarded.
+  // https://quicwg.org/base-drafts/draft-ietf-quic-tls.html#name-limits-on-aead-usage
+  return std::numeric_limits<QuicPacketCount>::max();
+}
+
 }  // namespace quic
diff --git a/quic/core/crypto/chacha20_poly1305_tls_encrypter.h b/quic/core/crypto/chacha20_poly1305_tls_encrypter.h
index 0ef7ae8..f83141f 100644
--- a/quic/core/crypto/chacha20_poly1305_tls_encrypter.h
+++ b/quic/core/crypto/chacha20_poly1305_tls_encrypter.h
@@ -27,6 +27,8 @@
   ChaCha20Poly1305TlsEncrypter& operator=(const ChaCha20Poly1305TlsEncrypter&) =
       delete;
   ~ChaCha20Poly1305TlsEncrypter() override;
+
+  QuicPacketCount GetConfidentialityLimit() const override;
 };
 
 }  // namespace quic
diff --git a/quic/core/crypto/null_encrypter.cc b/quic/core/crypto/null_encrypter.cc
index 1d5dd8b..f379ff2 100644
--- a/quic/core/crypto/null_encrypter.cc
+++ b/quic/core/crypto/null_encrypter.cc
@@ -83,6 +83,10 @@
   return plaintext_size + GetHashLength();
 }
 
+QuicPacketCount NullEncrypter::GetConfidentialityLimit() const {
+  return std::numeric_limits<QuicPacketCount>::max();
+}
+
 absl::string_view NullEncrypter::GetKey() const {
   return absl::string_view();
 }
diff --git a/quic/core/crypto/null_encrypter.h b/quic/core/crypto/null_encrypter.h
index bdfd2e7..82cb2c1 100644
--- a/quic/core/crypto/null_encrypter.h
+++ b/quic/core/crypto/null_encrypter.h
@@ -41,6 +41,7 @@
   size_t GetIVSize() const override;
   size_t GetMaxPlaintextSize(size_t ciphertext_size) const override;
   size_t GetCiphertextSize(size_t plaintext_size) const override;
+  QuicPacketCount GetConfidentialityLimit() const override;
   absl::string_view GetKey() const override;
   absl::string_view GetNoncePrefix() const override;
 
diff --git a/quic/core/crypto/quic_encrypter.h b/quic/core/crypto/quic_encrypter.h
index 89d7fed..b454997 100644
--- a/quic/core/crypto/quic_encrypter.h
+++ b/quic/core/crypto/quic_encrypter.h
@@ -57,6 +57,10 @@
   // to plaintext of size |plaintext_size|.
   virtual size_t GetCiphertextSize(size_t plaintext_size) const = 0;
 
+  // Returns the maximum number of packets that can be safely encrypted with
+  // this encrypter.
+  virtual QuicPacketCount GetConfidentialityLimit() const = 0;
+
   // For use by unit tests only.
   virtual absl::string_view GetKey() const = 0;
   virtual absl::string_view GetNoncePrefix() const = 0;
diff --git a/quic/core/http/end_to_end_test.cc b/quic/core/http/end_to_end_test.cc
index dddeb1f..025e257 100644
--- a/quic/core/http/end_to_end_test.cc
+++ b/quic/core/http/end_to_end_test.cc
@@ -5003,6 +5003,53 @@
   server_thread_->Resume();
 }
 
+TEST_P(EndToEndTest, KeyUpdateInitiatedByConfidentialityLimit) {
+  SetQuicReloadableFlag(quic_enable_aead_limits, true);
+  SetQuicReloadableFlag(quic_key_update_supported, true);
+  SetQuicFlag(FLAGS_quic_key_update_confidentiality_limit, 4U);
+
+  if (!version_.UsesTls()) {
+    // Key Update is only supported in TLS handshake.
+    ASSERT_TRUE(Initialize());
+    return;
+  }
+
+  ASSERT_TRUE(Initialize());
+
+  QuicConnection* client_connection = GetClientConnection();
+  ASSERT_TRUE(client_connection);
+  EXPECT_EQ(0u, client_connection->GetStats().key_update_count);
+
+  server_thread_->WaitUntil(
+      [this]() {
+        QuicConnection* server_connection = GetServerConnection();
+        if (server_connection != nullptr) {
+          EXPECT_EQ(0u, server_connection->GetStats().key_update_count);
+        } else {
+          ADD_FAILURE() << "Missing server connection";
+        }
+        return true;
+      },
+      QuicTime::Delta::FromSeconds(5));
+
+  SendSynchronousFooRequestAndCheckResponse();
+  SendSynchronousFooRequestAndCheckResponse();
+  SendSynchronousFooRequestAndCheckResponse();
+  // Don't know exactly how many packets will be sent in each request/response,
+  // so just test that at least one key update occurred.
+  EXPECT_LE(1u, client_connection->GetStats().key_update_count);
+
+  server_thread_->Pause();
+  QuicConnection* server_connection = GetServerConnection();
+  if (server_connection) {
+    QuicConnectionStats server_stats = server_connection->GetStats();
+    EXPECT_LE(1u, server_stats.key_update_count);
+  } else {
+    ADD_FAILURE() << "Missing server connection";
+  }
+  server_thread_->Resume();
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quic/core/quic_connection.cc b/quic/core/quic_connection.cc
index 4dec406..c3fa8fb 100644
--- a/quic/core/quic_connection.cc
+++ b/quic/core/quic_connection.cc
@@ -265,6 +265,8 @@
       direct_peer_address_(initial_peer_address),
       active_effective_peer_migration_type_(NO_CHANGE),
       support_key_update_for_connection_(false),
+      enable_aead_limits_(GetQuicReloadableFlag(quic_enable_aead_limits) &&
+                          version().UsesTls()),
       last_packet_decrypted_(false),
       last_size_(0),
       current_packet_data_(nullptr),
@@ -3067,11 +3069,17 @@
     handshake_packet_sent_ = true;
   }
 
-  if (packet->encryption_level == ENCRYPTION_FORWARD_SECURE &&
-      !lowest_packet_sent_in_current_key_phase_.IsInitialized()) {
-    QUIC_DLOG(INFO) << ENDPOINT << "lowest_packet_sent_in_current_key_phase_ = "
-                    << packet_number;
-    lowest_packet_sent_in_current_key_phase_ = packet_number;
+  if (packet->encryption_level == ENCRYPTION_FORWARD_SECURE) {
+    if (!lowest_packet_sent_in_current_key_phase_.IsInitialized()) {
+      QUIC_DLOG(INFO) << ENDPOINT
+                      << "lowest_packet_sent_in_current_key_phase_ = "
+                      << packet_number;
+      lowest_packet_sent_in_current_key_phase_ = packet_number;
+    }
+    if (!is_termination_packet &&
+        MaybeHandleAeadConfidentialityLimits(*packet)) {
+      return true;
+    }
   }
 
   if (in_flight || !retransmission_alarm_->IsSet()) {
@@ -3095,6 +3103,110 @@
   return true;
 }
 
+bool QuicConnection::MaybeHandleAeadConfidentialityLimits(
+    const SerializedPacket& packet) {
+  if (!enable_aead_limits_) {
+    return false;
+  }
+
+  if (packet.encryption_level != ENCRYPTION_FORWARD_SECURE) {
+    QUIC_BUG
+        << "MaybeHandleAeadConfidentialityLimits called on non 1-RTT packet";
+    return false;
+  }
+  if (!lowest_packet_sent_in_current_key_phase_.IsInitialized()) {
+    QUIC_BUG << "lowest_packet_sent_in_current_key_phase_ must be initialized "
+                "before calling MaybeHandleAeadConfidentialityLimits";
+    return false;
+  }
+
+  // Calculate the number of packets encrypted from the packet number, which is
+  // simpler than keeping another counter. The packet number space may be
+  // sparse, so this might overcount, but doing a key update earlier than
+  // necessary would only improve security and has negligible cost.
+  if (packet.packet_number < lowest_packet_sent_in_current_key_phase_) {
+    const std::string error_details = quiche::QuicheStrCat(
+        "packet_number(", packet.packet_number.ToString(),
+        ") < lowest_packet_sent_in_current_key_phase_ (",
+        lowest_packet_sent_in_current_key_phase_.ToString(), ")");
+    QUIC_BUG << error_details;
+    CloseConnection(QUIC_INTERNAL_ERROR, error_details,
+                    ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
+    return true;
+  }
+  const QuicPacketCount num_packets_encrypted_in_current_key_phase =
+      packet.packet_number - lowest_packet_sent_in_current_key_phase_ + 1;
+
+  const QuicPacketCount confidentiality_limit =
+      framer_.GetOneRttEncrypterConfidentialityLimit();
+
+  // Attempt to initiate a key update before reaching the AEAD
+  // confidentiality limit when the number of packets sent in the current
+  // key phase gets within |kKeyUpdateConfidentialityLimitOffset| packets of
+  // the limit, unless overridden by
+  // FLAGS_quic_key_update_confidentiality_limit.
+  constexpr QuicPacketCount kKeyUpdateConfidentialityLimitOffset = 1000;
+  QuicPacketCount key_update_limit = 0;
+  if (confidentiality_limit > kKeyUpdateConfidentialityLimitOffset) {
+    key_update_limit =
+        confidentiality_limit - kKeyUpdateConfidentialityLimitOffset;
+  }
+  const QuicPacketCount key_update_limit_override =
+      GetQuicFlag(FLAGS_quic_key_update_confidentiality_limit);
+  if (key_update_limit_override) {
+    key_update_limit = key_update_limit_override;
+  }
+
+  QUIC_DVLOG(2) << ENDPOINT << "Checking AEAD confidentiality limits: "
+                << "num_packets_encrypted_in_current_key_phase="
+                << num_packets_encrypted_in_current_key_phase
+                << " key_update_limit=" << key_update_limit
+                << " confidentiality_limit=" << confidentiality_limit
+                << " IsKeyUpdateAllowed()=" << IsKeyUpdateAllowed();
+
+  if (num_packets_encrypted_in_current_key_phase >= confidentiality_limit) {
+    // Reached the confidentiality limit without initiating a key update,
+    // must close the connection.
+    const std::string error_details = quiche::QuicheStrCat(
+        "encrypter confidentiality limit reached: "
+        "num_packets_encrypted_in_current_key_phase=",
+        num_packets_encrypted_in_current_key_phase,
+        " key_update_limit=", key_update_limit,
+        " confidentiality_limit=", confidentiality_limit,
+        " IsKeyUpdateAllowed()=", IsKeyUpdateAllowed());
+    CloseConnection(QUIC_AEAD_LIMIT_REACHED, error_details,
+                    ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
+    return true;
+  }
+
+  if (IsKeyUpdateAllowed() &&
+      num_packets_encrypted_in_current_key_phase >= key_update_limit) {
+    // Approaching the confidentiality limit, initiate key update so that
+    // the next set of keys will be ready for the next packet before the
+    // limit is reached.
+    if (key_update_limit_override) {
+      QUIC_DLOG(INFO) << ENDPOINT
+                      << "reached FLAGS_quic_key_update_confidentiality_limit, "
+                         "initiating key update: "
+                      << "num_packets_encrypted_in_current_key_phase="
+                      << num_packets_encrypted_in_current_key_phase
+                      << " key_update_limit=" << key_update_limit
+                      << " confidentiality_limit=" << confidentiality_limit;
+    } else {
+      QUIC_DLOG(INFO) << ENDPOINT
+                      << "approaching AEAD confidentiality limit, "
+                         "initiating key update: "
+                      << "num_packets_encrypted_in_current_key_phase="
+                      << num_packets_encrypted_in_current_key_phase
+                      << " key_update_limit=" << key_update_limit
+                      << " confidentiality_limit=" << confidentiality_limit;
+    }
+    InitiateKeyUpdate();
+  }
+
+  return false;
+}
+
 void QuicConnection::FlushPackets() {
   if (!connected_) {
     return;
diff --git a/quic/core/quic_connection.h b/quic/core/quic_connection.h
index 9531581..26629ba 100644
--- a/quic/core/quic_connection.h
+++ b/quic/core/quic_connection.h
@@ -1252,6 +1252,12 @@
   // writer is write blocked.
   bool WritePacket(SerializedPacket* packet);
 
+  // Enforce AEAD Confidentiality limits by iniating key update or closing
+  // connection if too many packets have been encrypted with the current key.
+  // Returns true if the connection was closed. Should not be called for
+  // termination packets.
+  bool MaybeHandleAeadConfidentialityLimits(const SerializedPacket& packet);
+
   // Flush packets buffered in the writer, if any.
   void FlushPackets();
 
@@ -1552,6 +1558,10 @@
   // key update but before the first packet has been sent.
   QuicPacketNumber lowest_packet_sent_in_current_key_phase_;
 
+  // Honor the AEAD confidentiality and integrity limits by initiating key
+  // update (if allowed) and/or closing the connection, as necessary.
+  bool enable_aead_limits_;
+
   // True if the last packet has gotten far enough in the framer to be
   // decrypted.
   bool last_packet_decrypted_;
diff --git a/quic/core/quic_connection_test.cc b/quic/core/quic_connection_test.cc
index 62f4b8a..738bc2e 100644
--- a/quic/core/quic_connection_test.cc
+++ b/quic/core/quic_connection_test.cc
@@ -110,6 +110,24 @@
   }
 }
 
+// A NullEncrypterWithConfidentialityLimit is a NullEncrypter that allows
+// specifying the confidentiality limit on the maximum number of packets that
+// may be encrypted per key phase in TLS+QUIC.
+class NullEncrypterWithConfidentialityLimit : public NullEncrypter {
+ public:
+  NullEncrypterWithConfidentialityLimit(Perspective perspective,
+                                        QuicPacketCount confidentiality_limit)
+      : NullEncrypter(perspective),
+        confidentiality_limit_(confidentiality_limit) {}
+
+  QuicPacketCount GetConfidentialityLimit() const override {
+    return confidentiality_limit_;
+  }
+
+ private:
+  QuicPacketCount confidentiality_limit_;
+};
+
 class TestConnectionHelper : public QuicConnectionHelperInterface {
  public:
   TestConnectionHelper(MockClock* clock, MockRandom* random_generator)
@@ -12041,6 +12059,8 @@
                            std::make_unique<TaggingEncrypter>(0x01));
   SetDecrypter(ENCRYPTION_FORWARD_SECURE,
                std::make_unique<StrictTaggingDecrypter>(0x01));
+  EXPECT_CALL(visitor_, GetHandshakeState())
+      .WillRepeatedly(Return(HANDSHAKE_CONFIRMED));
   connection_.OnHandshakeComplete();
 
   peer_framer_.SetEncrypter(ENCRYPTION_FORWARD_SECURE,
@@ -12150,6 +12170,288 @@
   EXPECT_FALSE(connection_.GetDiscardPreviousOneRttKeysAlarm()->IsSet());
 }
 
+TEST_P(QuicConnectionTest, InitiateKeyUpdateApproachingConfidentialityLimit) {
+  if (!connection_.version().UsesTls()) {
+    return;
+  }
+
+  QuicConnectionPeer::SetEnableAeadLimits(&connection_, true);
+  SetQuicFlag(FLAGS_quic_key_update_confidentiality_limit, 3U);
+
+  std::string error_details;
+  TransportParameters params;
+  // Key update is enabled.
+  params.key_update_not_yet_supported = false;
+  QuicConfig config;
+  EXPECT_THAT(config.ProcessTransportParameters(
+                  params, /* is_resumption = */ false, &error_details),
+              IsQuicNoError());
+  config.SetKeyUpdateSupportedLocally();
+  QuicConfigPeer::SetNegotiated(&config, true);
+  if (connection_.version().AuthenticatesHandshakeConnectionIds()) {
+    QuicConfigPeer::SetReceivedOriginalConnectionId(
+        &config, connection_.connection_id());
+    QuicConfigPeer::SetReceivedInitialSourceConnectionId(
+        &config, connection_.connection_id());
+  }
+  EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
+  connection_.SetFromConfig(config);
+
+  MockFramerVisitor peer_framer_visitor_;
+  peer_framer_.set_visitor(&peer_framer_visitor_);
+
+  use_tagging_decrypter();
+
+  uint8_t current_tag = 0x01;
+
+  connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
+  connection_.SetEncrypter(ENCRYPTION_FORWARD_SECURE,
+                           std::make_unique<TaggingEncrypter>(current_tag));
+  SetDecrypter(ENCRYPTION_FORWARD_SECURE,
+               std::make_unique<StrictTaggingDecrypter>(current_tag));
+  EXPECT_CALL(visitor_, GetHandshakeState())
+      .WillRepeatedly(Return(HANDSHAKE_CONFIRMED));
+  connection_.OnHandshakeComplete();
+
+  peer_framer_.SetKeyUpdateSupportForConnection(true);
+  peer_framer_.SetEncrypter(ENCRYPTION_FORWARD_SECURE,
+                            std::make_unique<TaggingEncrypter>(current_tag));
+
+  const QuicConnectionStats& stats = connection_.GetStats();
+
+  for (int packet_num = 1; packet_num <= 8; ++packet_num) {
+    if (packet_num == 3 || packet_num == 6) {
+      current_tag++;
+      EXPECT_CALL(visitor_, AdvanceKeysAndCreateCurrentOneRttDecrypter())
+          .WillOnce([current_tag]() {
+            return std::make_unique<StrictTaggingDecrypter>(current_tag);
+          });
+      EXPECT_CALL(visitor_, CreateCurrentOneRttEncrypter())
+          .WillOnce([current_tag]() {
+            return std::make_unique<TaggingEncrypter>(current_tag);
+          });
+    }
+    // Send packet.
+    QuicPacketNumber last_packet;
+    SendStreamDataToPeer(packet_num, "foo", 0, NO_FIN, &last_packet);
+    EXPECT_EQ(QuicPacketNumber(packet_num), last_packet);
+    if (packet_num >= 6) {
+      EXPECT_EQ(2U, stats.key_update_count);
+    } else if (packet_num >= 3) {
+      EXPECT_EQ(1U, stats.key_update_count);
+    } else {
+      EXPECT_EQ(0U, stats.key_update_count);
+    }
+
+    if (packet_num == 4 || packet_num == 7) {
+      // Pretend that peer accepts the key update.
+      EXPECT_CALL(peer_framer_visitor_,
+                  AdvanceKeysAndCreateCurrentOneRttDecrypter())
+          .WillOnce([current_tag]() {
+            return std::make_unique<StrictTaggingDecrypter>(current_tag);
+          });
+      EXPECT_CALL(peer_framer_visitor_, CreateCurrentOneRttEncrypter())
+          .WillOnce([current_tag]() {
+            return std::make_unique<TaggingEncrypter>(current_tag);
+          });
+      peer_framer_.DoKeyUpdate();
+    }
+    // Receive ack for packet.
+    EXPECT_CALL(*send_algorithm_, OnCongestionEvent(true, _, _, _, _));
+    QuicAckFrame frame1 = InitAckFrame(packet_num);
+    ProcessAckPacket(&frame1);
+  }
+}
+
+TEST_P(QuicConnectionTest,
+       CloseConnectionOnConfidentialityLimitKeyUpdateNotAllowed) {
+  if (!connection_.version().UsesTls()) {
+    return;
+  }
+
+  QuicConnectionPeer::SetEnableAeadLimits(&connection_, true);
+  // Set key update confidentiality limit to 1 packet.
+  SetQuicFlag(FLAGS_quic_key_update_confidentiality_limit, 1U);
+  // Use confidentiality limit for connection close of 3 packets.
+  constexpr size_t kConfidentialityLimit = 3U;
+
+  std::string error_details;
+  TransportParameters params;
+  // Key update is enabled.
+  params.key_update_not_yet_supported = false;
+  QuicConfig config;
+  EXPECT_THAT(config.ProcessTransportParameters(
+                  params, /* is_resumption = */ false, &error_details),
+              IsQuicNoError());
+  config.SetKeyUpdateSupportedLocally();
+  QuicConfigPeer::SetNegotiated(&config, true);
+  if (connection_.version().AuthenticatesHandshakeConnectionIds()) {
+    QuicConfigPeer::SetReceivedOriginalConnectionId(
+        &config, connection_.connection_id());
+    QuicConfigPeer::SetReceivedInitialSourceConnectionId(
+        &config, connection_.connection_id());
+  }
+  EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
+  connection_.SetFromConfig(config);
+
+  connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
+  connection_.SetEncrypter(
+      ENCRYPTION_FORWARD_SECURE,
+      std::make_unique<NullEncrypterWithConfidentialityLimit>(
+          Perspective::IS_CLIENT, kConfidentialityLimit));
+  EXPECT_CALL(visitor_, GetHandshakeState())
+      .WillRepeatedly(Return(HANDSHAKE_CONFIRMED));
+  connection_.OnHandshakeComplete();
+
+  QuicPacketNumber last_packet;
+  // Send 3 packets without receiving acks for any of them. Key update will not
+  // be allowed, so the confidentiality limit should be reached, forcing the
+  // connection to be closed.
+  SendStreamDataToPeer(1, "foo", 0, NO_FIN, &last_packet);
+  EXPECT_TRUE(connection_.connected());
+  SendStreamDataToPeer(2, "foo", 0, NO_FIN, &last_packet);
+  EXPECT_TRUE(connection_.connected());
+  EXPECT_CALL(visitor_, OnConnectionClosed(_, _));
+  SendStreamDataToPeer(3, "foo", 0, NO_FIN, &last_packet);
+  EXPECT_FALSE(connection_.connected());
+  const QuicConnectionStats& stats = connection_.GetStats();
+  EXPECT_EQ(0U, stats.key_update_count);
+  TestConnectionCloseQuicErrorCode(QUIC_AEAD_LIMIT_REACHED);
+}
+
+TEST_P(QuicConnectionTest,
+       CloseConnectionOnConfidentialityLimitKeyUpdateNotSupportedByPeer) {
+  if (!connection_.version().UsesTls()) {
+    return;
+  }
+
+  QuicConnectionPeer::SetEnableAeadLimits(&connection_, true);
+  // Set key update confidentiality limit to 1 packet.
+  SetQuicFlag(FLAGS_quic_key_update_confidentiality_limit, 1U);
+  // Use confidentiality limit for connection close of 3 packets.
+  constexpr size_t kConfidentialityLimit = 3U;
+
+  std::string error_details;
+  TransportParameters params;
+  // Key update not enabled for this connection as peer doesn't support it.
+  params.key_update_not_yet_supported = true;
+  QuicConfig config;
+  EXPECT_THAT(config.ProcessTransportParameters(
+                  params, /* is_resumption = */ false, &error_details),
+              IsQuicNoError());
+  // Key update is supported locally.
+  config.SetKeyUpdateSupportedLocally();
+  QuicConfigPeer::SetNegotiated(&config, true);
+  if (connection_.version().AuthenticatesHandshakeConnectionIds()) {
+    QuicConfigPeer::SetReceivedOriginalConnectionId(
+        &config, connection_.connection_id());
+    QuicConfigPeer::SetReceivedInitialSourceConnectionId(
+        &config, connection_.connection_id());
+  }
+  EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
+  connection_.SetFromConfig(config);
+
+  connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
+  connection_.SetEncrypter(
+      ENCRYPTION_FORWARD_SECURE,
+      std::make_unique<NullEncrypterWithConfidentialityLimit>(
+          Perspective::IS_CLIENT, kConfidentialityLimit));
+  EXPECT_CALL(visitor_, GetHandshakeState())
+      .WillRepeatedly(Return(HANDSHAKE_CONFIRMED));
+  connection_.OnHandshakeComplete();
+
+  QuicPacketNumber last_packet;
+  // Send 3 packets and receive acks for them. Since key update is not enabled
+  // the confidentiality limit should be reached, forcing the connection to be
+  // closed.
+  SendStreamDataToPeer(1, "foo", 0, NO_FIN, &last_packet);
+  EXPECT_TRUE(connection_.connected());
+  // Receive ack for packet.
+  EXPECT_CALL(*send_algorithm_, OnCongestionEvent(true, _, _, _, _));
+  QuicAckFrame frame1 = InitAckFrame(1);
+  ProcessAckPacket(&frame1);
+
+  SendStreamDataToPeer(2, "foo", 0, NO_FIN, &last_packet);
+  EXPECT_TRUE(connection_.connected());
+  // Receive ack for packet.
+  EXPECT_CALL(*send_algorithm_, OnCongestionEvent(true, _, _, _, _));
+  QuicAckFrame frame2 = InitAckFrame(2);
+  ProcessAckPacket(&frame2);
+
+  EXPECT_CALL(visitor_, OnConnectionClosed(_, _));
+  SendStreamDataToPeer(3, "foo", 0, NO_FIN, &last_packet);
+  EXPECT_FALSE(connection_.connected());
+  const QuicConnectionStats& stats = connection_.GetStats();
+  EXPECT_EQ(0U, stats.key_update_count);
+  TestConnectionCloseQuicErrorCode(QUIC_AEAD_LIMIT_REACHED);
+}
+
+TEST_P(QuicConnectionTest,
+       CloseConnectionOnConfidentialityLimitKeyUpdateNotEnabledLocally) {
+  if (!connection_.version().UsesTls()) {
+    return;
+  }
+
+  QuicConnectionPeer::SetEnableAeadLimits(&connection_, true);
+  // Set key update confidentiality limit to 1 packet.
+  SetQuicFlag(FLAGS_quic_key_update_confidentiality_limit, 1U);
+  // Use confidentiality limit for connection close of 3 packets.
+  constexpr size_t kConfidentialityLimit = 3U;
+
+  std::string error_details;
+  TransportParameters params;
+  // Key update is supported by peer but not locally
+  // (config.SetKeyUpdateSupportedLocally is not called.)
+  params.key_update_not_yet_supported = false;
+  QuicConfig config;
+  EXPECT_THAT(config.ProcessTransportParameters(
+                  params, /* is_resumption = */ false, &error_details),
+              IsQuicNoError());
+  QuicConfigPeer::SetNegotiated(&config, true);
+  if (connection_.version().AuthenticatesHandshakeConnectionIds()) {
+    QuicConfigPeer::SetReceivedOriginalConnectionId(
+        &config, connection_.connection_id());
+    QuicConfigPeer::SetReceivedInitialSourceConnectionId(
+        &config, connection_.connection_id());
+  }
+  EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
+  connection_.SetFromConfig(config);
+
+  connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
+  connection_.SetEncrypter(
+      ENCRYPTION_FORWARD_SECURE,
+      std::make_unique<NullEncrypterWithConfidentialityLimit>(
+          Perspective::IS_CLIENT, kConfidentialityLimit));
+  EXPECT_CALL(visitor_, GetHandshakeState())
+      .WillRepeatedly(Return(HANDSHAKE_CONFIRMED));
+  connection_.OnHandshakeComplete();
+
+  QuicPacketNumber last_packet;
+  // Send 3 packets and receive acks for them. Since key update is not enabled
+  // the confidentiality limit should be reached, forcing the connection to be
+  // closed.
+  SendStreamDataToPeer(1, "foo", 0, NO_FIN, &last_packet);
+  EXPECT_TRUE(connection_.connected());
+  // Receive ack for packet.
+  EXPECT_CALL(*send_algorithm_, OnCongestionEvent(true, _, _, _, _));
+  QuicAckFrame frame1 = InitAckFrame(1);
+  ProcessAckPacket(&frame1);
+
+  SendStreamDataToPeer(2, "foo", 0, NO_FIN, &last_packet);
+  EXPECT_TRUE(connection_.connected());
+  // Receive ack for packet.
+  EXPECT_CALL(*send_algorithm_, OnCongestionEvent(true, _, _, _, _));
+  QuicAckFrame frame2 = InitAckFrame(2);
+  ProcessAckPacket(&frame2);
+
+  EXPECT_CALL(visitor_, OnConnectionClosed(_, _));
+  SendStreamDataToPeer(3, "foo", 0, NO_FIN, &last_packet);
+  EXPECT_FALSE(connection_.connected());
+  const QuicConnectionStats& stats = connection_.GetStats();
+  EXPECT_EQ(0U, stats.key_update_count);
+  TestConnectionCloseQuicErrorCode(QUIC_AEAD_LIMIT_REACHED);
+}
+
 TEST_P(QuicConnectionTest, SendAckFrequencyFrame) {
   if (!version().HasIetfQuicFrames()) {
     return;
diff --git a/quic/core/quic_error_codes.cc b/quic/core/quic_error_codes.cc
index 63d96f1..75bbebd 100644
--- a/quic/core/quic_error_codes.cc
+++ b/quic/core/quic_error_codes.cc
@@ -233,6 +233,8 @@
     RETURN_STRING_LITERAL(QUIC_ZERO_RTT_RESUMPTION_LIMIT_REDUCED);
     RETURN_STRING_LITERAL(QUIC_SILENT_IDLE_TIMEOUT);
     RETURN_STRING_LITERAL(QUIC_MISSING_WRITE_KEYS);
+    RETURN_STRING_LITERAL(QUIC_KEY_UPDATE_ERROR);
+    RETURN_STRING_LITERAL(QUIC_AEAD_LIMIT_REACHED);
 
     RETURN_STRING_LITERAL(QUIC_LAST_ERROR);
     // Intentionally have no default case, so we'll break the build
@@ -271,6 +273,8 @@
     RETURN_STRING_LITERAL(PROTOCOL_VIOLATION);
     RETURN_STRING_LITERAL(INVALID_TOKEN);
     RETURN_STRING_LITERAL(CRYPTO_BUFFER_EXCEEDED);
+    RETURN_STRING_LITERAL(KEY_UPDATE_ERROR);
+    RETURN_STRING_LITERAL(AEAD_LIMIT_REACHED);
     // CRYPTO_ERROR is handled in the if before this switch, these cases do not
     // change behavior and are only here to make the compiler happy.
     case CRYPTO_ERROR_FIRST:
@@ -634,6 +638,10 @@
       return {true, static_cast<uint64_t>(PROTOCOL_VIOLATION)};
     case QUIC_MISSING_WRITE_KEYS:
       return {true, static_cast<uint64_t>(INTERNAL_ERROR)};
+    case QUIC_KEY_UPDATE_ERROR:
+      return {true, static_cast<uint64_t>(KEY_UPDATE_ERROR)};
+    case QUIC_AEAD_LIMIT_REACHED:
+      return {true, static_cast<uint64_t>(AEAD_LIMIT_REACHED)};
     case QUIC_LAST_ERROR:
       return {false, static_cast<uint64_t>(QUIC_LAST_ERROR)};
   }
diff --git a/quic/core/quic_error_codes.h b/quic/core/quic_error_codes.h
index e0cb0ed..4771272 100644
--- a/quic/core/quic_error_codes.h
+++ b/quic/core/quic_error_codes.h
@@ -500,8 +500,15 @@
   // Try to write data without the right write keys.
   QUIC_MISSING_WRITE_KEYS = 170,
 
+  // An endpoint detected errors in performing key updates.
+  QUIC_KEY_UPDATE_ERROR = 172,
+
+  // An endpoint has reached the confidentiality or integrity limit for the
+  // AEAD algorithm used by the given connection.
+  QUIC_AEAD_LIMIT_REACHED = 173,
+
   // No error. Used as bound while iterating.
-  QUIC_LAST_ERROR = 172,
+  QUIC_LAST_ERROR = 174,
 };
 // 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
@@ -533,6 +540,8 @@
   PROTOCOL_VIOLATION = 0xA,
   INVALID_TOKEN = 0xB,
   CRYPTO_BUFFER_EXCEEDED = 0xD,
+  KEY_UPDATE_ERROR = 0xE,
+  AEAD_LIMIT_REACHED = 0xF,
   CRYPTO_ERROR_FIRST = 0x100,
   CRYPTO_ERROR_LAST = 0x1FF,
 };
diff --git a/quic/core/quic_error_codes_test.cc b/quic/core/quic_error_codes_test.cc
index 0f567df..ee4cacd 100644
--- a/quic/core/quic_error_codes_test.cc
+++ b/quic/core/quic_error_codes_test.cc
@@ -53,6 +53,10 @@
   EXPECT_EQ("INVALID_TOKEN", QuicIetfTransportErrorCodeString(INVALID_TOKEN));
   EXPECT_EQ("CRYPTO_BUFFER_EXCEEDED",
             QuicIetfTransportErrorCodeString(CRYPTO_BUFFER_EXCEEDED));
+  EXPECT_EQ("KEY_UPDATE_ERROR",
+            QuicIetfTransportErrorCodeString(KEY_UPDATE_ERROR));
+  EXPECT_EQ("AEAD_LIMIT_REACHED",
+            QuicIetfTransportErrorCodeString(AEAD_LIMIT_REACHED));
 
   EXPECT_EQ("Unknown(1024)",
             QuicIetfTransportErrorCodeString(
@@ -74,7 +78,7 @@
     if (ietf_error_code.is_transport_close) {
       QuicIetfTransportErrorCodes transport_error_code =
           static_cast<QuicIetfTransportErrorCodes>(ietf_error_code.error_code);
-      bool is_valid_transport_error_code = transport_error_code <= 0x0d;
+      bool is_valid_transport_error_code = transport_error_code <= 0x0f;
       EXPECT_TRUE(is_valid_transport_error_code) << internal_error_code_string;
     } else {
       // Non-transport errors are application errors, either HTTP/3 or QPACK.
diff --git a/quic/core/quic_framer.cc b/quic/core/quic_framer.cc
index 6842211..ede689c 100644
--- a/quic/core/quic_framer.cc
+++ b/quic/core/quic_framer.cc
@@ -4639,6 +4639,14 @@
   return min_plaintext_size;
 }
 
+QuicPacketCount QuicFramer::GetOneRttEncrypterConfidentialityLimit() const {
+  if (!encrypter_[ENCRYPTION_FORWARD_SECURE]) {
+    QUIC_BUG << "1-RTT encrypter not set";
+    return 0;
+  }
+  return encrypter_[ENCRYPTION_FORWARD_SECURE]->GetConfidentialityLimit();
+}
+
 bool QuicFramer::DecryptPayload(absl::string_view encrypted,
                                 absl::string_view associated_data,
                                 const QuicPacketHeader& header,
diff --git a/quic/core/quic_framer.h b/quic/core/quic_framer.h
index 48a7877..763d0b8 100644
--- a/quic/core/quic_framer.h
+++ b/quic/core/quic_framer.h
@@ -585,6 +585,10 @@
   // to ciphertext no larger than |ciphertext_size|.
   size_t GetMaxPlaintextSize(size_t ciphertext_size);
 
+  // Returns the maximum number of packets that can be safely encrypted with
+  // the active AEAD. 1-RTT keys must be set before calling this method.
+  QuicPacketCount GetOneRttEncrypterConfidentialityLimit() const;
+
   const std::string& detailed_error() { return detailed_error_; }
 
   // The minimum packet number length required to represent |packet_number|.
diff --git a/quic/core/quic_framer_test.cc b/quic/core/quic_framer_test.cc
index da4319d..e21b2ef 100644
--- a/quic/core/quic_framer_test.cc
+++ b/quic/core/quic_framer_test.cc
@@ -126,6 +126,9 @@
   size_t GetCiphertextSize(size_t plaintext_size) const override {
     return plaintext_size;
   }
+  QuicPacketCount GetConfidentialityLimit() const override {
+    return std::numeric_limits<QuicPacketCount>::max();
+  }
   absl::string_view GetKey() const override { return absl::string_view(); }
   absl::string_view GetNoncePrefix() const override {
     return absl::string_view();
diff --git a/quic/test_tools/quic_connection_peer.cc b/quic/test_tools/quic_connection_peer.cc
index 6b092c8..d6395d0 100644
--- a/quic/test_tools/quic_connection_peer.cc
+++ b/quic/test_tools/quic_connection_peer.cc
@@ -297,6 +297,12 @@
 }
 
 // static
+void QuicConnectionPeer::SetEnableAeadLimits(QuicConnection* connection,
+                                             bool enabled) {
+  connection->enable_aead_limits_ = enabled;
+}
+
+// static
 void QuicConnectionPeer::SendConnectionClosePacket(QuicConnection* connection,
                                                    QuicErrorCode error,
                                                    const std::string& details) {
diff --git a/quic/test_tools/quic_connection_peer.h b/quic/test_tools/quic_connection_peer.h
index bf404fb..ecc98c5 100644
--- a/quic/test_tools/quic_connection_peer.h
+++ b/quic/test_tools/quic_connection_peer.h
@@ -124,6 +124,7 @@
                                   PacketHeaderFormat format);
   static void AddBytesReceived(QuicConnection* connection, size_t length);
   static void SetAddressValidated(QuicConnection* connection);
+  static void SetEnableAeadLimits(QuicConnection* connection, bool enabled);
 
   static void SendConnectionClosePacket(QuicConnection* connection,
                                         QuicErrorCode error,
diff --git a/quic/test_tools/quic_test_utils.h b/quic/test_tools/quic_test_utils.h
index a55f9a8..a46a11b 100644
--- a/quic/test_tools/quic_test_utils.h
+++ b/quic/test_tools/quic_test_utils.h
@@ -1859,6 +1859,10 @@
     return plaintext_size + kTagSize;
   }
 
+  QuicPacketCount GetConfidentialityLimit() const override {
+    return std::numeric_limits<QuicPacketCount>::max();
+  }
+
   absl::string_view GetKey() const override { return absl::string_view(); }
 
   absl::string_view GetNoncePrefix() const override {