暗号化と認証

 暗号化と認証①

 「読まれない」と「相手が本物」は別の話

HTTPS、VPN、電子証明書、公開鍵、秘密鍵、電子署名。

こうした技術は、普段は仕組みをよく知らなくても使えます。ブラウザーはHTTPSで接続してくれますし、VPNも設定が正しければつながります。

ところが、証明書エラーやTLSのハンドシェイクエラーが出た途端、急に何が起きているのか分からなくなります。

そこで、RSAや電子証明書の話へ進む前に、まずもっと根本的なところから整理してみます。

そもそも「安全な通信」とは何を守っているのでしょうか。

安全な通信って何だろう

ロミオからジュリエットへ、インターネットを使ってメッセージを送ることにします。

明日の10時に会いましょう

二人にとっては普通のメッセージですが、通信はインターネットを通ります。途中には、通信を盗み見ようとするティボルトがいるかもしれません。

「安全な通信って何だろう」
ロミオはジュリエットへ送っているつもりでも、インターネット上の通信経路を第三者に観測される可能性があります。

この状態でメッセージをそのまま送れば、ティボルトにも、

明日の10時に会いましょう

と読まれてしまいます。

まず必要なのは、通信の内容を第三者に読ませないことです。

まず「読まれない」ようにする

そこでメッセージを暗号化します。

平文だった、

明日の10時に会いましょう

を、暗号鍵を使って例えば、

8A3F 9C12 7D4E A091 ...

のような暗号文へ変換します。

ティボルトが通信内容を取得できたとしても、適切な暗号鍵を持っていなければ、そこから元の文章を読むことはできません。

このように、情報を読む権限のない第三者へ内容を知られない性質を、機密性(Confidentiality)と呼びます。

暗号化が主に守るものは「機密性」です。

平文
 ↓
暗号化
 ↓
暗号文

通信を見られても、内容までは読ませないための仕組みです。

でも「読めない」と「相手が本物」は別の話

ここで、もう一つ問題があります。

ロミオが通信している相手は、本当にジュリエットなのでしょうか。

仮にティボルトがジュリエットになりすまし、

私がジュリエットです

と言って通信を始めたとします。

ロミオとティボルトの間の通信がどれだけ強力に暗号化されていても、ロミオ自身がティボルトをジュリエットだと信じて秘密を送ってしまえば意味がありません。

暗号化は「第三者に内容を読ませない」ための仕組み。認証は「通信している相手が本当に意図した相手なのか」を確認する仕組みです。

この二つは、似ているようで役割が違います。

暗号化
「第三者に読ませない」
        ↓
      機密性

一方、

認証
「相手は本物なのか」
        ↓
      相手確認

です。

暗号化されているから安全な相手である、とは限りません。

逆に、相手が本物だと確認できても、通信そのものが平文なら途中で盗み見られる可能性があります。

さらに「途中で書き換えられていないか」

もう一つ守らなければならないものがあります。

例えばロミオが、

待ち合わせ:10時

と送ったのに、途中で誰かがデータを書き換え、

待ち合わせ:12時

としてジュリエットへ届けたら困ります。

つまり、内容を秘密にするだけでなく、送信したデータが途中で改ざんされていないことも確認する必要があります。

この性質を完全性(Integrity)と呼びます。

ここまでで、安全な通信には少なくとも次の問題があることが分かります。

機密性
第三者に内容を読ませない

認証
通信相手が本物か確認する

完全性
途中でデータが書き換えられていないか確認する

これらは、それぞれ別の問題です。

C#で共通鍵暗号を実際に動かしてみる

大量のデータを高速に暗号化する方法として、AESなどの共通鍵暗号があります。

共通鍵暗号の特徴は、その名前の通り、暗号化する側と復号する側が同じ秘密鍵を使うことです。

例えば、ロミオとジュリエットが同じ秘密鍵 K を持っているとします。

ロミオはその鍵を使って平文を暗号化し、ジュリエットは同じ鍵を使って復号します。

