译|王祖熙(诨名:金九)

蚂蚁集团开发工程师
负责国产化暗码库 Tongsuo 的开发和保护

专注于暗码学、高性能网络、网络安全等范畴

本文 6132 字 阅读 15 分钟

本文翻译 OpenSSL 官网文档:www.openssl.org/docs/OpenSS…

Tongsuo-8.4.0 是依据 OpenSSL-3.0.3 开发,所以本文对 Tongsuo 开发者相同适用,内容丰富,值得一读!

由于文章篇幅较长,今天带来的是 《Core 和 Provider 规划》 部分内容,上一篇《介绍、术语和架构》可检查已发布过的内容。后续内容将随每周推送完好发布,请持续重视铜锁

Core 和 Provider 规划

下图显示了与 Core 和 Provider 规划相关的交互,有四个主要组件:用户应用程序、EVP 组件、Core 和暗码 Provider (或许有多个 Provider,但在此不相关)

OpenSSL 3.0.0 设计(二)|Core 和 Provider 设计

Core 具有以下特色:

  • 完结Provider 的发现、加载、初始化和卸载功能
  • 支撑依据特点的算法查询
  • 完结了算法查询和完结细节的缓存
  • 在库上下文中运转,其间包括大局特点、查找缓存和分派表等数据

Provider 具有以下特色:

  • 供给对特定算法完结的拜访
  • 将算法完结与一组清晰界说的特点相相关
  • 以一种与详细完结无关的办法支撑参数传递
  • 能够在任何时刻点进行加载
  • 具有众所周知的模块进口点

接下来的末节描述了应用程序运用的流程,以加载 Provider、获取算法完结并运用它为例。此外,本节详细描述了算法、特点和参数的命名办法,怎么处理算法查询、注册和初始化算法,以及怎么加载 Provider。

