SSH接続の秘密鍵を可能な限り安全に暗号化する方法を調査しているときに、次の非常に基本的な理解の問題に遭遇しました(注:OpenSSLの最新の安定版リリース1.0.2hを使用して研究しています):
man enc
encユーティリティはGCMのような認証された暗号化モードをサポートしないことを示します。一方、OpenSSLを使用して接続を暗号化するWebサーバーでECDHE-RSA-AES128-GCM-SHA256のようなTLS 1.2暗号を使用しており、SSL/TLS経由で接続するときに、これらの暗号が実際にネゴシエートされることを確認しましたそれぞれのウェブサイト。
したがって、OpenSSLは明らかにAESxxx-GCMモードでデータを暗号化できます。 OpenSSLがSSL/TLS機能をWebサーバーに提供しながらデータを暗号化するために使用するのと同じように、OpenSSLがopenssl enc
経由で呼び出されたときに同じコードを使用してデータを暗号化すると予想しました。
私は何かを誤解しましたか? OpenSSLがTLS/SSLを実行しているときにAESxxx-GCMをサポートしているが、コマンドラインから呼び出されたときにその方法でデータを直接暗号化できない理由は何ですか?
使用可能な暗号名(暗号スイートではない)を表示する一般的な方法は、間違ったパラメーターを使用してopenssl enc
を呼び出すことです。 openssl enc --help
。これにより、opensslは利用可能な暗号名を含む短いヘルプを表示します。私の場合、出力には複数のAES-GCM暗号が含まれています。なぜopenssl enc
はそれらの暗号をサポートすると主張しているのに、それぞれのmanページはそうではないと主張しているのですか?
OpenSSLは、ほぼ1ダースの対称暗号と数ダースの暗号モードの組み合わせを実装していますが、それらすべてに(ほぼ)単一のインターフェースを提供していますEVPモジュール内(つまり、EVP_
で始まる外部関数と型名) ここにオンラインで文書化 または(クロスリンクされた)マニュアルページOpenSSLがインストールされているUnixyシステムのEVP_{Cipher,Encrypt,Decrypt}*
およびEVP_CIPHER_*
およびEVP_CIPHER_CTX_*
の場合。 (OSまたはディストリビューションのパッケージマネージャーまたは同等のものを使用するのではなく、自分でビルド/インストールした場合は、MANPATHまたは他のman
オプションを使用してマニュアルページを見つける必要がある場合があります。)同様に、多数のダイジェスト/ハッシュアルゴリズム公開鍵(およびハイブリッド)の暗号化および署名アルゴリズムには、汎用インターフェースを介してアクセスします。 man evpを参照 。ただし、AEADモード(GCMおよびCCM、および1.1.0で計画されているOCB)は、ジェネリックAPIおよびrequire追加 'control'呼び出し。上記のmanページの「GCMおよびOCBモード」および「CCMモード」のセクションを参照してください。
SSLおよびTLSモジュール(ソースの最上位ディレクトリssl/
)には、同じように(実装された)AEADスイートに対して異なるEVP
呼び出しを行うt1_enc.c
内のコードが含まれていますまた、CBCとストリーム暗号のバリエーション、廃止されたがまだコード化された「エクスポート」暗号、TLS1.1と以前のIVの処理、その他のプロトコルオプションとバリエーションも処理します。
ただし、コマンドラインenc
in apps/enc.c
は、ジェネリックインターフェイスのみを使用し、AEADスペシャルは使用しませんが、 リクエストトラッカーに未割り当てのエントリ(ログインゲスト/ゲスト) を追加しますこの。コマンドラインユーティリティは、一般に、libsslとlibcryptoの機能に関するかなり最小限のラッパーであり、完全で洗練された便利なものなどが必要な場合は、それらを変更または置換する必要があります。この場合、タグ、および場合によってはAADを暗号文ファイル形式で処理する方法を定義する必要があります。これは、現在のところ簡単な点まで単純であり、おそらく動作しない変更を覚えておいてください。ユーザーが過去20年間に保存した何百万ものenc
ファイルは、あなた以外の誰にも受け入れられません。 (更新)バグトラッカーは https://github.com/openssl/openssl/issues/471 に移動しました。1.1.1で「解決」されました-enc
はAEADをサポートしておらず、今後もサポートしません。また、初期の1.0.1リリースでは、パッチg(2012-03から2014-04)を通じて、この場合のエラーメッセージが表示されなかったことにも注意してください。 GCMまたはCCM暗号を使用してenc
コマンドを実行できましたが、暗号化時にタグが破棄され、復号化時にエラーが発生しました。
また、パスワード付きのenc
(実際のキーではなく、-K
大文字と-iv
を使用したIV)では、非常に貧弱なPBKDF、PBKDF1のバリアント EVP_BytesToKeyを参照1回のみの反復。
参照 openssl:キーとIVをパスフレーズで復元
および openssl encはmd5を使用してパスワードとソルトをハッシュします
および https://crypto.stackexchange.com/a/35614/12642 (開示:自分)。このPBKDFでAES256-GCMのようなベストプラクティスの暗号を使用することについて心配することは、牛の糞を金メッキすることのようです。
openssl enc -invalid
の使用法メッセージには、enc
がサポートしていないものも含め、EVPのすべての対称暗号/モードが一覧表示されます。気になれば、これをバグとして報告できます。 openssl list-cipher-algorithms
(1.1.0の最初のハイフンの代わりに計画されたスペース)も同じことを行いますが、openssl list-cipher-commands
(同上)は、AEADのものを除いて、コマンドとして使用できるものだけをリストします。 (更新)1.1.0以降では、すべてのコマンド解析が書き直され、使用法メッセージは-help
に置き換えられました。enc
の場合、暗号はリストされません。明示的なコマンドはlist -cipher-{algorithms,commands}
、つまりスペースおよびハイフンになりました。
最後に、SSHについて言及しますが、実際には尋ねません。 OpenSSH(唯一のSSHではない)を意味する場合、FYI OpenSSH 6.5より前のssh-keygen
は実際にOpenSSL libcryptoを使用してwriteOpenSSLの「レガシー」形式(PEMタイプRSA PRIVATE KEY
、DSA PRIVATE KEY
、EC PRIVATE KEY
)の秘密鍵。これもEVP_BytesToKey
を1回の反復で使用します。ただし、OpenSSLreadルーチンを使用するssh-keygen
およびssh
およびsshd
は、OpenSSLの「新しい」(2000年頃?)PKCS#も処理できます。 8暗号化形式、PEMタイプENCRYPTED PRIVATE KEY
、これは1.0.2までの2048回の反復でPBKDF2を使用でき(2000年頃に良好、現在はやや十分)、1.1.0でINT_MAXまで構成可能を計画。 OpenSSH 6.5(2014-01)以降では、bcryptを使用する独自の(非ASN.1でもPEM)形式のオプション-o
があり、キータイプed25519(OpenSSLがサポートしていない、まだです)。 (更新)OpenSSL 1.1.0(2016-08)は、PKCS8に-iter N
とオプションの-scrypt*
を期待どおりに追加しました。 OTOH OpenSSH 7.8(2018-08)は独自の「新しいフォーマット」をデフォルトにしました。 -o
は不要になりました。古くて悪いレガシーフォーマットを取得したい場合は、-m pem
を使用してください。
今まで、openssl enc
はAES-256-GCMをサポートしていません。次のCソースコードを記述して、openssl enc
は:
(この方法でコンパイルgcc -Wall -lcrypto -o aes256gcm aes256gcm.c
)
// AES-256-GCM with libcrypto
// gcc -Wall -lcrypto -o aes256gcm aes256gcm.c
// tag is 16 bytes long
// no AAD (Additional Associated Data)
// output format: tag is written just after cipher text (see RFC-5116, sections 5.1 and 5.2)
// KEY=a6a7ee7abe681c9c4cede8e3366a9ded96b92668ea5e26a31a4b0856341ed224
// IV=87b7225d16ea2ae1f41d0b13fdce9bba
// echo -n 'plain text' | ./aes256gcm $KEY $IV | od -t x1
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/conf.h>
#include <openssl/evp.h>
#include <openssl/err.h>
EVP_CIPHER_CTX *ctx = NULL;
unsigned char *iv = NULL;
unsigned char *buf_plain = NULL;
unsigned char *buf_cipher = NULL;
typedef enum { false, true } bool;
void freeCrypto() {
if (ctx) {
EVP_CIPHER_CTX_free(ctx);
ctx = NULL;
}
CRYPTO_cleanup_all_ex_data();
ERR_free_strings();
if (iv) {
free(iv);
iv = NULL;
}
if (buf_plain) {
free(buf_plain);
buf_plain = NULL;
}
if (buf_cipher) {
free(buf_cipher);
buf_cipher = NULL;
}
}
void handleCryptoError() {
fprintf(stderr, "ERROR\n");
ERR_print_errors_fp(stderr);
freeCrypto();
exit(1);
}
bool isValidHexChar(char c) {
return (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9');
}
unsigned char hex2uchar(char *hex) {
unsigned char ret;
if (hex[0] >= 'a' && hex[0] <= 'f') ret = (hex[0] - 'a' + 10) * 16;
else ret = (hex[0] - '0') * 16;
if (hex[1] >= 'a' && hex[1] <= 'f') ret += hex[1] - 'a' + 10;
else ret += hex[1] - '0';
return ret;
}
int main(int ac, char **av, char **ae)
{
const EVP_CIPHER *cipher;
unsigned char key[32];
int iv_len, len, i;
unsigned char tag[16];
if (ac != 3) {
fprintf(stderr, "usage: %s KEY IV\n", av[0]);
return 1;
}
char *key_txt = av[1];
char *iv_txt = av[2];
ERR_load_crypto_strings();
if (strlen(key_txt) != 2 * sizeof key) {
fprintf(stderr, "invalid key size\n");
freeCrypto();
return 1;
}
if (strlen(iv_txt) < 2 || strlen(iv_txt) % 2) {
fprintf(stderr, "invalid IV size\n");
freeCrypto();
return 1;
}
iv_len = strlen(iv_txt) / 2;
if (!(iv = malloc(iv_len))) {
perror("malloc");
freeCrypto();
return 1;
}
if (!(buf_plain = malloc(iv_len))) {
perror("malloc");
freeCrypto();
return 1;
}
if (!(buf_cipher = malloc(iv_len))) {
perror("malloc");
freeCrypto();
return 1;
}
for (i = 0; i < sizeof key; i++) {
if (!isValidHexChar(key_txt[2*i]) || !isValidHexChar(key_txt[2*i+1])) handleCryptoError();
key[i] = hex2uchar(key_txt + 2*i);
}
for (i = 0; i < iv_len; i++) {
if (!isValidHexChar(iv_txt[2*i]) || !isValidHexChar(iv_txt[2*i+1])) handleCryptoError();
iv[i] = hex2uchar(iv_txt + 2*i);
}
if (!(ctx = EVP_CIPHER_CTX_new())) handleCryptoError();
if (!(cipher = EVP_aes_256_gcm())) handleCryptoError();
if (1 != EVP_EncryptInit_ex(ctx, cipher, NULL, NULL, NULL)) handleCryptoError();
if (1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, NULL)) handleCryptoError();
if (1 != EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv)) handleCryptoError();
do {
size_t ret = fread(buf_plain, 1, iv_len, stdin);
if (!ret) {
if (ferror(stdin)) {
perror("fread");
freeCrypto();
return 1;
}
if (feof(stdin)) break;
}
if (1 != EVP_EncryptUpdate(ctx, buf_cipher, &len, buf_plain, ret)) handleCryptoError();
if (len && !fwrite(buf_cipher, len, 1, stdout)) {
if (feof(stderr)) fprintf(stderr, "EOF on output stream\n");
else perror("fwrite");
freeCrypto();
return 1;
}
} while (1);
if (1 != EVP_EncryptFinal_ex(ctx, buf_cipher, &len)) handleCryptoError();
if (len && !fwrite(buf_cipher, len, 1, stdout)) {
if (feof(stderr)) fprintf(stderr, "EOF on output stream\n");
else perror("fwrite");
freeCrypto();
return 1;
}
if (1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, sizeof tag, tag)) handleCryptoError();
if (!fwrite(tag, sizeof tag, 1, stdout)) {
if (feof(stderr)) fprintf(stderr, "EOF on output stream\n");
else perror("fwrite");
freeCrypto();
return 1;
}
fflush(stdout);
freeCrypto();
return 0;
}
上記のプログラムが暗号化したものを復号化する方法は次のとおりです。
(この方法でコンパイルgcc -Wall -lcrypto -o aes256gcm-decrypt aes256gcm-decrypt.c
)
// AES-256-GCM with libcrypto
// gcc -Wall -lcrypto -o aes256gcm-decrypt aes256gcm-decrypt.c
// tag is 16 bytes long
// no AAD (Additional Associated Data)
// input format: tag is read just after cipher text (see RFC-5116, sections 5.1 and 5.2)
// KEY=a6a7ee7abe681c9c4cede8e3366a9ded96b92668ea5e26a31a4b0856341ed224
// IV=87b7225d16ea2ae1f41d0b13fdce9bba
// cat ciphertext | ./aes256gcm-decrypt $KEY $IV
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/conf.h>
#include <openssl/evp.h>
#include <openssl/err.h>
EVP_CIPHER_CTX *ctx = NULL;
unsigned char *iv = NULL;
unsigned char *buf_plain = NULL;
unsigned char *buf_cipher = NULL;
unsigned char *input = NULL;
typedef enum { false, true } bool;
void freeCrypto() {
if (input) free(input);
if (ctx) {
EVP_CIPHER_CTX_free(ctx);
ctx = NULL;
}
CRYPTO_cleanup_all_ex_data();
ERR_free_strings();
if (iv) {
free(iv);
iv = NULL;
}
if (buf_plain) {
free(buf_plain);
buf_plain = NULL;
}
if (buf_cipher) {
free(buf_cipher);
buf_cipher = NULL;
}
}
void handleCryptoError() {
fprintf(stderr, "ERROR\n");
ERR_print_errors_fp(stderr);
freeCrypto();
exit(1);
}
bool isValidHexChar(char c) {
return (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9');
}
unsigned char hex2uchar(char *hex) {
unsigned char ret;
if (hex[0] >= 'a' && hex[0] <= 'f') ret = (hex[0] - 'a' + 10) * 16;
else ret = (hex[0] - '0') * 16;
if (hex[1] >= 'a' && hex[1] <= 'f') ret += hex[1] - 'a' + 10;
else ret += hex[1] - '0';
return ret;
}
unsigned char *loadInput(int *plen) {
int len = 0;
unsigned char *buf = NULL;
unsigned char *old_buf;
do {
int c = fgetc(stdin);
if (c == EOF) break;
if (c < 0) {
perror("fgetc");
exit(1);
}
len++;
old_buf = buf;
buf = malloc(len);
if (buf < 0) {
perror("malloc");
exit(1);
}
if (len > 1) bcopy(old_buf, buf, len - 1);
buf[len - 1] = c;
if (old_buf) free(old_buf);
} while (1);
*plen = len;
return buf;
}
int main(int ac, char **av, char **ae)
{
const EVP_CIPHER *cipher;
unsigned char key[32];
int iv_len, len, i;
unsigned char *current;
int input_len;
if (ac != 3) {
fprintf(stderr, "usage: %s KEY IV\n", av[0]);
return 1;
}
char *key_txt = av[1];
char *iv_txt = av[2];
input = loadInput(&input_len);
current = input;
ERR_load_crypto_strings();
if (strlen(key_txt) != 2 * sizeof key) {
fprintf(stderr, "invalid key size\n");
freeCrypto();
return 1;
}
if (strlen(iv_txt) < 2 || strlen(iv_txt) % 2) {
fprintf(stderr, "invalid IV size\n");
freeCrypto();
return 1;
}
iv_len = strlen(iv_txt) / 2;
if (!(iv = malloc(iv_len))) {
perror("malloc");
freeCrypto();
return 1;
}
if (!(buf_plain = malloc(iv_len))) {
perror("malloc");
freeCrypto();
return 1;
}
if (!(buf_cipher = malloc(iv_len))) {
perror("malloc");
freeCrypto();
return 1;
}
for (i = 0; i < sizeof key; i++) {
if (!isValidHexChar(key_txt[2*i]) || !isValidHexChar(key_txt[2*i+1])) handleCryptoError();
key[i] = hex2uchar(key_txt + 2*i);
}
for (i = 0; i < iv_len; i++) {
if (!isValidHexChar(iv_txt[2*i]) || !isValidHexChar(iv_txt[2*i+1])) handleCryptoError();
iv[i] = hex2uchar(iv_txt + 2*i);
}
if (!(ctx = EVP_CIPHER_CTX_new())) handleCryptoError();
if (!(cipher = EVP_aes_256_gcm())) handleCryptoError();
if (1 != EVP_DecryptInit_ex(ctx, cipher, NULL, NULL, NULL)) handleCryptoError();
if (1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, NULL)) handleCryptoError();
if (1 != EVP_DecryptInit_ex(ctx, NULL, NULL, key, iv)) handleCryptoError();
if (1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, input + input_len - 16)) handleCryptoError();
do {
int nbytes = input + input_len - 16 - current;
if (nbytes > iv_len) nbytes = iv_len;
if (!nbytes) break;
bcopy(current, buf_plain, nbytes);
current += nbytes;
if (1 != EVP_DecryptUpdate(ctx, buf_cipher, &len, buf_plain, nbytes)) handleCryptoError();
if (len && !fwrite(buf_cipher, len, 1, stdout)) {
if (feof(stderr)) fprintf(stderr, "EOF on output stream\n");
else perror("fwrite");
freeCrypto();
return 1;
}
} while (1);
// correct tag is checked here
if (EVP_DecryptFinal_ex(ctx, buf_cipher, &len) <= 0) handleCryptoError();
if (len && !fwrite(buf_cipher, len, 1, stdout)) {
if (feof(stderr)) fprintf(stderr, "EOF on output stream\n");
else perror("fwrite");
freeCrypto();
return 1;
}
fflush(stdout);
freeCrypto();
return 0;
}