🔐

「ED25519」が爆速な理由とその仕組み

に公開
4

Discussion

躍動感のある鹿躍動感のある鹿

楕円曲線暗号について大変よくまとまっている記事なのですが、記事の核心部分である「楕円曲線とはなんぞ」から「割った余りを高速に確認できる」までの節でEd25519と一般的な楕円曲線暗号を混同してしまっていたり、重要な事柄が抜けていたりして、Ed25519の事情とはかなり異なる不正確な記述になっているのではないかと見受けられます。
いくつか、私が気づいた範囲で訂正させていただきます。

Ed25519 に使われている楕円曲線について

「楕円曲線とはなんぞ」の節で提示されている y^2 \equiv x^3 + ax + b (ワイエルシュトラス型)ですが、Ed25519では ax^2 + y^2 \equiv 1 + dx^2y^2 (Twisted Edwards Curve)という特殊な楕円曲線で a = -1, d = -\frac{121665}{121666} としたものを使います(RFC8032 参照)。

一見全然違う式に見えますが、「特殊な」と言う通り、適切な変数変換をしてやるとワイエルシュトラス型に帰着できます。しかし、後述する通り、この Twisted Edwards Curve は加算について一般の楕円曲線とは少々性質が異なり、その点で速度面やセキュリティ面での優位を得ているはずなので、Ed25519 が Twisted Edwards Curve 上の暗号アルゴリズムであることは特筆すべきだと思います。そもそも Ed25519 の Ed は Edwards の Ed です。

ワイエルシュトラス型の楕円曲線上の加算について

「どうやって暗号化してるの?」の節で説明されている楕円曲線上での加算の定義ですが、 P \neq Q という重要な条件が抜けてしまっています。 P = Q の場合、「点 P と点 Q を結ぶ直線」は傾きが定まらないため無限にあることになってしまい、破綻します。計算上もゼロ除算が発生してぶっ壊れます。

記事内で直後に出てくる「k を秘密鍵として、ある点 Pk 回加算した結果 点 G 」の計算ですが、 PP を加算する時点でまさに先程の P = Q のケースとなり破綻します。

幸いにも、修正はそれほど難しくないです。P = Q の場合は、「点 P と点 Q を結ぶ直線」を「点 P で接する曲線の接線」と置き換えることで定義できます。PQ が近づいていった極限をイメージすると、自然な定義ですね。

以上をまとめてゴリゴリ計算すると、

(x_1,y_1) + (x_2,y_2) = \left(\lambda^2 - x_1 - x_2, \lambda(x_1 - x_3) - y_1\right)

となります(加算後の xx_3 と置いてます)。ただし、P \neq Q のとき \lambda = \frac{y_2 - y_1}{x_2 - x_1} (直線 PQ の傾き)、 P = Q のとき \lambda = \frac{3x_1^2 + A}{2y_1} (接線の傾き)と場合分けが必要になります。

Twisted Edwards Curve 上の加算について

Twisted Edwards Curve 上の点 P と点 Q の加算は、P \neq Q のとき、「 x 軸と y 軸に平行な軸を持ち、P, Q, (0, -1) を通る双曲線が曲線と交わるもう一つの点の、 x 軸に対する対称点」を求めることを意味します。P = Q のときは「P, Q, (0, -1) を通る双曲線」の代わりに「P, (0, -1) を通り、 P で曲線に接する双曲線」とします。

一見さっきと同じように思えますが、重要な違いとして、こちらは場合分けが必要ありません。P \neq Q だろうが P = Q だろうが、

(x_1,y_1) + (x_2,y_2) = \left(\frac{x_1y_2+y_1x_2}{1+dx_1x_2y_1y_2} , \frac{y_1y_2-ax_1x_2}{1-dx_1x_2y_1y_2}\right)

という一つの式で計算できます。

(ちなみにセキュリティ的には、場合分けが発生せず、どんな入力もほぼ同じクロック数で計算できることで、サイドチャネル攻撃への耐性を得ることができます。Ed25519の強みの一つです。)

読みやすさのために最適化などがほとんどされていない Ed25519 の Python リファレンス実装では、edwards 関数内でこの計算を愚直に実装していますね。

なお、 2^{255} - 19 を法とする剰余類環での計算なので、この世界での割り算は逆元を掛けるという操作になることに注意してください。 a の逆元とは、 a \cdot a^{-1} \equiv 1 \mod p となるような a^{-1} のことです。

射影座標への変換による高速化

逆元 a^{-1} を求めるには、フェルマーの小定理より a^{-1} \equiv a^{p-2} \mod p であることから、 a^{p-2} \mod p を計算してやればいいです。しかし、繰り返し二乗法でも O(\log(p)) の時間計算量がかかり、この計算においてはネックになるので避けたいです。具体的に見積もると、なんてったって p = 2^{255} - 19 ですから、逆元を1回取るごとに 255 回ループを回す必要がなり、ループの内側ではだいたい 10 命令ぐらいの計算を行い、x, y それぞれ 1 回逆元を取らなければならないので、これだけでざっくり 5000 ステップを超えることになります。これを k 回(Ed25519 の場合、 k は 256 bit です)繰り返すとなると(実際には楕円曲線上の加算の繰り返しにも繰り返し二乗法が使えるので \log(k) = だいたい 256回ですが)、結構重いのです。

そこで、うまく変形して、逆元の計算を消し去ることができれば…と思いますが、なんとその方法があります。

Twisted Edwards Curves
https://eprint.iacr.org/2008/013.pdf

タイトル通り Twisted Edwards Curve を導入したこの論文の6章で、もとの座標 (x, y)x = X / Z, y = Y / Z と対応するように座標を (X, Y, Z) に拡張すると、加算 (X_3, Y_3, Z_3) = (X_1, Y_1, Z_1) + (X_2, Y_2, Z_2) は、乗算10回、自乗1回、定数倍2回、加算7回で計算できるようになることが示されています。

Twisted Edwards Curves Revisited
https://eprint.iacr.org/2008/522.pdf

その後、この論文の3.1でさらに改善されて、もとの座標 (x, y)x = X / Z, y = Y / Z, xy = T / Z と対応するように座標を (X, Y, T, Z) に拡張すると、加算 (X_3, Y_3, Z_3) = (X_1, Y_1, Z_1) + (X_2, Y_2, Z_2) は、乗算9回、定数倍2回(、加算7回)で計算できるようになることが示されています。

実際のライブラリの実装

アルゴリズムの開発者である Daniel J. Bernstein 氏が SUPERCOP というツールキットに Ed25519 のリファレンス実装を収録していますが、ここではリファレンス実装とだいたい同等の最適化がされており、なおかつ GitHub に公開されている OpenSSL の実装を見てみましょう。

https://github.com/openssl/openssl/blob/c8b4a397d066857492c714735d7658cd1e545e81/crypto/ec/curve25519.c#L2025-L2041

Ed25519 の実装を見てみると、前項で取り上げた高速化からさらに a = -1 という特殊性、キャッシュなどで計算回数を削り、乗算4回、加算7回で Twisted Edwards Curve 上の加算を実装しています。
この他にも、255 bit のデータの分割単位を CPU によって 25, 26 bit の組み合わせから 51 bit や 64 bit に変えるとか、入力によらず評価可能な値は前計算して埋め込んでおくとか、様々な泥臭い高速化テクニックが詰まっているのが見て取れます。私も完全に理解しているわけではありませんが、暇なときに眺めてみると面白いでしょう。

さて、記事内でメインを張っている剰余の高速化ですが、これは多倍長整数の剰余の計算の際の一般的なテクニックというべきな気がして、Ed25519 に特有の手法かというと微妙な感じを受けます。
OpenSSL の実装でも fe_mul 関数では記事と同じ考え方で 19 の掛け算が使われていたりして(255 bit からはみ出た項は 2^{255} \equiv 19 を掛けて下位ビットに持ってくる)、確かに役立っていることは間違いないです。ただ、流石に脇役感があるというか、もっと取り上げるべき高速化がたくさんあると思ってコメントさせていただきました。

最後に、超長文のコメント本当にすみません!!!!!!!!!!!!!!!!!!!!!!!!!!! 思わず筆が乗ってしまって、気づいたら朝だよ!!!!!!!!!!!!!!!!

助けてくれ!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

reijireiji

躍動感のある鹿さん

非常に詳細かつ本質的なご指摘、本当にありがとうございます。
貴重なお時間を割いてくださり、深く感謝しております。

コメントを読んで、自分の理解がいかに表層的であったかを痛感させられました。
私は、ed25519の高速化はビットシフトによって作られているものだと思っていて、実はそれ自体は一般的な算術手法であり、メインのロジックは素数の選択が、エドワーズ曲線の公開パラメータや逆元計算を回避する工夫といった、システム全体の設計と密接に連携しているとは思い至っていませんでした。
そして何より、P=QP \neq Q の条件分岐をなくすことのセキュリティ上の重要性(サイドチャネル攻撃への耐性)については、全くの盲点でした。
Ed25519が「なぜ速いのか」という点と、「なぜ安全なのか」という点が、すべて一つの洗練された設計思想に集約されていくことに気づき、深く感銘を受けています。

ご指摘いただいた論文や実際のコードにも目を通し、この素晴らしい設計思想をより深く理解して、記事をより良いものにしていきたいと強く思いました。今回は、私の視野を大きく広げてくれる、本当に価値のあるご指摘をありがとうございました。

Yoshiya HinosawaYoshiya Hinosawa

この法則を使って 550 bitの大きい数字 SS を pp で割った余りを求める方法について考える。

510 bit?

reijireiji

ありがとうございます!
修正しました!