ロミオ
│
│ 秘密鍵 K
▼
AES-GCMで暗号化
│
│ 暗号文
▼
ジュリエット
│
│ 同じ秘密鍵 K
▼
AES-GCMで復号
共通鍵暗号は高速で便利ですが、通信を始める前に同じ秘密鍵を安全に共有しておく必要があります。

ここまでは図で見てきました。

では、本当に同じ鍵で暗号化と復号ができるのか、C#で確認してみます。

今回はAESを、GCM(Galois/Counter Mode)という方式で使います。

まず共通鍵を作る

C#では、暗号関連の機能を System.Security.Cryptography 名前空間から利用できます。

まず、AESで使う秘密鍵を作ります。

C#
using System.Security.Cryptography;
using System.Text;

byte[] key = RandomNumberGenerator.GetBytes(32);

32バイトなので、

32 × 8 = 256 bit

の鍵です。

この key が、ロミオとジュリエットの両方が持つ共通鍵になります。

当然ですが、この鍵は第三者に知られてはいけません。

ロミオがメッセージを暗号化する

暗号化するメッセージは、ここまで使ってきた、

明日の10時に会いましょう

とします。

まず、文字列をバイト列へ変換します。

C#
byte[] plainText =
    Encoding.UTF8.GetBytes("明日の10時に会いましょう");

AES-GCMでは、秘密鍵のほかに nonce という値を使います。

今回は12バイトのnonceを作ります。

C#
byte[] nonce =
    RandomNumberGenerator.GetBytes(12);

次に、暗号化されたデータを格納する cipherText と、認証タグを格納する tag を用意します。

C#
byte[] cipherText =
    new byte[plainText.Length];

byte[] tag =
    new byte[16];

そして暗号化します。

C#
using var romeoAes =
    new AesGcm(key, tag.Length);

romeoAes.Encrypt(
    nonce,
    plainText,
    cipherText,
    tag
);

これで、

明日の10時に会いましょう
↓
AES-GCM
↓
暗号文

へ変換されました。

暗号文を16進数で表示してみます。

C#
Console.WriteLine(
    Convert.ToHexString(cipherText)
);

実行するたびに鍵やnonceを新しく作っているため表示される値は変わりますが、元の日本語とはまったく異なるバイト列になります。

ティボルトが通信経路の途中でこの暗号文を取得しても、秘密鍵を知らなければ元の文章を読むことはできません。

これが、先ほど見た機密性です。

ジュリエットが同じ鍵で復号する

では、今度はジュリエット側で復号します。

復号したデータを格納する領域を用意します。

C#
byte[] decrypted =
    new byte[cipherText.Length];

そして、暗号化したときと同じ key を使います。

C#
using var julietAes =
    new AesGcm(key, tag.Length);

julietAes.Decrypt(
    nonce,
    cipherText,
    tag,
    decrypted
);

復号されたバイト列を文字列へ戻します。

C#
Console.WriteLine(
    Encoding.UTF8.GetString(decrypted)
);

結果は、

明日の10時に会いましょう

となります。

確かに、

共通鍵暗号では、同じ秘密鍵を使って暗号化と復号を行うことが確認できました。

ロミオ
同じ秘密鍵 K で暗号化
        ↓
      暗号文
        ↓
ジュリエット
同じ秘密鍵 K で復号
        ↓
元の平文

AES-GCMには認証タグもある

ところで、今回のコードには、

byte[] tag =
new byte[16];

という値も登場しました。

これはAES-GCMが使う認証タグ(Authentication Tag)です。

AES-GCMでは、単に内容を暗号化するだけではなく、復号時にこの認証タグを使って、

暗号文が途中で書き換えられていないか

も確認します。

例えばティボルトが暗号文を途中で変更した場合、元の認証タグと整合しなくなります。

つまりAES-GCMでは、

暗号化
第三者に内容を読ませない
↓
機密性

