Skip to content

Commit ebc3b66

Browse files
Merge pull request rails#47321 from jonathanhefner/messages-rotator-rework-tests
Factor rotator tests into dedicated classes
2 parents 10fcbee + b95fe24 commit ebc3b66

File tree

5 files changed

+167
-194
lines changed

5 files changed

+167
-194
lines changed

activesupport/test/message_encryptor_test.rb

Lines changed: 0 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -149,99 +149,6 @@ def test_backwards_compatibility_decrypt_previously_encrypted_messages_without_m
149149
assert_equal "Ruby on Rails", encryptor.decrypt_and_verify(encrypted_message)
150150
end
151151

152-
def test_rotating_secret
153-
old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], cipher: "aes-256-gcm").encrypt_and_sign("old")
154-
155-
encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
156-
encryptor.rotate secrets[:old]
157-
158-
assert_equal "old", encryptor.decrypt_and_verify(old_message)
159-
end
160-
161-
def test_rotating_serializer
162-
old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], cipher: "aes-256-gcm", serializer: JSON).
163-
encrypt_and_sign({ ahoy: :hoy })
164-
165-
encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm", serializer: JSON)
166-
encryptor.rotate secrets[:old]
167-
168-
assert_equal({ "ahoy" => "hoy" }, encryptor.decrypt_and_verify(old_message))
169-
end
170-
171-
def test_rotating_aes_cbc_secrets
172-
old_encryptor = ActiveSupport::MessageEncryptor.new(secrets[:old], "old sign", cipher: "aes-256-cbc")
173-
old_message = old_encryptor.encrypt_and_sign("old")
174-
175-
encryptor = ActiveSupport::MessageEncryptor.new(@secret)
176-
encryptor.rotate secrets[:old], "old sign", cipher: "aes-256-cbc"
177-
178-
assert_equal "old", encryptor.decrypt_and_verify(old_message)
179-
end
180-
181-
def test_multiple_rotations
182-
older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign("older")
183-
old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], "old sign").encrypt_and_sign("old")
184-
185-
encryptor = ActiveSupport::MessageEncryptor.new(@secret)
186-
encryptor.rotate secrets[:old], "old sign"
187-
encryptor.rotate secrets[:older], "older sign"
188-
189-
assert_equal "new", encryptor.decrypt_and_verify(encryptor.encrypt_and_sign("new"))
190-
assert_equal "old", encryptor.decrypt_and_verify(old_message)
191-
assert_equal "older", encryptor.decrypt_and_verify(older_message)
192-
end
193-
194-
def test_on_rotation_is_called_and_returns_modified_messages
195-
older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign({ encoded: "message" })
196-
197-
encryptor = ActiveSupport::MessageEncryptor.new(@secret)
198-
encryptor.rotate secrets[:old]
199-
encryptor.rotate secrets[:older], "older sign"
200-
201-
rotated = false
202-
message = encryptor.decrypt_and_verify(older_message, on_rotation: proc { rotated = true })
203-
204-
assert_equal({ encoded: "message" }, message)
205-
assert rotated
206-
end
207-
208-
def test_on_rotation_can_be_passed_at_the_constructor_level
209-
older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign({ encoded: "message" })
210-
211-
rotated = rotated = false # double assigning to suppress "assigned but unused variable" warning
212-
encryptor = ActiveSupport::MessageEncryptor.new(@secret, on_rotation: proc { rotated = true })
213-
encryptor.rotate secrets[:older], "older sign"
214-
215-
assert_changes(:rotated, from: false, to: true) do
216-
message = encryptor.decrypt_and_verify(older_message)
217-
218-
assert_equal({ encoded: "message" }, message)
219-
end
220-
end
221-
222-
def test_on_rotation_option_takes_precedence_over_the_one_given_in_constructor
223-
older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign({ encoded: "message" })
224-
225-
rotated = rotated = false # double assigning to suppress "assigned but unused variable" warning
226-
encryptor = ActiveSupport::MessageEncryptor.new(@secret, on_rotation: proc { rotated = true })
227-
encryptor.rotate secrets[:older], "older sign"
228-
229-
assert_changes(:rotated, from: false, to: "Yes") do
230-
message = encryptor.decrypt_and_verify(older_message, on_rotation: proc { rotated = "Yes" })
231-
232-
assert_equal({ encoded: "message" }, message)
233-
end
234-
end
235-
236-
def test_with_rotated_metadata
237-
old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], cipher: "aes-256-gcm").
238-
encrypt_and_sign("metadata", purpose: :rotation)
239-
240-
encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
241-
encryptor.rotate secrets[:old]
242-
243-
assert_equal "metadata", encryptor.decrypt_and_verify(old_message, purpose: :rotation)
244-
end
245152

