From 39f8ca64cec8667b66628e970211b4d18abbc373 Mon Sep 17 00:00:00 2001 From: Michael Coyne Date: Sat, 23 Sep 2017 17:16:21 -0400 Subject: Add key rotation message Encryptor and Verifier Both classes now have a rotate method where new instances are added for each call. When decryption or verification fails the next rotation instance is tried. --- activesupport/CHANGELOG.md | 11 ++ .../lib/active_support/message_encryptor.rb | 32 +++++- .../lib/active_support/message_verifier.rb | 36 ++++++- .../messages/rotation_configuration.rb | 25 +++++ .../lib/active_support/messages/rotator.rb | 81 ++++++++++++++ activesupport/test/message_encryptor_test.rb | 118 +++++++++++++++++++++ activesupport/test/message_verifier_test.rb | 90 ++++++++++++++++ .../test/messages/rotation_configuration_test.rb | 43 ++++++++ 8 files changed, 432 insertions(+), 4 deletions(-) create mode 100644 activesupport/lib/active_support/messages/rotation_configuration.rb create mode 100644 activesupport/lib/active_support/messages/rotator.rb create mode 100644 activesupport/test/messages/rotation_configuration_test.rb (limited to 'activesupport') diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 56013c5f95..487984cbd3 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,14 @@ +* Add key rotation support to `MessageEncryptor` and `MessageVerifier` + + This change introduces a `rotate` method to both the `MessageEncryptor` and + `MessageVerifier` classes. This method accepts the same arguments and + options as the given classes' constructor. The `encrypt_and_verify` method + for `MessageEncryptor` and the `verified` method for `MessageVerifier` also + accept an optional keyword argument `:on_rotation` block which is called + when a rotated instance is used to decrypt or verify the message. + + *Michael J Coyne* + * Deprecate `Module#reachable?` method. *bogdanvlviv* diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 27620f56be..003fb4c354 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -54,7 +54,37 @@ module ActiveSupport # # Then the messages can be verified and returned upto the expire time. # Thereafter, verifying returns +nil+. + # + # === Rotating keys + # + # This class also defines a +rotate+ method which can be used to rotate out + # encryption keys no longer in use. + # + # This method is called with an options hash where a +:cipher+ option and + # either a +:raw_key+ or +:secret+ option must be defined. If +:raw_key+ is + # defined, it is used directly for the underlying encryption function. If + # the +:secret+ option is defined, a +:salt+ option must also be defined and + # a +KeyGenerator+ instance will be used to derive a key using +:salt+. When + # +:secret+ is used, a +:key_generator+ option may also be defined allowing + # for custom +KeyGenerator+ instances. If CBC encryption is used a + # `:raw_signed_key` or a `:signed_salt` option must also be defined. A + # +:digest+ may also be defined when using CBC encryption. This method can be + # called multiple times and new encryptor instances will be added to the + # rotation stack on each call. + # + # # Specifying the key used for encryption + # crypt.rotate raw_key: old_aead_key, cipher: "aes-256-gcm" + # crypt.rotate raw_key: old_cbc_key, raw_signed_key: old_cbc_sign_key, cipher: "aes-256-cbc", digest: "SHA1" + # + # # Using a KeyGenerator instance with a secret and salt(s) + # crypt.rotate secret: old_aead_secret, salt: old_aead_salt, cipher: "aes-256-gcm" + # crypt.rotate secret: old_cbc_secret, salt: old_cbc_salt, signed_salt: old_cbc_signed_salt, cipher: "aes-256-cbc", digest: "SHA1" + # + # # Specifying the key generator instance + # crypt.rotate key_generator: old_key_gen, salt: old_salt, cipher: "aes-256-gcm" class MessageEncryptor + prepend Messages::Rotator::Encryptor + class << self attr_accessor :use_authenticated_message_encryption #:nodoc: @@ -126,7 +156,7 @@ module ActiveSupport # Decrypt and verify a message. We need to verify the message in order to # avoid padding attacks. Reference: https://www.limited-entropy.com/padding-oracle-attacks/. - def decrypt_and_verify(data, purpose: nil) + def decrypt_and_verify(data, purpose: nil, **) _decrypt(verifier.verify(data), purpose) end diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index 7110d6d2c9..0be13f6f03 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -4,6 +4,7 @@ require "base64" require_relative "core_ext/object/blank" require_relative "security_utils" require_relative "messages/metadata" +require_relative "messages/rotator" module ActiveSupport # +MessageVerifier+ makes it easy to generate and verify messages which are @@ -73,7 +74,36 @@ module ActiveSupport # Then the messages can be verified and returned upto the expire time. # Thereafter, the +verified+ method returns +nil+ while +verify+ raises # ActiveSupport::MessageVerifier::InvalidSignature. + # + # === Rotating keys + # + # This class also defines a +rotate+ method which can be used to rotate out + # verification keys no longer in use. + # + # This method is called with an options hash where a +:digest+ option and + # either a +:raw_key+ or +:secret+ option must be defined. If +:raw_key+ is + # defined, it is used directly for the underlying HMAC function. If the + # +:secret+ option is defined, a +:salt+ option must also be defined and a + # +KeyGenerator+ instance will be used to derive a key using +:salt+. When + # +:secret+ is used, a +:key_generator+ option may also be defined allowing + # for custom +KeyGenerator+ instances. This method can be called multiple + # times and new verifier instances will be added to the rotation stack on + # each call. + # + # # Specifying the key used for verification + # @verifier.rotate raw_key: older_key, digest: "SHA1" + # + # # Specify the digest + # @verifier.rotate raw_key: old_key, digest: "SHA256" + # + # # Using a KeyGenerator instance with a secret and salt + # @verifier.rotate secret: old_secret, salt: old_salt, digest: "SHA1" + # + # # Specifying the key generator instance + # @verifier.rotate key_generator: old_key_gen, salt: old_salt, digest: "SHA256" class MessageVerifier + prepend Messages::Rotator::Verifier + class InvalidSignature < StandardError; end def initialize(secret, options = {}) @@ -120,7 +150,7 @@ module ActiveSupport # # incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff" # verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format - def verified(signed_message, purpose: nil) + def verified(signed_message, purpose: nil, **) if valid_message?(signed_message) begin data = signed_message.split("--".freeze)[0] @@ -145,8 +175,8 @@ module ActiveSupport # # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit' # other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature - def verify(signed_message, purpose: nil) - verified(signed_message, purpose: purpose) || raise(InvalidSignature) + def verify(*args) + verified(*args) || raise(InvalidSignature) end # Generates a signed message for the provided value. diff --git a/activesupport/lib/active_support/messages/rotation_configuration.rb b/activesupport/lib/active_support/messages/rotation_configuration.rb new file mode 100644 index 0000000000..12566bdb63 --- /dev/null +++ b/activesupport/lib/active_support/messages/rotation_configuration.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module ActiveSupport + module Messages + class RotationConfiguration + attr_reader :signed, :encrypted + + def initialize + @signed, @encrypted = [], [] + end + + def rotate(kind = nil, **options) + case kind + when :signed + @signed << options + when :encrypted + @encrypted << options + else + rotate :signed, options + rotate :encrypted, options + end + end + end + end +end diff --git a/activesupport/lib/active_support/messages/rotator.rb b/activesupport/lib/active_support/messages/rotator.rb new file mode 100644 index 0000000000..21ae643138 --- /dev/null +++ b/activesupport/lib/active_support/messages/rotator.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module ActiveSupport + module Messages + module Rotator # :nodoc: + def initialize(*args) + super + + @rotations = [] + end + + def rotate(*args) + @rotations << create_rotation(*args) + end + + module Encryptor + include Rotator + + def decrypt_and_verify(*args, on_rotation: nil, **options) + super + rescue MessageEncryptor::InvalidMessage, MessageVerifier::InvalidSignature + run_rotations(on_rotation) { |encryptor| encryptor.decrypt_and_verify(*args, options) } || raise + end + + private + def create_rotation(raw_key: nil, raw_signed_key: nil, **options) + self.class.new \ + raw_key || extract_key(options), + raw_signed_key || extract_signing_key(options), + options.slice(:cipher, :digest, :serializer) + end + + def extract_key(cipher:, salt:, key_generator: nil, secret: nil, **) + key_generator ||= key_generator_for(secret) + key_generator.generate_key(salt, self.class.key_len(cipher)) + end + + def extract_signing_key(cipher:, signed_salt: nil, key_generator: nil, secret: nil, **) + if cipher.downcase.end_with?("cbc") + raise ArgumentError, "missing signed_salt for signing key generation" unless signed_salt + + key_generator ||= key_generator_for(secret) + key_generator.generate_key(signed_salt) + end + end + end + + module Verifier + include Rotator + + def verified(*args, on_rotation: nil, **options) + super || run_rotations(on_rotation) { |verifier| verifier.verified(*args, options) } + end + + private + def create_rotation(raw_key: nil, digest: nil, serializer: nil, **options) + self.class.new(raw_key || extract_key(options), digest: digest, serializer: serializer) + end + + def extract_key(key_generator: nil, secret: nil, salt:) + key_generator ||= key_generator_for(secret) + key_generator.generate_key(salt) + end + end + + private + def key_generator_for(secret) + ActiveSupport::KeyGenerator.new(secret, iterations: 1000) + end + + def run_rotations(on_rotation) + @rotations.find do |rotation| + if message = yield(rotation) rescue next + on_rotation.call if on_rotation + return message + end + end + end + end + end +end diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb index 1fbe655642..17baf3550b 100644 --- a/activesupport/test/message_encryptor_test.rb +++ b/activesupport/test/message_encryptor_test.rb @@ -115,6 +115,124 @@ class MessageEncryptorTest < ActiveSupport::TestCase assert_equal "Ruby on Rails", encryptor.decrypt_and_verify(encrypted_message) end + def test_with_rotated_raw_key + old_raw_key = SecureRandom.random_bytes(32) + old_encryptor = ActiveSupport::MessageEncryptor.new(old_raw_key, cipher: "aes-256-gcm") + old_message = old_encryptor.encrypt_and_sign("message encrypted with old raw key") + + encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm") + encryptor.rotate raw_key: old_raw_key, cipher: "aes-256-gcm" + + assert_equal "message encrypted with old raw key", encryptor.decrypt_and_verify(old_message) + end + + def test_with_rotated_secret_and_salt + old_secret, old_salt = SecureRandom.random_bytes(32), "old salt" + old_raw_key = ActiveSupport::KeyGenerator.new(old_secret, iterations: 1000).generate_key(old_salt, 32) + + old_encryptor = ActiveSupport::MessageEncryptor.new(old_raw_key, cipher: "aes-256-gcm") + old_message = old_encryptor.encrypt_and_sign("message encrypted with old secret and salt") + + encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm") + encryptor.rotate secret: old_secret, salt: old_salt, cipher: "aes-256-gcm" + + assert_equal "message encrypted with old secret and salt", encryptor.decrypt_and_verify(old_message) + end + + def test_with_rotated_key_generator + old_key_gen, old_salt = ActiveSupport::KeyGenerator.new(SecureRandom.random_bytes(32), iterations: 256), "old salt" + + old_raw_key = old_key_gen.generate_key(old_salt, 32) + old_encryptor = ActiveSupport::MessageEncryptor.new(old_raw_key, cipher: "aes-256-gcm") + old_message = old_encryptor.encrypt_and_sign("message encrypted with old key generator and salt") + + encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm") + encryptor.rotate key_generator: old_key_gen, salt: old_salt, cipher: "aes-256-gcm" + + assert_equal "message encrypted with old key generator and salt", encryptor.decrypt_and_verify(old_message) + end + + def test_with_rotated_aes_cbc_encryptor_with_raw_keys + old_raw_key, old_raw_signed_key = SecureRandom.random_bytes(32), SecureRandom.random_bytes(16) + + old_encryptor = ActiveSupport::MessageEncryptor.new(old_raw_key, old_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1") + old_message = old_encryptor.encrypt_and_sign("message encrypted with old raw keys") + + encryptor = ActiveSupport::MessageEncryptor.new(@secret) + encryptor.rotate raw_key: old_raw_key, raw_signed_key: old_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1" + + assert_equal "message encrypted with old raw keys", encryptor.decrypt_and_verify(old_message) + end + + def test_with_rotated_aes_cbc_encryptor_with_secret_and_salts + old_secret, old_salt, old_signed_salt = SecureRandom.random_bytes(32), "old salt", "old signed salt" + + old_key_gen = ActiveSupport::KeyGenerator.new(old_secret, iterations: 1000) + old_raw_key = old_key_gen.generate_key(old_salt, 32) + old_raw_signed_key = old_key_gen.generate_key(old_signed_salt) + + old_encryptor = ActiveSupport::MessageEncryptor.new(old_raw_key, old_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1") + old_message = old_encryptor.encrypt_and_sign("message encrypted with old secret and salts") + + encryptor = ActiveSupport::MessageEncryptor.new(@secret) + encryptor.rotate secret: old_secret, salt: old_salt, signed_salt: old_signed_salt, cipher: "aes-256-cbc", digest: "SHA1" + + assert_equal "message encrypted with old secret and salts", encryptor.decrypt_and_verify(old_message) + end + + def test_with_rotating_multiple_encryptors + older_raw_key, older_raw_signed_key = SecureRandom.random_bytes(32), SecureRandom.random_bytes(16) + old_raw_key, old_raw_signed_key = SecureRandom.random_bytes(32), SecureRandom.random_bytes(16) + + older_encryptor = ActiveSupport::MessageEncryptor.new(older_raw_key, older_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1") + older_message = older_encryptor.encrypt_and_sign("message encrypted with older raw key") + + old_encryptor = ActiveSupport::MessageEncryptor.new(old_raw_key, old_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1") + old_message = old_encryptor.encrypt_and_sign("message encrypted with old raw key") + + encryptor = ActiveSupport::MessageEncryptor.new(@secret) + encryptor.rotate raw_key: old_raw_key, raw_signed_key: old_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1" + encryptor.rotate raw_key: older_raw_key, raw_signed_key: older_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1" + + assert_equal "encrypted message", encryptor.decrypt_and_verify(encryptor.encrypt_and_sign("encrypted message")) + assert_equal "message encrypted with old raw key", encryptor.decrypt_and_verify(old_message) + assert_equal "message encrypted with older raw key", encryptor.decrypt_and_verify(older_message) + end + + def test_on_rotation_instance_callback_is_called_and_returns_modified_messages + callback_ran, message = nil, nil + + older_raw_key, older_raw_signed_key = SecureRandom.random_bytes(32), SecureRandom.random_bytes(16) + old_raw_key, old_raw_signed_key = SecureRandom.random_bytes(32), SecureRandom.random_bytes(16) + + older_encryptor = ActiveSupport::MessageEncryptor.new(older_raw_key, older_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1") + older_message = older_encryptor.encrypt_and_sign(encoded: "message") + + encryptor = ActiveSupport::MessageEncryptor.new(@secret) + encryptor.rotate raw_key: old_raw_key, raw_signed_key: old_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1" + encryptor.rotate raw_key: older_raw_key, raw_signed_key: older_raw_signed_key, cipher: "aes-256-cbc", digest: "SHA1" + + message = encryptor.decrypt_and_verify(older_message, on_rotation: proc { callback_ran = true }) + + assert callback_ran, "callback was ran" + assert_equal({ encoded: "message" }, message) + end + + def test_with_rotated_metadata + old_secret, old_salt = SecureRandom.random_bytes(32), "old salt" + old_raw_key = ActiveSupport::KeyGenerator.new(old_secret, iterations: 1000).generate_key(old_salt, 32) + + old_encryptor = ActiveSupport::MessageEncryptor.new(old_raw_key, cipher: "aes-256-gcm") + old_message = old_encryptor.encrypt_and_sign( + "message encrypted with old secret, salt, and metadata", purpose: "rotation") + + encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm") + encryptor.rotate secret: old_secret, salt: old_salt, cipher: "aes-256-gcm" + + assert_equal "message encrypted with old secret, salt, and metadata", + encryptor.decrypt_and_verify(old_message, purpose: "rotation") + end + private def assert_aead_not_decrypted(encryptor, value) assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index fbeafca203..3079c48c02 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -20,6 +20,7 @@ class MessageVerifierTest < ActiveSupport::TestCase def setup @verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!") @data = { some: "data", now: Time.utc(2010) } + @secret = SecureRandom.random_bytes(32) end def test_valid_message @@ -90,6 +91,95 @@ class MessageVerifierTest < ActiveSupport::TestCase signed_message = "BAh7BzoJc29tZUkiCWRhdGEGOgZFVDoIbm93SXU6CVRpbWUNIIAbgAAAAAAHOgtvZmZzZXRpADoJem9uZUkiCFVUQwY7BkY=--d03c52c91dfe4ccc5159417c660461bcce005e96" assert_equal @data, @verifier.verify(signed_message) end + + def test_with_rotated_raw_key + old_raw_key = SecureRandom.random_bytes(32) + + old_verifier = ActiveSupport::MessageVerifier.new(old_raw_key, digest: "SHA1") + old_message = old_verifier.generate("message verified with old raw key") + + verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA1") + verifier.rotate raw_key: old_raw_key, digest: "SHA1" + + assert_equal "message verified with old raw key", verifier.verified(old_message) + end + + def test_with_rotated_secret_and_salt + old_secret, old_salt = SecureRandom.random_bytes(32), "old salt" + + old_raw_key = ActiveSupport::KeyGenerator.new(old_secret, iterations: 1000).generate_key(old_salt) + old_verifier = ActiveSupport::MessageVerifier.new(old_raw_key, digest: "SHA1") + old_message = old_verifier.generate("message verified with old secret and salt") + + verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA1") + verifier.rotate secret: old_secret, salt: old_salt, digest: "SHA1" + + assert_equal "message verified with old secret and salt", verifier.verified(old_message) + end + + def test_with_rotated_key_generator + old_key_gen, old_salt = ActiveSupport::KeyGenerator.new(SecureRandom.random_bytes(32), iterations: 256), "old salt" + + old_raw_key = old_key_gen.generate_key(old_salt) + old_verifier = ActiveSupport::MessageVerifier.new(old_raw_key, digest: "SHA1") + old_message = old_verifier.generate("message verified with old key generator and salt") + + verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA1") + verifier.rotate key_generator: old_key_gen, salt: old_salt, digest: "SHA1" + + assert_equal "message verified with old key generator and salt", verifier.verified(old_message) + end + + def test_with_rotating_multiple_verifiers + old_raw_key, older_raw_key = SecureRandom.random_bytes(32), SecureRandom.random_bytes(32) + + old_verifier = ActiveSupport::MessageVerifier.new(old_raw_key, digest: "SHA256") + old_message = old_verifier.generate("message verified with old raw key") + + older_verifier = ActiveSupport::MessageVerifier.new(older_raw_key, digest: "SHA1") + older_message = older_verifier.generate("message verified with older raw key") + + verifier = ActiveSupport::MessageVerifier.new("new secret", digest: "SHA512") + verifier.rotate raw_key: old_raw_key, digest: "SHA256" + verifier.rotate raw_key: older_raw_key, digest: "SHA1" + + assert_equal "verified message", verifier.verified(verifier.generate("verified message")) + assert_equal "message verified with old raw key", verifier.verified(old_message) + assert_equal "message verified with older raw key", verifier.verified(older_message) + end + + def test_on_rotation_keyword_block_is_called_and_verified_returns_message + callback_ran, message = nil, nil + + old_raw_key, older_raw_key = SecureRandom.random_bytes(32), SecureRandom.random_bytes(32) + + older_verifier = ActiveSupport::MessageVerifier.new(older_raw_key, digest: "SHA1") + older_message = older_verifier.generate(encoded: "message") + + verifier = ActiveSupport::MessageVerifier.new("new secret", digest: "SHA512") + verifier.rotate raw_key: old_raw_key, digest: "SHA256" + verifier.rotate raw_key: older_raw_key, digest: "SHA1" + + message = verifier.verified(older_message, on_rotation: proc { callback_ran = true }) + + assert callback_ran, "callback was ran" + assert_equal({ encoded: "message" }, message) + end + + def test_with_rotated_metadata + old_secret, old_salt = SecureRandom.random_bytes(32), "old salt" + + old_raw_key = ActiveSupport::KeyGenerator.new(old_secret, iterations: 1000).generate_key(old_salt) + old_verifier = ActiveSupport::MessageVerifier.new(old_raw_key, digest: "SHA1") + old_message = old_verifier.generate( + "message verified with old secret, salt, and metadata", purpose: "rotation") + + verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA1") + verifier.rotate secret: old_secret, salt: old_salt, digest: "SHA1" + + assert_equal "message verified with old secret, salt, and metadata", + verifier.verified(old_message, purpose: "rotation") + end end class MessageVerifierMetadataTest < ActiveSupport::TestCase diff --git a/activesupport/test/messages/rotation_configuration_test.rb b/activesupport/test/messages/rotation_configuration_test.rb new file mode 100644 index 0000000000..41d938e119 --- /dev/null +++ b/activesupport/test/messages/rotation_configuration_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/messages/rotation_configuration" + +class MessagesRotationConfiguration < ActiveSupport::TestCase + def setup + @config = ActiveSupport::Messages::RotationConfiguration.new + end + + def test_signed_configurations + @config.rotate :signed, secret: "older secret", salt: "salt", digest: "SHA1" + @config.rotate :signed, secret: "old secret", salt: "salt", digest: "SHA256" + + assert_equal [{ + secret: "older secret", salt: "salt", digest: "SHA1" + }, { + secret: "old secret", salt: "salt", digest: "SHA256" + }], @config.signed + end + + def test_encrypted_configurations + @config.rotate :encrypted, raw_key: "old raw key", cipher: "aes-256-gcm" + + assert_equal [{ + raw_key: "old raw key", cipher: "aes-256-gcm" + }], @config.encrypted + end + + def test_rotate_without_kind + @config.rotate secret: "older secret", salt: "salt", digest: "SHA1" + @config.rotate raw_key: "old raw key", cipher: "aes-256-gcm" + + expected = [{ + secret: "older secret", salt: "salt", digest: "SHA1" + }, { + raw_key: "old raw key", cipher: "aes-256-gcm" + }] + + assert_equal expected, @config.encrypted + assert_equal expected, @config.signed + end +end -- cgit v1.2.3