16
12

More than 3 years have passed since last update.

opensslコマンドを使って暗号化したり、復号化したり、オプションの意味がわからなくてOpenSSL本体のソースを読んだりした

Last updated at Posted at 2021-07-02

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」というバージョンがインストールされるようです。
VagrantCentOS8の環境を構築すると「OpenSSL 1.1.1」(末尾に「g」がつかないバージョン)がインストールされるので、操作している環境によって同じCentOS8でもデフォルトのバージョンが異なる可能性があります)

# openssl version
OpenSSL 1.1.1g FIPS  21 Apr 2020

opensslコマンドとは

さっそくファイルの暗号化といきたいんですが、その前にopensslコマンドの概要について触れておきましょう。
サブコマンドで help を指定すると以下の3つの分類にわかれて利用できるコマンドが表示されます。

  1. Standard commands
    • SSL証明書の発行時に利用していた genrsax509 などは「Standard commands」に分類されます。
  2. Message Digest commands
    • 「Message Digest commands」は今回利用しませんが、入力に対するハッシュ値を出力する md5sha256 といったサブコマンドがあります。
  3. 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証明書の発行作業で利用していた genrsarsareqx509s_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 を引数として指定することを推奨しているようです。
ヘルプを見てみると、 -iterPBKDF2 を利用する際に指定するオプションのようで数値を渡す必要がありそうです。だとすると -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つです。

  • -iterPBKDF2 という鍵導出関数を利用する際に必要なオプションのはずなのに -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.c484行目の前後を見ると大きなヒントがありました。 Using -iter or -pbkdf2 would be better. というメッセージは変数 pbkdf21 以外の場合、つまり引数に -pbkdf2 が指定されていない場合に出力されるようです。

apps/enc.c#L463-L491
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 という変数も見つかりました。

apps/enc.c#L92-L114
 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行目で変数 pbkdf21 をセットしています。

次に268行目の case OPT_PBKDF2: です。 -pbkdf2 が引数に指定されていたら269行目で変数 pbkdf21 をセットして、かつ、270行目で変数 iter0 (初期値の状態)つまり引数 -iter が未指定だったら、次の271行目で変数 iter10000 をセットしています。

apps/enc.c#L138-L281
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 count10000 だということがわかりました。

ちなみに -iter10000 未満の数字、例えば -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 を利用する形式にして -iter99999 を指定してみます。
すると、今回は警告が消えた状態でファイルの暗号化が完了しました。

# 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ファイル

-iter10000 回の場合

# 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

-iter100000 回の場合

# 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

-iter1000000 回の場合

# 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

-iter10000000 回の場合

# 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

-iter100000000 回の場合

# 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

-iter1000000000 回の場合

# 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

16
12
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
12