Add Rails.application.message_verifiers
Currently, `Rails.application.message_verifier(name)` returns a
`MessageVerifier` instance using a secret derived from the given `name`.
Instances created this way always generate messages using the default
options for `MessageVerifier.new`. Also, if there are older options to
rotate for message verification, each instance created this way must be
configured individually. In cases where Rails itself uses
`Rails.application.message_verifier` (e.g. in Active Storage),
discovering the name of the instance may require digging through
Rails' source code.
To alleviate these issues, this commit adds a `MessageVerifiers` factory
class, which acts as a central point for configuring and creating
`MessageVerifier` instances. Options passed to `MessageVerifiers#rotate`
will be applied to `MessageVerifier` instances that `MessageVerifiers#[]`
creates. So the following:
```ruby
foo_verifier = Rails.application.message_verifier(:foo)
foo_verifier.rotate(old_options)
bar_verifier = Rails.application.message_verifier(:bar)
bar_verifier.rotate(old_options)
```
Can be rewritten as:
```ruby
Rails.application.message_verifiers.rotate(old_options)
foo_verifier = Rails.application.message_verifiers[:foo]
bar_verifier = Rails.application.message_verifiers[:bar]
```
Additionally, `Rails.application.message_verifiers` supports a
`:secret_key_base` option, so old `secret_key_base` values can be
rotated:
```ruby
Rails.application.message_verifiers.rotate(secret_key_base: "old secret_key_base")
```
`MessageVerifiers` memoizes `MessageVerifier` instances. This also
allows `MessageVerifier` instances to be overridden entirely:
```ruby
Rails.application.message_verifiers[:foo] = ActiveSupport::MessageVerifier.new(other_secret)
```
For parity, this commit also adds a `MessageEncryptors` factory class,
which fulfills the same role for `MessageEncryptor` instances.
2022-01-15 20:07:13 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2022-09-26 03:50:24 +00:00
|
|
|
require_relative "abstract_unit"
|
Add Rails.application.message_verifiers
Currently, `Rails.application.message_verifier(name)` returns a
`MessageVerifier` instance using a secret derived from the given `name`.
Instances created this way always generate messages using the default
options for `MessageVerifier.new`. Also, if there are older options to
rotate for message verification, each instance created this way must be
configured individually. In cases where Rails itself uses
`Rails.application.message_verifier` (e.g. in Active Storage),
discovering the name of the instance may require digging through
Rails' source code.
To alleviate these issues, this commit adds a `MessageVerifiers` factory
class, which acts as a central point for configuring and creating
`MessageVerifier` instances. Options passed to `MessageVerifiers#rotate`
will be applied to `MessageVerifier` instances that `MessageVerifiers#[]`
creates. So the following:
```ruby
foo_verifier = Rails.application.message_verifier(:foo)
foo_verifier.rotate(old_options)
bar_verifier = Rails.application.message_verifier(:bar)
bar_verifier.rotate(old_options)
```
Can be rewritten as:
```ruby
Rails.application.message_verifiers.rotate(old_options)
foo_verifier = Rails.application.message_verifiers[:foo]
bar_verifier = Rails.application.message_verifiers[:bar]
```
Additionally, `Rails.application.message_verifiers` supports a
`:secret_key_base` option, so old `secret_key_base` values can be
rotated:
```ruby
Rails.application.message_verifiers.rotate(secret_key_base: "old secret_key_base")
```
`MessageVerifiers` memoizes `MessageVerifier` instances. This also
allows `MessageVerifier` instances to be overridden entirely:
```ruby
Rails.application.message_verifiers[:foo] = ActiveSupport::MessageVerifier.new(other_secret)
```
For parity, this commit also adds a `MessageEncryptors` factory class,
which fulfills the same role for `MessageEncryptor` instances.
2022-01-15 20:07:13 +00:00
|
|
|
require_relative "rotation_coordinator_tests"
|
|
|
|
|
|
|
|
class MessageEncryptorsTest < ActiveSupport::TestCase
|
|
|
|
include RotationCoordinatorTests
|
|
|
|
|
|
|
|
test "can override secret generator" do
|
|
|
|
secret_generator = ->(salt, secret_length:) { salt[0] * secret_length }
|
|
|
|
coordinator = make_coordinator.rotate(secret_generator: secret_generator)
|
|
|
|
|
|
|
|
assert_equal "message", roundtrip("message", coordinator["salt"])
|
|
|
|
assert_nil roundtrip("message", @coordinator["salt"], coordinator["salt"])
|
|
|
|
end
|
|
|
|
|
|
|
|
test "supports arbitrary secret generator kwargs" do
|
|
|
|
secret_generator = ->(salt, secret_length:, foo:, bar: nil) { foo[bar] * secret_length }
|
|
|
|
coordinator = ActiveSupport::MessageEncryptors.new(&secret_generator)
|
|
|
|
coordinator.rotate(foo: "foo", bar: 0)
|
|
|
|
|
|
|
|
assert_equal "message", roundtrip("message", coordinator["salt"])
|
|
|
|
end
|
|
|
|
|
Support Message{Encryptors,Verifiers}#rotate block
This commit adds a block form of `ActiveSupport::MessageEncryptors#rotate`
and `ActiveSupport::MessageVerifiers#rotate`, which supports
fine-grained per-salt rotation options. The block will receive a salt,
and should return an appropriate options Hash. The block may also
return `nil` to indicate that the rotation does not apply to the given
salt. For example:
```ruby
verifiers = ActiveSupport::MessageVerifiers.new { ... }
verifiers.rotate(serializer: JSON, url_safe: true)
verifiers.rotate do |salt|
case salt
when :foo
{ serializer: Marshal, url_safe: true }
when :bar
{ serializer: Marshal, url_safe: false }
end
end
# Uses `serializer: JSON, url_safe: true`.
# Falls back to `serializer: Marshal, url_safe: true`.
verifiers[:foo]
# Uses `serializer: JSON, url_safe: true`.
# Falls back to `serializer: Marshal, url_safe: false`.
verifiers[:bar]
# Uses `serializer: JSON, url_safe: true`.
verifiers[:baz]
```
This can be particularly useful when migrating older configurations to a
unified configuration.
2022-12-15 06:19:59 +00:00
|
|
|
test "supports arbitrary secret generator kwargs when using #rotate block" do
|
|
|
|
secret_generator = ->(salt, secret_length:, foo:, bar: nil) { foo[bar] * secret_length }
|
|
|
|
coordinator = ActiveSupport::MessageEncryptors.new(&secret_generator)
|
|
|
|
coordinator.rotate { { foo: "foo", bar: 0 } }
|
|
|
|
|
|
|
|
assert_equal "message", roundtrip("message", coordinator["salt"])
|
|
|
|
end
|
|
|
|
|
Add Rails.application.message_verifiers
Currently, `Rails.application.message_verifier(name)` returns a
`MessageVerifier` instance using a secret derived from the given `name`.
Instances created this way always generate messages using the default
options for `MessageVerifier.new`. Also, if there are older options to
rotate for message verification, each instance created this way must be
configured individually. In cases where Rails itself uses
`Rails.application.message_verifier` (e.g. in Active Storage),
discovering the name of the instance may require digging through
Rails' source code.
To alleviate these issues, this commit adds a `MessageVerifiers` factory
class, which acts as a central point for configuring and creating
`MessageVerifier` instances. Options passed to `MessageVerifiers#rotate`
will be applied to `MessageVerifier` instances that `MessageVerifiers#[]`
creates. So the following:
```ruby
foo_verifier = Rails.application.message_verifier(:foo)
foo_verifier.rotate(old_options)
bar_verifier = Rails.application.message_verifier(:bar)
bar_verifier.rotate(old_options)
```
Can be rewritten as:
```ruby
Rails.application.message_verifiers.rotate(old_options)
foo_verifier = Rails.application.message_verifiers[:foo]
bar_verifier = Rails.application.message_verifiers[:bar]
```
Additionally, `Rails.application.message_verifiers` supports a
`:secret_key_base` option, so old `secret_key_base` values can be
rotated:
```ruby
Rails.application.message_verifiers.rotate(secret_key_base: "old secret_key_base")
```
`MessageVerifiers` memoizes `MessageVerifier` instances. This also
allows `MessageVerifier` instances to be overridden entirely:
```ruby
Rails.application.message_verifiers[:foo] = ActiveSupport::MessageVerifier.new(other_secret)
```
For parity, this commit also adds a `MessageEncryptors` factory class,
which fulfills the same role for `MessageEncryptor` instances.
2022-01-15 20:07:13 +00:00
|
|
|
test "supports separate secrets for encryption and signing" do
|
|
|
|
secret_generator = proc { |*args, **kwargs| [SECRET_GENERATOR.call(*args, **kwargs), "signing secret"] }
|
|
|
|
coordinator = ActiveSupport::MessageEncryptors.new(&secret_generator)
|
|
|
|
coordinator.rotate_defaults
|
|
|
|
|
|
|
|
assert_equal "message", roundtrip("message", coordinator["salt"])
|
|
|
|
assert_nil roundtrip("message", @coordinator["salt"], coordinator["salt"])
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
SECRET_GENERATOR = proc { |salt, secret_length:| "".ljust(secret_length, salt) }
|
|
|
|
|
|
|
|
def make_coordinator
|
|
|
|
ActiveSupport::MessageEncryptors.new(&SECRET_GENERATOR)
|
|
|
|
end
|
|
|
|
|
|
|
|
def roundtrip(message, encryptor, decryptor = encryptor)
|
|
|
|
decryptor.decrypt_and_verify(encryptor.encrypt_and_sign(message))
|
Use throw for message error handling control flow
There are multiple points of failure when processing a message with
`MessageEncryptor` or `MessageVerifier`, and there several ways we might
want to handle those failures. For example, swallowing a failure with
`MessageVerifier#verified`, or raising a specific exception with
`MessageVerifier#verify`, or conditionally ignoring a failure when
rotations are configured.
Prior to this commit, the _internal_ logic of handling failures was
implemented using a mix of `nil` return values and raised exceptions.
This commit reimplements the internal logic using `throw` and a few
precisely targeted `rescue`s. This accomplishes several things:
* Allow rotation of serializers for `MessageVerifier`. Previously,
errors from a `MessageVerifier`'s initial serializer were never
rescued. Thus, the serializer could not be rotated:
```ruby
old_verifier = ActiveSupport::MessageVerifier.new("secret", serializer: Marshal)
new_verifier = ActiveSupport::MessageVerifier.new("secret", serializer: JSON)
new_verifier.rotate(serializer: Marshal)
message = old_verifier.generate("message")
new_verifier.verify(message)
# BEFORE:
# => raises JSON::ParserError
# AFTER:
# => "message"
```
* Allow rotation of serializers for `MessageEncryptor` when using a
non-standard initial serializer. Similar to `MessageVerifier`, the
serializer could not be rotated when the initial serializer raised an
error other than `TypeError` or `JSON::ParserError`, such as
`Psych::SyntaxError` or a custom error.
* Raise `MessageEncryptor::InvalidMessage` from `decrypt_and_verify`
regardless of cipher. Previously, when a `MessageEncryptor` was using
a non-AEAD cipher such as AES-256-CBC, a corrupt or tampered message
would raise `MessageVerifier::InvalidSignature` due to reliance on
`MessageVerifier` for verification. Now, the verification mechanism
is transparent to the user:
```ruby
encryptor = ActiveSupport::MessageEncryptor.new("x" * 32, cipher: "aes-256-gcm")
message = encryptor.encrypt_and_sign("message")
encryptor.decrypt_and_verify(message.next)
# => raises ActiveSupport::MessageEncryptor::InvalidMessage
encryptor = ActiveSupport::MessageEncryptor.new("x" * 32, cipher: "aes-256-cbc")
message = encryptor.encrypt_and_sign("message")
encryptor.decrypt_and_verify(message.next)
# BEFORE:
# => raises ActiveSupport::MessageVerifier::InvalidSignature
# AFTER:
# => raises ActiveSupport::MessageEncryptor::InvalidMessage
```
* Support `nil` original value when using `MessageVerifier#verify`.
Previously, `MessageVerifier#verify` did not work with `nil` original
values, though both `MessageVerifier#verified` and
`MessageEncryptor#decrypt_and_verify` do:
```ruby
encryptor = ActiveSupport::MessageEncryptor.new("x" * 32)
message = encryptor.encrypt_and_sign(nil)
encryptor.decrypt_and_verify(message)
# => nil
verifier = ActiveSupport::MessageVerifier.new("secret")
message = verifier.generate(nil)
verifier.verified(message)
# => nil
verifier.verify(message)
# BEFORE:
# => raises ActiveSupport::MessageVerifier::InvalidSignature
# AFTER:
# => nil
```
* Improve performance of verifying a message when it has expired and one
or more rotations have been configured:
```ruby
# frozen_string_literal: true
require "benchmark/ips"
require "active_support/all"
verifier = ActiveSupport::MessageVerifier.new("new secret")
verifier.rotate("old secret")
message = verifier.generate({ "data" => "x" * 100 }, expires_at: 1.day.ago)
Benchmark.ips do |x|
x.report("expired message") do
verifier.verified(message)
end
end
```
__Before__
```
Warming up --------------------------------------
expired message 1.442k i/100ms
Calculating -------------------------------------
expired message 14.403k (± 1.7%) i/s - 72.100k in 5.007382s
```
__After__
```
Warming up --------------------------------------
expired message 1.995k i/100ms
Calculating -------------------------------------
expired message 19.992k (± 2.0%) i/s - 101.745k in 5.091421s
```
Fixes #47185.
2023-02-07 19:56:59 +00:00
|
|
|
rescue ActiveSupport::MessageEncryptor::InvalidMessage
|
Add Rails.application.message_verifiers
Currently, `Rails.application.message_verifier(name)` returns a
`MessageVerifier` instance using a secret derived from the given `name`.
Instances created this way always generate messages using the default
options for `MessageVerifier.new`. Also, if there are older options to
rotate for message verification, each instance created this way must be
configured individually. In cases where Rails itself uses
`Rails.application.message_verifier` (e.g. in Active Storage),
discovering the name of the instance may require digging through
Rails' source code.
To alleviate these issues, this commit adds a `MessageVerifiers` factory
class, which acts as a central point for configuring and creating
`MessageVerifier` instances. Options passed to `MessageVerifiers#rotate`
will be applied to `MessageVerifier` instances that `MessageVerifiers#[]`
creates. So the following:
```ruby
foo_verifier = Rails.application.message_verifier(:foo)
foo_verifier.rotate(old_options)
bar_verifier = Rails.application.message_verifier(:bar)
bar_verifier.rotate(old_options)
```
Can be rewritten as:
```ruby
Rails.application.message_verifiers.rotate(old_options)
foo_verifier = Rails.application.message_verifiers[:foo]
bar_verifier = Rails.application.message_verifiers[:bar]
```
Additionally, `Rails.application.message_verifiers` supports a
`:secret_key_base` option, so old `secret_key_base` values can be
rotated:
```ruby
Rails.application.message_verifiers.rotate(secret_key_base: "old secret_key_base")
```
`MessageVerifiers` memoizes `MessageVerifier` instances. This also
allows `MessageVerifier` instances to be overridden entirely:
```ruby
Rails.application.message_verifiers[:foo] = ActiveSupport::MessageVerifier.new(other_secret)
```
For parity, this commit also adds a `MessageEncryptors` factory class,
which fulfills the same role for `MessageEncryptor` instances.
2022-01-15 20:07:13 +00:00
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|