opensslコマンドというと、SSL証明書の更新作業の際にこんな感じで一連のコマンドを実行していた記憶があります。
鍵付きの秘密鍵を作成
$ openssl genrsa -des3 -out 鍵付き秘密鍵のファイル名 2048
鍵付きの秘密鍵から鍵なしの秘密鍵を作成
$ openssl rsa -in 鍵付き秘密鍵のファイル名 -out 鍵なし秘密鍵のファイル名
CSRファイルを作る前に現状のCSRファイルの中身を確認
$ openssl req -in CSRのファイル名 -text
鍵なしの秘密鍵からCSRファイルを作成
$ openssl req -nodes -new -key 鍵なし秘密鍵のファイル名 -out CSRのファイル名
ローカルでSSL証明書の有効期限を確認
$ openssl x509 -noout -dates -in SSL証明書のファイル名
HTTPS経由でSSL証明書の有効期限を確認
$ HOST=qiita.com && echo |openssl s_client -connect $HOST:443 -servername $HOST 2>/dev/null |openssl x509 -noout -dates
つい最近、PHPで文字列の暗号化と復号化を実装した際に、「openssl_encrypt」と「openssl_decrypt」という関数を使ったんですが、「openssl_」と関数名についているくらいですから、opensslコマンドでも同様のことができるんだろうなと思い、SSL証明書の更新作業のときにしか使ったことがなかったopensslコマンドのことが気になりはじめてしまい、opensslコマンドを利用した暗号化・復号化のやり方を調べてみました。
今回の実行環境
- macOS Catalina 10.15.7
- Docker 20.10.6
- CentOS Linux 8.3.2011
- OpenSSL 1.1.1g
事前準備
今回はDockerを利用し、CeontOS8の環境下で作業を行うので、コンテナを起動してコンテナの中に入っておきます。
$ docker container run -it --rm centos:8
コンテナに入った状態で、opensslコマンドをインストールします。
作業中にマニュアルを利用するのでman-dbも同時にインストールしておきます。
# dnf install openssl man-db -y
インストールが完了したらバージョンを確認しておきましょう。(2021年7月3日時点の)「centos:8」の環境ではデフォルトで「1.1.1g」というバージョンがインストールされるようです。
(VagrantでCentOS8の環境を構築すると「OpenSSL 1.1.1」(末尾に「g」がつかないバージョン)がインストールされるので、操作している環境によって同じCentOS8でもデフォルトのバージョンが異なる可能性があります)
# openssl version
OpenSSL 1.1.1g FIPS 21 Apr 2020
opensslコマンドとは
さっそくファイルの暗号化といきたいんですが、その前にopensslコマンドの概要について触れておきましょう。
サブコマンドで help
を指定すると以下の3つの分類にわかれて利用できるコマンドが表示されます。
- Standard commands
- SSL証明書の発行時に利用していた
genrsa
、x509
などは「Standard commands」に分類されます。
- SSL証明書の発行時に利用していた
- Message Digest commands
- 「Message Digest commands」は今回利用しませんが、入力に対するハッシュ値を出力する
md5
やsha256
といったサブコマンドがあります。
- 「Message Digest commands」は今回利用しませんが、入力に対するハッシュ値を出力する
- Cipher commands
- 今回利用する暗号化・復号化に関連するサブコマンドは「Cipher commands」に含まれています。
# openssl help
Standard commands
asn1parse ca ciphers cms
・・・省略・・・
ts verify version x509
Message Digest commands (see the `dgst' command for more details)
blake2b512 blake2s256 gost md2
・・・省略・・・
sm3
Cipher commands (see the `enc' command for more details)
aes-128-cbc aes-128-ecb aes-192-cbc aes-192-ecb
・・・省略・・・
zlib
各分類毎に利用できるコマンドを表示するには list
というサブコマンドが準備されています。
# openssl list -help
Usage: list [options]
Valid options are:
-help Display this summary
-1 List in one column
-commands List of standard commands
-digest-commands List of message digest commands
-digest-algorithms List of message digest algorithms
-cipher-commands List of cipher commands
-cipher-algorithms List of cipher algorithms
-public-key-algorithms List of public key algorithms
-public-key-methods List of public key methods
-disabled List of disabled features
-missing-help List missing detailed help strings
-options val List options for specified command
例えば openssl list -commands
を実行すると「Standard commands」で利用できるコマンドが一覧表示されます。
先ほども紹介しましたが、SSL証明書の発行作業で利用していた genrsa
、 rsa
、 req
、 x509
、 s_client
といったコマンドが表示されています。
# openssl list -commands
asn1parse ca ciphers cms
crl crl2pkcs7 dgst dhparam
dsa dsaparam ec ecparam
enc engine errstr gendsa
genpkey genrsa help list
nseq ocsp passwd pkcs12
pkcs7 pkcs8 pkey pkeyparam
pkeyutl prime rand rehash
req rsa rsautl s_client
s_server s_time sess_id smime
speed spkac srp storeutl
ts verify version x509
秘密が書かれたファイルを暗号化
前置きが長くなりましたが、暗号化を行いたい秘密が書かれたファイルを用意します。
# echo '犯人はヤス' > secret.txt
念のため秘密の内容を確認しておきましょう。
# cat secret.txt
犯人はヤス
opensslコマンドで暗号化を行う場合のサブコマンドは2種類の方法があります。
ここでは秘密が書かれたファイルの暗号化(Encrypt)を行いたいので、引数に -e
を指定します。
1. サブコマンド enc
の引数に暗号アルゴリズムを渡す方法。(暗号アルゴリズムの前にハイフンが付いた形式)
# openssl enc -aes256 -e -in secret.txt -out encrypted.txt
2. 暗号アルゴリズムをサブコマンドとして指定する方法。(暗号アルゴリズムの前にハイフンが付かない形式)
# openssl aes-256-cbc -e -in secret.txt -out encrypted.txt
サブコマンド enc
で指定可能な暗号アルゴリズムの一覧を確認するには引数として -list
または -ciphers
を渡します。 -ciphers
は -list
のエイリアスとして機能します。
# openssl enc -list
Supported ciphers:
-aes-128-cbc -aes-128-cfb -aes-128-cfb1
-aes-128-cfb8 -aes-128-ctr -aes-128-ecb
・・・省略・・・
-rc5-ofb -seed -seed-cbc
-seed-cfb -seed-ecb -seed-ofb
または
# openssl enc -ciphers
暗号アルゴリズムをサブコマンドとして渡す場合に指定可能な一覧を確認する方法は openssl list -help
で既に登場していましたが、サブコマンド list
に引数として -cipher-commands
を渡すことで表示されます。
# openssl list -cipher-commands
aes-128-cbc aes-128-ecb aes-192-cbc aes-192-ecb
aes-256-cbc aes-256-ecb aria-128-cbc aria-128-cfb
・・・省略・・・
seed-cbc seed-cfb seed-ecb seed-ofb
zlib
色々と説明しましたが、秘密のファイル「secret.txt」を暗号化して「encrypted.txt」を作成してみましょう。
コマンド実行時に enter 暗号アルゴリズム encryption password
というメッセージが表示されパスワード入力が求められますので、実際に利用する場合には秘密が守れそうなパスワードを入力しましょう。
※引数に -pass
を渡すことでパスワードの指定方法が選択できますが、今回 -pass
は未指定にしてコマンド実行時にパスワード入力が求められる形式で進めることとします。
# openssl enc -aes256 -e -in secret.txt -out encrypted.txt
enter aes-256-cbc encryption password:XXXXXXXXXXXX
Verifying - enter aes-256-cbc encryption password:XXXXXXXXXXXX
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
実行後に WARNING
といった気になるメッセージが表示されていますが、ひとまず無視して暗号化後の「encrypted.txt」の中身を見てみます。
catしてみるとなにやら不穏な表示ですが、これは出力後のファイルがバイナリ形式となっているためです。ファイルへの出力自体は正しく行われているので暗号化には成功しているようです。
# cat encrypted.txt
Salted__?!3*kLuݪn̖?D1ٵ`??[?)9v|??V\Ɩ? ??/
「encrypted.txt」が本当にバイナリ形式なのか不安な場合はfileコマンドで確認しましょう。
fileコマンドはDocker環境のCentOS8にはインストールされていないので、別途インストールが必要です。
# dnf install file -y
引数なしで実行すると親切な表示ですね。
# file encrypted.txt
encrypted.txt: openssl enc'd data with salted password
-i
を指定すると「charset」で文字コードが確認できます。バイナリ形式の場合は「binary」と表示されますので、想定通りバイナリ形式となっているようです。
# file -i encrypted.txt
encrypted.txt: application/octet-stream; charset=binary
暗号化時の気になるメッセージについて
秘密のファイルの暗号化には成功しましたが、暗号化のコマンドを実行した際に以下のような気になるメッセージが表示されていました。
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
deprecated key derivation used.
とのことですが、 -iter
または -pbkdf2
を引数として指定することを推奨しているようです。
ヘルプを見てみると、 -iter
は PBKDF2
を利用する際に指定するオプションのようで数値を渡す必要がありそうです。だとすると -iter
を指定する場合は -pbkdf2
も指定する必要がありそうで、 -iter or -pbkdf2
という表現はなんだかおかしいような気がします。
-pbkdf2
は直訳だと「パスワードベースの鍵導出関数のバージョン2を使用する」だそうです。全然わかりませんね。
# openssl enc -help
・・・省略・・・
-iter +int Specify the iteration count and force use of PBKDF2
-pbkdf2 Use password-based key derivation function 2
ヘルプだと情報が少ないので man openssl
でマニュアルを見てみましょう。
# man openssl
マニュアル内の SEE ALSO
の箇所に enc(1)
という記載があります。サブコマンド enc
については別途マニュアルが準備されているようです。
・・・省略・・・
NAME
openssl - OpenSSL command line tool
・・・省略・・・
SEE ALSO
asn1parse(1), ca(1), ciphers(1), cms(1), config(5), crl(1), crl2pkcs7(1), dgst(1), dhparam(1), dsa(1), dsaparam(1), ec(1), ecparam(1),
enc(1), engine(1), errstr(1), gendsa(1), genpkey(1), genrsa(1), nseq(1), ocsp(1), pkcs12(1), pkcs7(1), pkcs8(1), pkey(1), pkeyparam(1),
pkeyutl(1), prime(1), rehash(1), req(1), rsa(1), rsautl(1), s_client(1), s_server(1), s_time(1), sess_id(1), smime(1), speed(1), spkac(1),
srp(1), storeutl(1), sslpasswd(1), sslrand(1), ts(1), verify(1), version(1), x509(1), crypto(7), ssl(7), x509v3_config(5)
なので enc(1)
のマニュアルを見てみましょう。
# man enc
または
# man 1 enc
または
# man openssl enc
または
# man 1 openssl enc
-iter
と -pbkdf2
に関する記述がありました。
-iter
の回数に応じて暗号化を繰り返し行うという感じでしょうか。この回数が多いほどブルートフォース攻撃への耐性が高くなる(突破するための時間が長くかかる)といった説明のようです。
-pbkdf2
は -iter
で暗号化の回数が指定されていなければ、デフォルトの回数で暗号化を行うとのこと。いや、そのデフォルトの回数って何回。。。
・・・省略・・・
NAME
openssl-enc, enc - symmetric cipher routines
・・・省略・・・
-iter count
Use a given number of iterations on the password in deriving the encryption key. High values increase the time required to brute-force
the resulting file. This option enables the use of PBKDF2 algorithm to derive the key.
-pbkdf2
Use PBKDF2 algorithm with default iteration count unless otherwise specified.
以上のことを踏まえ、疑問は2つです。
-
-iter
はPBKDF2
という鍵導出関数を利用する際に必要なオプションのはずなのに-iter
または-pbkdf2
のどちらかを引数として指定すればOKみたいな記述になっている。 -
-pbkdf2
は-iter
が指定されていない場合はデフォルトの回数で暗号化を行うとのことだが、そのデフォルト回数が不明。
OpenSSLのソースファイルを読んでみる
ということで、マニュアルに載ってなければソースを読むしかないですね。
前回はOpenSSHのソースを読んでみましたが今回はOpenSSLです。まぁ、臆さずにいってみましょう。
「centos:8」の環境では「1.1.1g」というバージョンがインストールされることがわかっているので、OpenSSLのGitHubリポジトリで「1.1.1g」のタグのページを開きます。
ソースファイルのダウンロードを行います。
$ curl -LO https://github.com/openssl/openssl/archive/refs/tags/OpenSSL_1_1_1g.tar.gz
ダウンロードが完了したら展開しておきます。
$ tar zxvf OpenSSL_1_1_1g.tar.gz
$ cd openssl-OpenSSL_1_1_1g
これでソースファイルを読む準備が整いました。
2つの疑問を探る
手がかりは -iter
と -pbkdf2
の2つのキーワードのみなので、ひとまず何も考えずに -iter
をgrepしてみたところ、例の Using -iter or -pbkdf2 would be better.
というメッセージが apps/enc.c
でヒットしました。他はマニュアルページのファイルっぽいので、これは答えに近そうですね。
% grep -n '\-iter' * -r
apps/enc.c:484: "Using -iter or -pbkdf2 would be better.\n");
doc/man1/enc.pod:31:[B<-iter count>]
doc/man1/enc.pod:119:=item B<-iter count>
doc/man1/pkcs8.pod:19:[B<-iter count>]
doc/man1/pkcs8.pod:92:=item B<-iter count>
doc/man1/pkcs8.pod:281: openssl pkcs8 -in key.pem -topk8 -v2 aes-256-cbc -iter 1000000 -out pk8.pem
doc/man1/pkcs8.pod:308:The B<-iter> option was added in OpenSSL 1.1.0.
該当箇所である apps/enc.c
の484行目の前後を見ると大きなヒントがありました。 Using -iter or -pbkdf2 would be better.
というメッセージは変数 pbkdf2
が 1
以外の場合、つまり引数に -pbkdf2
が指定されていない場合に出力されるようです。
463 if (pbkdf2 == 1) {
・
・
481 } else {
482 BIO_printf(bio_err, "*** WARNING : "
483 "deprecated key derivation used.\n"
484 "Using -iter or -pbkdf2 would be better.\n");
・
・
491 }
まず、変数 pbkdf2
の定義元を探ってみます。ファイルの先頭から pbkdf2
を検索してみると enc_main
関数の中の113行目で定義されていました。変数 pbkdf2
の初期値は 0
のようです。そして、114行目には iter
という変数も見つかりました。
92 int enc_main(int argc, char **argv)
93 {
94 static char buf[128];
95 static const char magic[] = "Salted__";
96 ENGINE *e = NULL;
・
・
112 unsigned char *buff = NULL, salt[PKCS5_SALT_LEN];
113 int pbkdf2 = 0;
114 int iter = 0;
変数に値をセットする場合にこのソース内ではどのような書式が標準の形式かわかりませんでしたが、変数が定義されている箇所の周辺を見ると、どうやらこのソースの中では「変数名[空白]=[空白]値」が基本ルールなのではないかと予想されます。ということで pbkdf2 = 1
が行われている箇所をgrepしてみます。すると2箇所該当行が見つかりました。266行目と269行目ということで該当している箇所も近いので2箇所同時に見てみましょう。
$ grep -n 'pbkdf2 = 1' apps/enc.c
266: pbkdf2 = 1;
269: pbkdf2 = 1;
138行目以降ではループしながら渡ってきた各引数に応じて処理を行っているようですが、263〜272行目に答えが書かれていました。
まず263行目の case OPT_ITER:
です。引数に -iter
と数値が指定されていたら266行目で変数 pbkdf2
に 1
をセットしています。
次に268行目の case OPT_PBKDF2:
です。 -pbkdf2
が引数に指定されていたら269行目で変数 pbkdf2
に 1
をセットして、かつ、270行目で変数 iter
が 0
(初期値の状態)つまり引数 -iter
が未指定だったら、次の271行目で変数 iter
に 10000
をセットしています。
138 prog = opt_init(argc, argv, enc_options);
139 while ((o = opt_next()) != OPT_EOF) {
140 switch (o) {
141 case OPT_EOF:
142 case OPT_ERR:
・
・
263 case OPT_ITER:
264 if (!opt_int(opt_arg(), &iter))
265 goto opthelp;
266 pbkdf2 = 1;
267 break;
268 case OPT_PBKDF2:
269 pbkdf2 = 1;
270 if (iter == 0) /* do not overwrite a chosen value */
271 iter = 10000;
272 break;
・
・
281 }
はい、ということで疑問だった Using -iter or -pbkdf2 would be better.
と Use PBKDF2 algorithm with default iteration count
の2つ謎が解けました。
-iter
と -pbkdf2
のどちらかが引数にセットされていたら、いずれの引数も有効となるように補完しあう仕組みとなっており、 with default iteration count
は 10000
だということがわかりました。
ちなみに -iter
は 10000
未満の数字、例えば -iter 1
のように極端に小さな数字を引数に指定した場合でも特に警告が出たりすることはありません。
コード上は変数 pbkdf2
がセットされていれば警告は出力されないので、そりゃそうだという感じではありますが、デフォルト値の 10000
より小さくてもいいんだ、というモヤモヤ感は残りました。
それともう一点、enc_main関数の序盤で const
の定義をしている箇所があるのに、 iter = 10000;
のようにマジックナンバーで値をセットしているところが気になりました。
C言語の定数の書き方はよくわかってないですが、int型に対しても const int 変数名
のような定義はできそうな雰囲気なので、今後デフォルト値が変わる可能性があるかどうかはわかりませんが、 const int default_iter_count = 10000
みたいな定義しておいて、 case OPT_PBKDF2:
で iter = default_iter_count;
と書く方がよいかもなぁとか思いました。
改めて秘密が書かれたファイルを暗号化
めちゃくちゃ遠回りしましたが、改めて deprecated key derivation used.
の警告が出ない形式でファイルを暗号化してみたいと思います。
-pbkdf2
を利用する形式にして -iter
に 99999
を指定してみます。
すると、今回は警告が消えた状態でファイルの暗号化が完了しました。
# openssl enc -aes256 -e -pbkdf2 -iter 99999 -in secret.txt -out encrypted.txt
enter aes-256-cbc encryption password:XXXXXXXXXXXX
Verifying - enter aes-256-cbc encryption password:XXXXXXXXXXXX
秘密が書かれたファイルを復号化
暗号化ができたので、今度は秘密が書かれたファイルを復号化してみましょう。
ファイルの復号化(Decrypt)を行う場合は引数に -d
を指定します。復号化も暗号化の場合と同様に以下の2種類の方法で行えます。
1. サブコマンド enc の引数に暗号アルゴリズムを渡す方法。(暗号アルゴリズムの前にハイフンが付いた形式)
# openssl enc -aes256 -d -in encrypted.txt -out decrypted.txt
2. 暗号アルゴリズムをサブコマンドとして指定する方法。(暗号アルゴリズムの前にハイフンが付かない形式)
# openssl aes-256-cbc -d -in encrypted.txt -out decrypted.txt
復号化のやり方はわかったので実際にやってみましょう。注意点として、暗号化を行った際の -pbkdf2 -iter 99999
の部分は同じ引数を与える必要があります。パスワード入力後に何も表示されなければ復号化に成功しています。
# openssl enc -aes256 -d -pbkdf2 -iter 99999 -in encrypted.txt -out decrypted.txt
enter aes-256-cbc decryption password:XXXXXXXXXXXX
パスワードや引数の値を間違えると、 bad decrypt
というメッセージが表示され復号化に失敗したことがわかります。
# openssl enc -aes256 -d -pbkdf2 -iter 1000 -in encrypted.txt -out decrypted.txt
enter aes-256-cbc decryption password:XXXXXXXXXXXX
bad decrypt
140674163349312:error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt:crypto/evp/evp_enc.c:616:
それではいよいよ復号化されたファイル「decrypted.txt」の内容を確認してみましょう。
# cat decrypted.txt
犯人はヤス
とんでもない秘密を知ってしまいましたね。。。
まとめ
前回のOpenSSHの場合はただただ読むだけだったので非常に疲れましたが、今回は暗号化を行いたいファイルを作って、暗号化して、復号化してみるといった一連の手順を実際にやってみながらだったので、こういうオプションを渡すとこういう風に動くんだなといったライブ感があってとても楽しみながらソースを読むことができました。
SSHやSSLと聞くと、セキュリティ、暗号、難しい、無理となりがちかもしれませんが、今回自分が知りたかった箇所については案外簡単に読むことができました。
この仕事をやっているとドキュメントに書かれていない動作・仕様にぶつかることはよくある話なので、ソースを読む訓練はしておいた方がよいかなぁと思った次第です。
おまけ
今回は短い文字列が記載されたファイルの暗号化を行いました。ファイルサイズとしては3バイト。
コマンドを実行してから暗号化・復号化が完了するまでの時間は一瞬です。
# ls -l secret.txt
-rw-r--r-- 1 root root 16 Jul 3 02:15 secret.txt
ファイルサイズや -iter
の回数によっては暗号化・復号化にもっと時間がかかることが予想されます。
なので、小さなサイズのテキストファイルではなく以下のような約500KBのPDFファイルを準備して、実際にどれくらいの時間がかかるのか試してみました。
# ls -lh 500kb.pdf
-rw-r--r-- 1 root root 501K Jul 3 02:14 500kb.pdf
処理条件
- PC: MacBook Pro
- CPU: Intel Core i7 2.3GHz クアッドコア
- メモリ: 32GB
- 暗号アルゴリズム: aes-256-cbc
- 暗号対象のファイルサイズ: 500キロバイト
- 暗号対象のファイル形式: PDFファイル
-iter
が 10000
回の場合
# time openssl enc -aes256 -e -pbkdf2 -iter 10000 -in 500kb.pdf -out encrypted.pdf
enter aes-256-cbc encryption password:XXXXXXXXXXXX
Verifying - enter aes-256-cbc encryption password:XXXXXXXXXXXX
real 0m1.094s
user 0m0.025s
sys 0m0.007s
-iter
が 100000
回の場合
# time openssl enc -aes256 -e -pbkdf2 -iter 100000 -in 500kb.pdf -out encrypted.pdf
enter aes-256-cbc encryption password:XXXXXXXXXXXX
Verifying - enter aes-256-cbc encryption password:XXXXXXXXXXXX
real 0m1.370s
user 0m0.117s
sys 0m0.007s
-iter
が 1000000
回の場合
# time openssl enc -aes256 -e -pbkdf2 -iter 1000000 -in 500kb.pdf -out encrypted.pdf
enter aes-256-cbc encryption password:XXXXXXXXXXXX
Verifying - enter aes-256-cbc encryption password:XXXXXXXXXXXX
real 0m2.100s
user 0m1.000s
sys 0m0.007s
-iter
が 10000000
回の場合
# time openssl enc -aes256 -e -pbkdf2 -iter 10000000 -in 500kb.pdf -out encrypted.pdf
enter aes-256-cbc encryption password:XXXXXXXXXXXX
Verifying - enter aes-256-cbc encryption password:XXXXXXXXXXXX
real 0m11.103s
user 0m9.782s
sys 0m0.019s
-iter
が 100000000
回の場合
# time openssl enc -aes256 -e -pbkdf2 -iter 100000000 -in 500kb.pdf -out encrypted.pdf
enter aes-256-cbc encryption password:XXXXXXXXXXXX
Verifying - enter aes-256-cbc encryption password:XXXXXXXXXXXX
real 1m39.757s
user 1m38.319s
sys 0m0.015s
-iter
が 1000000000
回の場合
# time openssl enc -aes256 -e -pbkdf2 -iter 1000000000 -in 500kb.pdf -out encrypted.pdf
enter aes-256-cbc encryption password:
Verifying - enter aes-256-cbc encryption password:
real 16m38.381s
user 16m36.887s
sys 0m0.037s
予想通り -iter
の回数を増やすほど時間がかかることがわかりました。
いやしかし、1000000000回を指定するとPCが唸りを上げまくってうるさかったので処理結果が返ってくる頃には壊れちゃうんじゃないかと思いました。
参考URL
- SSL証明書の内容をopensslで確認する
- SSL証明書の有効期限や内容を確認する方法一覧
- [小ネタ]OpenSSLコマンドを使って色々確認する
- opensslコマンドの使い方
- opensslコマンドで簡単なファイル暗号化
- OpenSSLでファイルの暗号化・復号化するための備忘録
- OpenSSLでファイルの暗号化と復号を行う
- openssl ver.1.1.1 で 「Using -iter or -pbkdf2 would be better.」や「bad decrypt」 error
- 暗号化のAES方式とは?ほかの種類との違い・実施方法を解説!
- さらばDES暗号、2023年終了へカウントダウン