From 8b0af54bbe5ab8b598e980013dd53a50d819b636 Mon Sep 17 00:00:00 2001
From: Michael Coyne <mikeycgto@gmail.com>
Date: Sat, 23 Sep 2017 17:18:01 -0400
Subject: Add key rotation cookies middleware

Using the action_dispatch.cookies_rotations interface, key rotation is
now possible with cookies. Thus the secret_key_base as well as salts,
ciphers, and digests, can be rotated without expiring sessions.
---
 actionpack/CHANGELOG.md                            |   9 +
 .../lib/action_dispatch/middleware/cookies.rb      | 182 ++++++-------
 actionpack/lib/action_dispatch/railtie.rb          |   6 +-
 actionpack/test/controller/flash_test.rb           |   4 +-
 .../controller/request_forgery_protection_test.rb  |   4 +-
 actionpack/test/dispatch/cookies_test.rb           | 289 ++++++++++++---------
 actionpack/test/dispatch/routing_test.rb           |   4 +
 .../test/dispatch/session/cookie_store_test.rb     |   4 +
 8 files changed, 273 insertions(+), 229 deletions(-)

(limited to 'actionpack')

diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index a53d8efee1..1d4b27a0f9 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,12 @@
+*   Simplify cookies middleware with key rotation support
+
+    Use the `rotate` method for both `MessageEncryptor` and
+    `MessageVerifier` to add key rotation support for encrypted and
+    signed cookies. This also helps simplify support for legacy cookie
+    security.
+
+    *Michael J Coyne*
+
 *  Use Capybara registered `:puma` server config.
 
     The Capybara registered `:puma` server ensures the puma server is run in process so
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index 845df500d8..b3831649a8 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -49,6 +49,18 @@ module ActionDispatch
       get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT
     end
 
+    def use_authenticated_cookie_encryption
+      get_header Cookies::USE_AUTHENTICATED_COOKIE_ENCRYPTION
+    end
+
+    def encrypted_cookie_cipher
+      get_header Cookies::ENCRYPTED_COOKIE_CIPHER
+    end
+
+    def signed_cookie_digest
+      get_header Cookies::SIGNED_COOKIE_DIGEST
+    end
+
     def secret_token
       get_header Cookies::SECRET_TOKEN
     end
@@ -64,6 +76,11 @@ module ActionDispatch
     def cookies_digest
       get_header Cookies::COOKIES_DIGEST
     end
+
+    def cookies_rotations
+      get_header Cookies::COOKIES_ROTATIONS
+    end
+
     # :startdoc:
   end
 
@@ -157,10 +174,14 @@ module ActionDispatch
     ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
     ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
     AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze
+    USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption".freeze
+    ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher".freeze
+    SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest".freeze
     SECRET_TOKEN = "action_dispatch.secret_token".freeze
     SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
     COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
     COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
+    COOKIES_ROTATIONS = "action_dispatch.cookies_rotations".freeze
 
     # Cookies can typically store 4096 bytes.
     MAX_COOKIE_SIZE = 4096
@@ -201,12 +222,7 @@ module ActionDispatch
       #
       #   cookies.signed[:discount] # => 45
       def signed
-        @signed ||=
-          if upgrade_legacy_signed_cookies?
-            UpgradeLegacySignedCookieJar.new(self)
-          else
-            SignedCookieJar.new(self)
-          end
+        @signed ||= SignedKeyRotatingCookieJar.new(self)
       end
 
       # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
@@ -223,18 +239,11 @@ module ActionDispatch
       # Example:
       #
       #   cookies.encrypted[:discount] = 45
-      #   # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/
+      #   # => Set-Cookie: discount=DIQ7fw==--K3n//8vvnSbGq9dA--7Xh91HfLpwzbj1czhBiwOg==; path=/
       #
       #   cookies.encrypted[:discount] # => 45
       def encrypted
-        @encrypted ||=
-          if upgrade_legacy_signed_cookies?
-            UpgradeLegacyEncryptedCookieJar.new(self)
-          elsif upgrade_legacy_hmac_aes_cbc_cookies?
-            UpgradeLegacyHmacAesCbcCookieJar.new(self)
-          else
-            EncryptedCookieJar.new(self)
-          end
+        @encrypted ||= EncryptedKeyRotatingCookieJar.new(self)
       end
 
       # Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
@@ -256,33 +265,17 @@ module ActionDispatch
 
         def upgrade_legacy_hmac_aes_cbc_cookies?
           request.secret_key_base.present?                       &&
