diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb index 5c372b3384..97ac584f73 100644 --- a/activestorage/app/controllers/active_storage/disk_controller.rb +++ b/activestorage/app/controllers/active_storage/disk_controller.rb @@ -42,11 +42,13 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController end def decode_verified_key - ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key) + key = ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key) + key&.deep_symbolize_keys end def decode_verified_token - ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token) + token = ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token) + token&.deep_symbolize_keys end def acceptable_content?(token) diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index b4268cfc6e..49cb576028 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -72,7 +72,7 @@ module ActiveSupport # # === Alternative serializers # - # By default MessageVerifier uses Marshal to serialize the message. If you want to use + # By default MessageVerifier uses JSON to serialize the message. If you want to use # another serialization method, you can set the serializer in the options # hash upon initialization: # @@ -115,11 +115,20 @@ module ActiveSupport SEPARATOR = "--" # :nodoc: SEPARATOR_LENGTH = SEPARATOR.length # :nodoc: + cattr_accessor :default_message_verifier_serializer, instance_accessor: false, default: :marshal + def initialize(secret, digest: nil, serializer: nil) raise ArgumentError, "Secret should not be nil." unless secret @secret = secret @digest = digest&.to_s || "SHA1" - @serializer = serializer || Marshal + @serializer = serializer || + if @@default_message_verifier_serializer.equal?(:marshal) + Marshal + elsif @@default_message_verifier_serializer.equal?(:hybrid) + JsonWithMarshalFallback + elsif @@default_message_verifier_serializer.equal?(:json) + JSON + end end # Checks if a signed message could have been generated by signing an object diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index 7b5819ffb9..4915bfbe11 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -167,6 +167,15 @@ module ActiveSupport end end + initializer "active_support.set_default_message_verifier_serializer" do |app| + config.after_initialize do + unless app.config.active_support.default_message_verifier_serializer.nil? + ActiveSupport::MessageVerifier.default_message_verifier_serializer = + app.config.active_support.default_message_verifier_serializer + end + end + end + initializer "active_support.set_marshal_serialization" do |app| config.after_initialize do unless app.config.active_support.use_marshal_serialization.nil? diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index adab0369dd..9a0650cdb2 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -19,7 +19,7 @@ class MessageVerifierTest < ActiveSupport::TestCase def setup @verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!") - @data = { some: "data", now: Time.utc(2010) } + @data = { "some" => "data", "now" => Time.utc(2010) } @secret = SecureRandom.random_bytes(32) end @@ -70,25 +70,6 @@ class MessageVerifierTest < ActiveSupport::TestCase ActiveSupport.parse_json_times, Time.zone = previous end - def test_raise_error_when_argument_class_is_not_loaded - # To generate the valid message below: - # - # AutoloadClass = Struct.new(:foo) - # valid_message = @verifier.generate(foo: AutoloadClass.new('foo')) - # - valid_message = "BAh7BjoIZm9vbzonTWVzc2FnZVZlcmlmaWVyVGVzdDo6QXV0b2xvYWRDbGFzcwY6CUBmb29JIghmb28GOgZFVA==--f3ef39a5241c365083770566dc7a9eb5d6ace914" - exception = assert_raise(ArgumentError, NameError) do - @verifier.verified(valid_message) - end - assert_includes ["uninitialized constant MessageVerifierTest::AutoloadClass", - "undefined class/module MessageVerifierTest::AutoloadClass"], exception.message - exception = assert_raise(ArgumentError, NameError) do - @verifier.verify(valid_message) - end - assert_includes ["uninitialized constant MessageVerifierTest::AutoloadClass", - "undefined class/module MessageVerifierTest::AutoloadClass"], exception.message - end - def test_raise_error_when_secret_is_nil exception = assert_raise(ArgumentError) do ActiveSupport::MessageVerifier.new(nil) @@ -96,12 +77,6 @@ class MessageVerifierTest < ActiveSupport::TestCase assert_equal "Secret should not be nil.", exception.message end - def test_backward_compatibility_messages_signed_without_metadata - signed_message = "BAh7BzoJc29tZUkiCWRhdGEGOgZFVDoIbm93SXU6CVRpbWUNIIAbgAAAAAAHOgtvZmZzZXRpADoJem9uZUkiCFVUQwY7BkY=--d03c52c91dfe4ccc5159417c660461bcce005e96" - assert_equal @data, @verifier.verify(signed_message) - end - - def test_rotating_secret old_message = ActiveSupport::MessageVerifier.new("old", digest: "SHA1").generate("old") @@ -124,6 +99,35 @@ class MessageVerifierTest < ActiveSupport::TestCase assert_equal "older", verifier.verified(older_message) end + def test_rotations_with_metadata + old_message = ActiveSupport::MessageVerifier.new("old").generate("old", purpose: :rotation) + + verifier = ActiveSupport::MessageVerifier.new(@secret) + verifier.rotate "old" + + assert_equal "old", verifier.verified(old_message, purpose: :rotation) + end +end + +class DefaultMarshalSerializerMessageVerifierTest < MessageVerifierTest + def setup + @default_verifier = ActiveSupport::MessageVerifier.default_message_verifier_serializer + ActiveSupport::MessageVerifier.default_message_verifier_serializer = :marshal + + @verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!") + @data = { some: "data", now: Time.utc(2010) } + @secret = SecureRandom.random_bytes(32) + end + + def teardown + ActiveSupport::MessageVerifier.default_message_verifier_serializer = @default_verifier + end + + def test_backward_compatibility_messages_signed_without_metadata + signed_message = "BAh7BzoJc29tZUkiCWRhdGEGOgZFVDoIbm93SXU6CVRpbWUNIIAbgAAAAAAHOgtvZmZzZXRpADoJem9uZUkiCFVUQwY7BkY=--d03c52c91dfe4ccc5159417c660461bcce005e96" + assert_equal @data, @verifier.verify(signed_message) + end + def test_on_rotation_is_called_and_verified_returns_message older_message = ActiveSupport::MessageVerifier.new("older", digest: "SHA1").generate({ encoded: "message" }) @@ -138,13 +142,127 @@ class MessageVerifierTest < ActiveSupport::TestCase assert rotated end - def test_rotations_with_metadata - old_message = ActiveSupport::MessageVerifier.new("old").generate("old", purpose: :rotation) + def test_raise_error_when_argument_class_is_not_loaded + # To generate the valid message below: + # + # AutoloadClass = Struct.new(:foo) + # valid_message = @verifier.generate(foo: AutoloadClass.new('foo')) + # + valid_message = "BAh7BjoIZm9vbzonTWVzc2FnZVZlcmlmaWVyVGVzdDo6QXV0b2xvYWRDbGFzcwY6CUBmb29JIghmb28GOgZFVA==--f3ef39a5241c365083770566dc7a9eb5d6ace914" + exception = assert_raise(ArgumentError, NameError) do + @verifier.verified(valid_message) + end + assert_includes ["uninitialized constant MessageVerifierTest::AutoloadClass", + "undefined class/module MessageVerifierTest::AutoloadClass"], exception.message + exception = assert_raise(ArgumentError, NameError) do + @verifier.verify(valid_message) + end + assert_includes ["uninitialized constant MessageVerifierTest::AutoloadClass", + "undefined class/module MessageVerifierTest::AutoloadClass"], exception.message + end +end - verifier = ActiveSupport::MessageVerifier.new(@secret) - verifier.rotate "old" +class MarshalSerializeAndFallbackMessageVerifierTest < DefaultMarshalSerializerMessageVerifierTest + def setup + @default_verifier = ActiveSupport::MessageVerifier.default_message_verifier_serializer + @default_use_marshal = ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization + @default_fallback = ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization + ActiveSupport::MessageVerifier.default_message_verifier_serializer = :hybrid + ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization = true + ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization = true - assert_equal "old", verifier.verified(old_message, purpose: :rotation) + @verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!") + @data = { some: "data", now: Time.utc(2010) } + @secret = SecureRandom.random_bytes(32) + end + + def teardown + ActiveSupport::MessageVerifier.default_message_verifier_serializer = @default_verifier + ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization = @default_use_marshal + ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization = @default_fallback + end +end + +class JsonSerializeMarshalFallbackMessageVerifierTest < MessageVerifierTest + def setup + @default_verifier = ActiveSupport::MessageVerifier.default_message_verifier_serializer + @default_use_marshal = ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization + @default_fallback = ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization + ActiveSupport::MessageVerifier.default_message_verifier_serializer = :hybrid + ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization = false + ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization = true + + @verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!") + @data = { "some" => "data", "now" => Time.utc(2010) } + @secret = SecureRandom.random_bytes(32) + end + + def teardown + ActiveSupport::MessageVerifier.default_message_verifier_serializer = @default_verifier + ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization = @default_use_marshal + ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization = @default_fallback + end + + def test_on_rotation_is_called_and_verified_returns_message + older_message = ActiveSupport::MessageVerifier.new("older", digest: "SHA1").generate({ encoded: "message" }) + + verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512") + verifier.rotate "old", digest: "SHA256" + verifier.rotate "older", digest: "SHA1" + + rotated = false + message = verifier.verified(older_message, on_rotation: proc { rotated = true }) + + assert_equal({ "encoded" => "message" }, message) + assert rotated + end + + def test_backward_compatibility_messages_signed_marshal_serialized + marshal_serialized_signed_message = "BAh7B0kiCXNvbWUGOgZFVEkiCWRhdGEGOwBUSSIIbm93BjsAVEl1OglUaW1lDSCAG8AAAAAABjoJem9uZUkiCFVUQwY7AEY=--ae7480422168507f4a8aec6b1d68bfdfd5c6ef48" + assert_equal @data, @verifier.verify(marshal_serialized_signed_message) + end +end + +class JsonSerializeAndNoFallbackMessageVerifierTest < JsonSerializeMarshalFallbackMessageVerifierTest + def setup + @default_verifier = ActiveSupport::MessageVerifier.default_message_verifier_serializer + @default_use_marshal = ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization + @default_fallback = ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization + ActiveSupport::MessageVerifier.default_message_verifier_serializer = :hybrid + ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization = false + ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization = false + + @verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!") + @data = { "some" => "data", "now" => Time.utc(2010) } + @secret = SecureRandom.random_bytes(32) + end + + def teardown + ActiveSupport::MessageVerifier.default_message_verifier_serializer = @default_verifier + ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization = @default_use_marshal + ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization = @default_fallback + end + + def test_backward_compatibility_messages_signed_marshal_serialized + marshal_serialized_signed_message = "BAh7B0kiCXNvbWUGOgZFVEkiCWRhdGEGOwBUSSIIbm93BjsAVEl1OglUaW1lDSCAG8AAAAAABjoJem9uZUkiCFVUQwY7AEY=--ae7480422168507f4a8aec6b1d68bfdfd5c6ef48" + assert_raise(JSON::ParserError) do + @verifier.verify(marshal_serialized_signed_message) + end + end +end + +class DefaultJsonSerializerMessageVerifierTest < JsonSerializeAndNoFallbackMessageVerifierTest + def setup + @default_verifier = ActiveSupport::MessageVerifier.default_message_verifier_serializer + ActiveSupport::MessageVerifier.default_message_verifier_serializer = :json + + @verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!") + @data = { "some" => "data", "now" => Time.utc(2010) } + @secret = SecureRandom.random_bytes(32) + end + + def teardown + ActiveSupport::MessageVerifier.default_message_verifier_serializer = @default_verifier end end @@ -199,7 +317,22 @@ class MessageVerifierMetadataMarshalTest < MessageVerifierMetadataTest end end -class MessageVerifierMetadataJSONTest < MessageVerifierMetadataTest +class MessageVerifierMetadataJsonWithMarshalFallbackTest < MessageVerifierMetadataTest + private + def verifier_options + { serializer: ActiveSupport::JsonWithMarshalFallback } + end +end + +class MessageVerifierMetadataJsonTest < MessageVerifierMetadataTest + private + def verifier_options + { serializer: JSON } + end +end + + +class MessageVerifierMetadataCustomJSONTest < MessageVerifierMetadataTest private def verifier_options { serializer: MessageVerifierTest::JSONSerializer.new } diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 00e5cf7066..d3db6ba968 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -63,6 +63,7 @@ Below are the default values associated with each target version. In cases of co - [`config.action_dispatch.default_headers`](#config-action-dispatch-default-headers): `{ "X-Frame-Options" => "SAMEORIGIN", "X-XSS-Protection" => "0", "X-Content-Type-Options" => "nosniff", "X-Permitted-Cross-Domain-Policies" => "none", "Referrer-Policy" => "strict-origin-when-cross-origin" }` - [`config.add_autoload_paths_to_load_path`](#config-add-autoload-paths-to-load-path): `false` - [`config.active_support.default_message_encryptor_serializer`](#config-active-support-default-message-encryptor-serializer): `:json` +- [`config.active_support.default_message_verifier_serializer`](#config-active-support-default-message-verifier-serializer): `:json` #### Default Values for Target Version 7.0 @@ -1909,6 +1910,19 @@ Used to help migrate apps from `Marshal` to `JSON` as the default serializer for Defaults to `true`. +#### `config.active_support.default_message_verifier_serializer` + +Specifies what serializer the `MessageVerifier` class will use by default. + +Options are `:json`, `:hybrid`, and `:marshal`. `:hybrid` uses the `JsonWithMarshalFallback` class. + +The default value depends on the `config.load_defaults` target version: + +| Starting with version | The default value is | +| --------------------- | -------------------- | +| (original) | `:marshal` | +| 7.1 | `:json` | + ### Configuring Active Job `config.active_job` provides the following configuration options: diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 47941d4fa6..c50e6fd510 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -169,6 +169,85 @@ Alternatively, you could load defaults for 7.1 config.load_defaults 7.1 ``` +### New `ActiveSupport::MessageVerifier` default serializer + +As of Rails 7.1, the default serializer in use by the `MessageVerifier` is `JSON`. +This offers a more secure alternative to the current default serializer. + +The `MessageVerifier` offers the ability to migrate the default serializer from `Marshal` to `JSON`. + +If you would like to ignore this change in existing applications, set the following: `config.active_support.default_message_verifier_serializer = :marshal`. + +In order to roll out the new default when upgrading from `7.0` to `7.1`, there are three configuration variables to keep in mind. +``` +config.active_support.default_verifier_serializer +config.active_support.fallback_to_marshal_deserialization +config.active_support.use_marshal_serialization +``` + +`default_message_verifier_serializer` defaults to `:json` as of `7.1` but it offers both a `:hybrid` and `:marshal` option. + +In order to migrate an older deployment to `:json`, first ensure that the `default_message_verifier_serializer` is set to `:marshal`. +```ruby +# config/application.rb +config.load_defaults 7.0 +config.active_support.default_message_verifier_serializer = :marshal +``` + +Once this is deployed on all Rails processes, set `default_message_verifier_serializer` to `:hybrid` to begin using the +`ActiveSupport::JsonWithMarshalFallback` class as the serializer. The defaults for this class are to use `Marshal` +as the serializer and to allow the deserialisation of both `Marshal` and `JSON` serialized payloads. + +```ruby +config.load_defaults 7.0 +config.active_support.default_message_verifier_serializer = :hybrid +``` + +Once this is deployed on all Rails processes, set the following configuration options in order to stop the +`ActiveSupport::JsonWithMarshalFallback` class from using `Marshal` to serialize new payloads. + +```ruby +# config/application.rb +config.load_defaults 7.0 +config.active_support.default_message_verifier_serializer = :hybrid +config.active_support.use_marshal_serialization = false +``` + +Allow this configuration to run on all processes for a considerable amount of time. +`ActiveSupport::JsonWithMarshalFallback` logs the following each time the `Marshal` fallback +is used: +``` +JsonWithMarshalFallback: Marshal load fallback occurred. +``` + +Once those message stop appearing in your logs and you're confident that all `MessageVerifier` +payloads in transit are `JSON` serialized, the following configuration options will disable the +Marshal fallback in `ActiveSupport::JsonWithMarshalFallback`. + +```ruby +# config/application.rb +config.load_defaults 7.0 +config.active_support.default_message_verifier_serializer = :hybrid +config.active_support.use_marshal_serialization = false +config.active_support.fallback_to_marshal_deserialization = false +``` + +If all goes well, you should now be safe to migrate the Message Verifier from +`ActiveSupport::JsonWithMarshalFallback` to `ActiveSupport::JSON`. +To do so, simply swap the `:hybrid` serializer for the `:json` serializer. + +```ruby +# config/application.rb +config.load_defaults 7.0 +config.active_support.default_message_verifier_serializer = :json +``` + +Alternatively, you could load defaults for 7.1 +```ruby +# config/application.rb +config.load_defaults 7.1 +``` + Upgrading from Rails 6.1 to Rails 7.0 ------------------------------------- diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index ae64c59aed..701a3f1e2b 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -270,6 +270,7 @@ module Rails if respond_to?(:active_support) active_support.default_message_encryptor_serializer = :json + active_support.default_message_verifier_serializer = :json end else raise "Unknown version #{target_version.to_s.inspect}" diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index e8012cf3d5..457f64229a 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -3189,6 +3189,14 @@ module ApplicationTests assert_equal true, ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization end + test "ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization is true by default for upgraded apps" do + remove_from_config '.*config\.load_defaults.*\n' + + app "development" + + assert_equal true, ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization + end + test "ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization can be configured via config.active_support.fallback_to_marshal_deserialization" do remove_from_config '.*config\.load_defaults.*\n' @@ -3207,6 +3215,14 @@ module ApplicationTests assert_equal true, ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization end + test "ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization is true by default for upgraded apps" do + remove_from_config '.*config\.load_defaults.*\n' + + app "development" + + assert_equal true, ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization + end + test "ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization can be configured via config.active_support.use_marshal_serialization" do remove_from_config '.*config\.load_defaults.*\n' @@ -3246,6 +3262,32 @@ module ApplicationTests assert_equal :hybrid, ActiveSupport::MessageEncryptor.default_message_encryptor_serializer end + test "ActiveSupport::MessageVerifier.default_message_verifier_serializer is :json by default for new apps" do + app "development" + + assert_equal :json, ActiveSupport::MessageVerifier.default_message_verifier_serializer + end + + test "ActiveSupport::MessageVerifier.default_message_verifier_serializer is :marshal by default for upgraded apps" do + remove_from_config '.*config\.load_defaults.*\n' + + app "development" + + assert_equal :marshal, ActiveSupport::MessageVerifier.default_message_verifier_serializer + end + + test "ActiveSupport::MessageVerifier.default_message_verifier_serializer can be configured via config.active_support.default_message_verifier_serializer" do + remove_from_config '.*config\.load_defaults.*\n' + + app_file "config/initializers/default_message_verifier_serializer.rb", <<-RUBY + Rails.application.config.active_support.default_message_verifier_serializer = :hybrid + RUBY + + app "development" + + assert_equal :hybrid, ActiveSupport::MessageVerifier.default_message_verifier_serializer + end + test "unknown_asset_fallback is false by default" do app "development" diff --git a/railties/test/application/middleware/cookies_test.rb b/railties/test/application/middleware/cookies_test.rb index 02b7cd7fc7..4b31b0f91a 100644 --- a/railties/test/application/middleware/cookies_test.rb +++ b/railties/test/application/middleware/cookies_test.rb @@ -51,7 +51,7 @@ module ApplicationTests assert_equal false, ActionDispatch::Cookies::CookieJar.always_write_cookie end - test "signed cookies with SHA512 digest and rotated out SHA256 and SHA1 digests" do + test "signed cookies with SHA512 digest and marshal serializer and rotated out SHA256 and SHA1 digests" do app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':controller(/:action)' @@ -88,8 +88,8 @@ module ApplicationTests sha256_secret = Rails.application.key_generator.generate_key("sha256") ::TestVerifiers = Class.new do - class_attribute :sha1, default: ActiveSupport::MessageVerifier.new(sha1_secret, digest: "SHA1") - class_attribute :sha256, default: ActiveSupport::MessageVerifier.new(sha256_secret, digest: "SHA256") + class_attribute :sha1, default: ActiveSupport::MessageVerifier.new(sha1_secret, digest: "SHA1", serializer: Marshal) + class_attribute :sha256, default: ActiveSupport::MessageVerifier.new(sha256_secret, digest: "SHA256", serializer: Marshal) end config.action_dispatch.signed_cookie_digest = "SHA512" @@ -104,7 +104,77 @@ module ApplicationTests require "#{app_path}/config/environment" - verifier_sha512 = ActiveSupport::MessageVerifier.new(app.key_generator.generate_key("sha512 salt"), digest: :SHA512) + verifier_sha512 = ActiveSupport::MessageVerifier.new(app.key_generator.generate_key("sha512 salt"), digest: :SHA512, serializer: Marshal) + + get "/foo/write_raw_cookie_sha1" + get "/foo/read_signed" + assert_equal "signed cookie".inspect, last_response.body + + get "/foo/read_raw_cookie" + assert_equal "signed cookie", verifier_sha512.verify(last_response.body, purpose: "cookie.signed_cookie") + + get "/foo/write_raw_cookie_sha256" + get "/foo/read_signed" + assert_equal "signed cookie".inspect, last_response.body + + get "/foo/read_raw_cookie" + assert_equal "signed cookie", verifier_sha512.verify(last_response.body, purpose: "cookie.signed_cookie") + end + + test "signed cookies with SHA512 digest and json serializer and rotated out SHA256 and SHA1 digests" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get ':controller(/:action)' + post ':controller(/:action)' + end + RUBY + + controller :foo, <<-RUBY + class FooController < ActionController::Base + protect_from_forgery with: :null_session + + def write_raw_cookie_sha1 + cookies[:signed_cookie] = TestVerifiers.sha1.generate("signed cookie") + head :ok + end + + def write_raw_cookie_sha256 + cookies[:signed_cookie] = TestVerifiers.sha256.generate("signed cookie") + head :ok + end + + def read_signed + render plain: cookies.signed[:signed_cookie].inspect + end + + def read_raw_cookie + render plain: cookies[:signed_cookie] + end + end + RUBY + + add_to_config <<-RUBY + sha1_secret = Rails.application.key_generator.generate_key("sha1") + sha256_secret = Rails.application.key_generator.generate_key("sha256") + + ::TestVerifiers = Class.new do + class_attribute :sha1, default: ActiveSupport::MessageVerifier.new(sha1_secret, digest: "SHA1", serializer: JSON) + class_attribute :sha256, default: ActiveSupport::MessageVerifier.new(sha256_secret, digest: "SHA256", serializer: JSON) + end + + config.action_dispatch.signed_cookie_digest = "SHA512" + config.action_dispatch.signed_cookie_salt = "sha512 salt" + config.action_dispatch.cookies_serializer = :json + + config.action_dispatch.cookies_rotations.tap do |cookies| + cookies.rotate :signed, sha1_secret, digest: "SHA1" + cookies.rotate :signed, sha256_secret, digest: "SHA256" + end + RUBY + + require "#{app_path}/config/environment" + + verifier_sha512 = ActiveSupport::MessageVerifier.new(app.key_generator.generate_key("sha512 salt"), digest: :SHA512, serializer: JSON) get "/foo/write_raw_cookie_sha1" get "/foo/read_signed"