セキュアリモートパスワードプロトコル(SRP )は、既存の特許を回避するために特別に設計された、拡張パスワード認証鍵交換(PAKE)プロトコルです。 [ 1 ]
すべての PAKE プロトコルと同様に、盗聴者や中間者は、各推測ごとに当事者とさらにやり取りすることなく、パスワードを総当たりで推測したり、辞書攻撃を適用したりするのに十分な情報を取得できません。さらに、拡張 PAKE プロトコルであるため、サーバーはパスワードに相当するデータを保存しません。 [ 2 ]これは、サーバーデータを盗んだ攻撃者が、最初にパスワードの総当たり検索を実行しない限り、クライアントになりすますことができないことを意味します。
簡単に言うと、SRP(またはその他のPAKEプロトコル)認証では、一方の当事者(「クライアント」または「ユーザー」)が、パスワード自体やパスワードを推測できるその他の情報を送信することなく、もう一方の当事者(「サーバー」)に対してパスワードを知っていることを証明します。パスワードはクライアントから外部に送信されることはなく、サーバーにも知られていません。
さらに、サーバーは安全な接続を確立するためにパスワード(ただしパスワード自体ではない)を知っている必要があります。つまり、サーバーはクライアントに対して自身を認証するため、ユーザーが複雑なURLを解析する必要なくフィッシング攻撃を防ぐことができます。
SRP の数学的に証明された唯一のセキュリティ特性は、受動的な攻撃者に対して Diffie-Hellman と同等であるということです。[ 3 ]成熟していて広く普及している SRP ですが、古い設計であり、いくつかの変種には微妙な弱点があります。UC安全ではなく、すべての事前計算攻撃に対する耐性がなく、形式的証明が弱く、特定の最新の攻撃モデルに対する保護を提供しません。これらの理由から、SRP は現在ではほぼ廃止されていると考えられています。OPAQUE は好ましい拡張 PAKE であり、CPace または SPAKE2 は、両者がパスワードを共有するバランスのとれた PAKE シナリオで好まれています。[ 4 ] [ 5 ]
SRPプロトコルには、ユーザーがサーバーに対して自身を認証できる、盗聴者による辞書攻撃に耐性がある、信頼できる第三者を必要としないなど、多くの望ましい特性があります。ユーザーからサーバーへ、ゼロ知識パスワード証明を効果的に伝達します。プロトコルのリビジョン6では、接続試行ごとに推測できるパスワードは1つだけです。このプロトコルの興味深い特性の1つは、使用する暗号プリミティブの1つまたは2つが攻撃されたとしても、依然として安全であることです。SRPプロトコルは何度か改訂されており、現在はリビジョン6aです。
SRPプロトコルは、クライアント側がユーザーパスワードを、サーバー側がパスワードから導出された暗号検証子を持つことを前提として、 Diffie-Hellman鍵交換と同様の方法で、2者間で共有される大きな秘密鍵を作成します。共有公開鍵は、クライアントが生成する乱数とサーバーが生成する乱数の2つから生成され、それぞれログイン試行ごとに固有の値となります。暗号化された通信と認証の両方が必要な場合、SRPプロトコルは代替プロトコルであるSSHよりも安全で、署名付きメッセージを使用したDiffie-Hellman鍵交換よりも高速です。また、 Kerberosとは異なり、第三者に依存しません。
SRPプロトコルバージョン3はRFC 2945で説明されています。SRPバージョン6aは、SSL/TLS [ 6 ] ( TLS-SRP内)やEAP [ 7 ]、SAMLなどの他の標準における強力なパスワード認証にも使用され、 IEEE 1363.2およびISO/IEC 11770-4の一部となっています。
このプロトコル(バージョン6)の説明では、以下の表記法を使用します。
他のすべての変数は、これらを基準として定義される。
まず、サーバー Steve とパスワードpを確立するために、クライアント Carol はランダムなソルトsを選択し、x = H ( s , p ) 、v = g xを計算します。Steve は、 vとs をIでインデックス付けして、Carol のパスワード検証子とソルトとして保存します。Carol はx を誰とも共有してはならず、このステップで安全に消去する必要があります。なぜなら、 x は平文のパスワードpと同等だからです。このステップは、Steve とのユーザー登録の一部としてシステムを使用する前に完了します。ソルトsは後でセッション キーをネゴシエートするために共有および交換されるため、値はどちら側でも選択できますが、Carol がI、s、v を単一の登録要求で登録できるようにするために Carol が行います。登録要求の送信と認証は SRP ではカバーされていません。
そして後日パスワードの検証を行うために、以下の交換プロトコルが実行されます。
これで両者は共通の強力なセッションキーKを持つことになった。認証を完了するには、互いのキーが一致することを証明する必要がある。その方法の一つは以下のとおりである。
この方法では、なりすましを成功させるためには、鍵だけでなく、共有状態のうちより多くの情報を推測する必要がある。追加される状態のほとんどは公開情報だが、サーバーの秘密鍵など、プライベートな情報もハッシュ関数の入力に安全に追加できる。
あるいは、パスワードのみの証明では、 Kの計算を省略し、共有Sを以下のように証明できます。
SRPを使用して、ネゴシエーション後すぐに使用される共有鍵Kをネゴシエートする場合、 M1とM2の検証手順を省略したくなるかもしれません。サーバーは、復号できないクライアントからの最初の要求を拒否します。しかし、これは以下の「実装上の落とし穴」のセクションで示すように危険な場合があります。
両当事者は、以下の安全策も講じている。
""" SRP認証の例警告:テスト以外の実際の暗号化目的で使用しないでください。警告:以下のコードには重要な安全対策が欠けています。A、B、Uがゼロでないことを確認していません。http://srp.stanford.edu/design.html に基づく""" import hashlib import random# 注: str はそのまま変換されます。str([1,2,3,4]) は "[1,2,3,4]" に変換されます。def H ( * args ) -> int : """一方向ハッシュ関数。""" a = ":" . join ( str ( a ) for a in args ) return int ( hashlib . sha256 ( a . encode ( "utf-8" )) . hexdigest (), 16 )def cryptrand ( n : int = 1024 ) : return random.SystemRandom ( ) . getrandbits ( n ) % N# 大きな安全な素数 (N = 2q+1、q は素数) # すべての算術演算は N を法として行われます# (openssl dhparam -text 1024 を使用して生成) N = """00:c0:37:c3:75:88:b4:32:98:87:e6:1c:2d: a3:32: 4b:1b:a4:b8:1a:63:f9:74:8f:ed:2d:8a:41:0c:2f: c2:1b:12:32:f0:d3:bf:a0:24:27:6c:fd:88:44:81: 97:aa:e4:86:a6:3b:fc:a7:b8:bf:77:54:df:b3:27: c7:20:1f:6f:d1:7f:d7:fd:74:15:8b:d3:1c:e7:72: c9:f5:f8:ab:58:45:48:a9:9a:75:9b:5a:2c:05:32: 16:2b:7b:62:18:e8:f1:42:bc:e2:c3:0d:77:84:68: 9a:48:3e:09:5e:70:16:18:43:79:13:a8:c3:9c:3d: d0:d4:ca:3c:50:0b:88:5f:e3"""N = int ( "" . join ( N . split ()) . replace ( ":" , "" ), 16 ) g = 2 # N を法とする生成元k = H ( N , g ) # 乗数パラメータ (従来の SRP-6 では k=3)F = '#0x' # フォーマット指定子print ( "#. H、N、g、k はクライアントとサーバーの両方に事前に知られています:" ) print ( f ' { H = } \n { N = :{ F }} \n { g = :{ F }} \n { k = :{ F }} ' )print ( " \n 0. サーバーはパスワードデータベースに (I, s, v) を保存します" )# サーバーはまずパスワード検証器を生成する必要がありますI = "person" # ユーザー名p = "password1234" # パスワードs = cryptrand ( 64 ) # ユーザーのソルトx = H ( s , I , p ) # 秘密鍵v = pow ( g , x , N ) # パスワード検証器print ( f ' { I = } \n { p = } \n { s = :{ F }} \n { x = :{ F }} \n { v = :{ F }} ' )# 0. サーバーはパスワード データベースに I、s、v を保存します# I = 'person' # p = 'password1234' # s = 0x67bc8932cfd26a49 # x = 0x98a4bce8dde877762a90222f1a1161eba9248590a47eb83aa9e5bd7ecda5368d # v = 0xa7e2038e675d577ac0f318999cab67bba7ec2daf45d2d09f7911b1b78d2fc7f963cd0ac8f17851e0516f059e453672c3b70fcecf5f6843180b271a bdd01f552ccda7b24fe4719336409cbc1352f8517be651b8935cc0b74ff2819fa07a3f031537d4cfd9f8df7b788a5f2f88e1cd4106b35c38b3d7205a# <デモ> --- 停止 ---print ( " \n 1. クライアントはユーザー名 I と公開一時値 A をサーバーに送信します" ) a = cryptrand () A = pow ( g , a , N ) print ( f " { I = } \n { A = :{ F }} " ) # クライアント->サーバー (I, A)# 1. クライアントはユーザー名 I と公開一時値 A をサーバーに送信します# I = 'person' # A = 0x678556a7e76581e051af656e8cee57ae46df43f1fce790f7750a3ec5308a85da4ec4051e5cb74d3e463685ee975a2747cf49035be67c931b56e793 f23ea3524af8909dcfbc8675d872361025bf884778587ac49454a57c53a011ac2be2839bfb51bf7847a49a483aba870dc7a8b467a81cec91b8ae7813# <デモ> --- 停止 ---print ( " \n 2. サーバーはユーザーのソルト s と公開一時値 B をクライアントに送信します" ) b = cryptrand () B = ( k * v + pow ( g , b , N )) % N print ( f " { s = :{ F }} \n { B = :{ F }} " ) # server->client (s, B)# 2. サーバーはユーザーのソルト s と公開一時値 B をクライアントに送信します# s = 0x67bc8932cfd26a49 # B = 0xb615a0a5ea6abf138077bbd869f6a8da37dfc0b7e06a9f5fac5c1e4109c6302cb3e94dcc2cc76da7b3d87d7e9b68a1db998ab239cfde609f3f7a1e ce4a491ce3d9a665c20cf4e4f06730daaa8f52ed61e45bbb67cdc337bf648027ffa7f0f215d5ebe43f9f51832518f1142266aae0dfa960e0082b5154# <デモ> --- 停止 ---print ( " \n 3. クライアントとサーバーがランダムスクランブリングパラメータを計算します" ) u = H ( A , B ) # ランダムスクランブリングパラメータprint ( f " { u = :{ F }} " )# 3. クライアントとサーバーがランダムスクランブルパラメータを計算します# u = 0x796b07e354c04f672af8b76a46560655086355a9bbce11361f01b45d991c0c52# <デモ> --- 停止 ---print ( " \n 4. クライアントがセッションキーを計算します" ) x = H ( s , I , p ) S_c = pow ( B - k * pow ( g , x , N ), a + u * x , N ) K_c = H ( S_c ) print ( f " { S_c = :{ F }} \n { K_c = :{ F }} " )# 4. クライアントがセッションキーを計算します# S_c = 0x699170aff6e9f08ed09a1dff432bf0605b8bcba05aadcaeea665757d06dbda4348e211d16c10ef4678585bcb2809a83c62b6c19d97901274ddafd4075f90604c06baf036af587af8540342b47867eaa22b9ca5e35ac14c8e85a0c4e623bd855828dffd513cea4d829c407137a0dd81ab4cde8a904c45cc # K_c = 0x43f8df6e1d2ba762948c8316db5bf03a7af49391742f5f51029630711c1671e# <デモ> --- 停止 ---print ( " \n 5. サーバーがセッションキーを計算します" ) S_s = pow ( A * pow ( v , u , N ), b , N ) K_s = H ( S_s ) print ( f " { S_s = :{ F }} \n { K_s = :{ F }} " )# 5. サーバーがセッションキーを計算します# S_s = 0x699170aff6e9f08ed09a1dff432bf0605b8bcba05aadcaeea665757d06dbda4348e211d16c10ef4678585bcb2809a83c62b6c19d97901274ddafd4075f90604c06baf036af587af8540342b47867eaa22b9ca5e35ac14c8e85a0c4e623bd855828dffd513cea4d829c407137a0dd81ab4cde8a904c45cc # K_s = 0x43f8df6e1d2ba762948c8316db5bf03a7af49391742f5f51029630711c1671e# <デモ> --- 停止 ---print ( " \n 6. クライアントはセッションキーの証明をサーバーに送信します" ) M_c = H ( H ( N ) ^ H ( g ), H ( I ), s , A , B , K_c ) print ( f " { M_c = :{ F }} " ) # クライアント->サーバー (M_c) ; サーバーは M_c を検証します# 6. クライアントはセッションキーの証明をサーバーに送信します# M_c = 0x75500df4ea36e06406ac1f8a8241429b8e90a8cba3adda3405c07f19ea3101e8# <デモ> --- 停止 ---print ( " \n 7. サーバーはセッションキーの証明をクライアントに送信します" ) M_s = H ( A , M_c , K_s ) print ( f " { M_s = :{ F }} " ) # server->client (M_s) ; クライアントは M_s を検証します# 7. サーバーはセッションキーの証明をクライアントに送信します# M_s = 0x182ed24d1ad2fb55d2268c46b42435d1ef02e0fc49f647c03dab8b2a48b0bd3dサーバーがクライアントからの検証を待たずに暗号化メッセージを送信した場合、攻撃者はハッシュクラッキングと同様のオフライン総当たり攻撃を実行できます。これは、サーバーがソルトとBとともに2番目のパケットで暗号化メッセージを送信した場合、または鍵検証がスキップされ、サーバー(クライアントではなく)が最初の暗号化メッセージを送信した場合に発生します。最初のパケットの直後に、サーバーは共有鍵Kを計算するためのすべての情報を持っているため、これは魅力的な攻撃手段となります。
攻撃の手順は以下のとおりです。
キャロルはxもvも知らない。しかし、任意のパスワードpが与えられれば、彼女は以下を計算できる。
K pは、 p が想定されるパスワードである場合に Steve が使用する鍵です。K pの計算に必要な値はすべて、Carol が制御しているか、Steve からの最初のパケットから既知です。Carol は、パスワードを推測し、対応する鍵を生成し、Steve の暗号化されたメッセージcを復号して鍵を検証することができます。プロトコル メッセージは構造化されている傾向があるため、 cが正しく復号されたことを識別するのは容易であると想定されます。これにより、パスワードをオフラインで復元できます。
スティーブが暗号化メッセージを送信する前に、キャロルが正しい鍵を計算できることを証明するまで待っていれば、この攻撃は不可能だったでしょう。SRPの適切な実装では、攻撃者は鍵の検証ステップを通過できないため、この攻撃の影響を受けません。
2021年、Daniel De Almeida Braga、Pierre-Alain Fouque、Mohamed SabtはPARASITE [ 10 ]という論文を発表し、ネットワーク上でのタイミング攻撃の実践的な悪用を実証した。これは、大きな数のモジュラべき乗の非定数実装を悪用し、特にOpenSSLに影響を与えた。
SRPプロジェクトは1997年に開始されました。[ 11 ] SRP-1のセキュリティホールを修正するための2つの異なるアプローチにより、SRP-2とSRP-3が生まれました。[ 12 ] SRP-3は1998年の会議で初めて発表されました。[ 13 ] SHA1を使用したSRP-3を記述したRFC 2945は2000年に公開されました。[ 14 ]「2対1」推測攻撃とメッセージ順序攻撃を修正したSRP-6は2002年に公開されました。[ 8 ] SRP-6aは、2005年版の公式「libsrp」バージョン2.1.0に登場しました。[ 15 ] SRP-6aは標準規格で以下のように記載されています。
IEEE 1363.2には、離散対数を楕円曲線に置き換えたバリアントである「SRP5」の説明も含まれており、これは2001年にYongge Wangによって提案されたものです。 [ 18 ]また、RFC 2945にあるSRP-3についても説明しています。