認証タグ
途中で書き換えられていないか確認する
↓
完全性

の両方を扱えます。

ただし、ここでいう認証タグの「認証」と、

通信相手が本当にジュリエットなのか

という相手の認証は別の話です。

認証タグが正しいからといって、通信している相手が本当に意図した人物であることまで証明できるわけではありません。

AES-GCMで使う nonce は秘密情報ではありません。

ただし、同じ鍵で同じnonceを再利用してはいけないという重要な条件があります。

今回は仕組みを確認するため、暗号化のたびに12バイトのnonceを生成しています。

では、その秘密鍵を最初にどう渡す?

ここまでで、C#を使って、

平文
↓
AES-GCMで暗号化
↓
暗号文
↓
AES-GCMで復号
↓
平文

という流れを実際に確認できました。

しかも、認証タグを使えば改ざんも検出できます。

一見すると、これでもう安全な通信ができそうです。

ところが、コードをもう一度見てみます。

一番最初に、こんな行がありました。

byte[] key =
RandomNumberGenerator.GetBytes(32);

ロミオは、この key を使って暗号化しました。

そしてジュリエットも、復号するときに同じ key を使いました。

ここで問題があります。

ジュリエットは、この key をどうやって手に入れるのでしょうか。

今回のC#コードでは、一つのプログラムの中に同じ変数 key があるので簡単です。

しかし実際の通信では、

ロミオのコンピューター

Internet

ジュリエットのコンピューター

という別々のコンピューターで動いています。

ロミオ側で作った32バイトの秘密鍵を、何らかの方法でジュリエット側へ渡さなければなりません。

では、インターネットを使って、

これから使う秘密鍵は

K = A1B2C3D4...

と送ったらどうでしょう。

ティボルトが通信を盗み見ていれば、暗号文ではなく秘密鍵そのものを入手できてしまいます。

そうなれば、その後どれだけAESで暗号化しても、その鍵を使って復号されてしまいます。

では、秘密鍵を暗号化して送ればよいのでしょうか。

しかし、今度は、

その秘密鍵を暗号化するための鍵を、どうやって相手へ渡すのか?

という同じ問題が出てきます。

AESで通信したい
      ↓
同じ秘密鍵が必要
      ↓
秘密鍵を相手へ渡したい
      ↓
平文で送れば盗まれる
      ↓
では暗号化して送る?
      ↓
その暗号化に使う鍵は
どうやって渡す?

これが鍵配送問題です。

共通鍵暗号そのものが弱いわけではありません。
AESは大量のデータを高速に暗号化するために非常に有用です。
問題になるのは、

「そのAESで使う秘密鍵を、通信開始前にどうやって安全に共有するのか」

という部分です。

C#では、AESによる暗号化そのものは数行で書けました。

しかし、実際にコードを書いてみると、そのコードには最初から当然のように key が存在しています。

その key は、どこから来たのか。

この問題を別の方法で扱うために登場するのが、公開鍵暗号です。

そこで公開鍵暗号が出てくる

この問題を扱う方法の一つが、公開鍵暗号です。

公開鍵暗号では、同じ秘密鍵を双方が持つのではなく、数学的に関係した二つの鍵を使います。

公開鍵
他人に渡してよい

秘密鍵
本人だけが持つ

公開鍵は、その名前の通り公開して構いません。秘密鍵は本人だけが保持します。

この二つをまとめて鍵ペアと呼びます。

例えばジュリエットが鍵ペアを持っていれば、

ジュリエットの公開鍵
→ ロミオへ渡してよい

ジュリエットの秘密鍵
→ ジュリエットだけが持つ

という形にできます。

秘密情報そのものを通信経路へ流さずに済むので、共通鍵だけの場合とは違う方法で鍵共有の問題を扱えるようになります。

具体的に公開鍵と秘密鍵をどう使うのかは、次の記事で詳しく見ます。

でも、その公開鍵は本当にジュリエットのもの?

