LoginSignup
23
13

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-24

この記事は 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 

23
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
13