-            request.authenticated_encrypted_cookie_salt.present? &&
             request.encrypted_signed_cookie_salt.present?        &&
-            request.encrypted_cookie_salt.present?
+            request.encrypted_cookie_salt.present?               &&
+            request.use_authenticated_cookie_encryption
         end
-    end
-
-    # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream
-    # to the Message{Encryptor,Verifier} allows us to handle the
-    # (de)serialization step within the cookie jar, which gives us the
-    # opportunity to detect and migrate legacy cookies.
-    module VerifyAndUpgradeLegacySignedMessage # :nodoc:
-      def initialize(*args)
-        super
-        @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
-      end
 
-      def verify_and_upgrade_legacy_signed_message(name, signed_message)
-        deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value|
-          self[name] = { value: value }
+        def encrypted_cookie_cipher
+          request.encrypted_cookie_cipher || "aes-256-gcm"
         end
-      rescue ActiveSupport::MessageVerifier::InvalidSignature
-        nil
-      end
 
-      private
-        def parse(name, signed_message)
-          super || verify_and_upgrade_legacy_signed_message(name, signed_message)
+        def signed_cookie_digest
+          request.signed_cookie_digest || "SHA1"
         end
     end
 
@@ -524,6 +517,7 @@ module ActionDispatch
 
     module SerializedCookieJars # :nodoc:
       MARSHAL_SIGNATURE = "\x04\x08".freeze
+      SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
 
       protected
         def needs_migration?(value)
@@ -534,12 +528,16 @@ module ActionDispatch
           serializer.dump(value)
         end
 
-        def deserialize(name, value)
+        def deserialize(name)
+          rotate = false
+          value  = yield -> { rotate = true }
+
           if value
-            if needs_migration?(value)
-              Marshal.load(value).tap do |v|
-                self[name] = { value: v }
-              end
+            case
+            when needs_migration?(value)
+              self[name] = Marshal.load(value)
+            when rotate
+              self[name] = serializer.load(value)
             else
               serializer.load(value)
             end
@@ -561,24 +559,31 @@ module ActionDispatch
         def digest
           request.cookies_digest || "SHA1"
         end
-
-        def key_generator
-          request.key_generator
-        end
     end
 
-    class SignedCookieJar < AbstractCookieJar # :nodoc:
+    class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
       include SerializedCookieJars
 
       def initialize(parent_jar)
         super
-        secret = key_generator.generate_key(request.signed_cookie_salt)
-        @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
+
+        secret = request.key_generator.generate_key(request.signed_cookie_salt)
+        @verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER)
+
+        request.cookies_rotations.signed.each do |rotation_options|
+          @verifier.rotate serializer: SERIALIZER, **rotation_options
+        end
+
+        if upgrade_legacy_signed_cookies?
+          @verifier.rotate raw_key: request.secret_token, serializer: SERIALIZER
+        end
       end
 
       private
         def parse(name, signed_message)
-          deserialize name, @verifier.verified(signed_message)
+          deserialize(name) do |rotate|
+            @verifier.verified(signed_message, on_rotation: rotate)
+          end
         end
 
         def commit(options)
@@ -588,37 +593,38 @@ module ActionDispatch
         end
     end
 
-    # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if
-    # secrets.secret_token and secret_key_base are both set. It reads
-    # legacy cookies signed with the old dummy key generator and signs and
-    # re-saves them using the new key generator to provide a smooth upgrade path.
-    class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
-      include VerifyAndUpgradeLegacySignedMessage
-    end
-
-    class EncryptedCookieJar < AbstractCookieJar # :nodoc:
+    class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
       include SerializedCookieJars
 
       def initialize(parent_jar)
         super
 
-        if ActiveSupport::LegacyKeyGenerator === key_generator
-          raise "You didn't set secret_key_base, which is required for this cookie jar. " \
-            "Read the upgrade documentation to learn more about this new config option."
+        key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
+        secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len)
+        @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER)
+
+        request.cookies_rotations.encrypted.each do |rotation_options|
+          @encryptor.rotate serializer: SERIALIZER, **rotation_options
         end
 
-        cipher = "aes-256-gcm"
-        key_len = ActiveSupport::MessageEncryptor.key_len(cipher)
-        secret = key_generator.generate_key(request.authenticated_encrypted_cookie_salt || "")[0, key_len]
+        if upgrade_legacy_hmac_aes_cbc_cookies?
+          @encryptor.rotate \
+            key_generator: request.key_generator, salt: request.encrypted_cookie_salt, signed_salt: request.encrypted_signed_cookie_salt,
+            cipher: "aes-256-cbc", digest: digest, serializer: SERIALIZER
+        end
 