246153
private
247154
def assert_aead_not_decrypted(encryptor, value)
@@ -341,48 +248,6 @@ def test_backwards_compatibility_decrypt_previously_encrypted_messages_without_m
341248

342249
assert_equal "Ruby on Rails", encryptor.decrypt_and_verify(encrypted_message)
343250
end
344-
345-
def test_on_rotation_is_called_and_returns_modified_messages
346-
older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign({ encoded: "message" })
347-
348-
encryptor = ActiveSupport::MessageEncryptor.new(@secret)
349-
encryptor.rotate secrets[:old]
350-
encryptor.rotate secrets[:older], "older sign"
351-
352-
rotated = false
353-
message = encryptor.decrypt_and_verify(older_message, on_rotation: proc { rotated = true })
354-
355-
assert_equal({ "encoded" => "message" }, message)
356-
assert rotated
357-
end
358-
359-
def test_on_rotation_can_be_passed_at_the_constructor_level
360-
older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign({ encoded: "message" })
361-
362-
rotated = rotated = false # double assigning to suppress "assigned but unused variable" warning
363-
encryptor = ActiveSupport::MessageEncryptor.new(@secret, on_rotation: proc { rotated = true })
364-
encryptor.rotate secrets[:older], "older sign"
365-
366-
assert_changes(:rotated, from: false, to: true) do
367-
message = encryptor.decrypt_and_verify(older_message)
368-
369-
assert_equal({ "encoded" => "message" }, message)
370-
end
371-
end
372-
373-
def test_on_rotation_option_takes_precedence_over_the_one_given_in_constructor
374-
older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign({ encoded: "message" })
375-
376-
rotated = rotated = false # double assigning to suppress "assigned but unused variable" warning
377-
encryptor = ActiveSupport::MessageEncryptor.new(@secret, on_rotation: proc { rotated = true })
378-
encryptor.rotate secrets[:older], "older sign"
379-
380-
assert_changes(:rotated, from: false, to: "Yes") do
381-
message = encryptor.decrypt_and_verify(older_message, on_rotation: proc { rotated = "Yes" })
382-
383-
assert_equal({ "encoded" => "message" }, message)
384-
end
385-
end
386251
end
387252

388253
class MessageEncryptorWithHybridSerializerAndWithoutMarshalDumpTest < MessageEncryptorWithJsonSerializerTest

activesupport/test/message_verifier_test.rb

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -99,37 +99,6 @@ def test_raise_error_when_secret_is_nil
9999
end
100100
assert_equal "Secret should not be nil.", exception.message
101101
end
102-
103-
def test_rotating_secret
104-
old_message = ActiveSupport::MessageVerifier.new("old", digest: "SHA1").generate("old")
105-
106-
verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA1")
107-
verifier.rotate "old"
108-
109-
assert_equal "old", verifier.verified(old_message)
110-
end
111-
112-
def test_multiple_rotations
113-
old_message = ActiveSupport::MessageVerifier.new("old", digest: "SHA256").generate("old")
114-
older_message = ActiveSupport::MessageVerifier.new("older", digest: "SHA1").generate("older")
115-
116-
verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512")
117-
verifier.rotate "old", digest: "SHA256"
118-
verifier.rotate "older", digest: "SHA1"
119-
120-
assert_equal "new", verifier.verified(verifier.generate("new"))
121-
assert_equal "old", verifier.verified(old_message)
122-
assert_equal "older", verifier.verified(older_message)
123-
end
124-
125-
def test_rotations_with_metadata
126-
old_message = ActiveSupport::MessageVerifier.new("old").generate("old", purpose: :rotation)
127-
128-
verifier = ActiveSupport::MessageVerifier.new(@secret)
129-
verifier.rotate "old"
130-
131-
assert_equal "old", verifier.verified(old_message, purpose: :rotation)
132-
end
133102
end
134103

