暗号プログラミング Part2

Last modified: 2006/12/29 12:25:34

はじめに

かの有名なP2Pソフト「Winny」の通信内容は、TCP接続確立後の初期パケットの「先頭から3バイト目以降の4バイト」を鍵としたRC4で暗号化されています。しかし、本来、暗号通信を行う場合、ネットワーク上に流れるパケットデータの中に、そのままの状態の共通鍵は入れません。このような実装だと、通信内容を確認しただけで暗号を解読される恐れがあります。Winnyはそういった意味で、通信内容の暗号化に関しては、あまり労力を割かなかったことがうかがえます。

しかし、共通鍵暗号を使用する以上、通信相手と同じ鍵を共有しなければなりません。WinnyはRC4の鍵を乱数で生成しているため、その鍵をどうにかして相手に送る必要があり、そのためにはどうしても鍵を送信しなければなりません。ただ、鍵を送信するということは、通信データを傍受されることで暗号が解読されてしまう危険性を伴います。

これは、不特定多数のコンピュータが相互に通信を確立する可能性があるインターネットという世界に、共通鍵暗号方式があまり向いていないことを意味しています。もちろん、実行ファイル(バイナリ)の中に鍵を入れておけば、送信せずとも暗号通信を行うことは可能ですが、それだとすべての通信で鍵が同じになるため、あまりお勧めできません。

このように、インターネット上で共通鍵暗号のみの暗号通信を行う場合、鍵の送信をどうするかといった問題が発生します。この問題を解決するために、一般的に「公開鍵暗号」が使用されます。公開鍵暗号ならば、相手に公開鍵を送り、その公開鍵でデータを暗号化して、その暗号データを送信元に送ってもらえれば、安全にデータをやり取りできます。もし、最初に共通鍵の交換を行いたいならば、その共通鍵を公開鍵暗号でやり取りして、双方に共通鍵が渡った時点で、共通鍵暗号で暗号通信を行うことができます。こうすることで安全に暗号通信を行えます。もちろん、共通鍵暗号を使わずに、公開鍵暗号だけで以後の通信を行うこともできますが、処理速度に問題が出るため、時と場合に応じて使い分ける必要があるでしょう。

認証

「認証」とは、相手が誰であるかを確認することです。コンピュータやインターネットを利用していると、よくログイン名とパスワードを入力しますが、仮に世界中の人が善人だったなら、パスワードは必要ありません。誰もが善人ですから、他人のアカウントを利用することはないでしょう。よって、パスワードなんて必要ありません。しかし、世の中には、他人に成りすまそうとする悪人がいるために、パスワードという仕組みを導入し、アクセスしてきたユーザが、本当に「本人」であるかどうかを確認しなければなりません。それを「認証」と呼びます。

共通鍵暗号は、鍵を持っている者しか暗号文を作成できませんが、公開鍵暗号は誰もが鍵(公開鍵)を持っています。そして、公開鍵は基本的にすべての人に公開しますから、当然、すべての人が暗号文を作成できます。ということは、公開鍵暗号では、誰かに成り代わって暗号文を作成し、その暗号文を相手に送信することができるのです。

暗号文の送信

田中さんの公開鍵「Tpub」はすべての人に公開されています。よって、この公開鍵を使って、誰もが田中さん宛ての暗号文を作成できます。つまり、送信者を簡単に偽ることができます。しかし、それでは困るので、その対策として公開鍵暗号では、署名と検証という仕組みが存在します。

署名と検証

誰でも暗号文を作成できるため、「自分が確かにそのデータを作成した本人であること」を相手に証明する必要があります。そのために「署名」という技術があります。署名とは、簡単にいうと、「データの作成者が自分であること」と「データが改ざんされていないこと」を証明するデータを付加することです。

例えば、「こんにちは、私は鈴木です」というデータを、相手に送信したいとします。しかし、このデータだけを相手に送信しても、相手はこのデータを送信してきた者が本当に鈴木さんであるかを確認することができません。よって、送信者は「こんにちは、僕は鈴木です」というデータのハッシュ値を「秘密鍵」で暗号化し、その暗号化したデータと、「こんにちは、僕は鈴木です」を相手の公開鍵で暗号化したデータをいっしょに送信します。すると受信者は、この暗号化されたデータを「公開鍵」で復元して、ハッシュ値を求めることができます。そして、このハッシュ値と、「こんにちは、僕は鈴木です」というデータのハッシュ値とを比較することで、本当に鈴木さんから送られてきたものかどうかを確認できるのです。

署名と検証1

署名データは「データの作成者が自分であること」と「データが改ざんされていないこと」を証明するためのものであるため、本人しか作成できないものでなければなりません。よって、自分しか知ることのできない「秘密鍵」を利用し、平文のハッシュ値を暗号化します。

