OpenSSLコマンドによる公開鍵暗号、電子署名の方法


本エントリは[公開鍵暗号をプログラムで扱う方法のまとめ](http://qiita.com/kunichiko/items/7d4945d8b512ea1a3446)の一環で、OpenSSLコマンドを使ってRSAの公開鍵暗号や電子署名を行う方法をまとめています。

# 知識の前提

- 公開鍵暗号と電子署名の基礎知識
	- ある程度の基礎知識があることを前提としていますので、自信のない方は [公開鍵暗号と電子署名の基礎知識](http://qiita.com/kunichiko/items/ef5efdb41611d6cf7775) を先にお読みください。
- RSA鍵、証明書のファイルフォーマットについて
	- 諸々の操作で鍵や証明書のファイルが出てきますが、それらのファイルのフォーマットについてある程度の理解がある必要があります。自信のない方は [RSA鍵、証明書のファイルフォーマットについて](http://qiita.com/kunichiko/items/12cbccaadcbf41c72735)　を先にお読みください。

# 環境

OpenSSLがインストールされているコンピュータが必要です。macOSや Linux等の UNIX系コンピュータであればほぼ最初からインストールされているとおもいます。Windowsの場合はデフォルトではインストールされていませんが、[OpenSSL for Windows](https://www.openssl.org/related/binaries.html) などをインストールしておいてください。

# 鍵の生成

## 秘密鍵の生成

`openssl genrsa` コマンドを使うとRSAの秘密鍵を生成することができます。

```
> openssl genrsa 1024 > private-key.pem
Generating RSA private key, 1024 bit long modulus
..................++++++
........................................++++++
e is 65537 (0x10001)
```

標準出力出力される鍵はPEMエンコードされているので、ファイルの拡張子も.pemとしています。秘密鍵であることがわかるように、private という文字列をファイル名に入れておくと良いでしょう。

## 公開鍵の生成
RSAでは秘密鍵があればいつでも公開鍵を生成することができます。`openssl rsa` コマンドを使用して先ほど作成した秘密鍵を読み込ませ、-pubout で公開鍵を出力します。

```
> openssl rsa -in private-key.pem -pubout -out public-key.pem
writing RSA key
```

-outform オプションを指定していないので、PEMエンコードされた公開鍵が出力されます。公開鍵であることがわかるようにpublic-key.pem としています。


# 公開鍵暗号を行う

## 公開鍵で暗号化する
公開鍵を使って公開鍵暗号を行うためには ```openssl rsautl -encrypt``` を使います。

```
> openssl rsautl -encrypt -pubin -inkey public-key.pem -in target.txt -out target.encrypted
```

`target.txt` に書かれている元の文章は"hoge"という内容です。

```
> cat target.txt
hoge
```

生成された暗号文のほうは128バイトのバイナリデータになっています。

```
> hexdump target.encrypted
0000000 4e 56 f6 01 2b 09 c4 ac 06 c3 a0 cf 59 cf 1d fd
0000010 da df 57 13 3b 13 d1 d0 4a 40 b1 b4 ce df d4 d1
0000020 2c b6 de 43 1b c5 8e 49 9d a7 ef 22 63 2c 64 d9
0000030 5f 19 54 7b 7f 36 44 2a 9c c9 d0 79 dd 91 c6 f7
0000040 98 2c 90 ba c2 ce e8 25 65 13 59 86 44 fa e3 d8
0000050 1b 59 02 5c 27 ee 4b 19 82 4f e4 fa 7b 49 0a e7
0000060 2c ec 2a ef 51 90 44 cc 77 ff c6 e6 58 13 fc 81
0000070 79 45 5a f1 fa 4d 7a bc 6d 04 ea 7f c1 8f 02 b6
0000080
```

## 秘密鍵で平文化
これを秘密鍵で平文化するには以下のように ```openssl rsautl -decrypt``` を使います。

```
> openssl rsautl -decrypt -inkey private-key.pem -in target.encrypted
hoge
```


## 暗号化によってファイルサイズが増えた？

上の結果をよく見ると、4バイトの平文を暗号化したら128バイトになっていましたね。

ファイルサイズが増えたように見えますが、これは暗号化がブロック単位で行われれるためです。通常、暗号化は鍵のサイズと同じバイト長を１ブロックとして暗号化します。今回は1024ビットの鍵だったので、128バイトを1ブロックとして暗号化が行われました。

実際には128バイトのうち11バイト(88ビット)はPadding要素で、実際の鍵は117バイト(936ビット)分しかありませんので、1ブロックで暗号化できる平文は117バイトまでとなります。


### ブロック単位で暗号化する理由
これはRSAに限りませんが、実用的な暗号ではブロック単位で暗号化がおこなわれます。

一見、1バイトの平文が 1バイトの暗号になる(長さが保存される)暗号化の方が効率が良いように思えますが、長さから内容が推定できてしまうこともあります。たとえば、ある試験を受験したのが以下の5人だったとします。

- Abel
- Ben
- Cameron
- Daniel 
- Eric 

このうちの合格者一人の名前が書かれたファイルを暗号化した時に、そのバイト数が6バイトだったとすると、暗号を解かなくてもそれが Danielであることがわかってしまいます。 

また、1バイトの平文が1バイトの暗号になるということは、暗号の結果は高々256通りしかありません。このような暗号化は次の理由から脆弱です。たとえば'A'という平文を暗号化したら'Z'が得られた場合を考えます。

- 鍵の種類は256種類よりも多いため、鳩ノ巣原理から、異なる秘密鍵から同じ暗号'Z'が生成されることがあります
- 'B'という平文を多数の秘密鍵で暗号化すると、その中に`Z`を生成するものがあるはずです
	- もし、`Z`を生成する鍵が存在しない場合、`Z`を見たときに`B`ではないことがわかってしまいます
- 上記２つから、`Z`を平文化したときに `A`に戻る鍵と `B`に戻る鍵が存在するため、`Z`がもともとどちらを表していたのか、区別がつきません
	- 受け手と送り手でどの鍵を使うかを決めておけば不都合は無いかもしれませんが、このような性質の暗号は電子署名には使えません

### RSAで鍵長以上の長さのデータを暗号化するには?

1024ビットのRSA鍵の場合、1ブロックで暗号化できる最大長は117バイトです。これ以上長いファイルを与えると以下のようなエラーになります。

```
> openssl rsautl -encrypt -pubin -inkey public-key.pem -in large.txt
RSA operation error
19623:error:0406D06E:rsa routines:RSA_padding_add_PKCS1_type_2:data too large for key size:/SourceCache/OpenSSL098/OpenSSL098-52/src/crypto/rsa/rsa_pk1.c:151:
Exit 1
```

大きなファイルを 117バイトずつのブロックに分割してブロックごとに暗号化することも不可能ではありませんが、通常は以下の理由からそのようなことはしません。

- RSAの計算は、非常にコストが高い(時間がかかる)ので、大きなデータに対して繰り返し行うのは非効率
- 117バイトが128バイトになるため、データサイズが 約1.1倍くらいおおきくなる

そのため、大きなデータを公開鍵暗号で送る場合は、次のようにします。

- まずデータ本体をAESなどの他の共通鍵暗号方式で暗号化します(1)
	- 共通鍵は送り手が一時的に生成します
- その共通鍵をRSAで暗号化します(2)
- (1)と(2)を相手に送信します
 
共通鍵は送り手が一時的に生成した鍵を使えば良いため事前に共有しておく必要がありません。そのため、公開鍵暗号のメリットは残したまま共通鍵暗号を使うことができます。

opensslコマンドでも ```openssl enc -aes-256-cbc``` などでAESの暗号化ができるので、大きなファイルはこれを使って二段階に暗号化すれば、上記操作を再現できます。なお、opensslコマンド１回でこれを行う方法はわかりません。```openssl smime``` コマンドを使って S/MIME形式にする方法もあるようですが、あまり詳しく調べていません。

# 電子署名を行う

## 電子署名を生成する

対象となるデータのダイジェスト値を計算しそれを秘密鍵で暗号化すると、その暗号文は電子署名として利用できます。具体的なアルゴリズムの詳細は複雑なので別エントリで解説しますが、OpenSSLコマンドでは、```openssl dgst -sha1 -sign``` などで簡単に電子署名生成することができます。

```
> openssl dgst -sha1 -sign private-key.pem target.txt > sign.sig
```

生成された署名は暗号文であり、バイナリデータです。この中身についての詳細は別エントリで解説します。

```
> hexdump sign.sig
0000000 ac d0 75 ab 62 06 9a 3a 9b c4 c4 91 40 be 9d 64
0000010 58 4d 64 af 75 55 35 4f f3 20 c5 de 34 d2 30 af
0000020 5f db 91 cc 3d 2a ea 86 32 39 7a b0 a3 09 95 ed
0000030 60 dc 91 1b 1e 14 dd b2 78 65 ed 97 17 fa 05 e5
0000040 7e eb 4f 64 99 31 1c 8e d8 91 d6 b5 76 c7 df f8
0000050 6e 1a 32 24 a6 2a 81 5c 52 54 f1 1c de 0a a5 00
0000060 47 37 ee 8e 8b 57 34 ea 21 f5 d5 b0 e0 6c 84 2b
0000070 8b a2 2a e9 94 f1 f6 a7 38 0d 9d e7 3e 61 81 93
0000080
```

## 電子署名を検証する

電子署名の検証も ```openssl dgst -sha1 -verify``` で行えます。

```
> openssl dgst -sha1 -verify public-key.pem -signature sign.sig target.txt
Verified OK
```

異なるファイルに対して verifyするとエラーになります。

```
> openssl dgst -sha1 -verify public-key.pem -signature sign.sig other.txt
Verification Failure
Exit 1
```