@ Kaigi on Rails 2021, 2021/10/22
Build and Learn Rails AuthenticationRyo Kajiwara (sylph01)2021/10/21 @ Kaigi on Rails 2021
View Slide
本発表のレポジトリは以下ですスライドはslidesフォルダ以下にあります
誰?sylph01 /梶原 龍Twitter: @s01暗号とかできますElixirとかできますRailsまるでわからん
Rails(鉄道)にはよく乗ります
若干真面目な自己紹介やせいのプログラマ(要するにフリーランス)W3C, IETFなどでセキュリティ寄りのプロトコルの標準化のお手伝いをしていましたHTTPS in Local Network, Messaging LayerSecurityなど次世代OAuthの薄い本を書きました現バージョンは今は頒布中止していますが新バージョン出したいKaigi on Rails 1週間前に松山に引っ越しました
現場の宣伝株式会社コードタクトにて「まなびポケット」の認証認可基盤の開発をしています認証認可チームにて新規メンバーを募集中です既存の認証基盤を置き換える新規開発を行いますモダンな認証認可技術で日本の公教育にイノベーションをもたらしましょう
Railsの認証?要するにDeviseのことでしょ?
なんか強い人からDeviseはイケてないって聞いたんだけど?
Rails sucks? It's most likely that you suckDevise sucks? It's most likely that you suck
本発表の目的認証機能の自作と解説、また主要な認証ライブラリのアプローチの比較を通して、認証技術への理解を深めること、また認証ライブラリの選択を手助けすることを目指します
DisclaimerDo try this at home, butDo not try this in productionできる限り脆弱性を埋め込まないように気をつけて作ることをしますが、通常の場合productionでは多数の人によって検証されているライブラリを使用することをおすすめします。
Disclaimer (2)なんなら 認証機能は自分で持たないほうがいい。自分でIDを管理するのは飲水を確保するのに自分で井戸を掘ることID連携技術を使ってIdPを利用することは近代的な水道インフラに乗っかることOAuth/OpenID Connectの話もできるにはできますが今回はその話はしません。
第1部:認証を作ってみる
認証/認可についての概念人は対象をどのように認知するか?人/システムは 対象(Entity)を直接認知しない。人/システムは Identity(属性の集合)を通して対象(Entity)を認知する。1つの対象(Entity)は複数の属性(Identity)を持ち、文脈に応じて使い分ける。
認証/認可についての概念Authentication(認証)Entityがサービスの認知するIdentityに紐付いているという確証を得ること一般にいう「ログイン機能」は、識別子(ID)とパスワードの組を提示できることによって、 利用するEntityがサービス上のEntityに対応しているという確証を得られることによって成立するAuthorization(認可)リソースにアクセスするための条件を定めること
忙しい人のための暗号・ハッシュアルゴリズムこの後の説明に用いる道具を超高速で説明します。実際の中身はググって
暗号化(ここでは共通鍵暗号のこと)一般に128bit以上のAESを使う(256bitは実はoverkill気味)。3-DESって言われたら「臭い」を感じ取って欲しい。RSAは公開鍵暗号。ここでは説明しない。
ハッシュ化現代的にはSHA-2(SHA256, SHA512)が一般的。SHA-3などもあるにはある。あえて新規にSHA-1やMD5を使う理由はない。元の値に戻すのが困難という性質がセキュアなパスワード認証においてとても重要。
HMAC(超訳:鍵付きハッシュ)ハッシュ関数を使って秘密の値(鍵)で特徴づけられる関数を得る方式。秘密の値を知らないと関数の形がわからないのでMAC値が計算できない。「HMAC-SHA256」というように「方式名」+「利用するハッシュ関数」の組で呼ばれる。
Webサービスの認証で必要な機能必ず欲しいログイン・ログアウトクッキーからの自動再ログイン(remember)パスワードリセットできれば欲しいロックアウトEメール認証ワンタイムパスワード(機能はDeviseやRodauthのfeaturesから抜粋)
パスワード認証に対する攻撃パスワード認証に対する攻撃は一般にブルートフォース攻撃辞書攻撃もこれの亜種データベース流出を利用したハッシュクラックローカルで大量にハッシュ計算して衝突させるなお、「暗号化して保存」では復元できてしまうのでダメですの2つの形を取る。
ハッシュアルゴリズムの選択パスワードを保存する場合のハッシュアルゴリズムは遅いものほどよい。この理由は正規のハッシュは1回しか計算されないが攻撃の際は複数のハッシュを計算しなくてはいけないため。(詳細は省略、キーワードで検索してください)MD5:伸長攻撃が可能、そもそもハッシュ長が短いので衝突させやすいSHA-1:強衝突耐性が突破されているSHA-2:衝突耐性の面では現時点では十分だが、計算が十分に遅くない複数回適用して遅くするPBKDF2という方式があるパスワードハッシュ用に開発されたbcryptはわざと計算を遅くしている
(https://twitter.com/TerahashCorp/status/1155128018156892160より)bcryptなら8桁でもクラックするのにGPUクラスタでも18年かかる!
saltユーザーごとにランダムな文字列sを生成し、ハッシュ値の と平文の sを保存することによって、事前にハッシュを計算しておいてテーブルをルックアップするレインボーテーブルアタックを回避できる。bcryptでは出力される文字列がsaltとハッシュ値の組を結合したものになっている。h(p∣∣s)
has_secure_passwordRailsでは has_secure_passwordというモデルのメソッドがbcryptを利用してパスワードハッシュ化の面倒を見てくれる。パスワード入力の確認も面倒を見てくれる。便利。ソースは activemodel/lib/active_model/secure_password.rb。
応用攻撃者が正解のハッシュ値を得られないようにすればよいので、もっとがんばるなら以下のような方法が取れる:HMAC-SHA256のsecretを外部のHardware Security Moduleに保存して、HSMのAPIを通してハッシュ計算をするsecretはHSM上にしかないので、ハッシュ値の計算がHSMにしかできなくなるこのような手法をpassword pepperと呼ぶらしいハッシュ値そのもののアクセスを可能な限りさせないためにデータベース関数を利用する後で紹介する rodauthがこの方式を使っている
クッキーからの自動再ログイン、パスワードリセット、Eメール認証基本的に原理は同じで、有効期限つきの乱数列を払い出しユーザーモデルに関連づけるこの際、データベースに保存するのはハッシュ化された値乱数列が利用される際には有効期限を確認、利用されたら乱数列を破棄Eメール認証とパスワードリセットではこの乱数列を登録しているメールアドレスに対して送信するすべて同様に 「生成した乱数列を知っていて」「有効期限内で提示できる」 という性質をもってEntityが登録ユーザーに対応することを確認している。
クッキーからの自動再ログイン?セッションじゃダメなの?セッション:ブラウザウィンドウが開いている間のみ有効な CookieのことセッションはCookieのサブセットCookie:ここでは「 ブラウザウィンドウよりも長いライフタイムを持つ Cookie」のことを指す明示的に有効期限を指定したものを指すなのでブラウザウィンドウを閉じた後の再ログインに使えるRailsではセッションは自動で( secret_key_baseを使って)暗号化されるCookieは明示的にsignedかencryptedを指定する必要がある
ワンタイムパスワードHOTP: An HMAC-Based One-Time Password Algorithm (RFC 4226)TOTP: Time-Based One-Time Password Algorithm (RFC 6238)を用いるものが一般的。サーバーと共通の秘密を知っていて、共通の秘密から時刻などに基づいて特定の値を導出できる という性質をもってEntityが登録ユーザーに対応することを確認している。
ロックアウト(ブルートフォース攻撃対策)以下の性質を持つカウンタを用意不正なログイン試行でカウントが1増えて正常なログインで0に戻る一定以上のカウントを持っている場合、最終ログイン時間から一定時間が経過していない場合パスワードがあっていても自動的にログインに失敗する試行回数に対して指数でログイン不可能時間を設けるexponential backoffalgorithmという方式が一般的
実際に作ってみたhttps://github.com/sylph01/touch-and-learn-authentication/以下のRailsアプリにこれらの欲しい機能をできるだけプリミティブに実装したサンプルを置いています。
第2部:認証ライブラリの話
(再掲)Railsの認証?要するにDeviseのことでしょ?
何でライブラリが欲しいか楽をしたい読みやすいイディオム/DSLの形で認証機能を使いたいいろんな人の目が入ってるのでセキュリティバグを埋め込んでいる可能性が少ない
ライブラリ化する場合に行うこと初期設定手段を用意rails generateのジェネレータを用意するのが一般的モデル、コントローラーなどのクラスを拡張しDSLを追加する
何でライブラリがあるのに自作をしたか今回は学習目的productionでも一度自作しているユーザーインターフェースを伴わないJSON APIで、アクセストークンを払い出す機構だけが欲しかった一般にproductionでライブラリを使わない理由があるとすれば目的に合致しないから
ライブラリの比較DeviseSorceryAuthlogicRodauthを対象に比較をしていきます。発表者の経験としては「Deviseはproductionで使ったことがある」「あとは今回調べた」程度です。
Devise (1)Wardenの上に作られているWardenとは:認証用 rack middlewaresession middlewareの後に入って、sessionの情報を使って認証状態を確かめたり認証アクションをトリガーしたりする
超忙しい人のためのWardenenv['warden'].authenticated? -認証済みであるかを確かめるenv['warden'].authenticate(:password) - :password strategyで認証を行う。実際の認証は各々定義するstrategyの中で行う成功したら env['warden'].userにuser objectが入ってくる認証エラー時は throw(:warden)でWardenの例外を投げる
Devise (2)Deviseのstrategyは lib/devise/strategies以下にある。パスワード認証はDatabaseAuthenticatable strategyで実装されている。コントローラーで用いる signed_in? , sign_in , sign_outなどはDevise::Controllers::SignInOutで実装されている。Wardenの authenticate?やset_userや logoutが使われていることがわかる。
Devise (3)Routesに devise_for :usersを書くとそのUserが対応しているDeviseのモジュールに応じてDeviseの提供するcontrollerへのrouteが設定される。ControllerのアクションをカスタマイズしたいときにはDeviseの提供するcontrollerをそのまま使いたくない。多分これがDeviseを嫌う一番の理由か?
Sorcerycode generationを可能な限り使わない、シンプルに切り詰めた認証ライブラリDeviseではデフォルトから離れたことをしようと思うとコントローラーを継承したりoverrideしたりしないといけないSorceryではライブラリのメソッドを自分のMVCコードの中で使うただし自己責任の部分が増える設定はInitializerにまとまっているコード中で sorcery_configを取る動作がよく見られるのはこれ暗号コードはAuthlogicをベースにしているパスワードの暗号化が可能at your own risk...
require_loginlogin(email, password, remember_me = false)auto_login(user)logoutlogged_in?current_userredirect_back_or_to@user.external?@user.active_for_authentication?@user.valid_password?('secret')User.authenticates_with_sorcery!(GitHubのreadmeより)パスワード認証だけならメソッドは11個!
AuthlogicSessionオブジェクトを中心に据えた認証ライブラリ他のライブラリではログインセッションが明示的にオブジェクトで表されないことに注意モデルに acts_as_authenticと書くと機能が有効化される他では対応していない外部認証プロバイダ(OpenID, LDAP, PAM, x509)に対応できるgeneratorがないモデルのセットアップ時にREADMEにあるmigrationから必要な機能分を選んで手書きする
UserSession.create(:login => "bjohnson", :password => "my password", :remember_me => true)session = UserSession.new(:login => "bjohnson", :password => "my password", :remember_me => true)session.savesession = UserSession.findsession.destroy(GitHubのreadmeより抜粋)
Rodauth (1)"Ruby's most advanced authentication framework"の名に恥じない圧倒的高機能暗号技術ファンとして素直に感心するWebAuthn、ワンタイムパスワード、SMS、JWTのサポートデータベース関数によるパスワードハッシュへのアクセスHMACを使った"password pepper"の徹底Rails/ActiveRecordを前提としていないRodaとSequelで作られているがRailsでの利用方法はそんなに自明ではない
Rodauth (2):データベース関数の利用普通のRailsアプリではアプリを実行するユーザーがパスワードハッシュ値にアクセスできる通常のアプリユーザーとパスワードハッシュ値にアクセスできるユーザーを分離パスワードハッシュ値をアプリに見せることなく値の設定や比較を行うデータベース関数を定義アプリユーザーはデータベース関数を利用するだけデータベースの権限昇格が発生しない限りパスワードハッシュが漏れることがない
Rodauth (3): password pepperの徹底hmac_secretを設定することで、以下の値(p.29で説明した仕組み)の保存時にHMACが適用される(→共通のsecretを使う"password pepper")。Eメールで送信するtokenrememberで使用するtokenワンタイムパスワードで使用するトークンはユーザーに提示されるキーにHMACが適用される。hmac_secretをメモリ上にのみ存在させることで攻撃者はハッシュ(HMAC)値の計算に用いる関数を知ることができない。
外部認証プロバイダ利用DeviseはOmniAuthが利用できるAuthlogicはプラグインが複数あるレガシーな認証方式に対してもプラグインがあるRodauthでもLDAPは対応しているSorceryはExternalプラグインというのが同梱されているRodauthは見る限りまだ?
おまけ:ハッシュ済みパスワードのカラム名Deviseは encrypted_password一般にハッシュ化した値をencryptedであるとは言わないSorcery, Authlogicは crypted_passwordそもそも暗号化済みを指す語は "crypted"ではないRodauthは password_digestハッシュ化した値のことを "digest"と呼ぶのは正式な用法has_secure_passwordで実装した場合も password_digest
多分こういう使い分けになるRails/ActiveRecordに縛られないものが欲しい: Rodauthとにかく普通のパスワード認証+αをさくっと作りたい: Devise認証周りにたくさんカスタムコードがあって細かく制御したい: Sorcery, Authlogic外部認証プロバイダへの移行がありそう:今のところRodauth以外レガシー外部認証方式はAuthlogicに一日の長があるほぼ初期設定でとにかくセキュアにしたい: Rodauthが有利か?他がinsecureであるとは言ってないことに注意
まとめパスワード認証とその付随技術の実装の注意点を紹介しました主な認証ライブラリの特徴とその使い分けを紹介しました
Welcome to Authentication Hell(また今年も沼に人を誘ってしまった…)
Questions / Comments?Send them to @s01or see you in the Q&A session!
おまけ: Further Readingデジタルアイデンティティの考え方そのものについて『デジタルアイデンティティー 経営者が知らないサイバービジネスの核心』崎村夏彦暗号技術と認証技術『図解即戦力 暗号と認証のしくみと理論がこれ1冊でしっかりわかる教科書』光成滋生『暗号技術のすべて』IPUSIRON
おまけ:時間が足りなくて話せなかったことのメモハッシュの衝突耐性について:過去に記事書きましたsecure string comparison (timing-safe comparison)「前から順に一致判定して途中で打ち切る」方法では時間差を測定することで情報量の漏れが発生するセッションハイジャックの対策HTTPSを使おう、 Secureかつ HttpOnlyのCookieを使おう