【Golang】公開鍵と共通鍵で暗号化する【rsa, aes, pem, x509, cipher】

前回のプログラムを改良してサーバ/クライアントが送信するメッセージを暗号化してみました。


【Golang】jsonの内容をデコードする - くどはむと猫の窓

 

前回までに作ったプログラムはクライアントが送ったメッセージに" too!"を付けてサーバが送り返すプログラムでした。この通信はTCP通信なので経路上を流れるメッセージは簡単に除くことができます。

試しにクライアントから「hoge」というメッセージを送り、WireSharkを使ってサーバ側が開いているポート番号でやりとりされる内容を覗いてみます。

f:id:kudohamu:20141108030220j:plain

 

f:id:kudohamu:20141108030337p:plain

2つの画像の右下にクライアントから送られた「hoge」とサーバから返された「hoge too!」の文字があります。

このようにTCP通信でやりとりされるメッセージは外部から簡単に見ることが可能です。

そこで今回はこのメッセージを暗号化し、第3者からは簡単に見ることができないようにしてみました。

 

TLS/SSLなどでもお馴染みのハイブリッド暗号方式です。

具体的な手順としては

  1. (開始)
  2. クライアントから接続されたらサーバが公開鍵証明書を送る
  3. クライアントは送られてきた証明書が正しいことを確認する
  4. クライアントがセッション鍵(共通鍵)を生成し、それを公開鍵で暗号化してサーバに送る
  5. サーバは送られてきた「暗号化されたセッション鍵」を秘密鍵で復号化して取り出す
  6. クライアントはメッセージを共通鍵で暗号化し、サーバに送る
  7. サーバは送られてきた「暗号化されたメッセージ」を共通鍵を使って復号化してメッセージを取り出す
  8. サーバは送られてきたメッセージに「 too!」を付けたメッセージを作り、共通鍵で暗号化してクライアントに送る
  9. クライアントは送られてきた「暗号化されたメッセージ」を共通鍵を使って復号化してメッセージを取り出し、標準出力する
  10. (終了)

当然、共通鍵は1回のセッション(開始〜終了)ごとに新しいものを生成します。

 

少し長いですがサーバ側・クライアント側のコードを全部載せます。

サーバ側

 

クライアント側

 

これをWireSharkで見てみると

f:id:kudohamu:20141117134855j:plain

 

f:id:kudohamu:20141117134920j:plain

このようにちゃんと生の文章を見ることができなくなっています。

シェル上ではこのように表示されています。

f:id:kudohamu:20141117165852j:plain

f:id:kudohamu:20141117165837j:plain

 ちゃんと平文で見れてます。

f:id:kudohamu:20141117141507g:plain

 

 

やっていることは上で説明した通りです。

共通鍵にはAES、暗号モードはCTRを使っています。大体はgolang.orgの例を参考にやっています。

 

ブロック暗号モード?初期化ベクトル?という方にはcryptoパッケージ群は少々読み解きにくいかもしれません(僕もそうでした)。

そんな方は下の2つのサイトを読むだけでもパッケージの読みやすさがかなり変わると思います(特に1つ目のサイトは必見です)。

 

私は貧乏学生なのでちゃんとした証明書を発行できるはずもなく、いわゆるオレオレ証明書を使っているので

options := x509.VerifyOptions{
DNSName: config.Server.DNSName,
Roots: roots,
}
 
if _, err := crt.Verify(options); err != nil {
panic("failed to verify certificate: " + err.Error())
}

とDNSNameだけで証明書を検証していますがちゃんとした証明書を使う場合はVerifyOptionsにもっと指定したほうが良いかも。

 

encryptWriteは平文を共通鍵で暗号化してTCPConnにWriteする関数、decryptReadはTCPConnからReadした暗号文を共通鍵で復号して平文を返す関数です。本当は両方ともTCPConnをレシーバにしたかったのですが自分で定義した型でないとレシーバにできないようですね。悲しいです。

Connから連続でReadしたいときなんかもっとうまいやり方がありそうな気がしますが見つからないので一回応答を返してからReadしています。

何か良い方法があれば是非教えて下さい。

それとgolang.orgの例では暗号文の先頭バイトにNonce(初期化ベクトル)を入れていますが、これって「暗号文の先頭バイトにNonceが入っているぞ」ということが知られたらあまり良くない気がしますがどうなんでしょう?

 これもご存知の方、例よりもっと良い方法をお知りの方がいれば教えいただけると嬉しいです。

 

と、crypto以下の色々なパッケージを使ってtls/sslっぽい通信を実装しましたが実はこんなに面倒なことをしなくてもtlsパッケージがあります。大体のことをラップしてくれています。しかし、他の言語にも言えることですがGo言語は特にどれだけ多くのパッケージを知っているかが重要だと思っています。ググラビリティも低いですし。

なので今回はあえてtlsパッケージを使わずに実装してみました。

 

次の目標はDBを使うような改良、あと変数名が長くて真のGolanger(?)Gopherに怒られそうなのでもっと短くします(messageとか)。余力と時間があればtlsパッケージを使った形も書いてみたいです。

 

ʕ◔ϖ◔ʔ