-        @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
+        if upgrade_legacy_signed_cookies?
+          @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, digest: digest, serializer: SERIALIZER)
+        end
       end
 
       private
         def parse(name, encrypted_message)
-          deserialize name, @encryptor.decrypt_and_verify(encrypted_message)
-        rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
-          nil
+          deserialize(name) do |rotate|
+            @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate)
+          end
+        rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
+          parse_legacy_signed_message(name, encrypted_message)
         end
 
         def commit(options)
@@ -626,39 +632,15 @@ module ActionDispatch
 
           raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
         end
-    end
 
-    # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
-    # instead of EncryptedCookieJar if secrets.secret_token and secret_key_base
-    # are both set. It reads legacy cookies signed with the old dummy key generator and
-    # encrypts and re-saves them using the new key generator to provide a smooth upgrade path.
-    class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
-      include VerifyAndUpgradeLegacySignedMessage
-    end
+        def parse_legacy_signed_message(name, legacy_signed_message)
+          if defined?(@legacy_verifier)
+            deserialize(name) do |rotate|
+              rotate.call
 
-    # UpgradeLegacyHmacAesCbcCookieJar is used by ActionDispatch::Session::CookieStore
-    # to upgrade cookies encrypted with AES-256-CBC with HMAC to AES-256-GCM
-    class UpgradeLegacyHmacAesCbcCookieJar < EncryptedCookieJar
-      def initialize(parent_jar)
-        super
-
-        secret = key_generator.generate_key(request.encrypted_cookie_salt || "")[0, ActiveSupport::MessageEncryptor.key_len]
-        sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || "")
-
-        @legacy_encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
-      end
-
-      def decrypt_and_verify_legacy_encrypted_message(name, signed_message)
-        deserialize(name, @legacy_encryptor.decrypt_and_verify(signed_message)).tap do |value|
-          self[name] = { value: value }
-        end
-      rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
-        nil
-      end
-
-      private
-        def parse(name, signed_message)
-          super || decrypt_and_verify_legacy_encrypted_message(name, signed_message)
+              @legacy_verifier.verified(legacy_signed_message)
+            end
+          end
         end
     end
 
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index 4743a7ce61..855f2ffa47 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 require "action_dispatch"
+require "active_support/messages/rotation_configuration"
 
 module ActionDispatch
   class Railtie < Rails::Railtie # :nodoc:
@@ -18,6 +19,7 @@ module ActionDispatch
     config.action_dispatch.signed_cookie_salt = "signed cookie"
     config.action_dispatch.encrypted_cookie_salt = "encrypted cookie"
     config.action_dispatch.encrypted_signed_cookie_salt = "signed encrypted cookie"
+    config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie"
     config.action_dispatch.use_authenticated_cookie_encryption = false
     config.action_dispatch.perform_deep_munge = true
 
@@ -27,6 +29,8 @@ module ActionDispatch
       "X-Content-Type-Options" => "nosniff"
     }
 
+    config.action_dispatch.cookies_rotations = ActiveSupport::Messages::RotationConfiguration.new
+
     config.eager_load_namespaces << ActionDispatch
 
     initializer "action_dispatch.configure" do |app|
@@ -39,8 +43,6 @@ module ActionDispatch
       ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses)
       ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates)
 
-      config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie" if config.action_dispatch.use_authenticated_cookie_encryption
-
       config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil?
       ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie
 
diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb
index d92ae0b817..34bc2c0caa 100644
--- a/actionpack/test/controller/flash_test.rb
+++ b/actionpack/test/controller/flash_test.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 require "abstract_unit"
-require "active_support/key_generator"
+require "active_support/messages/rotation_configuration"
 
 class FlashTest < ActionController::TestCase
   class TestController < ActionController::Base
@@ -243,6 +243,7 @@ end
 class FlashIntegrationTest < ActionDispatch::IntegrationTest
   SessionKey = "_myapp_session"
   Generator  = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33")
+  Rotations  = ActiveSupport::Messages::RotationConfiguration.new
 
   class TestController < ActionController::Base
     add_flash_types :bar
@@ -348,6 +349,7 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest
       args[0] ||= {}
       args[0][:env] ||= {}
       args[0][:env]["action_dispatch.key_generator"] ||= Generator
+      args[0][:env]["action_dispatch.cookies_rotations"] = Rotations
       super(path, *args)
     end
 
diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb
index 12ae95d602..eb3d2f34a8 100644
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -2,6 +2,7 @@
 
 require "abstract_unit"
 require "active_support/log_subscriber/test_helper"
