关于 OpenSSL 和 AES-GCM 的基本问题

信息安全 tls openssl AES
2021-08-16 18:12:03

在研究如何尽可能安全地加密 SSH 连接的私钥时,我遇到了以下非常基本的理解问题(注意:我使用 OpenSSL 的最新稳定版本 1.0.2h 进行研究):

  1. man enc声明 enc 实用程序不支持 GCM 等经过身份验证的加密模式。另一方面,我在使用 OpenSSL 加密连接的 Web 服务器上使用 TLS 1.2 密码,例如 ECDHE-RSA-AES128-GCM-SHA256,并且我检查了这些密码在通过 SSL / TLS 连接到相应的网站。

    所以 OpenSSL 显然能够以 AESxxx-GCM 模式加密数据。我本来希望 OpenSSL 在调用 via 时使用相同的代码来加密数据,openssl enc因为它用于加密数据,同时为 Web 服务器提供 SSL / TLS 功能。

    我是不是误会了什么?OpenSSL 在执行 TLS / SSL 时支持 AESxxx-GCM,但在通过命令行调用时无法直接加密数据的原因是什么?

  2. 查看可用密码名称(不是密码套件)的常用方法是openssl enc使用错误的参数进行调用,例如openssl enc --help. 这使得 openssl 打印一个简短的帮助,包括可用的密码名称。就我而言,输出包括多个 AES-GCM 密码。为什么openssl enc声称支持这些密码,而相应的手册页却声称不支持?

2个回答

OpenSSL 实现了几乎十几个对称密码和几十个密码模式组合,但在 EVP 模块(即外部函数和类型名称开头)中为所有这些提供了一个(几乎)单一接口,在线文档或(交联)任何安装了 OpenSSLUnixy 系统的手册页。(如果您自己构建/安装它而不是使用软件包管理器或等效的操作系统或发行版,您可能需要使用 MANPATH 或其他选项来查找手册页。)同样大量的摘要/哈希算法和公钥(和混合)加密和签名算法通过通用接口访问;见人 evpEVP_EVP_{Cipher,Encrypt,Decrypt}*EVP_CIPHER_*EVP_CIPHER_CTX_*man. 但是AEAD 模式(GCM 和 CCM,加上 1.1.0 中计划的 OCB)并不完全适合通用 API,需要额外的“控制”调用,请参阅上述手册中的“GCM 和 OCB 模式”和“CCM 模式”部分页。

SSL-and-TLS 模块(ssl/源代码中的顶级目录)包含对(实现的)AEAD 套件执行t1_enc.c不同EVP调用的代码,同样它还处理 CBC 和流密码的变体,已过时但仍编码为“导出” ' 密码、TLS1.1 与早期的 IV 处理以及其他协议选项和变体。

但是命令行encapps/enc.c使用通用接口而不是 AEAD 特殊接口,尽管在请求跟踪器(登录访客/访客)中有一个未分配的条目来添加它。一般来说,命令行实用程序是对 libssl 和 libcrypto 中功能的最小包装,如果你想要一些完整、精致、方便等的东西,那么你应该修改或替换它们。对于这种情况,您(将)需要定义如何以密文文件格式处理标签(可能还有 AAD),这目前很简单,甚至可以说是微不足道的 - 并记住任何不适用于可能的更改enc用户在过去二十年中存储的数百万个文件不会被除您之外的任何人接受。(更新)bugtracker 移至https://github.com/openssl/openssl/issues/471它在 1.1.1 中已“解决”——通过记录enc不支持也不会支持 AEAD。另请注意,早期的 1.0.1 版本,通过补丁 g(2012-03 至 2014-04)未能针对这种情况给出错误消息;您可以使用 GCM 或 CCM 密码运行该enc命令,但它会丢弃加密标签并在解密时出错。

还请记住,enc使用密码(不是实际密钥和 IV 使用-K大写 and -iv)使用非常差的 PBKDF,PBKDF1 的变体参见 EVP_BytesToKey仅一次迭代
请参阅 openssl:recover key and IV by passphrase
openssl enc 使用 md5 散列密码和盐
以及https://crypto.stackexchange.com/a/35614/12642(披露:我的)。担心在此 PBKDF 中使用 AES256-GCM 等最佳实践密码就像众所周知的给牛粪镀金一样。

使用消息openssl enc -invalid列出了 EVP 中的所有对称密码/模式,即使那些enc不支持。如果您关心,您可以将此报告为错误。openssl list-cipher-algorithms(计划空间而不是 1.1.0 中的第一个连字符)做同样openssl list-cipher-commands的事情,但(同上)仅列出那些可用作命令的命令,不包括 AEAD 的命令。(更新)在 1.1.0+ 中,所有命令解析都被重写,使用消息被替换-help为 forenc不列出密码;显式命令现在是list -cipher-{algorithms,commands}空格连字符。

最后,您提到但实际上并未询问 SSH。如果您指的是OpenSSH(它不是唯一的 SSH),仅供参考,OpenSSH 6.5 之前的版本ssh-keygen实际上使用 OpenSSL libcrypto 以OpenSSL 的“传统”格式(PEM 类型、、、 )编写私钥,这些格式也与一次迭代一起使用。但是并且使用 OpenSSL读取例程也可以处理 OpenSSL 的“新”(大约 2000 年?)PKCS#8 加密格式,PEM 类型,它可以使用 PBKDF2 进行 2048 次迭代到 1.0.2(大约 2000 年好,现在勉强够用)和计划在 1.1.0 中可配置为 INT_MAX。从 6.5 (2014-01) 开始的 OpenSSH 有一个选项RSA PRIVATE KEYDSA PRIVATE KEYEC PRIVATE KEYEVP_BytesToKeyssh-keygensshsshdENCRYPTED PRIVATE KEY-o对于它自己的(非 ASN.1 但仍然是 PEM)格式,使用 bcrypt,并强制该选项用于密钥类型 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;
}