为了使应用程序能够运用算法,首要有必要经过算法查询来“获取 (fetch) ”其完结。咱们的规划方针是能够支撑显式 (事先) 获取算法和在运用时获取算法的办法。默许状况下,咱们期望在运用时进行获取 (例如运用EVP_sha256() ,这样算法一般会在init函数期间进行获取,并绑定到上下文目标 (一般命名为ctx 。显式获取选项将经过新的 API 调用完结 (例如EVP_MD_fetch()

上述图示展现了显式获取算法的办法。详细步骤如下:

  1. 需求加载每个 Provider,这将隐式产生 (默许 Provider 或经过装备指定) ,也能够由应用程序显式恳求加载。加载进程包括动态同享目标的加载 (依据需求) 和初始化。
  • Core 组件将模块物理加载到内存中。 (假如默许 Provider 已经在内存中,则无需加载)

  • Core 组件调用 Provider 的进口点,以便 Provider 对本身进行初始化。

    在进口点函数中,Provider 运用从 Core 组件传入的值初始化一些 Provider 变量。假如初始化成功,Provider 将回来一个用于 Provider 算法完结查询的回调函数给 Core 组件。

  1. 用户应用程序经过调用获取例程恳求算法。
  • EVP 将大局特点与调用特定特点以及算法标识相结合,以找到相应的算法完结,然后创立并回来一个库句柄 (例如EVP_MDEVP_CIPHER 给应用程序。

    在内部缓存中进行第一次完结调度表的查找。

    假如第一次查找失利,则经过问询 Provider 是否具有符合查询特点的算法完结来进行第2次查找,当完结此查找时,除非 Provider 挑选不进行缓存 (用于第一次查找 2.1.1) ,否则成果数据将被缓存。例如,PKCS#11 Provider 或许挑选不进行缓存,由于其算法或许随时刻可用和不可用。

  1. 然后,用户应用程序经过 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_CIPHEREVP_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 验证?

有用的输入及其意义如下:

OpenSSL 3.0.0 设计(二)|Core 和 Provider 设计

在一切状况下,特点称号将被界说为可打印的 ASCII 字符,而且不区别大小写,特点值能够带引号或不带引号,不带引号的值也有必要是可打印的 ASCII 字符,而且不区别大小写,引号中的值仅以原始字节比较的办法进行相等性测验。

Provider 将能够供给自己的称号或值,特点界说和查询的完好语法见附录 1-特点语法。

OpenSSL 保存一切没有句点的特点称号;供货商供给的特点称号有必要在称号中包括句点。预期 (但不强制要求) 特点称号中第一个句点之前的部分是供货商的称号或与之相关的内容,以经过命名空间供给必定程度的冲突防止。

在开发此版别的进程中,或许会界说其他特点,一个或许的候选是 Provider,表明供给完结的 Provider 称号。另一个或许性是 engine,表明此算法由伪装为 Provider 的 OpenSSL 1.1.1 动态加载的引擎完结。

将有一个内置的大局特点查询字符串,其值为”default”。

特点挑选算法

算法完结的挑选依据特点。

Provider 在其供给的算法上设置特点,应用程序在算法挑选进程中设置要用作筛选条件的特点查询。

能够在以下方位指定获取算法完结所需的特点:

  • 大局装备文件中的大局设置
  • 依据 API 调用的大局设置
  • 针对特定目标的每个目标的特点设置。例如 SSL_CTX,SSL

特点将在算法查找进程中运用 (参数标准的特点值)

特点集将以解析为每个指定特点 (关键字) 的特点的单个值的办法进行评价。关键字评价的优先顺序如下:

  1. 获取的每个目标或直接指定的 API 参数
  2. 经过 API 调用设置的大局 (默许) 特点
  3. 在装备文件中设置的大局 (默许) 特点

在开发进程中,或许会界说其他特点设置办法和评价办法。

默许状况下,OpenSSL 3.0 将主动加载装备文件 (其间包括大局特点和其他设置) ,而无需显式的应用程序 API 调用,这将在 libcrypto 中产生。请注意,在 OpenSSL 1.1.1 中,装备文件仅在默许 (主动) 初始化 libssl 时主动加载。

参数界说

OpenSSL Core 和 Provider 在坚持 OpenSSL 和 Provider 结构不透明的一起需求交换数据,一切复合值将作为项目数组传递,运用附录 2-参数传递 (后续将更新) 中界说的公共数据结构,参数将运用它们的称号 (作为字符串) 进行标识,每个参数包括自己的类型和大小信息。

Core 将界说一个 API,用于将参数值数组或值恳求传递给 Provider 或特定的算法完结,关于后者,还有由该完结处理的相关目标,关于根本机器类型,能够开发宏来辅佐构建和提取值。

操作和操作函数界说

虽然算法和参数称号根本上由 Provider 控制和分配,但由 libcrypto 调用的操作和相关函数根本上由 Core 控制和分配。

关于仅由 Core 控制的内容,咱们将运用宏来命名它们,运用数字作为索引值,分配的索引值是递加的,即关于任何新的操作或函数,将挑选下一个可用的数字。

算法查询

每种算法类型 (例如EVP_MDEVP_CIPHER等) 都有一个可用的“fetch”函数(例如EVP_MD_fetch()EVP_CIPHER_fetch() ,算法完结是经过其称号和特点来识别的。

如前文 (Core 和 Provider 规划) 中所述,每个 fetch 函数将运用 Core 供给的服务来找到适合的完结,假如找到适当的完结,它将被构形成适当的算法结构 (例如 EVP_MDEVP_CIPHER 并回来给调用应用程序。

假如多个完结与传递的称号和特点完全匹配,其间之一将在检索时回来,但详细回来哪个完结是不确定的,此外,并不能保证每次都回来相同的匹配完结。

算法查询缓存

算法查询将与其成果一起被缓存。

下列这些算法查询缓存都能够铲除:

  • 回来特定算法完结的一切查询
  • 来自特定 Provider 的一切算法完结
  • 一切算法完结

多级查询

为了处理大局特点和传递给特定调用 (例如获取调用) 的特点,大局特点查询设置将与传递的特点设置兼并,除非存在冲突,详细规矩如下:

OpenSSL 3.0.0 设计(二)|Core 和 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 算法供给签名和加密 (两个操作) ,经过initupdatefinal函数进行签名,以及initupdatefinal函数进行加密,函数集由上层 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特点,它将寻觅一个不是fipsAES-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_CIPHEREVP_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 算法的示例。

OpenSSL 3.0.0 设计(二)|Core 和 Provider 设计

请注意,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 模块的视点调查这个情形。

OpenSSL 3.0.0 设计(二)|Core 和 Provider 设计

EVP_DigestSign*操作愈加复杂,由于它触及两个算法:签名算法和摘要算法。一般状况下,这两个算法或许来自不同的 Provider,也或许来自同一个 Provider。在运用 FIPS 模块的状况下,这两个算法有必要来自同一个 FIPS 模块 Provider,假如尝试违背这个规矩,操作将失利。

虽然有两个算法的额定复杂性,但与之前图示中展现的简略的EVP_Digest*操作相同的概念依然适用。生成了两个上下文:EVP_MD_CTXEVP_PKEY_CTX。这两个上下文都不会传递给 Provider。相反,经过显式的 “newCtx” Provider 调用创立黑盒 *(void 句柄,然后在后续的initupdatefinal操作中传递这些句柄。

算法是经过提早运用显式的EVP_MD_fetch()EVP_ASYM_fetch()调用在 Core 调度表中查找的。

下周咱们将带来 FIPS 模块 部分内容,等不及的小伙伴,能够检查铜锁语雀中的全篇文档哦! www.yuque.com/tsdoc/ts/op…