この記事は 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))
でdigest
とgenerate_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力を高めていきます。
-
メイドインアビスネタ。 ↩