ここでまた別の問題が発生します。

ティボルトがロミオに、

これがジュリエットの公開鍵だ

と言って、実際にはティボルト自身の公開鍵を渡したらどうでしょう。

公開鍵なので、その鍵を知ること自体には何の問題もありません。

問題は、

「その公開鍵が誰のものなのか」

です。

暗号化されていても盗聴できる

ティボルトがロミオとジュリエットの間に入り、双方の公開鍵を自分の鍵へ差し替えたとします。

するとロミオは、

ジュリエットと暗号化通信している

と思っています。

ジュリエットも、

ロミオと暗号化通信している

と思っています。

しかし実際には、

ロミオ
  ↕ 暗号化
ティボルト
  ↕ 暗号化
ジュリエット

という二つの通信になっています。

暗号化されていても、通信相手の公開鍵をすり替えられれば中間者が通信へ介入できます。安全な通信には、暗号化だけでなく認証など複数の仕組みが必要です。

ティボルトはロミオから受け取った暗号文を自分の秘密鍵で復号し、中身を読んだあと、今度はジュリエットとの通信に使う鍵で再び暗号化して転送できます。

ロミオとジュリエットから見れば、どちらの区間も暗号化されています。

それでもティボルトには内容が読めます。

これが、中間者攻撃(Man-in-the-Middle Attack / MITM)の基本的な考え方です。

「暗号化されている」ことと「意図した相手と安全に通信している」ことは同じではありません。
暗号化だけでは、通信相手の正体までは保証できません。

安全な通信はいくつもの仕組みを組み合わせて作る

ここまで来ると、「安全な通信」という一言の中に、いくつもの別々の問題が含まれていることが分かります。

認証
相手は本物か?
        +
鍵交換
安全に通信鍵を共有できるか?
        +
暗号化
第三者に読まれないか?
        +
完全性
途中で書き換えられていないか?
        ↓
安全な通信

どれか一つだけあればよいわけではありません。

普段ブラウザーでHTTPSサイトを開くと、画面上では単に「安全な接続」として見えます。

しかし内部では、相手を確認し、安全に鍵を共有し、その鍵から通信に使う鍵を作り、高速な共通鍵暗号でデータを暗号化し、さらに改ざんも検出する――といった複数の技術が連携しています。

TLSは、一つの暗号アルゴリズムではありません。

安全な通信を成立させるための複数の仕組みをまとめたプロトコルです。

まとめ

今回は、RSAや電子証明書の仕組みに入る前に、「安全な通信では何を守る必要があるのか」を整理しました。

暗号化は重要ですが、それだけでは安全な通信にはなりません。第三者に読まれないこと、通信相手が本物であること、途中でデータが改ざんされていないこと、そして暗号化に使う鍵を安全に共有できること。それぞれは別の問題です。

これから出てくる公開鍵暗号、電子署名、電子証明書、RSA、TLSも、それぞれが何の問題を解決する部品なのかという視点で見ると、かなり整理しやすくなります。

普段、HTTPSやVPNが正常に動いていると、内部の仕組みを意識することはほとんどありません。

しかしエラーが起きたとき、

証明書の検証なのか?
相手の認証なのか?
鍵交換なのか?
暗号方式の交渉なのか?

と問題を分けて考えられるだけでも、見え方は大きく変わります。

「よく分からないけれど動いている」から、一つずつ中身を見ていきます。

次回

次回は、

暗号化と認証②
― 鍵ペア、暗号化、署名、そして公開鍵すり替え

です。

まず、

公開鍵で暗号化
↓
秘密鍵で復号

という公開鍵暗号の使い方を整理します。

そして初学者向けの説明でよく見かける、

秘密鍵で暗号化
↓
公開鍵で復号
= 電子署名

という説明についても、本当にそれでよいのかを掘ってみます。

ここから、暗号化と電子署名は同じ鍵ペアを使っていても目的がまったく違う、という話につなげます。

返信を残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

CAPTCHA