+require "active_support/messages/rotation_configuration"
 
 # common controller actions
 module RequestForgeryProtectionActions
@@ -630,13 +631,14 @@ end
 
 class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
   class NullSessionDummyKeyGenerator
-    def generate_key(secret)
+    def generate_key(secret, length = nil)
       "03312270731a2ed0d11ed091c2338a06"
     end
   end
 
   def setup
     @request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.new
+    @request.env[ActionDispatch::Cookies::COOKIES_ROTATIONS] = ActiveSupport::Messages::RotationConfiguration.new
   end
 
   test "should allow to set signed cookies" do
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index cb225c0f62..706d0be9c2 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -3,7 +3,7 @@
 require "abstract_unit"
 require "openssl"
 require "active_support/key_generator"
-require "active_support/message_verifier"
+require "active_support/messages/rotation_configuration"
 
 class CookieJarTest < ActiveSupport::TestCase
   attr_reader :request
@@ -287,15 +287,25 @@ class CookiesTest < ActionController::TestCase
 
   tests TestController
 
-  SALT = "b3c631c314c0bbca50c1b2843150fe33"
+  SECRET_KEY_BASE = "b3c631c314c0bbca50c1b2843150fe33"
+  SIGNED_COOKIE_SALT = "signed cookie"
+  ENCRYPTED_COOKIE_SALT = "encrypted cookie"
+  ENCRYPTED_SIGNED_COOKIE_SALT = "sigend encrypted cookie"
+  AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "authenticated encrypted cookie"
 
   def setup
     super
 
-    @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SALT, iterations: 2)
+    @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SECRET_KEY_BASE, iterations: 2)
+    @request.env["action_dispatch.cookies_rotations"] = ActiveSupport::Messages::RotationConfiguration.new
 
-    @request.env["action_dispatch.signed_cookie_salt"] =
-      @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] = SALT
+    @request.env["action_dispatch.secret_key_base"] = SECRET_KEY_BASE
+    @request.env["action_dispatch.use_authenticated_cookie_encryption"] = true
+
+    @request.env["action_dispatch.signed_cookie_salt"] = SIGNED_COOKIE_SALT
+    @request.env["action_dispatch.encrypted_cookie_salt"] = ENCRYPTED_COOKIE_SALT
+    @request.env["action_dispatch.encrypted_signed_cookie_salt"] = ENCRYPTED_SIGNED_COOKIE_SALT
+    @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] = AUTHENTICATED_ENCRYPTED_COOKIE_SALT
 
     @request.host = "www.nextangle.com"
   end
@@ -430,28 +440,96 @@ class CookiesTest < ActionController::TestCase
     assert_equal 45, cookies.signed[:user_id]
 
     key_generator = @request.env["action_dispatch.key_generator"]
-    signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
-    secret = key_generator.generate_key(signed_cookie_salt)
+    secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
 
     verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: "SHA1")
     assert_equal verifier.generate(45), cookies[:user_id]
   end
 
   def test_signed_cookie_using_custom_digest
-    @request.env["action_dispatch.cookies_digest"] = "SHA256"
+    @request.env["action_dispatch.signed_cookie_digest"] = "SHA256"
+
     get :set_signed_cookie
     cookies = @controller.send :cookies
     assert_not_equal 45, cookies[:user_id]
     assert_equal 45, cookies.signed[:user_id]
 
     key_generator = @request.env["action_dispatch.key_generator"]
-    signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
-    secret = key_generator.generate_key(signed_cookie_salt)
+    secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
 
     verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: "SHA256")
     assert_equal verifier.generate(45), cookies[:user_id]
   end
 
