Skip to content

Commit

Permalink
Configure serialization of metadata per MessageVerifier object
Browse files Browse the repository at this point in the history
  • Loading branch information
etiennebarrie authored and jonathanhefner committed Apr 15, 2023
1 parent 444df0e commit b3c3bb6
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 40 deletions.
51 changes: 39 additions & 12 deletions activesupport/lib/active_support/message_encryptor.rb
Expand Up @@ -123,21 +123,48 @@ class InvalidMessage < StandardError; end
# key by using ActiveSupport::KeyGenerator or a similar key
# derivation function.
#
# First additional parameter is used as the signature key for MessageVerifier.
# This allows you to specify keys to encrypt and sign data.
# The first additional parameter is used as the signature key for
# MessageVerifier. This allows you to specify keys to encrypt and sign
# data. Ignored when using an AEAD cipher like 'aes-256-gcm'.
#
# ActiveSupport::MessageEncryptor.new('secret', 'signature_secret')
#
# Options:
# * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
# <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-gcm'.
# * <tt>:digest</tt> - String of digest to use for signing. Default is
# +SHA1+. Ignored when using an AEAD cipher like 'aes-256-gcm'.
# * <tt>:serializer</tt> - Object serializer to use. Default is +JSON+.
# * <tt>:url_safe</tt> - Whether to encode messages using a URL-safe
# encoding. Default is +false+ for backward compatibility.
def initialize(secret, sign_secret = nil, cipher: nil, digest: nil, serializer: nil, url_safe: false)
super(serializer: serializer || @@default_message_encryptor_serializer, url_safe: url_safe)
# ==== Options
#
# [+:cipher+]
# Cipher to use. Can be any cipher returned by +OpenSSL::Cipher.ciphers+.
# Default is 'aes-256-gcm'.
#
# [+:digest+]
# Digest used for signing. Ignored when using an AEAD cipher like
# 'aes-256-gcm'.
#
# [+:serializer+]
# By default, MessageEncryptor uses JSON to serialize the message. If
# you want to use another serialization method, you can pass an object
# that responds to +load+ and +dump+, like +Marshal+ or +YAML+.
# +:marshal+, +:hybrid+, and +:json+ are also supported.
#
# [+:url_safe+]
# By default, MessageEncryptor generates RFC 4648 compliant strings
# which are not URL-safe. In other words, they can contain "+" and "/".
# If you want to generate URL-safe strings (in compliance with "Base 64
# Encoding with URL and Filename Safe Alphabet" in RFC 4648), you can
# pass +true+.
#
# [+:force_legacy_metadata_serializer+]
# Whether to use the legacy metadata serializer, which serializes the
# message first, then wraps it in an envelope which is also serialized. This
# was the default in \Rails 7.0 and below.
#
# If you don't pass a truthy value, the default is set using
# +config.active_support.use_message_serializer_for_metadata+.
def initialize(secret, sign_secret = nil, cipher: nil, digest: nil, serializer: nil, url_safe: false, force_legacy_metadata_serializer: false)
super(
serializer: serializer || @@default_message_encryptor_serializer,
url_safe: url_safe,
force_legacy_metadata_serializer: force_legacy_metadata_serializer,
)
@secret = secret
@cipher = cipher || self.class.default_cipher
@aead_mode = new_cipher.authenticated?
Expand Down
60 changes: 33 additions & 27 deletions activesupport/lib/active_support/message_verifier.rb
Expand Up @@ -70,20 +70,6 @@ module ActiveSupport
# Thereafter, the +verified+ method returns +nil+ while +verify+ raises
# <tt>ActiveSupport::MessageVerifier::InvalidSignature</tt>.
#
# === Alternative serializers
#
# 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:
#
# @verifier = ActiveSupport::MessageVerifier.new("secret", serializer: YAML)
#
# +MessageVerifier+ creates HMAC signatures using the SHA1 hash algorithm by default.
# If you want to use a different hash algorithm, you can change it by providing
# +:digest+ key as an option while initializing the verifier:
#
# @verifier = ActiveSupport::MessageVerifier.new("secret", digest: "SHA256")
#
# === Rotating keys
#
# MessageVerifier also supports rotating out old configurations by falling
Expand All @@ -107,17 +93,6 @@ module ActiveSupport
# Though the above would most likely be combined into one rotation:
#
# verifier.rotate(old_secret, digest: "SHA256", serializer: Marshal)
#
# === Generating URL-safe strings
#
# By default MessageVerifier generates RFC 4648 compliant strings which are
# not URL-safe. In other words, they can contain "+" and "/". If you want to
# generate URL-safe strings (in compliance with "Base 64 Encoding with URL and
# Filename Safe Alphabet" in RFC 4648), you can pass <tt>url_safe: true</tt>
# to the constructor:
#
# @verifier = ActiveSupport::MessageVerifier.new("secret", url_safe: true)
# @verifier.generate("signed message") #=> URL-safe string
class MessageVerifier < Messages::Codec
prepend Messages::Rotator

