OpenSSL

密码学和 SSL/TLS 工具包

crypto

名称

ossl-guide-libcrypto-introduction, crypto - OpenSSL 指南:libcrypto 简介

简介

OpenSSL 密码库 (libcrypto) 允许访问各种互联网标准中使用的广泛的密码算法。该库提供的服务被 OpenSSL 的 TLS 和 CMS 实现使用,并且也已被用于实现许多其他第三方产品和协议。

其功能包括对称加密、公钥密码学、密钥协商、证书处理、密码哈希函数、密码伪随机数生成器、消息认证码 (MAC)、密钥导出函数 (KDF) 和各种实用程序。

算法

在 OpenSSL 中,诸如 SHA256 摘要或 AES 加密之类的密码原语被称为“算法”。每个算法可能具有多个可供使用的实现。例如,RSA 算法可作为适用于一般用途的“默认”实现,以及已通过 FIPS 140 标准验证的“fips”实现,适用于对该标准重要的场景。第三方也可以添加额外的实现,例如在硬件安全模块 (HSM) 中。

算法是在提供者中实现的。有关提供者的信息,请参见 ossl-guide-libraries-introduction(7)

操作

不同的算法可以根据其目的进行分组。例如,存在用于加密的算法,以及用于对数据进行摘要的不同算法。在 OpenSSL 中,这些不同的组被称为“操作”。每个操作都有一组与之关联的不同函数。例如,要使用 AES(或任何其他加密算法)执行加密操作,您将使用 EVP_EncryptInit(3) 页面上详细介绍的加密函数。或者,要使用 SHA256 执行摘要操作,则将使用 EVP_DigestInit(3) 页面上的摘要函数。

算法获取

为了使用算法,必须首先“获取”它的实现。获取是指查看可用的实现、应用选择标准(通过属性查询字符串)以及最终选择将使用的实现的过程。

OpenSSL 支持两种类型的获取 - “显式获取”“隐式获取”

显式获取

显式获取涉及直接调用特定 API 从提供者获取算法实现。然后可以将该获取到的对象传递给其他 API。这些显式获取函数通常具有 APINAME_fetch 的名称,其中 APINAME 是操作的名称。例如,EVP_MD_fetch(3) 可用于显式获取摘要算法实现。用户有责任在不再需要时使用 APINAME_free 释放从 APINAME_fetch 函数返回的对象。

这些获取函数遵循一个相当常见的模式,其中传递三个参数

库上下文

有关更详细的描述,请参见 OSSL_LIB_CTX(3)。这可以是 NULL 表示默认(全局)库上下文,或由用户创建的上下文。仅在这个库上下文中加载的提供者(请参见 OSSL_PROVIDER_load(3))将被获取函数考虑。如果在这个库上下文中未加载任何提供者,则将加载默认提供者作为备用(请参见 OSSL_PROVIDER-default(7))。

标识符

对于当前实现的所有获取函数,这都是算法名称。每个提供者都支持一个算法实现列表。有关每个提供者中可用算法实现的信息,请参见提供者特定的文档:“OSSL_PROVIDER-default(7) 中的“操作和算法”“OSSL_PROVIDER-FIPS(7) 中的“操作和算法”“OSSL_PROVIDER-legacy(7) 中的“操作和算法” 以及 “OSSL_PROVIDER-base(7) 中的“操作和算法”

请注意,虽然提供者可以使用带有冒号分隔的名称列表的字符串来注册算法,但目前不支持使用该格式获取算法。

属性查询字符串

用于指导选择算法实现的属性查询字符串。请参见 “ossl-guide-libraries-introduction(7) 中的“属性查询字符串”

然后可以将获取到的算法实现与使用它们的各种其他函数一起使用。例如,EVP_DigestInit_ex(3) 函数将 EVP_MD 对象作为参数,该对象可能是从之前对 EVP_MD_fetch(3) 的调用返回的。

隐式获取

OpenSSL 有一些函数返回没有关联实现的算法对象,例如 EVP_sha256(3)EVP_aes_128_cbc(3)EVP_get_cipherbyname(3)EVP_get_digestbyname(3)。这些函数是为了与 3.0 版之前的 OpenSSL 保持兼容,在该版本之前,显式获取不可用。

当它们与 EVP_DigestInit_ex(3)EVP_CipherInit_ex(3) 等函数一起使用时,将使用默认搜索标准(使用 NULL 表示库上下文和属性查询字符串)隐式获取实际使用的实现。

在某些情况下,当提供 NULL 算法参数时,也可能发生隐式获取。在这种情况下,将使用默认搜索标准和与使用该算法的上下文一致的算法名称隐式获取算法实现。

使用 EVP_PKEY_CTXEVP_PKEY(3) 的函数(例如 EVP_DigestSignInit(3))都会隐式获取实现。通常,要获取的算法是根据正在使用的密钥类型和已调用的函数确定的。

