MessageVerifierは、RailsのActiveSupportライブラリに含まれるクラスで、データの署名と検証を行うために使用されます。これにより、データが改ざんされていないことを保証できます。以下に、ActiveSupport::MessageVerifierの実装を簡単に説明します。

1
2
3
4
5
6
7
8
verifier = Rails.application.message_verifier(:hoge)
token = verifier.generate('aiueo', expires_in: 1.hour)
=> "eyJfcmF...--bead..."

verifier.verify(token)
=> "aiueo"
verifier.verify('kakikukeko')
=> mismatched digest (ActiveSupport::MessageVerifier::InvalidSignature)

これがどういう仕組みなのか、実装を追いかけてみます。

なおrails8.1.2時点のコードを参照しています。

コードリーディング

Rails.application.message_verifier

Rails.application.message_verifier(:hoge)からたどります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def message_verifier(verifier_name)
  message_verifiers[verifier_name] # :hogeを渡す
end

def message_verifiers
  @message_verifiers ||=
    ActiveSupport::MessageVerifiers.new do |salt, secret_key_base: self.secret_key_base|
      key_generator(secret_key_base).generate_key(salt)
    end.rotate_defaults
end

https://github.com/rails/rails/blob/v8.1.2/railties/lib/rails/application.rb#L238-L240 https://github.com/rails/rails/blob/v8.1.2/railties/lib/rails/application.rb#L210-L215

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def initialize(&secret_generator)
  raise ArgumentError, "A secret generator block is required" unless secret_generator
  @secret_generator = secret_generator
  @rotate_options = []
  @on_rotation = nil
  @codecs = {}
end

def [](salt)
  @codecs[salt] ||= build_with_rotations(salt) # :hogeをsaltに渡す
end

https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/messages/rotation_coordinator.rb#L10-L16 https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/messages/rotation_coordinator.rb#L18-L20

1
2
3
4
5
def build(salt, secret_generator:, secret_generator_options:, **options)
  # ここでsalt=:hogeとsecret_key_baseが渡る
  # secret_generaterorはRails.application.message_verifiersのブロック
  MessageVerifier.new(secret_generator.call(salt, **secret_generator_options), **options)
end

https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/message_verifiers.rb#L185-L187

1
2
3
4
5
def key_generator(secret_key_base = self.secret_key_base)
  @key_generators[secret_key_base] ||= ActiveSupport::CachingKeyGenerator.new(
    ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
  )
end

https://github.com/rails/rails/blob/v8.1.2/railties/lib/rails/application.rb#L174-L180

1
2
3
4
def generate_key(salt, key_size = 64)
  # 1000回ハッシュをかけて鍵を生成するため少し重めな処理
  OpenSSL::PKCS5.pbkdf2_hmac(@secret, salt, @iterations, key_size, @hash_digest_class.new)
end

https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/key_generator.rb#L41-L43

ここまでで、verifier = Rails.application.message_verifier(:hoge)が完了し、MessageVerifierインスタンスが生成されました。 この時点で、以下のことが行われています。

  • secret_key_baseから鍵が生成されている
  • saltに:hogeが設定されている
  • pbkdf2_hmacで複数回ハッシュ化された鍵が生成されている

generate

次に、verifier.generate('aiueo')を見ていきます。

1
2
3
4
5
6
7
def generate(value, **options)
  create_message(value, **options)
end

def create_message(value, **options) # :nodoc:
  sign_encoded(encode(serialize_with_metadata(value, **options)))
end

https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/message_verifier.rb#L306-L312

1
2
3
def encode(data, url_safe: @url_safe)
  url_safe ? ::Base64.urlsafe_encode64(data, padding: false) : ::Base64.strict_encode64(data)
end

https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/messages/codec.rb#L25-L27

1
2
3
4
5
6
7
8
def sign_encoded(encoded)
  digest = generate_digest(encoded)
  encoded << SEPARATOR << digest
end

def generate_digest(data)
  OpenSSL::HMAC.hexdigest(@digest, @secret, data)
end

https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/message_verifier.rb#L330-L333 https://github.com/rails/rails/blob/v8.1.2/activesupport/lib/active_support/message_verifier.rb#L352C1-L354

digestのデフォルトはSHA1です。 secretOpenSSL::PKCS5.pbkdf2_hmac(@secret, salt, @iterations, key_size, @hash_digest_class.new)で生成した派生鍵です。

最終的に、BASE64エンコードされたデータとHMAC署名が--で連結されて返されます。 前半部分はデコード可能なので、ユーザーが簡単に知ることができます。 が、HMAC署名はsecret_key_baseとソルトを知らないと生成できないため、データの改ざんを防止できます。

まとめ

  • インスタンス生成時に、secret_key_baseとソルトから派生鍵を生成している
    • 決定論的な入出力関係にある
  • generate時に、データをBASE64エンコードし、HMAC署名を付与している
  • データストアを使用していないので、ステートレス

おまけ

冒頭のコードはこんな形でdecodeできます。

1
2
3
4
5
verifier = Rails.application.message_verifier(:hoge)
token = verifier.generate('aiueo', expires_in: 1.hour)
=> "eyJfcmF...--bead..."
::Base64.strict_decode64("eyJfcmFpbHMiOnsiZGF0YSI6ImFpdWVvIiwiZXhwIjoiMjAyNi0wMi0wN1QwODo0NDozMS44NzhaIn19")
=> "{\"_rails\":{\"data\":\"aiueo\",\"exp\":\"2026-02-07T08:44:31.878Z\"}}"