+  def test_signed_cookie_rotations_with_secret_key_base_and_digest
+    rotated_secret_key_base = "b3c631c314c0bbca50c1b2843150fe33"
+    rotated_salt = "signed cookie"
+
+    @request.env["action_dispatch.signed_cookie_digest"] = "SHA256"
+    @request.env["action_dispatch.cookies_rotations"].rotate :signed,
+      secret: rotated_secret_key_base, salt: rotated_salt, digest: "SHA1"
+
+    old_secret = ActiveSupport::KeyGenerator.new(rotated_secret_key_base, iterations: 1000).generate_key(rotated_salt)
+    old_message = ActiveSupport::MessageVerifier.new(old_secret, digest: "SHA1", serializer: Marshal).generate(45)
+
+    @request.headers["Cookie"] = "user_id=#{old_message}"
+
+    get :get_signed_cookie
+    assert_equal 45, @controller.send(:cookies).signed[:user_id]
+
+    key_generator = @request.env["action_dispatch.key_generator"]
+    secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+    verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA256", serializer: Marshal)
+    assert_equal 45, verifier.verify(@response.cookies["user_id"])
+  end
+
+  def test_signed_cookie_rotations_with_raw_key_and_digest
+    rotated_raw_key = "b3c631c314c0bbca50c1b2843150fe33"
+
+    @request.env["action_dispatch.signed_cookie_digest"] = "SHA256"
+    @request.env["action_dispatch.cookies_rotations"].rotate :signed,
+      raw_key: rotated_raw_key, digest: "SHA1"
+
+    old_message = ActiveSupport::MessageVerifier.new(rotated_raw_key, digest: "SHA1", serializer: Marshal).generate(45)
+
+    @request.headers["Cookie"] = "user_id=#{old_message}"
+
+    get :get_signed_cookie
+    assert_equal 45, @controller.send(:cookies).signed[:user_id]
+
+    key_generator = @request.env["action_dispatch.key_generator"]
+    secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+    verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA256", serializer: Marshal)
+    assert_equal 45, verifier.verify(@response.cookies["user_id"])
+  end
+
+  def test_signed_cookie_with_legacy_secret_scheme
+    @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+
+    old_message = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", digest: "SHA1", serializer: Marshal).generate(45)
+
+    @request.headers["Cookie"] = "user_id=#{old_message}"
+    get :get_signed_cookie
+    assert_equal 45, @controller.send(:cookies).signed[:user_id]
+
+    key_generator = @request.env["action_dispatch.key_generator"]
+    secret = key_generator.generate_key("signed cookie")
+    verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA1", serializer: Marshal)
+    assert_equal 45, verifier.verify(@response.cookies["user_id"])
+  end
+
+  def test_tampered_with_signed_cookie
+    key_generator = @request.env["action_dispatch.key_generator"]
+    secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+
+    verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: "SHA1")
+    message = verifier.generate(45)
+
+    @request.headers["Cookie"] = "user_id=#{Marshal.dump 45}--#{message.split("--").last}"
+    get :get_signed_cookie
+    assert_nil @controller.send(:cookies).signed[:user_id]
+  end
+
   def test_signed_cookie_using_default_serializer
     get :set_signed_cookie
     cookies = @controller.send :cookies
@@ -494,8 +572,7 @@ class CookiesTest < ActionController::TestCase
     @request.env["action_dispatch.cookies_serializer"] = :hybrid
 
     key_generator = @request.env["action_dispatch.key_generator"]
-    signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
-    secret = key_generator.generate_key(signed_cookie_salt)
+    secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
 
     marshal_value = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal).generate(45)
     @request.headers["Cookie"] = "user_id=#{marshal_value}"
@@ -514,8 +591,8 @@ class CookiesTest < ActionController::TestCase
     @request.env["action_dispatch.cookies_serializer"] = :hybrid
 
     key_generator = @request.env["action_dispatch.key_generator"]
-    signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
-    secret = key_generator.generate_key(signed_cookie_salt)
+    secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+
     json_value = ActiveSupport::MessageVerifier.new(secret, serializer: JSON).generate(45)
     @request.headers["Cookie"] = "user_id=#{json_value}"
 
@@ -578,11 +655,10 @@ class CookiesTest < ActionController::TestCase
   def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json
     @request.env["action_dispatch.cookies_serializer"] = :hybrid
 
-    cipher = "aes-256-gcm"
-    salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
-    secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
-    encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal)
+    key_generator = @request.env["action_dispatch.key_generator"]
+    secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32)
 
+    encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal)
     marshal_value = encryptor.encrypt_and_sign("bar")
     @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape marshal_value}"
 
@@ -592,7 +668,7 @@ class CookiesTest < ActionController::TestCase
     assert_not_equal "bar", cookies[:foo]
     assert_equal "bar", cookies.encrypted[:foo]
 
-    json_encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON)
+    json_encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON)
     assert_not_nil @response.cookies["foo"]
     assert_equal "bar", json_encryptor.decrypt_and_verify(@response.cookies["foo"])
   end
@@ -600,11 +676,10 @@ class CookiesTest < ActionController::TestCase
   def test_encrypted_cookie_using_hybrid_serializer_can_read_from_json_dumped_value
     @request.env["action_dispatch.cookies_serializer"] = :hybrid
 