ちなみに、ここでは秘密鍵を利用して「暗号化する」と記述していますが、この暗号文は、公開鍵で復号化できるため、結局のところ誰でも読むことができます。よって、厳密には「暗号化」とは呼べません。しかし、このテキストでは便宜上、これも暗号化という言葉を使わせていただきます。

さて、では今度は、鈴木さんが他人に成りすまそうとした場合の通信の流れを見ていくことにします。

署名と検証2

このように、他人に成りすまそうとしても、鈴木さんの秘密鍵で暗号化されたデータは加藤さんの公開鍵では復号化できないため、結果的に、送信者が偽者であることが分かります。これが署名と検証の仕組みです。

では、実際に署名と検証を行うプログラムを書くことにします。

署名検証プログラム(RSA)

OpenSSLを使い、書名と検証を行うプログラムを作成します。OpenSSLについては「暗号プログラミング Part1」を参照してください。

-----  rsatest2.cpp
#include <stdio.h>
#include <string.h>
#include <openssl/rsa.h>
#include <openssl/objects.h>
#include <openssl/md5.h>

#pragma comment(lib, "libeay32.lib")
#pragma comment(lib, "ssleay32.lib")

int hexoutput(char *first_str, unsigned char *data, int len)
{
    int i;
    printf("%s", first_str);
    for(i=0; i < len; i++)
        printf("%02X", data[i]);
    printf("\n");
    return 0;
}

int tanaka(RSA *myrsa, BIGNUM *Spub_e, BIGNUM *Spub_n, 
           unsigned char *plain, unsigned int plain_len, 
           unsigned char *encrypt, unsigned int *encrypt_len,
           unsigned char *sigret, unsigned int *sigret_size)
{
    RSA *yoursa;
    unsigned char hash[16];
    
    yoursa = RSA_new();
    BN_hex2bn(&(yoursa->e), BN_bn2hex(Spub_e));  // copy public key e
    BN_hex2bn(&(yoursa->n), BN_bn2hex(Spub_n));  // copy public key n
    
    // encryption by suzuki public key
    *encrypt_len = RSA_public_encrypt(plain_len, plain, 
        encrypt, yoursa, RSA_PKCS1_OAEP_PADDING);

    // sign by tanaka private key (MD5 and sign)
    MD5(plain, plain_len, hash);
    RSA_sign(NID_md5, hash, 16, sigret, sigret_size, myrsa);
    
    // print data
    hexoutput("ENCRYPT = ", encrypt, *encrypt_len);
    hexoutput("ENCSIGN = ", sigret, *sigret_size);
    
    RSA_free(yoursa);
    return 0;
}

int suzuki(RSA *myrsa, BIGNUM *Tpub_e, BIGNUM *Tpub_n, 
           unsigned char *encrypt, unsigned int encrypt_len,
           unsigned char *decrypt, unsigned int *decrypt_len, 
           unsigned char *sigret, unsigned int sigret_size)
{
    RSA *yoursa;
    unsigned char hash[16];
    
    yoursa = RSA_new();
    BN_hex2bn(&(yoursa->e), BN_bn2hex(Tpub_e));  // copy public key e
    BN_hex2bn(&(yoursa->n), BN_bn2hex(Tpub_n));  // copy public key n
    
    // decryption by suzuki private key
    *decrypt_len = RSA_private_decrypt(encrypt_len, encrypt, 
        decrypt, myrsa, RSA_PKCS1_OAEP_PADDING);

    // sign by tanaka public key (MD5 and verify)
    MD5(decrypt, *decrypt_len, hash);
    if( ! RSA_verify(NID_md5, hash, 16, sigret, sigret_size, yoursa)){
        printf("verify error\n");
        return -1;
    }
    
    hexoutput("DECRYPT = ", decrypt, *decrypt_len);
    
    RSA_free(yoursa);
    return 0;
}

