暗号化したファイルは、盗み見られる経路で送っても平気です。ところが、それを開けるパスワードは平気ではありません。安全な経路が要る。しかしその経路があるなら、ファイルをそこで送れば済んでしまう。前回はこの堂々巡りで終わりました。

ではどうするか。鍵を渡すのをやめて、錠前のほうを配ります。今日はその仕掛けを作ります。

OpenSSL で手を動かして学ぶ暗号の基礎、第3回は公開鍵暗号です。環境の準備は第0回、行き詰まりの経緯は第2回からどうぞ。

コマンドを打つ前に、今日の発想を3コマにしておきます。

錠前を配り、鍵は隠す。
アリス 秘密鍵=鍵(隠す) 配る … 公開鍵=錠前(誰にでも配る) アリス 秘密鍵=鍵(隠す) 配る … 公開鍵=錠前(誰にでも配る)

錠前は盗まれても平気。できるのは「閉める」だけ。

錠前で閉めて、鍵で開ける。
ボブ アリスの錠前で閉める アリスの鍵で開ける アリス ボブ アリスの錠前で閉める アリスの鍵で開ける アリス

閉めるのは誰でもできる。開けられるのは、鍵を持つアリスだけ。

錠前では、開かない。
開かない ✕ 錠前(公開鍵)では 開かない ✕ 錠前(公開鍵)では

閉めた本人でも、錠前では開けられない。だから錠前は、誰に渡してもいい。

鍵を2つに分ける

共通鍵暗号は、ひとつの鍵で施錠も開錠もしました。だから鍵を渡せなかったのです。渡した鍵が盗まれたら、施錠も開錠も相手の自由になる。

公開鍵暗号は、この鍵を2つに分けます。施錠専用の鍵と、開錠専用の鍵。ペアで作り、施錠専用のほうは世界中に配ってしまいます。盗まれても構いません。その鍵でできるのは施錠だけで、閉めた本人にも開けられない。開くのは、手元に隠してある開錠専用の鍵だけです。

配るほうを公開鍵、隠すほうを秘密鍵と呼びます。

ここで、第0回の教科書の二人に再登場してもらいます。アリスに秘密を送りたい。アリスは鍵ペアを作り、公開鍵だけをこちらに送ってきます。この公開鍵は盗み見られてもよいので、メールで送れます。こちらはそれで施錠して送り返す。開けられるのは秘密鍵を持つアリスだけです。パスワードを運ぶ安全な経路は、最後まで登場しません。

ターミナルは1つしかないので、今日はアリスの役もあなたがやります。

openssl genpkey で RSA 鍵ペアを作る

アリスの秘密鍵から作ります。

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out alice.pem

実行すると、点と記号がしばらく画面を流れます。RSA の鍵の材料は巨大な素数で、乱数を引いては素数かどうか確かめる作業の経過表示です。第0回で打った openssl rand と、ここで再会しています。鍵の素性は乱数です。

できた alice.pem が秘密鍵です。中を覗いてみます。

openssl pkey -in alice.pem -text -noout | head -6
# Private-Key: (2048 bit, 2 primes)
# modulus:
#     00:a0:44:02:97:1d:67:f1:15:f6:a0:99:b5:57:65:
#     a6:1b:70:f8:a9:0e:0c:46:af:5d:b0:bb:ed:16:aa:
#     77:a3:32:79:13:59:1c:82:44:9f:9e:b1:09:66:46:
#     38:50:85:b9:99:68:1b:7b:bc:ff:4f:83:21:95:9e:

2048 ビットの巨大な数と、2つの素数(2 primes)でできている、と書いてあります。第0回の教科書の図で右へ左へ飛び交っていたあの鍵のマークは、中を開くとこの数字の束でした。

次に、このペアの公開鍵を取り出します。

openssl pkey -in alice.pem -pubout -out alice.pub
cat alice.pub
# -----BEGIN PUBLIC KEY-----
# MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoEQClx1n8RX2oJm1V2Wm
# (中略)
# CwIDAQAB
# -----END PUBLIC KEY-----

秘密鍵から公開鍵は取り出せますが、逆はできません。公開鍵から秘密鍵を割り出すには巨大な数を素因数分解する必要があり、現実的な時間では終わらない。この一方通行が RSA の安全性の土台です。

公開鍵で施錠し、秘密鍵で開ける

送りたい秘密を作って、アリスの公開鍵で暗号化します。

printf 'AES の合鍵: kagi-2026-torigoe\n' > message.txt
openssl pkeyutl -encrypt -pubin -inkey alice.pub -in message.txt -out message.enc

wc -c message.txt message.enc
#       33 message.txt
#      256 message.enc

pkeyutl は鍵ペアを使う操作の汎用コマンドで、-pubin は「渡した鍵は公開鍵です」という指定です。33バイトの平文が256バイトの暗号文になりました。

アリス役に交代して、秘密鍵で開けます。

openssl pkeyutl -decrypt -inkey alice.pem -in message.enc
# AES の合鍵: kagi-2026-torigoe

戻りました。では、施錠に使った公開鍵で開けてみます。閉めた本人なら開けられそうなものです。

openssl pkeyutl -decrypt -pubin -inkey alice.pub -in message.enc
# A private key is needed for this operation
# pkeyutl: Error loading key

門前払いです。復号という操作に公開鍵を渡すこと自体を、openssl が受け付けません。閉める鍵と開ける鍵が本当に別物であること、これが今日の10秒です。公開鍵が盗まれても平気な理由が、このエラーメッセージに詰まっています。

RSA 2048 で暗号化できるのは245バイトまで

これで解決かというと、まだ半分です。300バイトのファイルを暗号化してみます。