性能

如果您使用相同的算法多次执行相同的操作,建议显式获取一次算法,然后在每次后续使用该算法时重用该显式获取的算法。这通常比每次使用该算法时隐式获取它更快。有关显式获取的示例,请参见 “在应用程序中使用算法”

在 OpenSSL 3.0 之前,EVP_sha256() 等返回“const”对象的函数直接用于指示在各种函数调用中使用的算法。如果您将这些便利函数的返回值传递给操作,则您正在使用隐式获取。如果您正在转换与 3.0 版之前的 OpenSSL 版本一起工作的应用程序,请考虑将隐式获取的实例更改为显式获取。

如果未将显式获取的对象传递给操作,则任何隐式获取都将使用内部缓存的预获取对象,但仍比直接传递显式获取的对象慢。

以下函数可用于显式获取

EVP_MD_fetch(3)

获取消息摘要/哈希算法实现。

EVP_CIPHER_fetch(3)

获取对称密码算法实现。

EVP_KDF_fetch(3)

获取密钥导出函数 (KDF) 算法实现。

EVP_MAC_fetch(3)

获取消息认证码 (MAC) 算法实现。

EVP_KEM_fetch(3)

获取密钥封装机制 (KEM) 算法实现

OSSL_ENCODER_fetch(3)

获取编码器算法实现(例如,将密钥编码为指定格式)。

OSSL_DECODER_fetch(3)

获取解码器算法实现(例如,从指定格式解码密钥)。

EVP_RAND_fetch(3)

获取伪随机数生成器 (PRNG) 算法实现。

有关可以获取的算法名称列表,请参见 “OSSL_PROVIDER-default(7) 中的“操作和算法”“OSSL_PROVIDER-FIPS(7) 中的“操作和算法”“OSSL_PROVIDER-legacy(7) 中的“操作和算法” 以及 “OSSL_PROVIDER-base(7) 中的“操作和算法”

获取示例

以下部分提供了获取算法实现的示例系列。

在默认上下文中获取 SHA2-256 的任何可用实现。请注意,某些算法具有别名。因此,“SHA256”和“SHA2-256”是同义词

EVP_MD *md = EVP_MD_fetch(NULL, "SHA2-256", NULL);
...
EVP_MD_free(md);

在默认上下文中获取 AES-128-CBC 的任何可用实现

EVP_CIPHER *cipher = EVP_CIPHER_fetch(NULL, "AES-128-CBC", NULL);
...
EVP_CIPHER_free(cipher);

在默认上下文中从默认提供者获取 SHA2-256 的实现

EVP_MD *md = EVP_MD_fetch(NULL, "SHA2-256", "provider=default");
...
EVP_MD_free(md);

在默认上下文中获取来自默认提供者以外的 SHA2-256 的实现

EVP_MD *md = EVP_MD_fetch(NULL, "SHA2-256", "provider!=default");
...
EVP_MD_free(md);

在默认上下文中获取优选来自 FIPS 提供者的 SHA2-256 的实现

EVP_MD *md = EVP_MD_fetch(NULL, "SHA2-256", "provider=?fips");
...
EVP_MD_free(md);

在指定的库上下文中从默认提供者获取 SHA2-256 的实现

EVP_MD *md = EVP_MD_fetch(libctx, "SHA2-256", "provider=default");
...
EVP_MD_free(md);

将 legacy 提供者加载到默认上下文中,然后从中获取 WHIRLPOOL 的实现

/* This only needs to be done once - usually at application start up */
OSSL_PROVIDER *legacy = OSSL_PROVIDER_load(NULL, "legacy");

EVP_MD *md = EVP_MD_fetch(NULL, "WHIRLPOOL", "provider=legacy");
...
EVP_MD_free(md);

请注意,在上面的示例中,属性字符串“provider=legacy”是可选的,因为假设未加载其他提供者,那么“whirlpool”算法的唯一实现是在“legacy”提供者中。另请注意,如果需要默认提供者以及其他提供者,则应显式加载默认提供者

/* This only needs to be done once - usually at application start up */
OSSL_PROVIDER *legacy = OSSL_PROVIDER_load(NULL, "legacy");
OSSL_PROVIDER *default = OSSL_PROVIDER_load(NULL, "default");

EVP_MD *md_whirlpool = EVP_MD_fetch(NULL, "whirlpool", NULL);
EVP_MD *md_sha256 = EVP_MD_fetch(NULL, "SHA2-256", NULL);
...
EVP_MD_free(md_whirlpool);
EVP_MD_free(md_sha256);

在应用程序中使用算法

密码算法通过使用“EVP” API 提供给应用程序。各种操作(例如加密、摘要、消息认证码等)都有一个可调用以使用它们的 EVP 函数调用集。有关更多详细信息,请参见 evp(7) 页面。