int main(int argc, char *argv[])
{
    RSA *rsa_t, *rsa_s;
    unsigned char encryptdata[1024], decryptdata[1024], sign[1024];
    unsigned int datalen, encryptlen, decryptlen, signlen;
    
    if(argc < 2){
        fprintf(stderr, "%s <RSA data>\n", argv[0]);
        return 1;
    }
    
    datalen = strlen(argv[1]);
    hexoutput("PLAIN   = ", (unsigned char *)argv[1], datalen);

    // make private key & public key
    rsa_t = RSA_generate_key(256 * 2, RSA_F4, NULL, NULL);
    rsa_s = RSA_generate_key(256 * 2, RSA_F4, NULL, NULL);

    // encrypt and sign by tanaka
    tanaka(rsa_t, rsa_s->e, rsa_s->n, (unsigned char *)argv[1], datalen, 
        encryptdata, &encryptlen, sign, &signlen);

    // decrypt and verify by suzuki
    suzuki(rsa_s, rsa_t->e, rsa_t->n, encryptdata, encryptlen, 
        decryptdata, &decryptlen, sign, signlen);

    RSA_free(rsa_t);
    RSA_free(rsa_s);
    return 0;
}
-----
-----  コマンドプロンプト
C:\>bcc32 -w rsatest2.cpp
Borland C++ 5.6.4 for Win32 Copyright (c) 1993, 2002 Borland
rsatest2.cpp:
Turbo Incremental Link 5.65 Copyright (c) 1997-2002 Borland
C:\>rsatest2 AAAA
PLAIN   = 41414141
ENCRYPT = 530F1153E6C5078B47FCDD737227C1A782D2A878DEA499CF65F086EF046F98522042B1
C6CDE5AFD03346CAA5F4014D73412D280BCA2C3C2A185572FB28487E01
ENCSIGN = 2588A2DF9AA429BEE4A6EEC60E71ACB75FCA1D6A87FF67A89115F2ECC878FD813CE5A9
5DF7862D5E97A82FF1316BC98DD6A192A499B7D34B8F20ECD85A9C9A26
DECRYPT = 41414141
C:\>
-----

rsatest2.cppをコンパイルし実行すると、暗号文と署名データが出力されます。署名データは、サイズが16バイトのデータ(MD5ハッシュ値)を秘密鍵で暗号化しているため、上記のプログラムでは、0x80バイトの固定値になります。暗号文は平文のサイズによって、任意バイト(上記のプログラムでは0x80バイト)ごとに大きくなるため、平文のサイズが4バイト(AAAA)の場合は、残りの0x7Cバイトがパディングされて暗号化されます。

また、プログラムでは、署名を行うためにRSA_sign関数を、検証を行うためにRSA_verify関数を使っています。

-----  RSA_sign関数
int RSA_sign(              // 戻り値は、成功時1、失敗時0
    int type,              // ハッシュタイプ
    unsigned char *m,      // ハッシュデータ
    unsigned int m_len,    // ハッシュデータサイズ
    unsigned char *sigret, // 署名データ格納バッファ
    unsigned int *siglen,  // 署名データサイズ格納バッファ
    RSA *rsa               // 秘密鍵(RSA構造体)
);
-----
-----  RSA_verify関数
int RSA_verify(            // 戻り値は、成功時1、失敗時0
    int type,              // ハッシュタイプ
    unsigned char *m,      // ハッシュデータ
    unsigned int m_len,    // ハッシュデータサイズ
    unsigned char *sigbuf, // 署名データ
    unsigned int siglen,   // 署名データサイズ
    RSA *rsa               // 公開鍵(RSA構造体)
);
-----

typeには、ハッシュタイプを指定します。これは、NID_sha1、NID_ripemd160、NID_md5辺りから選択します。そして、ここで指定したアルゴリズムを用いて計算したハッシュ値を次の引数であるmに渡します。上記のプログラムでは、MD5を使用したため、typeをNID_md5とし、mに16バイトのMD5ハッシュ値を入れています。

あとは、署名データ格納バッファと秘密鍵、もしくは署名データと公開鍵を入れて、署名や検証を行います。検証の成功はRSA_verify関数の戻り値で確認します。検証が成功したら1、何かしらのエラーで失敗したら0が返ります。

OpenSSLを使えば、基本的な暗号知識だけで、本格的な暗号プログラミングを行うことができます。ぜひ活用してみてください。

署名と検証を行うための条件

ここでもう一度、検証について考えてみます。検証を行うためには、相手の公開鍵が必要であることは分かりました。よって、検証を行うためには、どこかから相手の公開鍵を手に入れなければなりません。もちろん、通信の相手からもらってもよいですし、知り合いからもらってもよいのですが、ここでひとつの問題が発生します。それは、「あなたが持っている公開鍵は、本当に相手の公開鍵なのか?」ということです。

もう一度、署名と検証の流れを示します。

署名と検証2

この署名と検証という仕組みでもっとも重要なことは、田中さんが持っている「加藤さんの公開鍵」が、必ず「加藤さんの公開鍵」でなければならないということです。これがもし鈴木さんの公開鍵であったなら、残念ながら検証は成功してしまいます。

いやいや、「加藤さんの公開鍵」と書かれてあるから、当然、加藤さんの公開鍵なんじゃないの? と思われるかもしませんが、田中さんが持っている「加藤さんの公開鍵」が本当に「加藤さんの公開鍵」であるかどうかは、実は誰にも保証できません。もしかしたら、「これは加藤さんの公開鍵だよ」と言われて、鈴木さんの公開鍵を渡されているかもしれません。つまり「公開鍵の受け渡しの時点ですでに相手が偽者であった場合」、当然、検証で偽者だと判断することはできなくなります。