-    cipher = "aes-256-gcm"
-    salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
-    secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
-    encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON)
+    key_generator = @request.env["action_dispatch.key_generator"]
+    secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32)
 
+    encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON)
     json_value = encryptor.encrypt_and_sign("bar")
     @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape json_value}"
 
@@ -691,65 +766,8 @@ class CookiesTest < ActionController::TestCase
     }
   end
 
-  def test_signed_uses_signed_cookie_jar_if_only_secret_token_is_set
-    @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
-    @request.env["action_dispatch.secret_key_base"] = nil
-    get :set_signed_cookie
-    assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed
-  end
-
-  def test_signed_uses_signed_cookie_jar_if_only_secret_key_base_is_set
-    @request.env["action_dispatch.secret_token"] = nil
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
-    get :set_signed_cookie
-    assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed
-  end
-
-  def test_signed_uses_upgrade_legacy_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
-    @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
-    get :set_signed_cookie
-    assert_kind_of ActionDispatch::Cookies::UpgradeLegacySignedCookieJar, cookies.signed
-  end
-
-  def test_signed_or_encrypted_uses_signed_cookie_jar_if_only_secret_token_is_set
-    @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
-    @request.env["action_dispatch.secret_key_base"] = nil
-    get :get_encrypted_cookie
-    assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed_or_encrypted
-  end
-
-  def test_signed_or_encrypted_uses_encrypted_cookie_jar_if_only_secret_key_base_is_set
-    @request.env["action_dispatch.secret_token"] = nil
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
-    get :get_encrypted_cookie
-    assert_kind_of ActionDispatch::Cookies::EncryptedCookieJar, cookies.signed_or_encrypted
-  end
-
-  def test_signed_or_encrypted_uses_upgrade_legacy_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
-    @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
-    get :get_encrypted_cookie
-    assert_kind_of ActionDispatch::Cookies::UpgradeLegacyEncryptedCookieJar, cookies.signed_or_encrypted
-  end
-
-  def test_encrypted_uses_encrypted_cookie_jar_if_only_secret_key_base_is_set
-    @request.env["action_dispatch.secret_token"] = nil
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
-    get :get_encrypted_cookie
-    assert_kind_of ActionDispatch::Cookies::EncryptedCookieJar, cookies.encrypted
-  end
-
-  def test_encrypted_uses_upgrade_legacy_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
-    @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
-    get :get_encrypted_cookie
-    assert_kind_of ActionDispatch::Cookies::UpgradeLegacyEncryptedCookieJar, cookies.encrypted
-  end
-
   def test_legacy_signed_cookie_is_read_and_transparently_upgraded_by_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
     @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
 
     legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45)
 
@@ -766,9 +784,6 @@ class CookiesTest < ActionController::TestCase
 
   def test_legacy_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
     @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
-    @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
-    @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
 
     legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate("bar")
 
@@ -777,17 +792,14 @@ class CookiesTest < ActionController::TestCase
 
     assert_equal "bar", @controller.send(:cookies).encrypted[:foo]
 
-    cipher = "aes-256-gcm"
-    salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
-    secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
-    encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal)
+    secret = @request.env["action_dispatch.key_generator"].generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32)
+    encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal)
     assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
   end
 
   def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
     @request.env["action_dispatch.cookies_serializer"] = :json
     @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
 
     legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45)
 
@@ -805,7 +817,6 @@ class CookiesTest < ActionController::TestCase
   def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
     @request.env["action_dispatch.cookies_serializer"] = :json
     @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
 
     legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar")
 
@@ -824,7 +835,6 @@ class CookiesTest < ActionController::TestCase
   def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set
     @request.env["action_dispatch.cookies_serializer"] = :hybrid
     @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
 
     legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45)
 
@@ -842,7 +852,6 @@ class CookiesTest < ActionController::TestCase
   def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
     @request.env["action_dispatch.cookies_serializer"] = :hybrid
     @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
 
     legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar")
 
@@ -851,17 +860,15 @@ class CookiesTest < ActionController::TestCase
 
     assert_equal "bar", @controller.send(:cookies).encrypted[:foo]
 
-    cipher = "aes-256-gcm"
     salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
-    secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
-    encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON)
+    secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")]
+    encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON)
     assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
   end
 
   def test_legacy_marshal_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set
     @request.env["action_dispatch.cookies_serializer"] = :hybrid
     @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
 
     legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45)
 
@@ -878,6 +885,8 @@ class CookiesTest < ActionController::TestCase
 
   def test_legacy_marshal_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
     @request.env["action_dispatch.cookies_serializer"] = :hybrid
