Tech Racho エンジニアの「?」を「!」に。
  • 開発

RSA公開鍵暗号アルゴリズムを理解する

こんにちは、yoshiです。弊社製品の電子書籍ビューアー「超縦書」シリーズの開発に携わっています。

はじめに

電子書籍に限らず、Webを介したコンテンツ産業では、暗号化は切り離すことのできない技術です。ユーザー情報や通信内容の秘匿という点もありますし、著作物がファイル共有や違法アップロードされないように、ファイルそのものを暗号化することを考える必要もあります。

私も多分に漏れずそういう技術に触れることがあるのですが、大抵の場合、HTTPサーバーの設定で鍵ペアの場所を指定するだけだとか、アプリケーションに組み込む場合でも、OpenSSLを使うだけで済んでしまいます。私は作業に関わっていませんが、TechRachoも先日HTTPS移行作業を行っていましたね。

もちろん、一般的にはそれで十分でしょう。暗号化はセキュリティ上もっとも重要な技術なので、誰もが自由に、そして簡単に使えるようになっていなければなりません。

技術的詳細を気にすることなく利用できるのは素晴らしいことでもあるのですが、一方で、そこにはどんなメカニズムがあるのか気になると思います。気になりませんか? 気になりますよね。

公開鍵暗号アルゴリズムの中で最も有名なのはRSA暗号でしょう。

Wikipediaにも詳しい記事が載っているので、読んだことのある方もいるのではないかと思います。

RSA暗号が素因数分解の困難さを利用しているというのはそれなりに知られた話だと思いますが、具体的にどのような計算が行われているのか私は詳しくは知りません。というわけで、ちょっと実際に計算してみようと思います。

と言っても、RSA暗号を普通に使うときと同じような長さの暗号鍵を用意しても、とても人間に把握できる計算にはなりません。鍵長を長くするのは暗号化を破られないようにするための措置ですが、今回は計算を確かめるだけなので、人間に扱えるレベルの数を用意してみようかと思います。

Wikipediaを見ると、

k をセキュリティパラメータとする。

p, q \ (p \ne q)k/2 ビットの素数とし、n = pq とする。e\phi(n) 未満の正の整数で、\phi(n) と互いに素な数とし、d を、\phi(n) を法とした e の逆数( de \equiv 1 \ (\text{mod}\ (\phi(n))) )とする。 ただしここで \phi は オイラーのφ関数で、この場合は \phi(n) = (p - 1)(q - 1) である。d は、e, \phi(n) が既知のときには拡張されたユークリッドの互除法を使えば容易に求まる(de\phi(n) で割った整数商を x とした場合、de + (-x)\phi(n) = 1が成り立ち、かつ e の取り方から \gcd (e\phi(n)) = 1 であるのでこれを解けば良い)。d を秘密鍵とし、n, e を公開鍵とする (p, q が漏れると d が計算で求まるため、pq は安全に破棄すること)

とあります。

数学アレルギーの人は「あ、もういいです」と言ってしまいそうな説明ですね。私も、真面目に考える気がなければこういう説明を見ても、「あとでいいや」と思ってスルーしてしまうことが多々あります。

ともあれ、今回はちょっと真面目に考えてみます。

鍵ペアの生成

k をセキュリティパラメータとする。

いきなり、「セキュリティパラメーター」なる k という変数が出てきました。 k は自然数を表すのに使われることが多いので、多分自然数なんだろうなと思いますが、今のところよく分かりません。
パラメーターと言うからには、外部から与える値なのかな、という気もします。

続けて、

p, q \ (p \ne q)k/2 ビットの素数とし、n = pq とする。

とのことから、なるほど k はどうやら p, q の大きさを決めるための値らしいぞ、ということが見えてきます。 p, q がそれぞれ k/2 ビットの数であるということは、 n = pqk ビットになります。 後の方を見れば n は公開鍵の一部になる事が分かるので、どうやら k というのは鍵の長さを表すようです。

つまり、

openssl genrsa -out key.pem 2048

と、鍵を生成する時に指定する鍵長が k だということですね。

続いて、

e\phi(n) 未満の正の整数で、\phi(n) と互いに素な数とし、d を、\phi(n) を法とした e の逆数( de \equiv 1 \ (\text{mod}\ (\phi(n))) )とする。 ただしここで \phi は オイラーのφ関数で、この場合は \phi(n) = (p - 1)(q - 1) である。

オイラーのφ関数という、また聞き覚えのない関数が出てきましたが、ざっくり言うと、

自然数 n に関して、 n と互いに素な n 未満の自然数の個数を \phi(n) とする

ということらしいです。

