Help us understand the problem. What is going on with this article?

RailsのMessageVerifierの内部実装を追ってみた

More than 1 year has passed since last update.

この記事は Akatsuki Advent Calendar 2017 の 24 日目の記事です。
23日目 Alternating Direction Method of Multipliers (ADMM) で Lasso 回帰

はじめに

鈴付き1から赤笛になれるように日々仕事をしている新卒エンジニアです。
数ヶ月前にMessageVerifierで引っかかったことがあり、今更ではありますが内部でどのように実装されているのかを確認しました。

環境
ruby 2.4.1p111
Rails 5.0.5

MessageVerifierについて

RailsではMessageVerifierをCookieで改ざんのチェックに利用していて、署名をつけたメッセージを作成と、メッセージの署名を検証するのに利用されています。
他にはパスワード再発行メールでのトークンなどにも使われているそうです。

内部の処理

作成

Rails.application.message_verifier(verifier_name).generate(message)のような形で利用できます。
まず、message_verifierメソッドでsecrets.ymlで設定したsecret_key_baseと引数に渡したverifier_nameで鍵を作ります(key_generator
そして、作成した鍵を引数に渡してActiveSupport::MessageVerifierオブジェクトを作成します。

def message_verifier(verifier_name)
  @message_verifiers[verifier_name] ||= begin
    secret = key_generator.generate_key(verifier_name.to_s)
    ActiveSupport::MessageVerifier.new(secret)
  end
end

そのあとにActiveSupport::MessageVerifier.generateで引数に渡したRubyオブジェクトを文字列化しBase64エンコードした文字列と、Base64エンコードしたものを鍵と組み合わせてSHA1でハッシュ化した文字列を--で繋げます。

def generate(value)
  data = encode(@serializer.dump(value))
  "#{data}--#{generate_digest(data)}"
end

def generate_digest(data)
  require 'openssl' unless defined?(OpenSSL)
  OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)
end

検証

署名付きメッセージの検証とメッセージをRubyオブジェクトに戻す処理を行います。
Rails.application.message_verifier(verifier_name).verify(message)のような形で利用できます。
message_verifier(verifier_name)は送信時の処理で説明したので割愛しますが、verifier_nameは送信時に使った値と同じにしないと鍵の作成で異なるものが作成されてしまい正しくチェックできません。

verifiedメソッドのvalid_message?で改ざんチェックを行い、チェックが通ればdecode(data)でデコードして@serializer.loadで元のオブジェクトと同じ状態のオブジェクトを生成します。
改ざんが見つかった場合、ActiveSupport::MessageVerifier::InvalidSignatureがraiseされます。

def verify(signed_message)
  verified(signed_message) || raise(InvalidSignature)
end

def verified(signed_message)
  if valid_message?(signed_message)
    begin
      data = signed_message.split("--".freeze)[0]
      @serializer.load(decode(data))
    rescue ArgumentError => argument_error
      return if argument_error.message =~ %r{invalid base64}
      raise
    end
  end
end

チェック処理を追ってみるとActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))digestgenerate_digest(data)のバイト毎の比較を行なっています。(ActiveSupport:: SecurityUtils.secure_compare
ここで署名付きメッセージが改ざんされていないかを調べているのですね。

def valid_message?(signed_message)
  return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank?

  data, digest = signed_message.split("--".freeze)
  data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))
end

最後に

MessageVerifierがどのような処理を行なって署名の作成と検証を行なっているかを調べました。
月並みですが、Railsの内部を読むのは勉強になりますね。
MessageVerifierのコードはそこまで深くコードを追うこともなく、自分にとってはちょうどいいくらいの深さでした。
また、コードを追っていった結果タイミングアタック2という攻撃があることも知りました。
今後は、詰まった時にRails内部を追えるように日頃からRails力を高めていきます。


  1. メイドインアビスネタ。 

  2. http://kengos.jp/2016/01/31/CVE-2015-7576.html 

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away