Close Quic connections on AEAD integrity limit of received packets that fail authentication. Protected by FLAGS_quic_reloadable_flag_quic_enable_aead_limits. PiperOrigin-RevId: 337912276 Change-Id: I1bf2ba93e693a0b3326d8e6a9122aa8346f094b9
diff --git a/quic/core/crypto/aes_base_decrypter.cc b/quic/core/crypto/aes_base_decrypter.cc index 5bd9c33..464e765 100644 --- a/quic/core/crypto/aes_base_decrypter.cc +++ b/quic/core/crypto/aes_base_decrypter.cc
@@ -36,4 +36,17 @@ return out; } +QuicPacketCount AesBaseDecrypter::GetIntegrityLimit() const { + // For AEAD_AES_128_GCM ... endpoints that do not attempt to remove + // protection from packets larger than 2^11 bytes can attempt to remove + // protection from at most 2^57 packets. + // For AEAD_AES_256_GCM [the limit] is substantially larger than the limit for + // AEAD_AES_128_GCM. However, this document recommends that the same limit be + // applied to both functions as either limit is acceptably large. + // https://quicwg.org/base-drafts/draft-ietf-quic-tls.html#name-integrity-limit + static_assert(kMaxIncomingPacketSize <= 2048, + "This key limit requires limits on decryption payload sizes"); + return 144115188075855872U; +} + } // namespace quic
diff --git a/quic/core/crypto/aes_base_decrypter.h b/quic/core/crypto/aes_base_decrypter.h index e3613e4..ced9810 100644 --- a/quic/core/crypto/aes_base_decrypter.h +++ b/quic/core/crypto/aes_base_decrypter.h
@@ -21,6 +21,7 @@ bool SetHeaderProtectionKey(absl::string_view key) override; std::string GenerateHeaderProtectionMask( QuicDataReader* sample_reader) override; + QuicPacketCount GetIntegrityLimit() const override; private: // The key used for packet number encryption.
diff --git a/quic/core/crypto/chacha20_poly1305_decrypter.cc b/quic/core/crypto/chacha20_poly1305_decrypter.cc index e2f55aa..b860348 100644 --- a/quic/core/crypto/chacha20_poly1305_decrypter.cc +++ b/quic/core/crypto/chacha20_poly1305_decrypter.cc
@@ -32,4 +32,12 @@ return TLS1_CK_CHACHA20_POLY1305_SHA256; } +QuicPacketCount ChaCha20Poly1305Decrypter::GetIntegrityLimit() const { + // For AEAD_CHACHA20_POLY1305, the integrity limit is 2^36 invalid packets. + // https://quicwg.org/base-drafts/draft-ietf-quic-tls.html#name-limits-on-aead-usage + static_assert(kMaxIncomingPacketSize < 16384, + "This key limit requires limits on decryption payload sizes"); + return 68719476736U; +} + } // namespace quic
diff --git a/quic/core/crypto/chacha20_poly1305_decrypter.h b/quic/core/crypto/chacha20_poly1305_decrypter.h index 50bb348..2e4b32b 100644 --- a/quic/core/crypto/chacha20_poly1305_decrypter.h +++ b/quic/core/crypto/chacha20_poly1305_decrypter.h
@@ -33,6 +33,7 @@ ~ChaCha20Poly1305Decrypter() override; uint32_t cipher_id() const override; + QuicPacketCount GetIntegrityLimit() const override; }; } // namespace quic
diff --git a/quic/core/crypto/chacha20_poly1305_tls_decrypter.cc b/quic/core/crypto/chacha20_poly1305_tls_decrypter.cc index 8d98da8..7bf8478 100644 --- a/quic/core/crypto/chacha20_poly1305_tls_decrypter.cc +++ b/quic/core/crypto/chacha20_poly1305_tls_decrypter.cc
@@ -34,4 +34,12 @@ return TLS1_CK_CHACHA20_POLY1305_SHA256; } +QuicPacketCount ChaCha20Poly1305TlsDecrypter::GetIntegrityLimit() const { + // For AEAD_CHACHA20_POLY1305, the integrity limit is 2^36 invalid packets. + // https://quicwg.org/base-drafts/draft-ietf-quic-tls.html#name-limits-on-aead-usage + static_assert(kMaxIncomingPacketSize < 16384, + "This key limit requires limits on decryption payload sizes"); + return 68719476736U; +} + } // namespace quic
diff --git a/quic/core/crypto/chacha20_poly1305_tls_decrypter.h b/quic/core/crypto/chacha20_poly1305_tls_decrypter.h index 702fb8c..04ee5b8 100644 --- a/quic/core/crypto/chacha20_poly1305_tls_decrypter.h +++ b/quic/core/crypto/chacha20_poly1305_tls_decrypter.h
@@ -31,6 +31,7 @@ ~ChaCha20Poly1305TlsDecrypter() override; uint32_t cipher_id() const override; + QuicPacketCount GetIntegrityLimit() const override; }; } // namespace quic
diff --git a/quic/core/crypto/null_decrypter.cc b/quic/core/crypto/null_decrypter.cc index b34e6cd..2fc1ace 100644 --- a/quic/core/crypto/null_decrypter.cc +++ b/quic/core/crypto/null_decrypter.cc
@@ -102,6 +102,10 @@ return 0; } +QuicPacketCount NullDecrypter::GetIntegrityLimit() const { + return std::numeric_limits<QuicPacketCount>::max(); +} + bool NullDecrypter::ReadHash(QuicDataReader* reader, QuicUint128* hash) { uint64_t lo; uint32_t hi;
diff --git a/quic/core/crypto/null_decrypter.h b/quic/core/crypto/null_decrypter.h index a760ab5..3c856f4 100644 --- a/quic/core/crypto/null_decrypter.h +++ b/quic/core/crypto/null_decrypter.h
@@ -50,6 +50,7 @@ absl::string_view GetNoncePrefix() const override; uint32_t cipher_id() const override; + QuicPacketCount GetIntegrityLimit() const override; private: bool ReadHash(QuicDataReader* reader, QuicUint128* hash);
diff --git a/quic/core/crypto/quic_decrypter.h b/quic/core/crypto/quic_decrypter.h index 21712f6..4a6cc59 100644 --- a/quic/core/crypto/quic_decrypter.h +++ b/quic/core/crypto/quic_decrypter.h
@@ -73,6 +73,10 @@ // selector'. virtual uint32_t cipher_id() const = 0; + // Returns the maximum number of packets that can safely fail decryption with + // this decrypter. + virtual QuicPacketCount GetIntegrityLimit() 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/quic_connection.cc b/quic/core/quic_connection.cc index a19eae0..fa02801 100644 --- a/quic/core/quic_connection.cc +++ b/quic/core/quic_connection.cc
@@ -267,6 +267,7 @@ support_key_update_for_connection_(false), enable_aead_limits_(GetQuicReloadableFlag(quic_enable_aead_limits) && version().UsesTls()), + num_failed_authentication_packets_received_(0), last_packet_decrypted_(false), last_size_(0), current_packet_data_(nullptr), @@ -2210,6 +2211,29 @@ debug_visitor_->OnUndecryptablePacket(decryption_level, /*dropped=*/!should_enqueue); } + + if (has_decryption_key) { + num_failed_authentication_packets_received_++; + if (enable_aead_limits_) { + // Should always be non-null if has_decryption_key is true. + DCHECK(framer_.GetDecrypter(decryption_level)); + const QuicPacketCount integrity_limit = + framer_.GetDecrypter(decryption_level)->GetIntegrityLimit(); + QUIC_DVLOG(2) << ENDPOINT << "Checking AEAD integrity limits:" + << " num_failed_authentication_packets_received_=" + << num_failed_authentication_packets_received_ + << " integrity_limit=" << integrity_limit; + if (num_failed_authentication_packets_received_ >= integrity_limit) { + const std::string error_details = quiche::QuicheStrCat( + "decrypter integrity limit reached:" + " num_failed_authentication_packets_received_=", + num_failed_authentication_packets_received_, + " integrity_limit=", integrity_limit); + CloseConnection(QUIC_AEAD_LIMIT_REACHED, error_details, + ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET); + } + } + } } bool QuicConnection::ShouldEnqueueUnDecryptablePacket(
diff --git a/quic/core/quic_connection.h b/quic/core/quic_connection.h index 73c6706..4be1b96 100644 --- a/quic/core/quic_connection.h +++ b/quic/core/quic_connection.h
@@ -1567,6 +1567,10 @@ // update (if allowed) and/or closing the connection, as necessary. bool enable_aead_limits_; + // Counts the number of undecryptable packets received across all keys. Does + // not include packets where a decryption key for that level was absent. + QuicPacketCount num_failed_authentication_packets_received_; + // 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 fd84b30..0ccfefe 100644 --- a/quic/core/quic_connection_test.cc +++ b/quic/core/quic_connection_test.cc
@@ -128,6 +128,20 @@ QuicPacketCount confidentiality_limit_; }; +class StrictTaggingDecrypterWithIntegrityLimit : public StrictTaggingDecrypter { + public: + StrictTaggingDecrypterWithIntegrityLimit(uint8_t tag, + QuicPacketCount integrity_limit) + : StrictTaggingDecrypter(tag), integrity_limit_(integrity_limit) {} + + QuicPacketCount GetIntegrityLimit() const override { + return integrity_limit_; + } + + private: + QuicPacketCount integrity_limit_; +}; + class TestConnectionHelper : public QuicConnectionHelperInterface { public: TestConnectionHelper(MockClock* clock, MockRandom* random_generator) @@ -12457,6 +12471,252 @@ TestConnectionCloseQuicErrorCode(QUIC_AEAD_LIMIT_REACHED); } +TEST_P(QuicConnectionTest, CloseConnectionOnIntegrityLimitDuringHandshake) { + if (!connection_.version().UsesTls()) { + return; + } + + QuicConnectionPeer::SetEnableAeadLimits(&connection_, true); + + constexpr uint8_t correct_tag = 0x01; + constexpr uint8_t wrong_tag = 0xFE; + constexpr QuicPacketCount kIntegrityLimit = 3; + + SetDecrypter(ENCRYPTION_HANDSHAKE, + std::make_unique<StrictTaggingDecrypterWithIntegrityLimit>( + correct_tag, kIntegrityLimit)); + connection_.SetEncrypter(ENCRYPTION_HANDSHAKE, + std::make_unique<TaggingEncrypter>(correct_tag)); + connection_.SetDefaultEncryptionLevel(ENCRYPTION_HANDSHAKE); + peer_framer_.SetEncrypter(ENCRYPTION_HANDSHAKE, + std::make_unique<TaggingEncrypter>(wrong_tag)); + for (uint64_t i = 1; i <= kIntegrityLimit; ++i) { + EXPECT_TRUE(connection_.connected()); + if (i == kIntegrityLimit) { + EXPECT_CALL(visitor_, OnConnectionClosed(_, _)); + EXPECT_CALL(visitor_, OnHandshakePacketSent()).Times(AnyNumber()); + } + ProcessDataPacketAtLevel(i, !kHasStopWaiting, ENCRYPTION_HANDSHAKE); + } + EXPECT_FALSE(connection_.connected()); + TestConnectionCloseQuicErrorCode(QUIC_AEAD_LIMIT_REACHED); +} + +TEST_P(QuicConnectionTest, CloseConnectionOnIntegrityLimitAfterHandshake) { + if (!connection_.version().UsesTls()) { + return; + } + + QuicConnectionPeer::SetEnableAeadLimits(&connection_, true); + + constexpr uint8_t correct_tag = 0x01; + constexpr uint8_t wrong_tag = 0xFE; + constexpr QuicPacketCount kIntegrityLimit = 3; + + use_tagging_decrypter(); + SetDecrypter(ENCRYPTION_FORWARD_SECURE, + std::make_unique<StrictTaggingDecrypterWithIntegrityLimit>( + correct_tag, kIntegrityLimit)); + connection_.SetEncrypter(ENCRYPTION_FORWARD_SECURE, + std::make_unique<TaggingEncrypter>(correct_tag)); + connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE); + EXPECT_CALL(visitor_, GetHandshakeState()) + .WillRepeatedly(Return(HANDSHAKE_CONFIRMED)); + connection_.OnHandshakeComplete(); + connection_.RemoveEncrypter(ENCRYPTION_INITIAL); + peer_framer_.SetEncrypter(ENCRYPTION_FORWARD_SECURE, + std::make_unique<TaggingEncrypter>(wrong_tag)); + for (uint64_t i = 1; i <= kIntegrityLimit; ++i) { + EXPECT_TRUE(connection_.connected()); + if (i == kIntegrityLimit) { + EXPECT_CALL(visitor_, OnConnectionClosed(_, _)); + } + ProcessDataPacketAtLevel(i, !kHasStopWaiting, ENCRYPTION_FORWARD_SECURE); + } + EXPECT_FALSE(connection_.connected()); + TestConnectionCloseQuicErrorCode(QUIC_AEAD_LIMIT_REACHED); +} + +TEST_P(QuicConnectionTest, + CloseConnectionOnIntegrityLimitAcrossEncryptionLevels) { + if (!connection_.version().UsesTls()) { + return; + } + + QuicConnectionPeer::SetEnableAeadLimits(&connection_, true); + + constexpr uint8_t correct_tag = 0x01; + constexpr uint8_t wrong_tag = 0xFE; + constexpr QuicPacketCount kIntegrityLimit = 4; + + use_tagging_decrypter(); + SetDecrypter(ENCRYPTION_HANDSHAKE, + std::make_unique<StrictTaggingDecrypterWithIntegrityLimit>( + correct_tag, kIntegrityLimit)); + connection_.SetEncrypter(ENCRYPTION_HANDSHAKE, + std::make_unique<TaggingEncrypter>(correct_tag)); + connection_.SetDefaultEncryptionLevel(ENCRYPTION_HANDSHAKE); + peer_framer_.SetEncrypter(ENCRYPTION_HANDSHAKE, + std::make_unique<TaggingEncrypter>(wrong_tag)); + for (uint64_t i = 1; i <= 2; ++i) { + EXPECT_TRUE(connection_.connected()); + ProcessDataPacketAtLevel(i, !kHasStopWaiting, ENCRYPTION_HANDSHAKE); + } + + SetDecrypter(ENCRYPTION_FORWARD_SECURE, + std::make_unique<StrictTaggingDecrypterWithIntegrityLimit>( + correct_tag, kIntegrityLimit)); + connection_.SetEncrypter(ENCRYPTION_FORWARD_SECURE, + std::make_unique<TaggingEncrypter>(correct_tag)); + connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE); + EXPECT_CALL(visitor_, GetHandshakeState()) + .WillRepeatedly(Return(HANDSHAKE_CONFIRMED)); + connection_.OnHandshakeComplete(); + connection_.RemoveEncrypter(ENCRYPTION_INITIAL); + connection_.RemoveEncrypter(ENCRYPTION_HANDSHAKE); + peer_framer_.SetEncrypter(ENCRYPTION_FORWARD_SECURE, + std::make_unique<TaggingEncrypter>(wrong_tag)); + for (uint64_t i = 3; i <= kIntegrityLimit; ++i) { + EXPECT_TRUE(connection_.connected()); + if (i == kIntegrityLimit) { + EXPECT_CALL(visitor_, OnConnectionClosed(_, _)); + } + ProcessDataPacketAtLevel(i, !kHasStopWaiting, ENCRYPTION_FORWARD_SECURE); + } + EXPECT_FALSE(connection_.connected()); + TestConnectionCloseQuicErrorCode(QUIC_AEAD_LIMIT_REACHED); +} + +TEST_P(QuicConnectionTest, IntegrityLimitDoesNotApplyWithoutDecryptionKey) { + if (!connection_.version().UsesTls()) { + return; + } + + QuicConnectionPeer::SetEnableAeadLimits(&connection_, true); + + constexpr uint8_t correct_tag = 0x01; + constexpr uint8_t wrong_tag = 0xFE; + constexpr QuicPacketCount kIntegrityLimit = 3; + + use_tagging_decrypter(); + SetDecrypter(ENCRYPTION_HANDSHAKE, + std::make_unique<StrictTaggingDecrypterWithIntegrityLimit>( + correct_tag, kIntegrityLimit)); + connection_.SetEncrypter(ENCRYPTION_HANDSHAKE, + std::make_unique<TaggingEncrypter>(correct_tag)); + connection_.SetDefaultEncryptionLevel(ENCRYPTION_HANDSHAKE); + + peer_framer_.SetEncrypter(ENCRYPTION_FORWARD_SECURE, + std::make_unique<TaggingEncrypter>(wrong_tag)); + for (uint64_t i = 1; i <= kIntegrityLimit * 2; ++i) { + EXPECT_TRUE(connection_.connected()); + ProcessDataPacketAtLevel(i, !kHasStopWaiting, ENCRYPTION_FORWARD_SECURE); + } + EXPECT_TRUE(connection_.connected()); +} + +TEST_P(QuicConnectionTest, CloseConnectionOnIntegrityLimitAcrossKeyPhases) { + if (!connection_.version().UsesTls()) { + return; + } + + constexpr QuicPacketCount kIntegrityLimit = 4; + + QuicConnectionPeer::SetEnableAeadLimits(&connection_, true); + TransportParameters params; + params.key_update_not_yet_supported = false; + QuicConfig config; + std::string error_details; + 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(); + connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE); + connection_.SetEncrypter(ENCRYPTION_FORWARD_SECURE, + std::make_unique<TaggingEncrypter>(0x01)); + SetDecrypter(ENCRYPTION_FORWARD_SECURE, + std::make_unique<StrictTaggingDecrypterWithIntegrityLimit>( + 0x01, kIntegrityLimit)); + EXPECT_CALL(visitor_, GetHandshakeState()) + .WillRepeatedly(Return(HANDSHAKE_CONFIRMED)); + connection_.OnHandshakeComplete(); + connection_.RemoveEncrypter(ENCRYPTION_INITIAL); + + peer_framer_.SetEncrypter(ENCRYPTION_FORWARD_SECURE, + std::make_unique<TaggingEncrypter>(0xFF)); + for (uint64_t i = 1; i <= 2; ++i) { + EXPECT_TRUE(connection_.connected()); + ProcessDataPacketAtLevel(i, !kHasStopWaiting, ENCRYPTION_FORWARD_SECURE); + } + + peer_framer_.SetEncrypter(ENCRYPTION_FORWARD_SECURE, + std::make_unique<TaggingEncrypter>(0x01)); + // Send packet 1. + QuicPacketNumber last_packet; + SendStreamDataToPeer(1, "foo", 0, NO_FIN, &last_packet); + EXPECT_EQ(QuicPacketNumber(1u), last_packet); + // Receive ack for packet 1. + EXPECT_CALL(*send_algorithm_, OnCongestionEvent(true, _, _, _, _)); + QuicAckFrame frame1 = InitAckFrame(1); + ProcessAckPacket(&frame1); + // Key update should now be allowed, initiate it. + EXPECT_CALL(visitor_, AdvanceKeysAndCreateCurrentOneRttDecrypter()) + .WillOnce([kIntegrityLimit]() { + return std::make_unique<StrictTaggingDecrypterWithIntegrityLimit>( + 0x02, kIntegrityLimit); + }); + EXPECT_CALL(visitor_, CreateCurrentOneRttEncrypter()).WillOnce([]() { + return std::make_unique<TaggingEncrypter>(0x02); + }); + EXPECT_CALL(visitor_, OnKeyUpdate(KeyUpdateReason::kLocalForTests)); + EXPECT_TRUE(connection_.InitiateKeyUpdate(KeyUpdateReason::kLocalForTests)); + + // Pretend that peer accepts the key update. + EXPECT_CALL(peer_framer_visitor_, + AdvanceKeysAndCreateCurrentOneRttDecrypter()) + .WillOnce( + []() { return std::make_unique<StrictTaggingDecrypter>(0x02); }); + EXPECT_CALL(peer_framer_visitor_, CreateCurrentOneRttEncrypter()) + .WillOnce([]() { return std::make_unique<TaggingEncrypter>(0x02); }); + peer_framer_.SetKeyUpdateSupportForConnection(true); + peer_framer_.DoKeyUpdate(KeyUpdateReason::kLocalForTests); + + // Send packet 2. + SendStreamDataToPeer(2, "bar", 0, NO_FIN, &last_packet); + EXPECT_EQ(QuicPacketNumber(2u), last_packet); + // Receive ack for packet 2. + EXPECT_CALL(*send_algorithm_, OnCongestionEvent(true, _, _, _, _)); + QuicAckFrame frame2 = InitAckFrame(2); + ProcessAckPacket(&frame2); + + // Do two more undecryptable packets. Integrity limit should be reached. + peer_framer_.SetEncrypter(ENCRYPTION_FORWARD_SECURE, + std::make_unique<TaggingEncrypter>(0xFF)); + for (uint64_t i = 3; i <= kIntegrityLimit; ++i) { + EXPECT_TRUE(connection_.connected()); + if (i == kIntegrityLimit) { + EXPECT_CALL(visitor_, OnConnectionClosed(_, _)); + } + ProcessDataPacketAtLevel(i, !kHasStopWaiting, ENCRYPTION_FORWARD_SECURE); + } + EXPECT_FALSE(connection_.connected()); + TestConnectionCloseQuicErrorCode(QUIC_AEAD_LIMIT_REACHED); +} + TEST_P(QuicConnectionTest, SendAckFrequencyFrame) { if (!version().HasIetfQuicFrames()) { return;
diff --git a/quic/core/quic_framer_test.cc b/quic/core/quic_framer_test.cc index 949d246..d9baee2 100644 --- a/quic/core/quic_framer_test.cc +++ b/quic/core/quic_framer_test.cc
@@ -183,6 +183,9 @@ } // Use a distinct value starting with 0xFFFFFF, which is never used by TLS. uint32_t cipher_id() const override { return 0xFFFFFFF2; } + QuicPacketCount GetIntegrityLimit() const override { + return std::numeric_limits<QuicPacketCount>::max(); + } QuicPacketNumber packet_number_; std::string associated_data_; std::string ciphertext_;
diff --git a/quic/test_tools/quic_test_utils.h b/quic/test_tools/quic_test_utils.h index cff9d1a..a5a774d 100644 --- a/quic/test_tools/quic_test_utils.h +++ b/quic/test_tools/quic_test_utils.h
@@ -1927,6 +1927,9 @@ } // Use a distinct value starting with 0xFFFFFF, which is never used by TLS. uint32_t cipher_id() const override { return 0xFFFFFFF0; } + QuicPacketCount GetIntegrityLimit() const override { + return std::numeric_limits<QuicPacketCount>::max(); + } protected: virtual uint8_t GetTag(absl::string_view ciphertext) {