大多数这些操作都遵循一个常见的模式。首先创建一个“上下文”对象。例如,对于摘要操作,您将使用 EVP_MD_CTX,而对于加密/解密操作,您将使用 EVP_CIPHER_CTX。然后通过“init”函数将操作初始化为准备使用 - 可选地传入一组参数(使用 OSSL_PARAM(3) 类型)来配置操作的行为方式。接下来,通过一系列“update”调用将数据馈送到操作中。“final”调用用于完成操作,该调用通常会提供某种输出。最后,清理并释放上下文。

以下展示了使用 SHA256 对数据进行摘要的此过程的完整示例。加密/解密、签名、消息认证码等其他操作的过程类似。在 OpenSSL 演示中可以找到其他示例(请参见 “ossl-guide-libraries-introduction(7) 中的“演示应用程序”)。

#include <stdio.h>
#include <openssl/evp.h>
#include <openssl/bio.h>
#include <openssl/err.h>

int main(void)
{
    EVP_MD_CTX *ctx = NULL;
    EVP_MD *sha256 = NULL;
    const unsigned char msg[] = {
        0x00, 0x01, 0x02, 0x03
    };
    unsigned int len = 0;
    unsigned char *outdigest = NULL;
    int ret = 1;

    /* Create a context for the digest operation */
    ctx = EVP_MD_CTX_new();
    if (ctx == NULL)
        goto err;

    /*
     * Fetch the SHA256 algorithm implementation for doing the digest. We're
     * using the "default" library context here (first NULL parameter), and
     * we're not supplying any particular search criteria for our SHA256
     * implementation (second NULL parameter). Any SHA256 implementation will
     * do.
     * In a larger application this fetch would just be done once, and could
     * be used for multiple calls to other operations such as EVP_DigestInit_ex().
     */
    sha256 = EVP_MD_fetch(NULL, "SHA256", NULL);
    if (sha256 == NULL)
        goto err;

   /* Initialise the digest operation */
   if (!EVP_DigestInit_ex(ctx, sha256, NULL))
       goto err;

    /*
     * Pass the message to be digested. This can be passed in over multiple
     * EVP_DigestUpdate calls if necessary
     */
    if (!EVP_DigestUpdate(ctx, msg, sizeof(msg)))
        goto err;

    /* Allocate the output buffer */
    outdigest = OPENSSL_malloc(EVP_MD_get_size(sha256));
    if (outdigest == NULL)
        goto err;

    /* Now calculate the digest itself */
    if (!EVP_DigestFinal_ex(ctx, outdigest, &len))
        goto err;

    /* Print out the digest result */
    BIO_dump_fp(stdout, outdigest, len);

    ret = 0;

 err:
    /* Clean up all the resources we allocated */
    OPENSSL_free(outdigest);
    EVP_MD_free(sha256);
    EVP_MD_CTX_free(ctx);
    if (ret != 0)
       ERR_print_errors_fp(stderr);
    return ret;
}

编码和解码密钥

许多算法都需要使用密钥。可以使用 EVP API 动态生成密钥(例如,请参见 EVP_PKEY_Q_keygen(3))。但是,通常需要将密钥(或其关联参数)保存到或从某些外部格式(例如 PEM 或 DER(请参见 openssl-glossary(7)))加载到或从该格式加载。OpenSSL 使用编码器和解码器来执行此任务。

编码器和解码器与 OpenSSL 中的任何其他算法实现一样,只是算法实现。它们由提供者实现。OpenSSL 编码器和解码器在默认提供者中可用。它们也在基本提供者中复制。

有关编码器的信息,请参见 OSSL_ENCODER_CTX_new_for_pkey(3)。有关解码器的信息,请参见 OSSL_DECODER_CTX_new_for_pkey(3)

除了直接使用编码器/解码器之外,还有一些辅助函数可用于某些众所周知和常用的格式。例如,有关从 PEM 编码文件读取和写入密钥数据的更多信息,请参见 PEM_read_PrivateKey(3)PEM_write_PrivateKey(3)

进一步阅读

有关使用 libssl 的介绍,请参见 ossl-guide-libssl-introduction(7)

另请参见

openssl(1)ssl(7)evp(7)OSSL_LIB_CTX(3)openssl-threads(7)property(7)OSSL_PROVIDER-default(7)OSSL_PROVIDER-base(7)OSSL_PROVIDER-FIPS(7)OSSL_PROVIDER-legacy(7)OSSL_PROVIDER-null(7)openssl-glossary(7)provider(7)

版权所有 2000-2024 OpenSSL 项目作者。保留所有权利。

根据 Apache 许可证 2.0 版(“许可证”)授权。除遵守许可证的规定外,您不得使用此文件。您可以在源代码分发中的 LICENSE 文件或 https://www.openssl.org/source/license.html 中获取副本。