head -c 300 /usr/share/dict/words > big.txt
openssl pkeyutl -encrypt -pubin -inkey alice.pub -in big.txt -out big.enc
# Public Key operation error
# 805D22FC01000000:error:0200006E:rsa routines:ossl_rsa_padding_add_PKCS1_type_2_ex:data too large for key size:crypto/rsa/rsa_pk1.c:132:

data too large、大きすぎると断られました。RSA 2048 が一度に暗号化できるのは、詰め物の領域を除いた245バイトまでです(手元で試すと245バイトは通り、246バイトでこのエラーになります)。しかも RSA の計算は AES に比べてずっと重い。ファイル本体を運ぶ道具としては、公開鍵暗号は小さすぎて遅いのです。

ただし、大きな荷物は入らない。
報告書(250万バイト) ✕ 入らない 錠前の箱(245バイトまで) 報告書(250万バイト) ✕ 入らない 錠前の箱(245バイトまで)

錠前の箱は小さくて、しかも遅い。荷物そのものを運ぶ道具ではない。

ハイブリッド暗号:AES が本文を、RSA が鍵を運ぶ

そこで、前回の AES と今日の RSA に分業させます。荷物は AES が運び、AES の鍵だけを RSA が運ぶ。

分業する。荷物は共通鍵、鍵だけ錠前。
7fK#q2Zp… 荷物は共通鍵で閉める + 共通鍵だけ錠前で包む アリス 7fK#q2Zp… 荷物は共通鍵で閉める + 共通鍵だけ錠前で包む アリス

重い荷物は共通鍵が運び、その鍵の受け渡しを錠前が守る。HTTPS も、この形。

まず AES 用の鍵を乱数で作り、本文を暗号化します。ここは第2回の道具そのままです。本文には、第1回でも使った約250万バイトの英単語辞書を報告書に見立てて使います。

cp /usr/share/dict/words report.txt
openssl rand -hex 32 > key.txt
openssl enc -aes-256-cbc -pbkdf2 -in report.txt -out report.enc -pass file:key.txt

次に、その鍵ファイルだけをアリスの公開鍵で包みます。-hex 32 で作った鍵ファイルは16進数64文字と改行の65バイトなので、245バイトの制限に余裕で収まります。

openssl pkeyutl -encrypt -pubin -inkey alice.pub -in key.txt -out key.enc

wc -c key.txt key.enc
#       65 key.txt
#      256 key.enc

アリスに送るのは report.enc と key.enc の2つです。受け取ったアリスは、秘密鍵で key.enc を開けて AES の鍵を取り出し、その鍵で本文を復号します。

openssl pkeyutl -decrypt -inkey alice.pem -in key.enc -out key2.txt
openssl enc -d -aes-256-cbc -pbkdf2 -in report.enc -out report2.txt -pass file:key2.txt

手元で試すと、約250万バイトの report.txt が1ビットも欠けずに戻ります(確認は第1回の openssl dgst でどうぞ)。前回「安全に渡す方法がない」と詰んだ AES の鍵が、公開鍵という施錠専用の鍵に包まれて、盗聴されてもよい経路を通って届きました。

この分業は苦肉の策ではなく、標準形です。HTTPS も中身はこの構図で、皆さんが今日この記事を読み込んだ通信も、本文を運んだのは共通鍵暗号、その鍵の合意を守ったのは公開鍵の仕組みでした。詳しくは最終回で組み立てます。

注意点

秘密鍵は、送らない・見せない・置きっぱなしにしない

この仕組みの安全は、秘密鍵が秘密であることにすべて乗っています。alice.pem をメールに添付した瞬間、今日の話は全部崩れます。ファイルの権限は、自分だけが読める状態にしておいてください。

chmod 600 alice.pem

genpkey に -aes256 を足すと、秘密鍵ファイル自体をパスフレーズで暗号化した状態で保存できます。ファイルが盗まれたときの最後の砦になるので、持ち運ぶ鍵には付けてください。

鍵長は 2048 で足りる、実務は楕円曲線暗号へ

この連載は RSA 2048 で進めます。現在の標準的な選択で、当面の用途には足ります。より長く使う鍵なら 4096 という選択もありますが、実務の流れはむしろ、同じ強度をずっと短い鍵で出せる楕円曲線暗号(EC)へ移りつつある。最終回で本物の HTTPS の中身を覗くとき、この系統の名前と再会します。

pkeyutl の生 RSA は学習用

今日のように RSA でデータを直接暗号化するのは、仕組みを見るための教材と思ってください。実務のライブラリは、詰め物(パディング)の方式やその検証まで含めて安全に設計された手順を使います。自分のコードで pkeyutl の手順を再現して本番の秘密を守るのは、第2回の「自作の暗号化を本番に使わない」と同じ理由でやめておくのが賢明です。

錠前は配ってよく、鍵だけ隠す。これが公開鍵暗号の全部です。渡せなかった AES の鍵は公開鍵で包んで送れるようになり、前回名前を付けた鍵配送問題はここで解けました。

ところで、配られた錠前には新しい問題があります。「この公開鍵は、本当にアリスのものか」。届いた錠前が実はイブのすり替えた偽物なら、こちらは律儀にイブ宛てに施錠してしまいます。この問題は第5回まで持ち越しです。

この錠前、本当にアリスの?
アリス ボブ アリスの錠前? すり替え のぞき見 アリス ボブ アリスの錠前? のぞき見 すり替え

届いた錠前が偽物なら、律儀に偽物宛てに閉めてしまう。

次回はその前に、秘密鍵と公開鍵の役割をそっくり逆向きに使います。秘密鍵で作り、公開鍵で確かめる。デジタル署名です。「本人が書いた」を証明します。