Expand All @@ -128,9 +103,40 @@ class InvalidSignature < StandardError; end

cattr_accessor :default_message_verifier_serializer, instance_accessor: false, default: :marshal

def initialize(secret, digest: nil, serializer: nil, url_safe: false)
# Initialize a new MessageVerifier with a secret for the signature.
#
# ==== Options
#
# [+:digest+]
# Digest used for signing. The default is +"SHA1"+. See +OpenSSL::Digest+
# for alternatives.
#
# [+:serializer+]
# By default, MessageVerifier uses JSON to serialize the message. If you want
# to use another serialization method, you can pass an object that responds
# to +load+ and +dump+, like +Marshal+ or +YAML+.
# +:marshal+, +:hybrid+, and +:json+ are also supported.
#
# [+:url_safe+]
# By default, MessageVerifier generates RFC 4648 compliant strings which are
# not URL-safe. In other words, they can contain "+" and "/". If you want to
# generate URL-safe strings (in compliance with "Base 64 Encoding with URL
# and Filename Safe Alphabet" in RFC 4648), you can pass +true+.
#
# [+:force_legacy_metadata_serializer+]
# Whether to use the legacy metadata serializer, which serializes the
# message first, then wraps it in an envelope which is also serialized. This
# was the default in \Rails 7.0 and below.
#
# If you don't pass a truthy value, the default is set using
# +config.active_support.use_message_serializer_for_metadata+.
def initialize(secret, digest: nil, serializer: nil, url_safe: false, force_legacy_metadata_serializer: false)
raise ArgumentError, "Secret should not be nil." unless secret
super(serializer: serializer || @@default_message_verifier_serializer, url_safe: url_safe)
super(
serializer: serializer || @@default_message_verifier_serializer,
url_safe: url_safe,
force_legacy_metadata_serializer: force_legacy_metadata_serializer,
)
@secret = secret
@digest = digest&.to_s || "SHA1"
end
Expand Down
7 changes: 6 additions & 1 deletion activesupport/lib/active_support/messages/codec.rb
Expand Up @@ -7,7 +7,7 @@ module Messages # :nodoc:
class Codec # :nodoc:
include Metadata

def initialize(serializer:, url_safe:)
def initialize(serializer:, url_safe:, force_legacy_metadata_serializer: false)
@serializer =
case serializer
when :marshal
Expand All @@ -21,6 +21,7 @@ def initialize(serializer:, url_safe:)
end

@url_safe = url_safe
@force_legacy_metadata_serializer = force_legacy_metadata_serializer
end

private
Expand Down Expand Up @@ -60,6 +61,10 @@ def catch_and_raise(throwable, as: nil, &block)
error = as.new(error.to_s) if as
raise error
end

def use_message_serializer_for_metadata?
!@force_legacy_metadata_serializer && super
end
end
end
end
15 changes: 15 additions & 0 deletions activesupport/test/messages/message_verifier_metadata_test.rb
Expand Up @@ -71,6 +71,21 @@ class MessageVerifierMetadataTest < ActiveSupport::TestCase
end
end

test "messages are readable by legacy versions when force_legacy_metadata_serializer is true" do
# Message generated by Rails 7.0 using:
#
# verifier = ActiveSupport::MessageVerifier.new("secret", serializer: JSON)
# legacy_message = verifier.generate("legacy", purpose: "test", expires_at: Time.utc(3000))
#
legacy_message = "eyJfcmFpbHMiOnsibWVzc2FnZSI6IklteGxaMkZqZVNJPSIsImV4cCI6IjMwMDAtMDEtMDFUMDA6MDA6MDAuMDAwWiIsInB1ciI6InRlc3QifX0=--81b11c317dba91cedd86ab79b7d7e68de8d290b3"

using_message_serializer_for_metadata(true) do
verifier = ActiveSupport::MessageVerifier.new("secret", serializer: JSON, force_legacy_metadata_serializer: true)

assert_equal legacy_message, verifier.generate("legacy", purpose: "test", expires_at: Time.utc(3000))
end
end

private
def make_codec(**options)
ActiveSupport::MessageVerifier.new("secret", **options)
Expand Down

0 comments on commit b3c3bb6

Please sign in to comment.