n = pqp, q が素数の時、ap ( a = 1, 2, ..., q-1) と、 bq ( b = 1, 2, ..., p-1)n と互いに素でないのですから、

    \[\phi(n) = n - 1 - (p - 1) - (q - 1) = pq -p - q + 1 = (p - 1)(q - 1)\]

ということになるのが分かります。

つまり、 e(p - 1)(q - 1) 未満かつ (p - 1)(q - 1) と互いに素な数から適当に選べば良いようですが、ごちゃごちゃ考えずに素数を一個選べば、常に互いに素だよね、ということで、 65537 が選ばれることが一般的なようです。 65537 という数は 2^{16}+1 なので、累乗の計算量が減るのだとか。

ちなみに、openssl genrsaコマンドでは-3オプションを渡すことで e の値を 3 にすることも可能です。また、 e の値は標準エラー出力から確認することができます。

e=65537 にしてしまうと手で計算するには面倒なので、今回は私も e=3 を使うことにします。

秘密鍵を作ってみる

さて、実際に適当な数を使って、秘密鍵を作ってみましょう。

鍵は小さいほうが手で計算しやすいです。ということで、(p, q) = (3, 5)として、とても小さな鍵にすることにします。この条件で計算すると、

    \[n = 3 \times 5 = 15\]

    \[\phi(n) = (3-1)(5-1) = 8\]

となります。

さて、鍵ペアを作るなら、秘密鍵 d を求めなければなりません。dの求め方も書いてあって、

    \[de \equiv 1 \ (\text{mod}\ (\phi(n)))\]

ということのようなので、

    \[3d \equiv 1 \  (\text{mod} \ 8)\]

となります。暗算でもとりあえず 3 を選べばこの条件は満たせそうだとわかりますが、Wikipediaの記述のように拡張されたユークリッドの互除法を使うと、

    \[1 = 3 - 1 \times 2 = 3 - 1 \times (8 - 2 \times 3) = 3 \times 3 - 1 \times 8\]

となりますので、 de + (-x)\phi(n) = 1 の解は、 e=3, \phi(n)=8 のとき、 (d, x) = (3, 1) となり、 d を求めることができます。

さて、これで鍵ペアが作成できたはずです。

公開鍵は (e, n) = (3, 15) となり、秘密鍵は d = 3 です。

暗号化と復号

というわけで、この鍵ペアを利用して実際に暗号化と復号をしてみます。
Wikipediaを見ると、

  • 暗号化(平文 m から暗号文 c を作成する): c=m^e \ \text{mod}\ n
  • 復号(暗号文 c から元の平文 m を得る): m=c^d \ \text{mod}\ n

と書いてあります。上記の式を見れば分かるように、暗号化も復号も、 n 未満のデータにしか適用できません。今回の場合0~14に対してしか暗号化ができないということになります。したがって、それを超えるサイズのデータは分割して暗号化することになります。

適当に m=3 の場合を考えてみましょう。

    \[c = 3^3 \ \text{mod} \ 15 = 27 \ \text{mod} \ 15 = 12\]

    \[m = 12^3 \ \text{mod} \ 15 = 1728 \ \text{mod} \ 15 = 3\]

となります。なるほど、復号できているようですね。
この程度なら、全部同じように計算してしまえるので、やってみます。

平文 暗号文
0 0
1 1
2 8
3 12
4 4
5 5
6 6
7 13
8 2
9 9
10 10
11 11
12 3
13 7
14 14

こうしてみると、2, 3, 7, 8, 12, 13しか変化していないですね。これでは全く役に立ちません。まあ、この大きさの鍵では全く役に立たないのは当然なのですが。

とは言え、計算式を見てもらえば、 01 の場合はどう頑張っても平文と暗号文が等しくなってしまうことが分かるかと思います。特に、非圧縮データで 0 が連続するというものは珍しくないため、そういったデータを暗号化しようとしても 0 のままになってしまい、意味がありません。

なので、RSA暗号を使う場合、暗号化時にランダムな値でパディングして、復号してからそれを取り去るという操作を行います。そのため、実際に暗号化するブロックサイズはもっと大きなものでなければならず、したがって、 n の値が小さいため暗号化できるデータサイズがそもそも小さい鍵では役に立ちません。

また、暗号化ブロックサイズが小さければ復号テーブルを用意して破ることができてしまうのは当然のことです。

というわけで、破られない暗号鍵を作るには大きな数を用意しなければなりません。
現在、RSAでは2048bit以上の大きさの鍵であれば安全であると言われています。ですが、将来はさらに大きな数でなければ破られるようになってしまうかもしれませんね。

まとめ

今まであまり深く考えたことのなかった暗号化アルゴリズムでしたが、こうして計算してみることで理解が深まったように思います。
公開鍵暗号アルゴリズムには、他にも楕円曲線暗号などがあるので、そちらに関してもそのうち調べてみたいと思っています。


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。