135104
class DefaultMarshalSerializerMessageVerifierTest < MessageVerifierTest
@@ -151,20 +120,6 @@ def test_backward_compatibility_messages_signed_without_metadata
151120
assert_equal @data, @verifier.verify(signed_message)
152121
end
153122

154-
def test_on_rotation_is_called_and_verified_returns_message
155-
older_message = ActiveSupport::MessageVerifier.new("older", digest: "SHA1").generate({ encoded: "message" })
156-
157-
verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512")
158-
verifier.rotate "old", digest: "SHA256"
159-
verifier.rotate "older", digest: "SHA1"
160-
161-
rotated = false
162-
message = verifier.verified(older_message, on_rotation: proc { rotated = true })
163-
164-
assert_equal({ encoded: "message" }, message)
165-
assert rotated
166-
end
167-
168123
def test_raise_error_when_argument_class_is_not_loaded
169124
# To generate the valid message below:
170125
#
@@ -226,20 +181,6 @@ def teardown
226181
ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization = @default_fallback
227182
end
228183

229-
def test_on_rotation_is_called_and_verified_returns_message
230-
older_message = ActiveSupport::MessageVerifier.new("older", digest: "SHA1").generate({ encoded: "message" })
231-
232-
verifier = ActiveSupport::MessageVerifier.new(@secret, digest: "SHA512")
233-
verifier.rotate "old", digest: "SHA256"
234-
verifier.rotate "older", digest: "SHA1"
235-
236-
rotated = false
237-
message = verifier.verified(older_message, on_rotation: proc { rotated = true })
238-
239-
assert_equal({ "encoded" => "message" }, message)
240-
assert rotated
241-
end
242-
243184
def test_backward_compatibility_messages_signed_marshal_serialized
244185
marshal_serialized_signed_message = "BAh7B0kiCXNvbWUGOgZFVEkiCWRhdGEGOwBUSSIIbm93BjsAVEl1OglUaW1lDSCAG8AAAAAABjoJem9uZUkiCFVUQwY7AEY=--ae7480422168507f4a8aec6b1d68bfdfd5c6ef48"
245186
assert_equal @data, @verifier.verify(marshal_serialized_signed_message)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../abstract_unit"
4+
require_relative "message_rotator_tests"
5+
6+
class MessageEncryptorRotatorTest < ActiveSupport::TestCase
7+
include MessageRotatorTests
8+
9+
test "rotate cipher" do
10+
assert_rotate [cipher: "aes-256-gcm"], [cipher: "aes-256-cbc"]
11+
end
12+
13+
test "rotate serializer" do
14+
assert_rotate [serializer: JSON], [serializer: Marshal]
15+
end
16+
17+
test "rotate serializer when message has purpose" do
18+
assert_rotate [serializer: JSON], [serializer: Marshal], purpose: "purpose"
19+
end
20+
21+
test "rotate verifier secret when using non-authenticated encryption" do
22+
with_authenticated_encryption(false) do
23+
assert_rotate \
24+
[secret("encryption"), secret("new verifier")],
25+
[secret("encryption"), secret("old verifier")],
26+
[secret("encryption"), secret("older verifier")]
27+
end
28+
end
29+
30+
test "rotate verifier digest when using non-authenticated encryption" do
31+
with_authenticated_encryption(false) do
32+
assert_rotate [digest: "SHA256"], [digest: "SHA1"], [digest: "MD5"]
33+
end
34+
end
35+
36+
private
37+
def secret(key)
38+
@secrets ||= {}
39+
@secrets[key] ||= SecureRandom.random_bytes(32)
40+
end
41+
42+
def make_codec(secret = secret("secret"), verifier_secret = nil, **options)
43+
ActiveSupport::MessageEncryptor.new(secret, verifier_secret, **options)
44+
end
45+
46+
def encode(data, encryptor, **options)
47+
encryptor.encrypt_and_sign(data, **options)
48+
end
49+
50+
def decode(message, encryptor, **options)
51+
encryptor.decrypt_and_verify(message, **options)
52+
rescue ActiveSupport::MessageVerifier::InvalidSignature
53+
nil
54+
end
55+
56+
def with_authenticated_encryption(value = true)
57+
original_value = ActiveSupport::MessageEncryptor.use_authenticated_message_encryption
58+
ActiveSupport::MessageEncryptor.use_authenticated_message_encryption = value
59+
yield
60+
ensure
61+
ActiveSupport::MessageEncryptor.use_authenticated_message_encryption = original_value
62+
end
63+
end
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
require "json"
4+
5+
module MessageRotatorTests
6+
extend ActiveSupport::Concern
7+
8+
included do
9+
test "rotate secret" do
10+
assert_rotate [secret("new")], [secret("old")], [secret("older")]
11+
end
12+
13+
test "rotate secret when message has purpose" do
14+
assert_rotate [secret("new")], [secret("old")], purpose: "purpose"
15+
end
16+
17+
test "rotate url_safe" do
18+
assert_rotate [url_safe: true], [url_safe: false]
19+
end
20+
21+
test "rotate secret and options" do
22+
assert_rotate [secret("new"), url_safe: true], [secret("old"), url_safe: false]
23+
end
24+
25+
test "on_rotation is called on successful rotation" do
26+
called = nil
27+
assert_rotate [secret("new"), on_rotation: proc { called = true }], [secret("old")]
28+
assert called
29+
end
30+
31+
test "on_rotation is not called when no rotation is necessary" do
32+
called = nil
33+
assert_rotate [secret("same"), on_rotation: proc { called = true }], [secret("same")]
34+
assert_not called
35+
end
36+
37+
test "on_rotation is not called when no rotation is successful" do
38+
called = nil
39+
codec = make_codec(secret("new"), on_rotation: proc { called = true })
40+
codec.rotate(secret("old"))
41+
other_message = encode(DATA, make_codec(secret("other")))
42+
43+
assert_nil decode(other_message, codec)
44+
assert_not called
45+
end
46+
47+
test "on_rotation method option takes precedence over constructor option" do
48+
called = ""
49+
codec = make_codec(secret("new"), on_rotation: proc { called += "via constructor" })
50+
codec.rotate(secret("old"))
51+
old_message = encode(DATA, make_codec(secret("old")))
52+
53+
assert_equal DATA, decode(old_message, codec, on_rotation: proc { called += "via method" })
54+
assert_equal "via method", called
55+
end
56+
end
57+
58+
private
59+
DATA = [{ "a_boolean" => true, "a_number" => 123, "a_string" => "abc" }]
60+
61+
def secret(key)
62+
key
63+
end
64+
65+
def assert_rotate(current, *old, **message_metadata)
66+
current_options = current.extract_options!
67+
current_codec = make_codec(*current, **current_options)
68+
69+
old.each do |old_args|
70+
old_options = old_args.extract_options!
71+
current_codec.rotate(*old_args, **old_options)
72+
old_codec = make_codec(*old_args, **old_options)
73+
old_message = encode(DATA, old_codec, **message_metadata)
74+
75+
assert_equal DATA, decode(old_message, current_codec, **message_metadata)
76+
assert_nil decode(old_message, current_codec) if !message_metadata.empty?
77+
end
78+
end
79+
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../abstract_unit"
4+
require_relative "message_rotator_tests"
5+
6+
class MessageVerifierRotatorTest < ActiveSupport::TestCase
7+
include MessageRotatorTests
8+
9+
test "rotate digest" do
10+
assert_rotate [digest: "SHA256"], [digest: "SHA1"], [digest: "MD5"]
11+
end
12+
13+
private
14+
def make_codec(secret = secret("secret"), **options)
15+
ActiveSupport::MessageVerifier.new(secret, **options)
16+
end
17+
18+
def encode(data, verifier, **options)
19+
verifier.generate(data, **options)
20+
end
21+
22+
def decode(message, verifier, **options)
23+
verifier.verified(message, **options)
24+
end
25+
end

0 commit comments

Comments
 (0)