译|王祖熙(花名:金九 )
蚂蚁集团开发工程师
负责国产化密码库 Tongsuo 的开发和维护
专注于密码学、高性能网络、网络安全等领域
本文 6132 字 阅读 15 分钟
本文翻译 OpenSSL 官网文档:https://www.openssl.org/docs/OpenSSL300Design.html
Tongsuo-8.4.0 是基于 OpenSSL-3.0.3 开发,所以本文对 Tongsuo 开发者同样适用,内容丰富,值得一读!
由于文章篇幅较长,今天带来的是 《Core 和 Provider 设计》 部分内容,上一篇《介绍、术语和架构》可查看已发布过的内容。后续内容将随每周推送完整发布,请持续关注铜锁。
Core 和 Provider 设计
下图显示了与 Core 和 Provider 设计相关的交互,有四个主要组件:用户应用程序、EVP 组件、Core 和密码 Provider (可能有多个 Provider,但在此不相关) 。
Core 具有以下特点:
- 实现 Provider 的发现、加载、初始化和卸载功能
- 支持基于属性的算法查询
- 实现了算法查询和实现细节的缓存
- 在库上下文中运行,其中包含全局属性、搜索缓存和分派表等数据
Provider 具有以下特点:
- 提供对特定算法实现的访问
- 将算法实现与一组明确定义的属性相关联
- 以一种与具体实现无关的方式支持参数传递
- 可以在任何时间点进行加载
- 具有众所周知的模块入口点
接下来的小节描述了应用程序使用的流程,以加载 Provider、获取算法实现并使用它为例。此外,本节详细描述了算法、属性和参数的命名方式,如何处理算法查询、注册和初始化算法,以及如何加载 Provider。
为了使应用程序能够使用算法,首先必须通过算法查询来“获取 (fetch) ”其实现。我们的设计目标是能够支持显式 (事先) 获取算法和在使用时获取算法的方式。默认情况下,我们希望在使用时进行获取 (例如使用 EVP_sha256()
) ,这样算法通常会在 init
函数期间进行获取,并绑定到上下文对象 (通常命名为 ctx
) 。显式获取选项将通过新的 API 调用实现 (例如 EVP_MD_fetch()
) 。
上述图示展示了显式获取算法的方法。具体步骤如下:
- 需要加载每个 Provider,这将隐式发生 (默认 Provider 或通过配置指定) ,也可以由应用程序显式请求加载。加载过程包括动态共享对象的加载 (根据需要) 和初始化。
- Core 组件将模块物理加载到内存中。 (如果默认 Provider 已经在内存中,则无需加载)
Core 组件调用 Provider 的入口点,以便 Provider 对自身进行初始化。
在入口点函数中,Provider 使用从 Core 组件传入的值初始化一些 Provider 变量。如果初始化成功,Provider 将返回一个用于 Provider 算法实现查询的回调函数给 Core 组件。
- 用户应用程序通过调用获取例程请求算法。
EVP 将全局属性与调用特定属性以及算法标识相结合,以找到相应的算法实现,然后创建并返回一个库句柄 (例如
EVP_MD
,EVP_CIPHER
) 给应用程序。在内部缓存中进行第一次实现调度表的搜索。
如果第一次搜索失败,则通过询问 Provider 是否具有符合查询属性的算法实现来进行第二次搜索,当完成此搜索时,除非 Provider 选择不进行缓存 (用于第一次搜索 2.1.1) ,否则结果数据将被缓存。例如,PKCS#11 Provider 可能选择不进行缓存,因为其算法可能随时间可用和不可用。
- 然后,用户应用程序通过 EVP API (例如
EVP_DigestInit()
,EVP_DigestUpdate()
,EVP_DigestFinal()
等) 使用算法。
- 函数指针被调用,并最终进入 Provider 的实现,执行请求的密码算法。
对于现有的 EVP_{algorithm}()
函数 (例如 EVP_sha256()
等) ,大部分情况下保持不变。特别是,当 EVP_{algorithm}()
调用返回时,并不会立即执行获取算法的操作,而是在将上下文对象 (例如 EVP_MD_CTX
) 绑定到相应的 EVP 初始化函数内部时隐式地进行。具体来说,步骤 2.1 发生在步骤 3.1 之前,这被称为 "隐式获取",隐式获取总是在默认的库上下文中进行操作。
方法调度表是一个由<函数 ID,函数指针>对组成的列表,其中函数 ID 是 OpenSSL 公开定义并已知的,同时还包括一组用于标识每个特定实现的属性。Core 可以根据属性查询找到相应的调度表,以供适用的操作使用。这种方法允许 Provider 灵活地传递函数引用,以便 OpenSSL 代码可以动态创建其方法结构。
Provider 可以在任何时间点加载,也可以在任何时间点请求卸载。在卸载 Provider 时,应用程序需要确保该 Provider 当前未被使用或引用,如果尝试使用不再可用的实现,则会返回错误信息。
关于 EVP_{algorithm}()
函数的返回值,目前应用程序可以做出的假设是:
- 常量指针
- 不需要由应用程序释放
- 可以安全地进行比较,用于检查算法是否相同 (即特定比较
EVP_CIPHER
、EVP_MD
等指针)
对于应用程序直接使用显式获取 (而不是使用现有的 EVP_{algorithm}()
函数) 的情况,语义将有所不同:
- 非常量指针
- 需要由应用程序释放
- 指针之间不能安全地进行比较 (后文将详细说明)
将提供新的 API 来测试可以用于显式获取对象和静态变体对象的相等性,这些 API 将使得可以比较算法标识本身或具体的算法实现。
库上下文
库上下文是一个不透明的结构,用于保存库的“全局”数据,OpenSSL 将提供这样的结构,仅限于 Core 必须保留的全局数据,未来的扩展可能会包括其他现有的全局数据,应用程序可以创建和销毁一个或多个库上下文,所有后续与 Core 的交互都将在其中进行,如果应用程序不创建并提供自己的库上下文,则将使用内部的默认上下文。
OPENSSL_CTX *OPENSSL_CTX_new();
void OPENSSL_CTX_free(OPENSSL_CTX *ctx);
库上下文可以传递给显式获取函数。如果将 NULL 传递给它们,将使用内部默认上下文。
可以分配多个库上下文,这意味着任何 Provider 模块可能会被初始化多次,这使得应用程序既可以直接链接到 libcrypto 并加载所需的 Provider,又可以链接到使用其自己 Provider 模块的其他库,而二者是相互独立的。
命名
算法、参数和属性需要命名,为了确保一致性,并使外部 Provider 实现者能够以一致的方式定义新名称,将建立一个推荐或已使用名称的注册表。它将与源代码分开维护。
需要能够定义名称的别名,因为在某些情况下,对同一事物存在多个名称 (例如对于具有通用名称和 NIST 名称的椭圆曲线) 的上下文。
算法实现选择属性
算法实现 (包括加密和非加密) 将具有一些属性,用于从可用的实现中选择一个实现。在 3.0 版本中,定义了两个属性:
- 该实现是否为默认实现?
- 该实现是否经过 FIPS 验证?
有效的输入及其含义如下:
在所有情况下,属性名称将被定义为可打印的 ASCII 字符,并且不区分大小写,属性值可以带引号或不带引号,不带引号的值也必须是可打印的 ASCII 字符,并且不区分大小写,引号中的值仅以原始字节比较的方式进行相等性测试。
Provider 将能够提供自己的名称或值,属性定义和查询的完整语法见附录 1-属性语法。
OpenSSL 保留所有没有句点的属性名称;供应商提供的属性名称必须在名称中包含句点。预期 (但不强制要求) 属性名称中第一个句点之前的部分是供应商的名称或与之相关的内容,以通过命名空间提供一定程度的冲突避免。
在开发此版本的过程中,可能会定义其他属性,一个可能的候选是 Provider,表示提供实现的 Provider 名称。另一个可能性是 engine,表示此算法由伪装为 Provider 的 OpenSSL 1.1.1 动态加载的引擎实现。
将有一个内置的全局属性查询字符串,其值为"default"。
属性选择算法
算法实现的选择基于属性。
Provider 在其提供的算法上设置属性,应用程序在算法选择过程中设置要用作筛选条件的属性查询。
可以在以下位置指定获取算法实现所需的属性:
- 全局配置文件中的全局设置
- 基于 API 调用的全局设置
- 针对特定对象的每个对象的属性设置。例如 SSL_CTX,SSL
属性将在算法查找过程中使用 (参数规范的属性值) 。
属性集将以解析为每个指定属性 (关键字) 的属性的单个值的方式进行评估。关键字评估的优先顺序如下:
- 获取的每个对象或直接指定的 API 参数
- 通过 API 调用设置的全局 (默认) 属性
- 在配置文件中设置的全局 (默认) 属性
在开发过程中,可能会定义其他属性设置方法和评估方法。
默认情况下,OpenSSL 3.0 将自动加载配置文件 (其中包含全局属性和其他设置) ,而无需显式的应用程序 API 调用,这将在 libcrypto 中发生。请注意,在 OpenSSL 1.1.1 中,配置文件仅在默认 (自动) 初始化 libssl 时自动加载。
参数定义
OpenSSL Core 和 Provider 在保持 OpenSSL 和 Provider 结构不透明的同时需要交换数据,所有复合值将作为项目数组传递,使用附录 2-参数传递 (后续将更新) 中定义的公共数据结构,参数将使用它们的名称 (作为字符串) 进行标识,每个参数包含自己的类型和大小信息。
Core 将定义一个 API,用于将参数值数组或值请求传递给 Provider 或特定的算法实现,对于后者,还有由该实现处理的相关对象,对于基本机器类型,可以开发宏来辅助构建和提取值。
操作和操作函数定义
虽然算法和参数名称基本上由 Provider 控制和分配,但由 libcrypto 调用的操作和相关函数基本上由 Core 控制和分配。
对于仅由 Core 控制的内容,我们将使用宏来命名它们,使用数字作为索引值,分配的索引值是递增的,即对于任何新的操作或函数,将选择下一个可用的数字。
算法查询
每种算法类型 (例如 EVP_MD
、EVP_CIPHER
等) 都有一个可用的“fetch”函数(例如 EVP_MD_fetch()
、EVP_CIPHER_fetch()
) ,算法实现是通过其名称和属性来识别的。
如前文 (Core 和 Provider 设计) 中所述,每个 fetch 函数将使用 Core 提供的服务来找到适合的实现,如果找到适当的实现,它将被构造成适当的算法结构 (例如 EVP_MD
、EVP_CIPHER
) 并返回给调用应用程序。
如果多个实现与传递的名称和属性完全匹配,其中之一将在检索时返回,但具体返回哪个实现是不确定的,此外,并不能保证每次都返回相同的匹配实现。
算法查询缓存
算法查询将与其结果一起被缓存。
下列这些算法查询缓存都可以清除:
- 返回特定算法实现的所有查询
- 来自特定 Provider 的所有算法实现
- 所有算法实现
多级查询
为了处理全局属性和传递给特定调用 (例如获取调用) 的属性,全局属性查询设置将与传递的属性设置合并,除非存在冲突,具体规则如下:
Provider 模块加载
Provider 可以是内置的或可动态加载的模块。
所有算法都是由 Provider 实现的,OpenSSL Core 最初未加载任何 Provider,因此没有可用的算法,需要查找和加载 Provider,随后,Core 可以在稍后的时间查询其中包含的算法实现,这些查询可能会被缓存。
如果在第一次获取 (隐式或显式) 时尚未加载任何 Provider,则会自动加载内置的默认 Provider。
请注意,Provider 可能针对 libcrypto 当前版本之前的旧版本 Core API 进行编写,例如,用户可以运行与 OpenSSL 主版本不同的 FIPS Provider 模块版本,这意味着 Core API 必须保持稳定和向后兼容 (就像任何其他公共 API 一样) 。
OpenSSL 构建的所有命令行应用程序都将获得一个 -provider xxx
选项,用于加载 Provider,该选项可以在命令行上多次指定 (可以始终加载多个 Provider) ,并且如果 Provider 在特定操作中未使用 (例如在进行 SHA256 摘要时加载仅提供 AES 的 Provider) ,并不会导致错误。
查找和加载动态 Provider 模块
动态 Provider 模块在 UNIX 类型操作系统上是 .so
文件,在 Windows 类型操作系统上是 .dll
文件,或者在其他操作系统上对应的文件类型。默认情况下,它们将被安装在一个众所周知的目录中。
Provider 模块的加载可以通过以下几种方式进行:
- 按需加载,应用程序必须明确指定要加载的 Provider 模块。
- 通过配置文件加载,加载的 Provider 模块集合将在配置文件中指定。
其中一些方法可以进行组合使用。
Provider 模块可以通过完整路径指定,因此即使它不位于众所周知的目录中,也可以加载。
Core 加载 Provider 模块后,会调用 Provider 模块的入口点函数。
Provider 模块入口点
一个 Provider 模块必须具有以下众所周知的入口点函数:
int OSSL_provider_init(const OSSL_PROVIDER *provider,
const OSSL_DISPATCH *in,
const OSSL_DISPATCH **out
void **provider_ctx);
如果动态加载的对象中不存在该入口点,则它不是一个有效的模块,加载会失败。
in
是核心传递给 Provider 的函数数组。
out
是 Provider 传递回 Core 的 Provider 函数数组。
provider_ctx
(在本文档的其他地方可能会缩写为 provctx
) 是 Provider 可选创建的对象,用于自身使用 (存储它需要安全保留的数据) ,这个指针将传递回适当的 Provider 函数。
provider
是指向 Core 所属 Provider 对象的句柄,它可以作为唯一的 Provider 标识,在某些 API 调用中可能需要,该对象还将填充各种数据,如模块路径、Provider 的 NCONF 配置结构 (了解如何实现可参见后文 CONF / NCONF 值作为参数的示例) ,Provider 可以使用 Core 提供的参数获取回调来检索这些各种值,类型 OSSL_PROVIDER
是不透明的。
OSSL_DISPATCH
是一个开放结构,实现了前文介绍中提到的 <函数 ID,函数指针> 元组。
typedef struct ossl_dispatch_st {
int function_id;
void *(*function)();
} OSSL_DISPATCH;
function_id
标识特定的函数,function
是指向该函数的指针。这些函数的数组以 function_id
设置为 0 来终止。
Provider 模块可以链接或者不链接到 libcrypto,如果没有链接,则它将无法直接访问任何 libcrypto 函数,所有与 libcrypto 的基本通信将通过 Core 提供的回调函数进行。重要的是,由特定 Provider 分配的内存应由相同的 Provider 来释放,同样,libcrypto 中分配的内存应由 libcrypto 释放。
API 将指定一组众所周知的回调函数编号,在后续发布中,可以根据需要添加更多的函数编号,而不会破坏向后兼容性。
/* Functions provided by the Core to the provider */
#define OSSL_FUNC_ERR_PUT_ERROR 1
#define OSSL_FUNC_GET_PARAMS 2
/* Functions provided by the provider to the Core */
#define OSSL_FUNC_PROVIDER_QUERY_OPERATION 3
#define OSSL_FUNC_PROVIDER_TEARDOWN 4 4
Core 将设置一个众所周知的回调函数数组:
static OSSL_DISPATCH core_callbacks[] = {
{ OSSL_FUNC_ERR_PUT_ERROR, ERR_put_error },
/* int ossl_get_params(OSSL_PROVIDER *prov, OSSL_PARAM params[]); */
{ OSSL_FUNC_GET_PARAMS, ossl_get_params, }
/* ... and more */
};
这只是核心可能决定传递给 Provider 的一些函数之一。根据需要,我们还可以传递用于日志记录、测试、仪表等方面的函数。
一旦模块加载完成并找到了众所周知的入口点,Core 就可以调用初始化入口点:
/*
* NOTE: this code is meant as a simple demonstration of what could happen
* in the core. This is an area where the OSSL_PROVIDER type is not opaque.
*/
OSSL_PROVIDER *provider = OSSL_PROVIDER_new();
const OSSL_DISPATCH *provider_callbacks;
/*
* The following are diverse parameters that the provider can get the values
* of with ossl_get_params.
*/
/* reference to the loaded module, or NULL if built in */
provider->module = dso;
/* reference to the path of the loaded module */
provider->module_path = dso_path;
/* reference to the NCONF structure used for this provider */
provider->conf_module = conf_module;
if (!OSSL_provider_init(provider, core_callbacks, &provider_callbacks))
goto err;
/* populate |provider| with functions passed by the provider */
while (provider_callbacks->func_num > 0) {
switch (provider_callbacks->func_num) {
case OSSL_FUNC_PROVIDER_QUERY_OPERATION:
provider->query_operation = provider_callbacks->func;
break;
case OSSL_FUNC_PROVIDER_TEARDOWN:
provider->teardown = provider_callbacks->func;
break;
}
provider_callbacks++;
}
OSSL_provider_init
入口点不会注册任何需要的算法,但它将返回至少这两个回调函数以启用这个过程:
OSSL_FUNC_QUERY_OPERATION
,用于查找可用的操作实现。它必须返回一个 OSSL_ALGORITHM
数组 (见下文) ,将算法名称和属性定义字符串映射到实现调度表,该函数还必须能够指示结果数组是否可以被 Core 缓存,下面将详细解释这一点。
OSSL_FUNC_TEARDOWN
,在 Provider 被卸载时使用。
Provider 注册回调只能在 OSSL_provider_init()
调用成功后执行。
Provider 初始化和算法注册
一个算法提供一组操作 (功能、特性等) ,这些操作通过函数调用,例如,RSA 算法提供签名和加密 (两个操作) ,通过 init
、update
、final
函数进行签名,以及 init
、update
、final
函数进行加密,函数集由上层 EVP 代码的实现确定。
操作通过唯一的编号进行标识,例如:
#define OSSL_OP_DIGEST 1
#define OSSL_OP_SYM_ENCRYPT 2
#define OSSL_OP_SEAL 3
#define OSSL_OP_DIGEST_SIGN 4
#define OSSL_OP_SIGN 5
#define OSSL_OP_ASYM_KEYGEN 6
#define OSSL_OP_ASYM_PARAMGEN 7
#define OSSL_OP_ASYM_ENCRYPT 8
#define OSSL_OP_ASYM_SIGN 9
#define OSSL_OP_ASYM_DERIVE 10
要使 Provider 中的算法可供 libcrypto 使用,它必须注册一个操作查询回调函数,该函数根据操作标识返回一个实现描述符数组:
<算法名称,属性定义字符串,实现的 OSSL_DISPATCH*
>
因此,例如,如果给定的操作是 OSSL_OP_DIGEST
,此查询回调将返回其所有摘要的列表。
算法通过字符串进行标识。
Core 库以函数表的形式提供了一组服务供 Provider 使用。
Provider 还将通过提供的回调函数提供返回信息的服务 (以附录-参数传递中指定的参数形式) ,例如:
- 版本号
- 构建字符串 - 根据当前 OpenSSL 相关的构建信息 (仅在 Provider 级别)
- Provider 名称
为了实现一个操作,可能需要定义多个函数回调,每个函数将通过数字函数标识进行标识,对于操作和函数的组合,每个标识都是唯一的,即为摘要操作的 init
函数分配的编号不能用于其他操作的 init
函数,它们将有自己的唯一编号。例如,对于摘要操作,需要以下这些函数:
#define OSSL_OP_DIGEST_NEWCTX_FUNC 1
#define OSSL_OP_DIGEST_INIT_FUNC 2
#define OSSL_OP_DIGEST_UPDATE_FUNC 3
#define OSSL_OP_DIGEST_FINAL_FUNC 4
#define OSSL_OP_DIGEST_FREECTX_FUNC 5
typedef void *(*OSSL_OP_digest_newctx_fn)(void *provctx);
typedef int (*OSSL_OP_digest_init_fn)(void *ctx);
typedef int (*OSSL_OP_digest_update_fn)(void *ctx, void *data, size_t len);
typedef int (*OSSL_OP_digest_final_fn)(void *ctx, void *md, size_t mdsize,
size_t *outlen);
typedef void (*OSSL_OP_digest_freectx_fn)(void *ctx);
对于无法处理多部分操作的设备,还建议使用多合一版本:
#define OSSL_OP_DIGEST_FUNC 6
typedef int (*OSSL_OP_digest)(void *provctx,
const void *data, size_t len,
unsigned char *md, size_t mdsize,
size_t *outlen);
然后,Provider 定义包含每个算法实现的函数集的数组,并为每个操作定义一个算法描述符数组,算法描述符在前面提到过,并且可以公开定义如下:
typedef struct ossl_algorithm_st {
const char *name;
const char *properties;
OSSL_DISPATCH *impl;
} OSSL_ALGORITHM;
例如 (这只是一个示例,Provider 可以按照自己的方式组织这些内容,重要的是算法查询函数(如下面的 fips_query_operation
返回的内容)) ,FIPS 模块可以定义如下数组来表示 SHA1 算法:
static OSSL_DISPATCH fips_sha1_callbacks[] = {
{ OSSL_OP_DIGEST_NEWCTX_FUNC, fips_sha1_newctx },
{ OSSL_OP_DIGEST_INIT_FUNC, fips_sha1_init },
{ OSSL_OP_DIGEST_UPDATE_FUNC, fips_sha1_update },
{ OSSL_OP_DIGEST_FINAL_FUNC, fips_sha1_final },
{ OSSL_OP_DIGEST_FUNC, fips_sha1_digest },
{ OSSL_OP_DIGEST_FREECTX_FUNC, fips_sha1_freectx },
{ 0, NULL }
};
static const char prop_fips[] = "fips";
static const OSSL_ALGORITHM fips_digests[] = {
{ "sha1", prop_fips, fips_sha1_callbacks },
{ "SHA-1", prop_fips, fips_sha1_callbacks }, /* alias for "sha1" */
{ NULL, NULL, NULL }
};
FIPS Provider 初始化模块入口点函数可能如下所示:
static int fips_query_operation(void *provctx, int op_id,
const OSSL_ALGORITHM **map)
{
*map = NULL;
switch (op_id) {
case OSSL_OP_DIGEST:
*map = fips_digests;
break;
}
return *map != NULL;
}
#define param_set_string(o,s) do { \
(o)->buffer = (s); \
(o)->data_type = OSSL_PARAM_UTF8_STRING_PTR; \
if ((o)->result_size != NULL) *(o)->result_size = sizeof(s); \
} while(0)
static int fips_get_params(void *provctx, OSSL_PARAM *outparams)
{
while (outparams->key != NULL) {
if (strcmp(outparams->key, "provider.name") == 0) {
param_set_string(outparams, "OPENSSL_FIPS");
} else if if (strcmp(outparams->key, "provider.build") == 0) {
param_set_string(outparams, OSSL_FIPS_PROV_BUILD_STRING);
}
}
return 1;
}
OSSL_DISPATCH provider_dispatch[] = {
{ OSSL_FUNC_PROVIDER_QUERY_OPERATION, fips_query_operation },
{ OSSL_FUNC_PROVIDER_GET_PARAMS, fips_get_params },
{ OSSL_FUNC_PROVIDER_STATUS, fips_get_status },
{ OSSL_FUNC_PROVIDER_TEARDOWN, fips_teardown },
{ 0, NULL }
};
static core_put_error_fn *core_put_error = NULL;
static core_get_params_fn *core_get_params = NULL;
int OSSL_provider_init(const OSSL_PROVIDER *provider,
const OSSL_DISPATCH *in,
const OSSL_DISPATCH **out
void **provider_ctx)
{
int ret = 0;
/*
* Start with collecting the functions provided by the core
* (we could write it more elegantly, but ...)
*/
while (in->func_num > 0) {
switch (in->func_num) {
case OSSL_FUNC_ERR_PUT_ERROR:
core_put_error = in->func;
break;
case OSSL_FUNC_GET_PARAMS:
core_get_params = in->func;
Break;
}
in++;
}
/* Get all parameters required for self tests */
{
/*
* All these parameters come from a configuration saying this:
*
* [provider]
* selftest_i = 4
* selftest_path = "foo"
* selftest_bool = true
* selftest_name = "bar"
*/
OSSL_PARAM selftest_params[] = {
{ "provider.selftest_i", OSSL_PARAM_NUMBER,
&selftest_i, sizeof(selftest_i), NULL },
{ "provider.selftest_path", OSSL_PARAM_STRING,
&selftest_path, sizeof(selftest_path), &selftest_path_ln },
{ "provider.selftest_bool", OSSL_PARAM_BOOLEAN,
&selftest_bool, sizeof(selftest_bool), NULL },
{ "provider.selftest_name", OSSL_PARAM_STRING,
&selftest_name, sizeof(selftest_name), &selftest_name_ln },
{ NULL, 0, NULL, 0, NULL }
}
core_get_params(provider, selftest_params);
}
/* Perform the FIPS self test - only return params if it succeeds. */
if (OSSL_FIPS_self_test()) {
*out = provider_dispatch;
return 1;
}
return 0;
}
算法选择
同时可能存在多个 Provider,重新编译为此版本的现有应用程序代码应该可以继续工作。与此同时,通过进行轻微的代码调整,应该能够使用基于属性的新算法查找功能来查找和使用算法。
为了说明这个过程是如何工作的,下面的代码是使用 OpenSSL 1.1.1 进行简单的 AES-CBC-128 加密的示例。为简洁起见,所有的错误处理都已被剥离。
EVP_CIPHER_CTX *ctx;
EVP_CIPHER *ciph;
ctx = EVP_CIPHER_CTX_new();
ciph = EVP_aes_128_cbc();
EVP_EncryptInit_ex(ctx, ciph, NULL, key, iv);
EVP_EncryptUpdate(ctx, ciphertext, &clen, plaintext, plen);
EVP_EncryptFinal_ex(ctx, ciphertext + clen, &clentmp);
clen += clentmp;
EVP_CIPHER_CTX_free(ctx);
在 OpenSSL 3.0 中,这样的代码仍然可以正常工作,并且将使用来自 Provider 的算法 (假设没有进行其他配置,将使用默认 Provider) ,它也可以通过显式获取进行重写,如下所示。显式获取还可以使应用程序在需要时指定非默认的库上下文 (在此示例中为 osslctx
) :
EVP_CIPHER_CTX *ctx;
EVP_CIPHER *ciph;
ctx = EVP_CIPHER_CTX_new();
ciph = EVP_CIPHER_fetch(osslctx, "aes-128-cbc", NULL); /* <=== */
EVP_EncryptInit_ex(ctx, ciph, NULL, key, iv);
EVP_EncryptUpdate(ctx, ciphertext, &clen, plaintext, plen);
EVP_EncryptFinal_ex(ctx, ciphertext + clen, &clentmp);
clen += clentmp;
EVP_CIPHER_CTX_free(ctx);
EVP_CIPHER_free(ciph); /* <=== */
应用程序可能希望使用来自不同 Provider 的算法。
例如,考虑这样的情况:应用程序希望使用 FIPS Provider 的某些算法,但在某些情况下仍然使用默认算法。可以以不同的方式实现,例如:
- 只使用 FIPS 算法
- 默认使用 FIPS 算法,但能够在需要时进行覆盖,以获得对非 FIPS 算法的访问
- 默认不关心 FIPS 算法,但能够在需要时进行覆盖,以获得 FIPS 算法
只使用 FIPS 算法
与 OpenSSL 3.0.0 之前版本编写的代码相比,如果您只需要 FIPS 实现,则只需要像这样进行一些更改:
int main(void)
{
EVP_set_default_alg_properties(NULL, "fips=yes"); /* <=== */
...
}
然后,使用 EVP_aes_128_cbc()
的上述加密代码将继续像以前一样工作,EVP_EncryptInit_ex()
调用将使用默认的算法属性,并通过 Core 查找以获取与 FIPS 实现关联的句柄,然后,该实现将与 EVP_CIPHER_CTX
对象关联起来,如果没有适用的算法实现可用,EVP_Encrypt_init_ex()
调用将失败。
EVP_set_default_alg_properties
的第一个参数是库上下文,NULL 表示默认的内部上下文。
默认使用 FIPS 算法,但允许覆盖
要将默认设置为使用 FIPS 算法,但根据需要覆盖为非 FIPS 算法,与 pre-3.0.0 OpenSSL 的代码相比,应用程序可能会进行以下更改:
int main(void)
{
EVP_set_default_alg_properties(osslctx, "fips=yes"); /* <=== */
...
}
EVP_CIPHER_CTX *ctx;
EVP_CIPHER *ciph;
ctx = EVP_CIPHER_CTX_new();
ciph = EVP_CIPHER_fetch(osslctx, "aes-128-cbc", "fips!=yes"); /* <=== */
EVP_EncryptInit_ex(ctx, ciph, NULL, key, iv);
EVP_EncryptUpdate(ctx, ciphertext, &clen, plaintext, plen);
EVP_EncryptFinal_ex(ctx, ciphertext + clen, &clentmp);
clen += clentmp;
EVP_CIPHER_CTX_free(ctx);
EVP_CIPHER_free(ciph); /* <=== */
这里的 EVP_CIPHER_fetch()
调用会结合以下属性:
- 默认的算法属性
- 作为参数传入的属性 (传入的属性优先级更高)
因为 EVP_CIPHER_fetch()
调用覆盖了默认的 fips
属性,它将寻找一个不是 fips
的 AES-CBC-128
的实现。
在这个例子中,我们看到使用了非默认的库上下文,这只有在明确获取实现的情况下才可能发生。 (注意:对于细心的读者, fips!=yes
也可以写为 fips=no
,但这里提供的是“不等于”运算符的一个示例)
默认不关注 FIPS 算法,并允许覆盖 FIPS
为了默认不使用 FIPS 算法,但可以根据需要覆盖为使用 FIPS 算法,应用程序代码可能如下所示 (与 3.0.0 之前版本的 OpenSSL 代码相比) :
EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new(privkey, NULL);
EVP_ASYM *asym = EVP_ASYM_fetch(osslctx, EVP_PKEY_EC, "fips=yes");
EVP_PKEY_CTX_set_alg(pctx, asym));
EVP_PKEY_derive_init(pctx);
EVP_PKEY_derive_set_peer(pctx, pubkey);
EVP_PKEY_derive(pctx, out, &outlen);
EVP_PKEY_CTX_free(pctx);
在这个版本中,我们没有在 main
中覆盖默认的算法属性,因此你将获得默认的开箱即用设置,即不要求使用 FIPS 算法。然而,我们在 EVP_CIPHER_fetch()
级别上明确设置了 fips
属性,因此它覆盖了默认设置。当 EVP_CIPHER_fetch()
使用 Core 查找算法时,它将获得对 FIPS 算法的引用;如果没有这样的算法,则失败。
非对称算法选择
请注意,对于对称加密/解密和消息摘要,存在现有的 OpenSSL 对象可用于表示算法,即 EVP_CIPHER
和 EVP_MD
。对于非对称算法,没有等效的对象,使用的算法从 EVP_PKEY
的类型隐式推断出来。
为了解决这个问题,将引入一个新的非对称算法对象。在下面的示例中,执行了一个 ECDH 密钥派生操作,我们使用一个新的算法对象 EVP_ASYM
来查找 FIPS 的 ECDH 实现 (需要假设我们知道给定的私钥是 ECC 私钥) :
EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new(privkey, NULL);
EVP_ASYM *asym = EVP_ASYM_fetch(osslctx, EVP_PKEY_EC, "fips=yes");
EVP_PKEY_CTX_set_alg(pctx, asym));
EVP_PKEY_derive_init(pctx);
EVP_PKEY_derive_set_peer(pctx, pubkey);
EVP_PKEY_derive(pctx, out, &outlen);
EVP_PKEY_CTX_free(pctx);
算法选择动态视图示例
下面的时序图展示了如何从默认 Provider 中选择和调用 SHA256 算法的示例。
请注意,EVP 层的每个调用都由 EVP 层中的薄封装器实现,这些封装器按照算法的方式在 Provider 中调用同名函数,要使用的特定 Provider 函数将通过显式的 EVP_MD_fetch()
调用在 Core 调度表中查找,该调用指定了消息摘要名称作为字符串以及其他相关属性,返回的 "md" 对象包含对所选 Provider 中算法实现的函数指针。
EVP_MD_CTX
对象没有传递给 Provider,因为我们不知道任何特定的 Provider 模块是否与 libcrypto 链接,相反,我们只是传递一个黑盒句柄 (void指针)* ,Provider 将与其所需的任何结构相关联。在操作开始时,通过对 Provider 进行明确的 digestNewCtx()
调用来分配此句柄,并在结束时通过 digestFreeCtx()
调用来释放。
下一个图示展示了稍微复杂一些的情景,即使用 RSA 和 SHA256 的 EVP_DigestSign*
操作。该图示从 libcrypto 的角度绘制,其中算法由 FIPS 模块提供,稍后的章节将从 FIPS 模块的角度考察这个情景。
EVP_DigestSign*
操作更加复杂,因为它涉及两个算法:签名算法和摘要算法。通常情况下,这两个算法可能来自不同的 Provider,也可能来自同一个 Provider。在使用 FIPS 模块的情况下,这两个算法必须来自同一个 FIPS 模块 Provider,如果尝试违反这个规则,操作将失败。
尽管有两个算法的额外复杂性,但与之前图示中展示的简单的 EVP_Digest*
操作相同的概念仍然适用。生成了两个上下文:EVP_MD_CTX
和 EVP_PKEY_CTX
。这两个上下文都不会传递给 Provider。相反,通过显式的 "newCtx" Provider 调用创建黑盒 (void )* 句柄,然后在后续的 init
、update
和 final
操作中传递这些句柄。
算法是通过提前使用显式的 EVP_MD_fetch()
和 EVP_ASYM_fetch()
调用在 Core 调度表中查找的。
下周我们将带来 FIPS 模块 部分内容,等不及的小伙伴,可以查看铜锁语雀中的全篇文档哦!
https://www.yuque.com/tsdoc/ts/openssl-300-design#CckIP