【Go】Sodiumで認証付き公開鍵暗号

この記事は Go 2 Advent Calendar 2020 の23日目の記事です。

Sodiumとは

Sodium[1]は使いやすさを目的に開発された暗号学ライブラリです。 暗号の誤用に関する研究分野では、誤りの検出や復元のため技術とそれに伴う影響が議論されています。 一方で誤用そのものを防ぐような方法についての議論は比較的少数です[13]。 Sodiumは使いやすさからこの課題を解決するライブラリの一つです。

また使いやすさだけでなく、portable、cross-compilable、installableであることも謳われています。 awesome-cryptography[2]で検索してみた所、以下のように多様な言語で利用することができます。

C

C#

JavaScript

Java

PHP

Rust

Swift

GoでSodiumを使う

GoでもSodiumを扱うことができます。packageはnacl/ · pkg.go.devです。[1][14]にあるようにSodiumはNaClのforkで、Goはnacl packageとして準標準ライブラリに用意されています。

今回はnacl/boxが提供している、公開鍵を使ったメッセージの認証と暗号化を試してみようと思います。内容は[16]をベースとしています。

実装に移る前にSodiumの特徴や注意点をいくつか述べておきます。
まず、メッセージは暗号化されますが、メッセージの長さは秘匿されません。暗号文の長さから元の平文が推測できてしまうようなアプリケーションには向きません。
また、Sodiumではメッセージにnonceを付与します。メッセージごとに異なるnonceを用いることは呼び出し側の責任です。
その他の注意点としては、[16]にも書かれていますが、メッセージ全体を処理するためにメッセージをメモリ上に保持する必要があるため、小さなメッセージが推奨されています。なお、暗号化のオーバーヘッドも存在しますが、8KB程度のメッセージでは十分に償却されるとのことです。

では本題の実装に入っていきましょう。 今回はclientからserverを送信するメッセージの暗号化を行います。

まず、GenerateKeyを用いて、鍵ペアを生成します。 認証のためclient、serverの両方で鍵ペアを生成します。

// client
clientPublicKey, clientPrivateKey, err := box.GenerateKey(crypto_rand.Reader)
if err != nil {
    panic(err)
}

// server
serverPublicKey, serverPrivateKey, err := box.GenerateKey(crypto_rand.Reader)
if err != nil {
    panic(err)
}

次にメッセージに付与するnonceを生成します。nonceの長さは24 bytesです。 nonceの使い回しは厳禁です。メッセージごとに生成しましょう。

var nonce [24]byte
if _, err := io.ReadFull(crypto_rand.Reader, nonce[:]); err != nil {
    panic(err)
}

いよいよ暗号化です。暗号化にはSealを用います。 暗号化対象のメッセージをmsg := []byte("sodium msg")とし、認証付きで公開鍵暗号を施します。

encrypted := box.Seal(nonce[:], msg, &nonce, serverPublicKey, clientPrivateKey)

clientの秘密鍵clientPrivateKeyだけでなく、serverの公開鍵serverPublicKeyを渡していることに注意してください。 復号にはサーバの秘密鍵が必要であるとともに、clientの公開鍵によってclientからのメッセージであることを認証します。

ちなみにnonceは暗号文の先頭に付与されます。 筆者の環境で実行した結果は以下のようになりました。 encryptedの先頭24 bytesがnonceとなっていることがわかります。

nonce:     [176 240 87 14 190 92 224 60 245 16 119 163 71 18 1 177 57 118 139 46 141 4 99 117]
encrypted: [176 240 87 14 190 92 224 60 245 16 119 163 71 18 1 177 57 118 139 46 141 4 99 117 65 153 138 25 206 247 18 42 0 162 85 88 223 68 203 63 241 28 35 232 242 176 184 56 97 177]

また上記のlen(encrypted)は50 bytesです。 msgsodium msg)が10 bytesなので、nonceの24 bytesを差し引きいても16 bytesのオーバーヘッドがあります。このオーバーヘッドは認証のために付与されたもので、暗号文とはオーバーラップしない設計となっています。

では復号してみましょう。 実際のアプリケーションではclientからserverへencryptedが送信されてからの復号となりますが、ここではserverで受信済みとして先に進みます。

Sealされたメッセージの復号にはOpenを用います。 Openシグネチャは以下のようになっています。

func Open(out, box []byte, nonce *[24]byte, peersPublicKey, privateKey *[32]byte) ([]byte, bool)

2つ目の引数に注目いただきたいのですが、nonceが必要です。 encryptedの先頭24 bytesがnonceになっていたことを思い出し、encryptedから取り出します。

var decryptNonce [24]byte
copy(decryptNonce[:], encrypted[:24])

必要な引数がすべて揃ったので、serverの秘密鍵serverPrivateKeyとclientの公開鍵clientPublicKeyを用いてOpenを実行します。

decrypted, _ := box.Open(nil, encrypted[24:], &decryptNonce, clientPublicKey, serverPrivateKey)

正しいserverの秘密鍵とclientの公開鍵があれば、認証と復号に成功し、元のmsgと同一のdecryptedを得ることができます。

まとめ

Sodiumの紹介と、Goで認証付き公開鍵暗号の暗号化/復号を試してみました。認証付きで公開鍵暗号を使えるので正しい相手から受信したことを保証したい場合などに有用でしょう。また利用できる言語が多いことも魅力的ですね。筆者はRustとjsのcompatibilityを手元でも動作確認しました。機会があれば、いつか記事にしたいと思います。Goとのcompatibilityも試してみたいですね。

また、同じようなプロジェクトは他にもMonocypher[17]、Themis[18]、Tink[19]もあるようです。特にTinkはGoogleのプロジェクトであることからも気になっています。APIのインターフェースが違うようなので、使い勝手といった視点も含めて触っておきたいところです。

References