Add SimpleTicketCrypter implementation gfe-relnote: n/a (test tools only change) PiperOrigin-RevId: 310001350 Change-Id: I280cce083e50c4f4d17418a3a7cbd4329a91decb
diff --git a/quic/tools/simple_ticket_crypter.cc b/quic/tools/simple_ticket_crypter.cc new file mode 100644 index 0000000..3da114e --- /dev/null +++ b/quic/tools/simple_ticket_crypter.cc
@@ -0,0 +1,109 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/third_party/quiche/src/quic/tools/simple_ticket_crypter.h" + +#include "third_party/boringssl/src/include/openssl/aead.h" +#include "third_party/boringssl/src/include/openssl/rand.h" + +namespace quic { + +namespace { + +constexpr QuicTime::Delta kTicketKeyLifetime = + QuicTime::Delta::FromSeconds(60 * 60 * 24 * 7); + +// The format of an encrypted ticket is 1 byte for the key epoch, followed by +// 16 bytes of IV, followed by the output from the AES-GCM Seal operation. The +// seal operation has an overhead of 16 bytes for its auth tag. +constexpr size_t kEpochSize = 1; +constexpr size_t kIVSize = 16; +constexpr size_t kAuthTagSize = 16; + +// Offsets into the ciphertext to make message parsing easier. +constexpr size_t kIVOffset = kEpochSize; +constexpr size_t kMessageOffset = kIVOffset + kIVSize; + +} // namespace + +SimpleTicketCrypter::SimpleTicketCrypter(QuicClock* clock) : clock_(clock) { + RAND_bytes(&key_epoch_, 1); + current_key_ = NewKey(); +} + +SimpleTicketCrypter::~SimpleTicketCrypter() = default; + +size_t SimpleTicketCrypter::MaxOverhead() { + return kEpochSize + kIVSize + kAuthTagSize; +} + +std::vector<uint8_t> SimpleTicketCrypter::Encrypt( + quiche::QuicheStringPiece in) { + MaybeRotateKeys(); + std::vector<uint8_t> out(in.size() + MaxOverhead()); + out[0] = key_epoch_; + RAND_bytes(out.data() + kIVOffset, kIVSize); + size_t out_len; + const EVP_AEAD_CTX* ctx = current_key_->aead_ctx.get(); + if (!EVP_AEAD_CTX_seal(ctx, out.data() + kMessageOffset, &out_len, + out.size() - kMessageOffset, out.data() + kIVOffset, + kIVSize, reinterpret_cast<const uint8_t*>(in.data()), + in.size(), nullptr, 0)) { + return std::vector<uint8_t>(); + } + out.resize(out_len + kMessageOffset); + return out; +} + +std::vector<uint8_t> SimpleTicketCrypter::Decrypt( + quiche::QuicheStringPiece in) { + MaybeRotateKeys(); + if (in.size() < kMessageOffset) { + return std::vector<uint8_t>(); + } + const uint8_t* input = reinterpret_cast<const uint8_t*>(in.data()); + std::vector<uint8_t> out(in.size() - kMessageOffset); + size_t out_len; + const EVP_AEAD_CTX* ctx = current_key_->aead_ctx.get(); + if (input[0] != key_epoch_) { + if (input[0] == static_cast<uint8_t>(key_epoch_ - 1) && previous_key_) { + ctx = previous_key_->aead_ctx.get(); + } else { + return std::vector<uint8_t>(); + } + } + if (!EVP_AEAD_CTX_open(ctx, out.data(), &out_len, out.size(), + input + kIVOffset, kIVSize, input + kMessageOffset, + in.size() - kMessageOffset, nullptr, 0)) { + return std::vector<uint8_t>(); + } + out.resize(out_len); + return out; +} + +void SimpleTicketCrypter::Decrypt( + quiche::QuicheStringPiece in, + std::unique_ptr<quic::ProofSource::DecryptCallback> callback) { + callback->Run(Decrypt(in)); +} + +void SimpleTicketCrypter::MaybeRotateKeys() { + QuicTime now = clock_->ApproximateNow(); + if (current_key_->expiration < now) { + previous_key_ = std::move(current_key_); + current_key_ = NewKey(); + key_epoch_++; + } +} + +std::unique_ptr<SimpleTicketCrypter::Key> SimpleTicketCrypter::NewKey() { + auto key = std::make_unique<SimpleTicketCrypter::Key>(); + RAND_bytes(key->key, kKeySize); + EVP_AEAD_CTX_init(key->aead_ctx.get(), EVP_aead_aes_128_gcm(), key->key, + kKeySize, EVP_AEAD_DEFAULT_TAG_LENGTH, nullptr); + key->expiration = clock_->ApproximateNow() + kTicketKeyLifetime; + return key; +} + +} // namespace quic
diff --git a/quic/tools/simple_ticket_crypter.h b/quic/tools/simple_ticket_crypter.h new file mode 100644 index 0000000..4c6b9ce --- /dev/null +++ b/quic/tools/simple_ticket_crypter.h
@@ -0,0 +1,55 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef QUICHE_QUIC_TOOLS_SIMPLE_TICKET_CRYPTER_H_ +#define QUICHE_QUIC_TOOLS_SIMPLE_TICKET_CRYPTER_H_ + +#include "third_party/boringssl/src/include/openssl/aead.h" +#include "net/third_party/quiche/src/quic/core/crypto/proof_source.h" +#include "net/third_party/quiche/src/quic/core/quic_clock.h" +#include "net/third_party/quiche/src/quic/core/quic_time.h" + +namespace quic { + +// SimpleTicketCrypter implements the QUIC ProofSource::TicketCrypter interface. +// It generates a random key at startup and every 7 days it rotates the key, +// keeping track of the previous key used to facilitate decrypting older +// tickets. This implementation is not suitable for server setups where multiple +// servers need to share keys. +class QUIC_EXPORT SimpleTicketCrypter + : public quic::ProofSource::TicketCrypter { + public: + explicit SimpleTicketCrypter(QuicClock* clock); + ~SimpleTicketCrypter() override; + + size_t MaxOverhead() override; + std::vector<uint8_t> Encrypt(quiche::QuicheStringPiece in) override; + void Decrypt( + quiche::QuicheStringPiece in, + std::unique_ptr<quic::ProofSource::DecryptCallback> callback) override; + + private: + std::vector<uint8_t> Decrypt(quiche::QuicheStringPiece in); + + void MaybeRotateKeys(); + + static constexpr size_t kKeySize = 16; + + struct Key { + uint8_t key[kKeySize]; + bssl::ScopedEVP_AEAD_CTX aead_ctx; + QuicTime expiration = QuicTime::Zero(); + }; + + std::unique_ptr<Key> NewKey(); + + std::unique_ptr<Key> current_key_; + std::unique_ptr<Key> previous_key_; + uint8_t key_epoch_ = 0; + QuicClock* clock_; +}; + +} // namespace quic + +#endif // QUICHE_QUIC_TOOLS_SIMPLE_TICKET_CRYPTER_H_
diff --git a/quic/tools/simple_ticket_crypter_test.cc b/quic/tools/simple_ticket_crypter_test.cc new file mode 100644 index 0000000..e609dc0 --- /dev/null +++ b/quic/tools/simple_ticket_crypter_test.cc
@@ -0,0 +1,112 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/third_party/quiche/src/quic/tools/simple_ticket_crypter.h" + +#include "net/third_party/quiche/src/quic/platform/api/quic_test.h" +#include "net/third_party/quiche/src/quic/test_tools/mock_clock.h" + +namespace quic { +namespace test { + +namespace { + +constexpr QuicTime::Delta kOneDay = QuicTime::Delta::FromSeconds(60 * 60 * 24); + +} // namespace + +class DecryptCallback : public quic::ProofSource::DecryptCallback { + public: + explicit DecryptCallback(std::vector<uint8_t>* out) : out_(out) {} + + void Run(std::vector<uint8_t> plaintext) override { *out_ = plaintext; } + + private: + std::vector<uint8_t>* out_; +}; + +quiche::QuicheStringPiece StringPiece(const std::vector<uint8_t>& in) { + return quiche::QuicheStringPiece(reinterpret_cast<const char*>(in.data()), + in.size()); +} + +class SimpleTicketCrypterTest : public QuicTest { + public: + SimpleTicketCrypterTest() : ticket_crypter_(&mock_clock_) {} + + protected: + MockClock mock_clock_; + SimpleTicketCrypter ticket_crypter_; +}; + +TEST_F(SimpleTicketCrypterTest, EncryptDecrypt) { + std::vector<uint8_t> plaintext = {1, 2, 3, 4, 5}; + std::vector<uint8_t> ciphertext = + ticket_crypter_.Encrypt(StringPiece(plaintext)); + EXPECT_NE(plaintext, ciphertext); + + std::vector<uint8_t> out_plaintext; + ticket_crypter_.Decrypt(StringPiece(ciphertext), + std::make_unique<DecryptCallback>(&out_plaintext)); + EXPECT_EQ(out_plaintext, plaintext); +} + +TEST_F(SimpleTicketCrypterTest, CiphertextsDiffer) { + std::vector<uint8_t> plaintext = {1, 2, 3, 4, 5}; + std::vector<uint8_t> ciphertext1 = + ticket_crypter_.Encrypt(StringPiece(plaintext)); + std::vector<uint8_t> ciphertext2 = + ticket_crypter_.Encrypt(StringPiece(plaintext)); + EXPECT_NE(ciphertext1, ciphertext2); +} + +TEST_F(SimpleTicketCrypterTest, DecryptionFailureWithModifiedCiphertext) { + std::vector<uint8_t> plaintext = {1, 2, 3, 4, 5}; + std::vector<uint8_t> ciphertext = + ticket_crypter_.Encrypt(StringPiece(plaintext)); + EXPECT_NE(plaintext, ciphertext); + + // Check that a bit flip in any byte will cause a decryption failure. + for (size_t i = 0; i < ciphertext.size(); i++) { + SCOPED_TRACE(i); + std::vector<uint8_t> munged_ciphertext = ciphertext; + munged_ciphertext[i] ^= 1; + std::vector<uint8_t> out_plaintext; + ticket_crypter_.Decrypt(StringPiece(munged_ciphertext), + std::make_unique<DecryptCallback>(&out_plaintext)); + EXPECT_TRUE(out_plaintext.empty()); + } +} + +TEST_F(SimpleTicketCrypterTest, DecryptionFailureWithEmptyCiphertext) { + std::vector<uint8_t> out_plaintext; + ticket_crypter_.Decrypt(quiche::QuicheStringPiece(), + std::make_unique<DecryptCallback>(&out_plaintext)); + EXPECT_TRUE(out_plaintext.empty()); +} + +TEST_F(SimpleTicketCrypterTest, KeyRotation) { + std::vector<uint8_t> plaintext = {1, 2, 3}; + std::vector<uint8_t> ciphertext = + ticket_crypter_.Encrypt(StringPiece(plaintext)); + EXPECT_FALSE(ciphertext.empty()); + + // Advance the clock 8 days, so the key used for |ciphertext| is now the + // previous key. Check that decryption still works. + mock_clock_.AdvanceTime(kOneDay * 8); + std::vector<uint8_t> out_plaintext; + ticket_crypter_.Decrypt(StringPiece(ciphertext), + std::make_unique<DecryptCallback>(&out_plaintext)); + EXPECT_EQ(out_plaintext, plaintext); + + // Advance the clock 8 more days. Now the original key should be expired and + // decryption should fail. + mock_clock_.AdvanceTime(kOneDay * 8); + ticket_crypter_.Decrypt(StringPiece(ciphertext), + std::make_unique<DecryptCallback>(&out_plaintext)); + EXPECT_TRUE(out_plaintext.empty()); +} + +} // namespace test +} // namespace quic