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