aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichael Coyne <mikeycgto@gmail.com>2017-09-23 17:18:01 -0400
committerMichael Coyne <mikeycgto@gmail.com>2017-09-24 12:23:38 -0400
commit8b0af54bbe5ab8b598e980013dd53a50d819b636 (patch)
tree05c883f46d687c0483db2313185420804e13c4c7
parent39f8ca64cec8667b66628e970211b4d18abbc373 (diff)
downloadrails-8b0af54bbe5ab8b598e980013dd53a50d819b636.tar.gz
rails-8b0af54bbe5ab8b598e980013dd53a50d819b636.tar.bz2
rails-8b0af54bbe5ab8b598e980013dd53a50d819b636.zip
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.
-rw-r--r--actionpack/CHANGELOG.md9
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb182
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb6
-rw-r--r--actionpack/test/controller/flash_test.rb4
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb4
-rw-r--r--actionpack/test/dispatch/cookies_test.rb289
-rw-r--r--actionpack/test/dispatch/routing_test.rb4
-rw-r--r--actionpack/test/dispatch/session/cookie_store_test.rb4
-rw-r--r--guides/source/configuring.md11
-rw-r--r--guides/source/security.md130
-rw-r--r--railties/lib/rails/application.rb1
11 files changed, 396 insertions, 248 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..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
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index 1c720ad82f..86c8364d83 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -487,6 +487,17 @@ Defaults to `'signed cookie'`.
authenticated encrypted cookie salt. Defaults to `'authenticated encrypted
cookie'`.
+* `config.action_dispatch.encrypted_cookie_cipher` sets the cipher to be
+ used for encrypted cookies. This defaults to `"aes-256-gcm"`.
+
+* `config.action_dispatch.signed_cookie_digest` sets the digest to be
+ used for signed cookies. This defaults to `"SHA1"`.
+
+* `config.action_dispatch.cookies_rotations` is set to an instance of
+ [RotationConfiguration](http://api.rubyonrails.org/classes/ActiveSupport/RotationConfiguration.html).
+ It provides an interface for rotating keys, salts, ciphers, and
+ digests for encrypted and signed cookies.
+
* `config.action_dispatch.perform_deep_munge` configures whether `deep_munge`
method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation)
for more information. It defaults to `true`.
diff --git a/guides/source/security.md b/guides/source/security.md
index 0b2d8de0fb..b0b71cad7d 100644
--- a/guides/source/security.md
+++ b/guides/source/security.md
@@ -85,37 +85,129 @@ This will also be a good idea, if you modify the structure of an object and old
* _Critical data should not be stored in session_. If the user clears their cookies or closes the browser, they will be lost. And with a client-side session storage, the user can read the data.
-### Session Storage
+### Encrypted Session Storage
NOTE: _Rails provides several storage mechanisms for the session hashes. The most important is `ActionDispatch::Session::CookieStore`._
-Rails 2 introduced a new default session storage, CookieStore. CookieStore saves the session hash directly in a cookie on the client-side. The server retrieves the session hash from the cookie and eliminates the need for a session ID. That will greatly increase the speed of the application, but it is a controversial storage option and you have to think about the security implications of it:
+The `CookieStore` saves the session hash directly in a cookie on the
+client-side. The server retrieves the session hash from the cookie and
+eliminates the need for a session ID. That will greatly increase the
+speed of the application, but it is a controversial storage option and
+you have to think about the security implications and storage
+limitations of it:
+
+* Cookies imply a strict size limit of 4kB. This is fine as you should
+ not store large amounts of data in a session anyway, as described
+ before. Storing the current user's database id in a session is common
+ practice.
+
+* Session cookies do not invalidate themselves and can be maliciously
+ reused. It may be a good idea to have your application invalidate old
+ session cookies using a stored timestamp.
+
+The `CookieStore` uses the
+[encrypted](http://api.rubyonrails.org/classes/ActionDispatch/Cookies/ChainedCookieJars.html#method-i-encrypted)
+cookie jar to provide a secure, encrypted location to store session
+data. Cookie-based sessions thus provide both integrity as well as
+confidentiality to their contents. The encryption key, as well as the
+verification key used for
+[signed](http://api.rubyonrails.org/classes/ActionDispatch/Cookies/ChainedCookieJars.html#method-i-signed)
+cookies, is derived from the `secret_key_base` configuration value.
+
+As of Rails 5.2 encrypted cookies and sessions are protected using AES
+GCM encryption. This form of encryption is a type of Authenticated
+Encryption and couples authentication and encryption in single step
+while also producing shorter ciphertexts as compared to other
+algorithms previously used. The key for cookies encrypted with AES GCM
+are derived using a salt value defined by the
+`config.action_dispatch.authenticated_encrypted_cookie_salt`
+configuration value.
+
+Prior to this version, encrypted cookies were secured using AES in CBC
+mode with HMAC using SHA1 for authentication. The keys for this type of
+encryption and for HMAC verification were derived via the salts defined
+by `config.action_dispatch.encrypted_cookie_salt` and
+`config.action_dispatch.encrypted_signed_cookie_salt` respectively.
+
+Prior to Rails version 4 in both versions 2 and 3, session cookies were
+protected using only HMAC verification. As such, these session cookies
+only provided integrity to their content because the actual session data
+was stored in plaintext encoded as base64. This is how `signed` cookies
+work in the current version of Rails. These kinds of cookies are still
+useful for protecting the integrity of certain client-stored data and
+information.
+
+__Do not use a trivial secret for the `secret_key_base`, i.e. a word
+from a dictionary, or one which is shorter than 30 characters! Instead
+use `rails secret` to generate secret keys!__
+
+It is also important to use different salt values for encrypted and
+signed cookies. Using the same value for different salt configuration
+values may lead to the same derived key being used for different
+security features which in turn may weaken the strength of the key.
-* Cookies imply a strict size limit of 4kB. This is fine as you should not store large amounts of data in a session anyway, as described before. _Storing the current user's database id in a session is usually ok_.
+In test and development applications get a `secret_key_base` derived from the app name. Other environments must use a random key present in `config/credentials.yml.enc`, shown here in its decrypted state:
-* The client can see everything you store in a session, because it is stored in clear-text (actually Base64-encoded, so not encrypted). So, of course, _you don't want to store any secrets here_. To prevent session hash tampering, a digest is calculated from the session with a server-side secret (`secrets.secret_token`) and inserted into the end of the cookie.
+ secret_key_base: 492f...
-In Rails 4, encrypted cookies through AES in CBC mode with HMAC using SHA1 for
-verification was introduced. This prevents the user from accessing and tampering
-the content of the cookie. Thus the session becomes a more secure place to store
-data. The encryption is performed using a server-side `secret_key_base`.
-Two salts are used when deriving keys for encryption and verification. These
-salts are set via the `config.action_dispatch.encrypted_cookie_salt` and
-`config.action_dispatch.encrypted_signed_cookie_salt` configuration values.
+If you have received an application where the secret was exposed (e.g. an application whose source was shared), strongly consider changing the secret.
-Rails 5.2 uses AES-GCM for the encryption which couples authentication
-and encryption in one faster step and produces shorter ciphertexts.
+### Rotating Keys for Encrypted and Signed Cookies
-Encrypted cookies are automatically upgraded if the
-`config.action_dispatch.use_authenticated_cookie_encryption` is enabled.
+It is possible to rotate the `secret_key_base` as well as the salts,
+ciphers, and digests used for both encrypted and signed cookies. Rotating
+the `secret_key_base` is necessary if the value was exposed or leaked.
+It is also useful to rotate this value for other more benign reasons,
+such as an employee leaving your organization or changing hosting
+environments.
-_Do not use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters! Instead use `rails secret` to generate secret keys!_
+Key rotations can be defined through the
+`config.action_dispatch.cookies_rotations` configuration value. This
+value is set to an instance of
+[RotationConfiguration](http://api.rubyonrails.org/classes/ActiveSupport/RotationConfiguration.html)
+which provides an interface for rotating signed and encrypted cookie
+keys, salts, digests, and ciphers.
-In test and development applications get a `secret_key_base` derived from the app name. Other environments must use a random key present in `config/credentials.yml.enc`, shown here in its decrypted state:
+For example, suppose we want to rotate out an old `secret_key_base`, we
+can define a signed and encrypted key rotation as follows:
- secret_key_base: 492f...
+```ruby
+config.action_dispatch.cookies_rotations.rotate :encrypted,
+ cipher: "aes-256-gcm",
+ secret: Rails.application.credentials.old_secret_key_base,
+ salt: config.action_dispatch.authenticated_encrypted_cookie_salt
-If you have received an application where the secret was exposed (e.g. an application whose source was shared), strongly consider changing the secret.
+config.action_dispatch.cookies_rotations.rotate :signed,
+ digest: "SHA1",
+ secret: Rails.application.credentials.old_secret_key_base,
+ salt: config.action_dispatch.signed_cookie_salt
+```
+
+Multiple rotations are possible by calling `rotate` multiple times. For
+example, suppose we want to use SHA512 for signed cookies while rotating
+out SHA256 and SHA1 digests using the same `secret_key_base`
+
+```ruby
+config.action_dispatch.signed_cookie_digest = "SHA512"
+
+config.action_dispatch.cookies_rotations.rotate :signed,
+ digest: "SHA256",
+ secret: Rails.application.credentials.secret_key_base,
+ salt: config.action_dispatch.signed_cookie_salt
+
+config.action_dispatch.cookies_rotations.rotate :signed,
+ digest: "SHA1",
+ secret: Rails.application.credentials.secret_key_base,
+ salt: config.action_dispatch.signed_cookie_salt
+```
+
+For more details on key rotation with encrypted and signed messages as
+well as the various options the `rotate` method accepts, please refer to
+the
+[MessageEncryptor API](api.rubyonrails.org/classes/ActiveSupport/MessageEncryptor.html)
+and
+[MessageVerifier API](api.rubyonrails.org/classes/ActiveSupport/MessageVerifier.html)
+documentation.
### Replay Attacks for CookieStore Sessions
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index f691156921..24f5eeae87 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -259,6 +259,7 @@ module Rails
"action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt,
"action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt,
"action_dispatch.authenticated_encrypted_cookie_salt" => config.action_dispatch.authenticated_encrypted_cookie_salt,
+ "action_dispatch.use_authenticated_cookie_encryption" => config.action_dispatch.use_authenticated_cookie_encryption,
"action_dispatch.encrypted_cookie_cipher" => config.action_dispatch.encrypted_cookie_cipher,
"action_dispatch.signed_cookie_digest" => config.action_dispatch.signed_cookie_digest,
"action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer,