現在知られている攻撃法に対して、楕円曲線暗号(ECC)は、RSAより短い鍵の長さで同等の強度を実現できる。 言い換えれば、ECCは、RSAより少ないリソースで実装可能である。 したがって、RSAを実装可能な JavaScript に、ECCを実装できないわけがない。
JavaScript自身の整数型の限界である 253 より内側でさえ──つまり積をとってもオーバーフローしない最大26ビット程度(10進7桁)のパラメータでさえ──、 ざっとRSAの100ビットにも匹敵する強度を実現できる(RSA1024ビットがECC160ビットとされている)。 その場合、自力で無限精度演算を実装しなくて良いのだから、実装も容易であり、動作も速い。 JavaScript 自身の整数型の限界近くで、 手元の試験では、Mozilla の上で70ミリ秒で鍵交換を行える。0.1秒未満の一瞬だ。
「ECCの方がRSAより新しく高級だから、JavaScriptなんかでできるわけない」と錯覚しやすいが、 じつは、ある意味、ECCのほうが簡単なのだ。
ただし、ECCの設計と実装は、まるで話が別である。
ECCを設計するとは、そのシステムで使う楕円曲線の各パラメータを定義することである。 おおざっぱに言うなら、 楕円曲線上の点の数が素数になるように(または大きな素因数を含むように)、 有限体の上でうまく楕円曲線を定義することである。 本質的には、楕円曲線の上に乗る点の個数をどうやって数えるか、という問題だ。
現在、楕円曲線暗号系は、一般には、曲線パラメータがあらかじめ定義されて、ハードコーディングされている。 曲線を動的に生成するわけではなく、決まったひとつの曲線のうえで、100人なら100人のユーザが、鍵を動かす。 一般に、「決まったひとつの曲線」を定義する部分と、 曲線が決まってあとで、それを使って暗号通信を行う部分は、別だ。
うまい楕円曲線を見つけるところまではC言語などのより強力な手段を使って、 あとはそれをJavaScriptで運用する形にしても良いであろう。 しかし、10進7桁なら、まだ JavaScript 自身で設計もできる。 以下ではその方法を説明する。
繰り返し強調しておくが、このセクションで考えるのはECCの設計であって、 ECCの運用ではない。 次のセクションで実演するように、このサイズの楕円曲線暗号系の運用は、JavaScript でもきわめて容易であり、 さらにかなりサイズが大きくても容易であろう。 設計が大変だ、ということは、運用が大変だ、ということではない。
Schoof法、SEA法では、Hasseの定理の誤差項を直接決定するが、 そこまで大掛かりにしなくても、 10進7桁に対しては、10進5桁に対する方法を微修正することで、対応できる。
この方法ではさらなる前進は期待できず、無意味で不毛な袋小路の努力のようでもあるが、 意味などもともとない。 JavaScript をぎりぎりまで使い込む限界バトルそれ自体が目的なのだ。
非力な JavaScript でこの問題にいどむため、 まずは MyPrimes テーブルの両端を捨てて真ん中へんだけ残す。 Hasseの誤差項は0を中心に分布していて、最大誤差近くは薄いからである。 このことにより、実際には素数位数である「良い」系が、誤差がたまたま大きすぎたあまりに「悪い」と報告される可能性が出てくる。 本当は素数位数になっている myECC を一度は構成しながら、 「悪い」と誤判定して捨てる可能性はもったいない気もするが、 総合的にはこのほうが時間が節約できる可能性が高いのである。 探しても見つかる可能性が低いところは、最初から探さない、ということだ。 誤判定は常に「厳しめ」に発生するのであって、良いものを悪いと言うことはあっても、 悪いものが良いとされる可能性はない。
ただし、繰り返し二乗法で g_multiplyS_Work テーブルを生成する同じ手間で、 MyPrimes テーブルを総なめにできるので、 あまり両端を捨てすぎてはかえって損をする。 ぎりぎりの端は可能性が薄いので捨てていいと思う。
g_multiplyS_Work テーブルが従来の Work テーブルと違うのは、 g_multiplyS_Work はグローバル変数で一度計算したら使いまわせる、 という点である。せこい話だが、実効がある。 このテーブルを使うには、従来の multiply メソッドの代わりに、_multiplyS メソッドを呼び出す。 _multiplyS は上のグローバル・テーブルが空ではないとそれを使い、空ならば自分でテーブルを生成する。 だから、最初の呼び出しの前には、g_multiplyS_Work.length = 0 でテーブルをクリアすることと、 1回目の呼び出しのときにいちばん大きなスカラー倍を行い、テーブルを最後まで生成してしまうことが重要である。 これを怠ると、前回の計算で使った無関係な作業値を読んでしまったり、大きなスカラーをかけるとき必要な作業値が未定義だったり、 といった不都合が発生する。まあ、下線から始まるメソッドなど、そんなものだ。 MyPrimes テーブルで先頭にいちばん大きい素数が来ているのは、このトリックである。
次が最も本質的なトリックだ。
5桁のときのアルゴリズムを7桁に適用すると、
点を探すところでひどく待たされる。
MyPrimes[i] をかける作業より、点を探す作業がずっと長くなる。
そこで、
検索型の findPoint を使わずに点を自分で構成できるようにする。
使う点を x=0 に限定しよう。すると、
y2 = b (mod modulus)
の形になるから、b の平方根 β を直接求めれば、(0, β) は曲線上の点である。
これで findPoint の手間が一挙に省ける。ただし、b が平方剰余でない場合、
つまり上の二次式が解を持たない場合には、この方法では点を作れないので、点をスカラー倍する判定法が使えない。
いったんは良い曲線を選んでも、確率2分の1で判定不能になる。
しかしその場合に無駄になる時間は一瞬であるのに対して、
findPoint をしないことによる時間の節約は莫大なので、トータルでは、このほうがずっと良い。
このトリックがうまくいくためには、
modulus を ≡3 (mod 4) 型の素数にしておくと、つごうが良い。
1を足すと4で割り切れることを保証しておくのである。そうすれば、平方剰余である b に対しては、その平方根βを
β = ± b(modulus+1)/4
として、直接構成できる。このβの二乗は、確かに
b(modulus+1)/2 ≡ b(modulus-1)/2 × b ≡ b
である。b が平方剰余だからオイラーの規準によって、b(modulus-1)/2 = +1 だ。
ここで7桁の整数 b の大きなべき──指数は 7桁程度の整数である (modulus+1)/4 ──を7桁の法のもとで決めなければならない。 これは JavaScript 自身の組み込み関数ではできないから、RSAのときに用意した Bigint ライブラリを使う。 Bigint ライブラリは実装が手抜きで遅いのだが、とりあえず、この目的には使える。
もうひとつ必要なのが、b が平方剰余かどうか判定する関数である。 ヤコビ記号の計算をコーディングしてしまえばよいであろう。 ちょっと間違っているかもしれないが、だいたい次のようにできると思う。
function isQR(A, B) { var sign = 1; while(1) { // (2/B)をくくりだす var power = 0; while( A % 2 === 0 ) { A = A / 2; power++; } if( power % 2 === 1 ) { var test = B % 8; if( test === 3 || test === 5) sign *= (-1); } if( A === 1 ) return (sign===1)? true : false; // Aは奇数なので、ひっくり返す old_A = A; A = B; B = old_A; if( A % 4 === 3 && B % 4 === 3 ) sign *= (-1); A %= B; } }
以上の方法を使った楕円曲線暗号系の設計作業は次のとおりであり、 2003年では型遅れである Pentium III 850MHz 程度のマシンでも、 10進7桁(約24ビット)のオーダーでは現実に実行できる。 JavaScriptで設計したこの規模の系は、すぐ後で例示する。
手元でやったときには、実際には a を素数(奇数)として、 b として偶数だけを選んだ。合理的な根拠はないのだが、 たまたま10進5桁のテストで、 a が素数だと b が偶数のほうが確率が高かったからだ。 それがたまたまで、素数位数をもたらす b の偶奇がランダムに分布しているとしても、 調べる場所が飛び飛びになるだけで、べつに損をするわけではない。
以上の方法で構成したのが、次の系である。
運用は、10進6桁でも7桁でも5桁と同様にできる。計算量もごくわずかに増加するが、ほとんど変わらない。 10ミリ秒のオーダーである。 しかし全数攻撃に要する時間は、16ビットで1分程度、20ビットで10分程度である。 上記の系は約23ビットであって、これまでの攻撃法でクラックするには、1時間くらいかかりそうだ。 RSAであれば、23ビットはJavaScript自身の試行除法であっという間に鍵が割れてしまう、ということを思えば、 ECCの強さが良く分かるだろう。 そのかわり、ECCはたった23ビットでも、構成に必要な計算量はばかにならない。
以下の実演では攻撃は行わず、正規ユーザの鍵交換だけを行う。計算時間は一瞬だ。
http://www.faireal.net/demo/ecc4_2
HTMLページから読み込んでいるライブラリは、ここにある。大きな間違いがあるかもしれないので注意。
http://www.faireal.net/demo/libecc_0.1a.js
以下のライブラリは運用には関係ない。 設計のとき、べき剰余を計算するのに Bigint.powmod を使っただけである。 このライブラリは特に割り算の実装がふまじめで、全体としてはあまり参考にならない。
http://www.faireal.net/demo/bigint_0_4c.js
追記: もう少しまともな bigint.js v0.5 の開発版。
http://www.faireal.net/demo/bigint0.5/
JavaScript で無限精度演算を簡易的に実装すること自体に興味があるかたは、こちらをみてください。 v0.4 より場合によっては数千倍も高速化されています。
くどいようだが、このメモで苦労したように書いているのは「ECCの設計」であって、 この程度の大きさなら、JavaScript上でECCを運用することそれ自体は造作ない。
たぶん、もっと暗号強度を強くしても、 運用のほうは快適であろう。2桁、3桁……7桁と鍵を伸ばしても10ミリ秒オーダーで桁数の一次式程度なので、 そのまま進めば10進10桁はせいぜい200ミリ秒である。 実際には8桁を超えると、積が 253 を超えてJavaScript自身の整数型には入らなくなるので、 無限精度演算を自力でやる必要があるが、 その手間を考えても、10進10桁くらいは1秒程度でできるのかもしれない。 (内部的には「整数型」ではなく倍精度のIEEE浮動小数点演算だが、 見かけ上、253 までは浮動小数点にcastされない。 つまり、シームレスに誤差のない整数演算を行える。 それを超えると精度が保証されない。)
ただし、そのような曲線をどうやって設計するかはまったくの別問題である。 位数がどうなってもいいのであれば、JavaScriptでも簡単に設計できるが、 位数をきっちり素数に落とすには、上の方法では、もう限界に近い。 すると、設計自体はJavaScript以外の方法を使う、というのが合理的な考えであり、 設計は別の方法を使ってもJavaScript上で運用することには(十分に速いのなら)一応の実用上の意味があると言える。
しかし、ここでやっていることは、もともと実用などというくだらない話ではなく、合理的ですらない。 非力な JavaScript でこれをやってしまうというばかげた話、 そのチープなスリルがすべてなのだから……。 JavaScriptでどうやって大きな楕円曲線の位数を数えられるのか、具体的なめどはまだない。 天下り的に曲線を与えられるのでも、与えられた曲線の点の数が実際に素数であることを検証することは JavaScript でできるので、 とりあえずは、そのくらいで満足すべきだろう。
JavaScript は53ビットなので、26.5ビットの二乗まではぎりぎりオーバーフローしないはずだ。
10進8桁の最後までは行けないが
226.5 = 94906265
までは、組み込み整数型でも行けるはずだ。
すでに23ビットに達しているので、
あと3ビット長くしてもなんぼのものだ、という感じだし、
その限界を超えられるように無限精度演算を自力で実装するのはさほど大変でないことも分かっている。
曲線の設計をJavaScript自身で行えるかどうかは別問題として、
もっと大きなECCをJavaScript上で運用することは、余裕で可能のはずだ。