+
+    @request.env["action_dispatch.use_authenticated_cookie_encryption"] = true
     @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
     @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
 
@@ -888,16 +897,14 @@ class CookiesTest < ActionController::TestCase
 
     assert_equal "bar", @controller.send(:cookies).encrypted[:foo]
 
-    cipher = "aes-256-gcm"
     salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
-    secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
-    encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON)
+    secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")]
+    encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON)
     assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
   end
 
   def test_legacy_signed_cookie_is_treated_as_nil_by_signed_cookie_jar_if_tampered
     @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
 
     @request.headers["Cookie"] = "user_id=45"
     get :get_signed_cookie
@@ -908,7 +915,6 @@ class CookiesTest < ActionController::TestCase
 
   def test_legacy_signed_cookie_is_treated_as_nil_by_encrypted_cookie_jar_if_tampered
     @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
 
     @request.headers["Cookie"] = "foo=baz"
     get :get_encrypted_cookie
@@ -918,17 +924,12 @@ class CookiesTest < ActionController::TestCase
   end
 
   def test_legacy_hmac_aes_cbc_encrypted_marshal_cookie_is_upgraded_to_authenticated_encrypted_cookie
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
-
-    @request.env["action_dispatch.encrypted_cookie_salt"] =
-      @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT
-
     key_generator = @request.env["action_dispatch.key_generator"]
     encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
     encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
-    secret = key_generator.generate_key(encrypted_cookie_salt)
+    secret = key_generator.generate_key(encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-cbc"))
     sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
-    marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: Marshal).encrypt_and_sign("bar")
+    marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: Marshal).encrypt_and_sign("bar")
 
     @request.headers["Cookie"] = "foo=#{marshal_value}"
 
@@ -938,27 +939,22 @@ class CookiesTest < ActionController::TestCase
     assert_not_equal "bar", cookies[:foo]
     assert_equal "bar", cookies.encrypted[:foo]
 
-    aead_cipher = "aes-256-gcm"
     aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
-    aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len(aead_cipher)]
-    aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: aead_cipher, serializer: Marshal)
+    aead_secret = key_generator.generate_key(aead_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm"))
+    aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: "aes-256-gcm", serializer: Marshal)
 
     assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"])
   end
 
   def test_legacy_hmac_aes_cbc_encrypted_json_cookie_is_upgraded_to_authenticated_encrypted_cookie
     @request.env["action_dispatch.cookies_serializer"] = :json
-    @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
-
-    @request.env["action_dispatch.encrypted_cookie_salt"] =
-      @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT
 
     key_generator = @request.env["action_dispatch.key_generator"]
     encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
     encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
-    secret = key_generator.generate_key(encrypted_cookie_salt)
+    secret = key_generator.generate_key(encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-cbc"))
     sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
-    marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON).encrypt_and_sign("bar")
+    marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: JSON).encrypt_and_sign("bar")
 
     @request.headers["Cookie"] = "foo=#{marshal_value}"
 
@@ -968,19 +964,17 @@ class CookiesTest < ActionController::TestCase
     assert_not_equal "bar", cookies[:foo]
     assert_equal "bar", cookies.encrypted[:foo]
 
-    aead_cipher = "aes-256-gcm"
     aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
-    aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len(aead_cipher)]
-    aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: aead_cipher, serializer: JSON)
+    aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")]
+    aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: "aes-256-gcm", serializer: JSON)
 
     assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"])
   end
 
   def test_legacy_hmac_aes_cbc_encrypted_cookie_using_64_byte_key_is_upgraded_to_authenticated_encrypted_cookie
     @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
-
-    @request.env["action_dispatch.encrypted_cookie_salt"] =
-      @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT
+    @request.env["action_dispatch.encrypted_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
+    @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
 
     # Cookie generated with 64 bytes secret
     message = ["566d4e75536d686e633246564e6b493062557079626c566d51574d30515430394c53315665564a694e4563786555744f57537454576b396a5a31566a626e52525054303d2d2d34663234333330623130623261306163363562316266323335396164666364613564643134623131"].pack("H*")
@@ -991,15 +985,60 @@ class CookiesTest < ActionController::TestCase
     cookies = @controller.send :cookies
     assert_not_equal "bar", cookies[:foo]
     assert_equal "bar", cookies.encrypted[:foo]
-    cipher = "aes-256-gcm"
 
     salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"]
-    secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)]
-    encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal)
+    secret = @request.env["action_dispatch.key_generator"].generate_key(salt, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm"))
+    encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal)
 
     assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
   end
 