つまり、署名と検証を行う場合、前提条件として、次の2つのことが成り立っていなければなりません。

この2つが成り立っていれば、仮に誰かが他人に成りすましたとしても、検証を行うことで、偽者であることを確認できます。しかし、この2つのどちらかが成り立っていなければ、残念ながら署名と検証はあまり意味を成しません。

では、どうやってこの2つの条件をクリアするのでしょうか? その答えとなるのが、認証局です。

認証局

認証局とは、簡単に言うと「どの公開鍵が誰のものであるか?」を管理してくれるシステムです。そして、認証局は「電子証明書」といういかにも堅苦しいデータを発行してくれます。その電子証明書には「登録された公開鍵」「その公開鍵の持ち主の情報」「発行元の認証局の情報」「発行元の認証局の署名」といったものが記述されています。つまり、認証局が発行した電子証明書には、「公開鍵」と「誰の公開鍵であるか」ということが正確に記述されており、ここ(認証局)から取得した公開鍵は「基本的に」誰のものであるかが証明されていると考えることができます。これによって、「田中さんの公開鍵を鈴木さんが持っていること」そして「鈴木さんの公開鍵を田中さんが持っていること」を証明することができるわけです。

ただ、実は電子証明書は、誰でも発行することができます。とてもお手軽です(ぉぃ。なので、認証局自身も、発行する電子証明書に対して署名を行っています。それが電子証明書に書かれてある「発行元の認証局の署名」です。ということは、この電子証明書に対して、検証を行わなければならないわけですが、検証には、当然、公開鍵が必要です。よって、認証局が発行した電子証明書を検証するために「認証局の公開鍵」を入手しなければなりません。では、この「認証局の公開鍵」は、いったい誰が発行しているのかというと、さらに上位の認証局が存在し、その上位の認証局が発行していたりします。じゃあ、その上位の認証局が発行する電子証明書を検証するためにはどうするのかというと、さらにさらに上位の認証局が発行する公開鍵を手に入れて…、ではさらにさらに上位の認証局が発行する電子証明書を検証するためには……、さらにさらにさらに上位の………、と延々と続くことになります。しかし、それでは困るので、どこかで、認証局自身が公開鍵を発行することになります。つまり、結局、認証局の公開鍵は「上位の認証局」もしくは「その認証局自身」が発行することになります。

あと、実は、私は認証局や公開鍵の信頼性関連の話題について、あまり詳しくありません。というか、ほぼ知りません(^^;。ただ、まぁプログラマなら、認証局というのがあって、そこが公開鍵の信頼性を保っているのだろうというくらいの認識で良いかと思います(ホントか?)。つまり、結局のところ、認証局というある程度信頼できるシステムから受け取った電子証明書(公開鍵)を、信頼できるものと考えて、暗号通信に使用するということだと思います(適当ですみません(汗))。

さいごに

署名と検証を行うことで、データの改ざんと送信者を保証することができる、というのは分かりました。では、WinnyのようなP2Pネットワークにおいて、公開鍵暗号と署名検証というシステムを利用し、送信者を保証することは可能でしょうか?

署名と認証には、相手の公開鍵を自分が持っているという証明が必要です。しかし、現在のP2Pネットワークには認証局という概念が存在しないため、接続完了後、自前で公開鍵を相手に送信する必要があります。しかし、公開鍵を送信する時点で、その通信を中継している者が公開鍵を変更してしまったら、結局、検証はできません。つまり、Winnyを含めた存続のP2Pソフトは、例え、公開鍵暗号で通信したとしても、暗号アルゴリズムさえ特定されてしまえば、以後は、中継者によってその暗号を解読されてしまうことになります。しかし、それでもWinnyを始めとする国産のP2Pソフトウェアの多くは、プロトコルを非公開とし、通信内容にもある程度の暗号化を施しています。海外のP2Pソフトウェアの多くがオープンであるのとは対照的に、日本のP2Pソフトウェアは驚くほど秘匿なネットワークとなっています。この違いが、今後の日本のP2P技術の進化にどう影響するかはわかりませんが、海外とは異なった方向で独自の進化を進めていけば、また面白いP2Pソフトウェアが開発されるかもしれません。

というわけで、今回の記事、楽しんでいただけたなら幸いですが、実は私自身、暗号もP2Pも最近勉強し始めたばかりですので、このテキスト、大いに内容が間違っている可能性があります(^^;。もし間違いを見つけたら、メールもしくは、BBSへ書き込んでもらえると有難いです。

さて、最後になりましたが、ここまで読んでくれて本当にありがとうございます。

では、また会う日まで...


Copyright (C) 2003-2006 Kenji Aiko All Rights Reserved