暗号化について、わかっていなかったことが多かったので、ちょっとだけ勉強してみた。
主に、RSAについて。
備忘録のようなもの。まだ間違っていることは多そうなので、見つけたらご指摘いただけるとありがたい。
鍵の生成について
ここで少し書いたけど、そのときは他のサイトで書いてあることをあまり理解せずに書いていた。
RSAの鍵の生成に必要なのは、以下。
% openssl genrsa -out private.pem 1024
- genrsa は、RSAの鍵を生成すると言うopensslのコマンド
- -out は、出力する鍵ファイル名
- 1024 は、鍵のbit数。(省略すると512)
- 鍵の生成時に、-des3 をつけると、生成された鍵をdes3で暗号化する。des3による暗号化には、パスフレーズを使う。
私が理解していなかったのはこの -des3 の部分で、このオプションをつけると鍵の生成アルゴリズムとして des3 が採用されると思っていたのだが、どうも違うらしい。
-des3 はあくまで生成された鍵を暗号化して保存するかどうかで、生成される鍵自身の強度にはなんら影響を与えない。
だから、apacheのサーバー鍵などで、apache起動時にパスフレーズを聞かれたくない場合には、最初から -des3 オプションなしでサーバ鍵を作ればよい。
ちなみに、上記リンク先で、
% openssl rsa -in server.key -out server_.key
とやるとパスフレーズを外せると書いたが、-des3で鍵を作ってこの作業を行うことと、最初から-des3をつけないことは等価である。
さらに、des3で暗号化されていない鍵に対して、
% openssl rsa -in server.key -des3 -out server_.key
とやると、逆に鍵を暗号化することができる。
genrsa で生成したファイルには、秘密鍵と公開鍵が入っている。
このファイルから、公開鍵のみを取り出すには、
% openssl rsa -in private.pem -out public.pem -pubout -outform PEM
のようにする。以下で述べるような署名、暗号化を行うには、鍵を分離して公開鍵のみを通信相手に配布しておく。
署名と検証について
データが改竄されていないことを証明するために、RSAのキーペアを使って署名と検証を行うことができる。
ただし、データそのものに対してやるとはげしくコストがかかるので、メッセージダイジェストに対して署名を行う。
※ -sha1 の部分は、ダイジェストのアルゴリズムを指定する。-sha1 の他に、-md5, -dss1 など。
暗号化/復号化
RSA を使った暗号化/復号化は、「秘密鍵で暗号化→公開鍵で復号」、「公開鍵で暗号化→秘密鍵で復号」が行える。
openssl を使う場合、rsautl コマンドで実現できるのだが、鍵サイズよりも小さなデータしか扱えない。
じゃあ、元データがでかい場合はどうするかと言うと、元データを小さく分割して暗号化してやれば良いのではないだろうか?
サンプルとして、以下のプログラムを書いてみた。(あくまでサンプルなので、エラー処理とか、リリースの解放とかぜんぜんです)
#include <openssl/ssl.h>
int main(int argc, char *argv[]) {
FILE *keyfp, *orig, *enc, *dec;
EVP_PKEY *private;
RSA *rsa;
char *orig_buf;
char *crypted_buf;
char *decrypted_buf;
int crypted_len;
int decrypted_len;
int size;
int read_len;
time_t t1, t2, t3;
ERR_load_crypto_strings();
keyfp = fopen(argv[1], "r");
if (keyfp == NULL) {
fprintf(stderr, "ファイルが開けません: %s\n", argv[1]);
return 1;
}
private = PEM_read_PrivateKey(keyfp, NULL, NULL, NULL);
if (private == NULL) {
fprintf(stderr, "PEM_read_PrivateKey:%s\n", ERR_error_string(ERR_get_error(), NULL));
return 1;
}
fclose(keyfp);
rsa = EVP_PKEY_get1_RSA(private);
if (rsa == NULL) {
fprintf(stderr, "EVP_PKEY_get1_RSA:%s\n", ERR_error_string(ERR_get_error(), NULL));
return 1;
}
RSA_print_fp(stdout, rsa, NULL);
printf("RSA_size: %d\n", RSA_size(rsa));
orig = fopen(argv[2], "r");
if (orig == NULL) {
fprintf(stderr, "ファイルが開けません: %s\n", argv[2]);
return 1;
}
enc = fopen(argv[3], "w");
if (enc == NULL) {
fprintf(stderr, "ファイルが開けません: %s\n", argv[3]);
return 1;
}
size = RSA_size(rsa);
orig_buf = malloc(size);
crypted_buf = malloc(size);
decrypted_buf = malloc(size);
t1 = time(NULL);
while (read_len = fread(orig_buf, 1, size - 11, orig)) {
crypted_len = RSA_private_encrypt(read_len, orig_buf, crypted_buf, rsa, RSA_PKCS1_PADDING);
if (crypted_len == -1) {
fprintf(stderr, "RSA_private_encrypt:%s\n", ERR_error_string(ERR_get_error(), NULL));
return 1;
}
fwrite(crypted_buf, 1, crypted_len, enc);
}
fclose(orig);
fclose(enc);
t2 = time(NULL);
printf("encrypt: %d\n", t2 - t1);
enc = fopen(argv[3], "r");
if (enc == NULL) {
fprintf(stderr, "ファイルが開けません: %s\n", argv[3]);
return 1;
}
dec = fopen(argv[4], "w");
if (dec == NULL) {
fprintf(stderr, "ファイルが開けません: %s\n", argv[4]);
return 1;
}
while (read_len = fread(crypted_buf, 1, size, enc)) {
decrypted_len = RSA_public_decrypt(read_len, crypted_buf, decrypted_buf, rsa, RSA_PKCS1_PADDING);
if (decrypted_len == -1) {
fprintf(stderr, "RSA_public_decrypt:%s\n", ERR_error_string(ERR_get_error(), NULL));
return 1;
}
fwrite(decrypted_buf, 1, decrypted_len, dec);
}
fclose(enc);
fclose(dec);
t3 = time(NULL);
printf("decrypt: %d\n", t3 - t2);
EVP_PKEY_free(private);
RSA_free(rsa);
return 0;
}
これを、
% gcc test.c -lssl -lcrypto
とコンパイルして、
% ./a.out private.pem 元ファイル名 暗号化されたファイル名 復号化されたファイル名
なんて実行すると、private.pem と元ファイル名から、暗号化されたファイル名、復号化されたファイル名が作成される。
- private.pem は、 -des3 指定していないものしか使えない。(これ、PEM_read_PrivateKey の第3パラメータにコールバックとか指定してみても駄目なんですよね~。暗号化されてる鍵は読み方が違うんだろうな・・・)
- 元ファイルが大きいと、果てしない時間がかかる。
(Pentium3 500MHzのマシンで、14Mのファイルの暗号化に4000秒、復号化に200秒)
果てしなく遅いって言うことは、httpsとかsshとかの暗号は、rsaじゃないんでしょうね~。まだまだ勉強しないと、暗号のことはわからないことばかりです・・・。