+  def test_encrypted_cookie_rotations_with_secret_and_salt
+    rotated_secret_key_base = "b3c631c314c0bbca50c1b2843150fe33"
+    rotated_salt = "authenticated encrypted cookie"
+
+    @request.env["action_dispatch.encrypted_cookie_cipher"] = "aes-256-gcm"
+    @request.env["action_dispatch.cookies_rotations"].rotate :encrypted,
+      secret: rotated_secret_key_base, salt: rotated_salt, cipher: "aes-256-gcm"
+
+    key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")
+
+    old_secret = ActiveSupport::KeyGenerator.new(rotated_secret_key_base, iterations: 1000).generate_key(rotated_salt, key_len)
+    old_message = ActiveSupport::MessageEncryptor.new(old_secret, cipher: "aes-256-gcm", serializer: Marshal).encrypt_and_sign("bar")
+
+    @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape old_message}"
+
+    get :get_encrypted_cookie
+    assert_equal "bar", @controller.send(:cookies).encrypted[:foo]
+
+    key_generator = @request.env["action_dispatch.key_generator"]
+    secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], key_len)
+    encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal)
+    assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
+  end
+
+  def test_encrypted_cookie_rotations_with_raw_key
+    raw_key = "b3c631c314c0bbca50c1b2843150fe33"
+
+    @request.env["action_dispatch.encrypted_cookie_cipher"] = "aes-256-gcm"
+    @request.env["action_dispatch.cookies_rotations"].rotate :encrypted,
+      raw_key: raw_key, cipher: "aes-256-gcm"
+
+    key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")
+
+    old_message = ActiveSupport::MessageEncryptor.new(raw_key, cipher: "aes-256-gcm", serializer: Marshal).encrypt_and_sign(45)
+
+    @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape old_message}"
+
+    get :get_encrypted_cookie
+    assert_equal 45, @controller.send(:cookies).encrypted[:foo]
+
+    key_generator = @request.env["action_dispatch.key_generator"]
+    secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], key_len)
+    encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal)
+    assert_equal 45, encryptor.decrypt_and_verify(@response.cookies["foo"])
+  end
+
   def test_cookie_with_all_domain_option
     get :set_cookie_with_domain
     assert_response :success
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index 446b65a9b9..44f902c163 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -3,6 +3,7 @@
 require "erb"
 require "abstract_unit"
 require "controller/fake_controllers"
+require "active_support/messages/rotation_configuration"
 
 class TestRoutingMapper < ActionDispatch::IntegrationTest
   SprocketsApp = lambda { |env|
@@ -4947,6 +4948,7 @@ end
 class FlashRedirectTest < ActionDispatch::IntegrationTest
   SessionKey = "_myapp_session"
   Generator  = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33")
+  Rotations  = ActiveSupport::Messages::RotationConfiguration.new
 
   class KeyGeneratorMiddleware
     def initialize(app)
@@ -4955,6 +4957,8 @@ class FlashRedirectTest < ActionDispatch::IntegrationTest
 
     def call(env)
       env["action_dispatch.key_generator"] ||= Generator
+      env["action_dispatch.cookies_rotations"] ||= Rotations
+
       @app.call(env)
     end
   end
diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb
index 6517cf4c99..cf51c47068 100644
--- a/actionpack/test/dispatch/session/cookie_store_test.rb
+++ b/actionpack/test/dispatch/session/cookie_store_test.rb
@@ -3,11 +3,13 @@
 require "abstract_unit"
 require "stringio"
 require "active_support/key_generator"
+require "active_support/messages/rotation_configuration"
 
 class CookieStoreTest < ActionDispatch::IntegrationTest
   SessionKey = "_myapp_session"
   SessionSecret = "b3c631c314c0bbca50c1b2843150fe33"
   Generator = ActiveSupport::LegacyKeyGenerator.new(SessionSecret)
+  Rotations = ActiveSupport::Messages::RotationConfiguration.new
 
   Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, digest: "SHA1")
   SignedBar = Verifier.generate(foo: "bar", session_id: SecureRandom.hex(16))
@@ -346,6 +348,8 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
       args[0] ||= {}
       args[0][:headers] ||= {}
       args[0][:headers]["action_dispatch.key_generator"] ||= Generator
+      args[0][:headers]["action_dispatch.cookies_rotations"] ||= Rotations
+
       super(path, *args)
     end
 
-- 
cgit v1.2.3