diff options
Diffstat (limited to 'actionpack')
-rw-r--r-- | actionpack/CHANGELOG.md | 9 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/middleware/cookies.rb | 187 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/railtie.rb | 6 | ||||
-rw-r--r-- | actionpack/test/controller/flash_test.rb | 4 | ||||
-rw-r--r-- | actionpack/test/controller/request_forgery_protection_test.rb | 4 | ||||
-rw-r--r-- | actionpack/test/dispatch/cookies_test.rb | 240 | ||||
-rw-r--r-- | actionpack/test/dispatch/routing_test.rb | 4 | ||||
-rw-r--r-- | actionpack/test/dispatch/session/cookie_store_test.rb | 4 |
8 files changed, 227 insertions, 231 deletions
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..baffe200bc 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. @@ -255,34 +264,18 @@ module ActionDispatch end 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.secret_key_base.present? && + request.encrypted_signed_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 |*secrets, **options| + @verifier.rotate(*secrets, serializer: SERIALIZER, **options) + end + + if upgrade_legacy_signed_cookies? + @verifier.rotate 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,39 @@ 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 |*secrets, **options| + @encryptor.rotate(*secrets, serializer: SERIALIZER, **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? + secret = request.key_generator.generate_key(request.encrypted_cookie_salt) + sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt) - @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: ActiveSupport::MessageEncryptor::NullSerializer) + @encryptor.rotate secret, sign_secret, cipher: "aes-256-cbc", digest: digest, serializer: SERIALIZER + end + + 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 +633,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..70587fa2b0 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,72 @@ 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_rotating_secret_and_digest + secret = "b3c631c314c0bbca50c1b2843150fe33" + + @request.env["action_dispatch.signed_cookie_digest"] = "SHA256" + @request.env["action_dispatch.cookies_rotations"].rotate :signed, secret, digest: "SHA1" + + old_message = ActiveSupport::MessageVerifier.new(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_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 +548,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 +567,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 +631,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 +644,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 +652,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 +742,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 +760,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 +768,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 +793,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 +811,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 +828,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 +836,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 +861,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 +873,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 +891,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 +900,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 +915,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 +940,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 +961,35 @@ 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_rotating_secret + secret = "b3c631c314c0bbca50c1b2843150fe33" + + @request.env["action_dispatch.encrypted_cookie_cipher"] = "aes-256-gcm" + @request.env["action_dispatch.cookies_rotations"].rotate :encrypted, secret + + key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-gcm") + + old_message = ActiveSupport::MessageEncryptor.new(secret, 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 |