OpenSSL 3.0 简介(4)

        先介绍一些概念:

库上下文(Library Context)
        库上下文是一个在 OpenSSL 3.0 版源码中定义的不透明的结构体,在其内部保存全局性的数据。当前对这个结构体的使用,仅限于用来保存核心用到的全局数据,将来可能会做扩展,将其他存在的全局数据也放进去。一个应用程序可以创建和销毁一个或多个供核心使用的库上下文。如果应用程序不创建库上下文,这时一个内部默认的库上下文将被使用。
        创建和销毁库上下文的函数分别是:
OPENSSL_CTX  *OPENSSL_CTX_new( );
void  OPENSSL_CTX_free(OPENSSL_CTX *ctx);

属性(Properties)
        算法实现(包括密码学算法和非密码学算法)都具有属性。对于 OpenSSL 3.0,两种属性分别被定义为:
1)是否是默认的实现?
2)是否是 FIPS 验证通过的实现?
        通过使用可打印的 ASCII 字符串来定义属性,字符串是大小写敏感的。例如对于 default=yes 这个字符串,它定义了一个属性,含义是“这是默认的实现”,其中 default 是属性名,yes 是属性值。再如对于 fips=no 这个字符串,含义是“这不是 FIPS 验证通过的实现”,其中 fips 是属性名,no 是属性值。

算法查询(Algorithm Query)
        每一种算法类型都有一个对应的“获取函数”(fetch function),例如 EVP_MD 类型算法对应的获取函数是 EVP_MD_fetch( ),EVP_CIPHER 类型算法对应的获取函数是 EVP_CIPHER_fetch( )。每一个获取函数都会通过使用核心提供的服务,查找所需的算法实现。当具体的那一个算法实现被找到后,它将被存入一个与算法有关的结构体(例如 EVP_MD, EVP_CIPHER),然后返回给应用程序,供其调用。在根据名称和属性查询某一个具体的算法实现时,可能会出现多个同样好的结果被找到的情况,被返回的查询结果是不确定的。此时一个随机选择的结果将被返回,而在下一次查询时,可能另一个查询结果会被随机选中,然后被返回,这样每次返回的查询结果都有可能不一样。

算法查询缓存(Algorithm Query Caching)
        一般情况下查询具体算法实现的结果将被缓存,但该缓存也可以被手动清空。

基于属性的算法选择(Property-based Algorithm Selection):
        提供者要对它能提供的每一种算法实现都设置属性值。应用程序在查询某一个具体的算法实现时,将指定的属性和值作为过滤规则的一部分来执行检索。
        为了指定属性值,可以通过三种途径:
1)在一个配置文件中进行全局性指定;
2)通过调用 API 进行全局性指定;
3)针对某一个对象(例如 SSL_CTX 类型的对象)指定相关的属性。
        默认情况下,OpenSSL 3.0 会加载一个配置文件,该文件中包含全局属性和其他设置,libcrypto 库文件将自动加载这个配置文件。注意 OpenSSL 3.0 与 OpenSSL 1.1.1 不同,在 1.1.1 版中是由 libssl 库文件在初始化时自动加载配置文件。
        当设置了全局属性,但获取函数在查询时使用了一个与全局属性所设值冲突的属性值时,获取函数使用的那个属性值优先级更高,查询将以获取函数使用的属性值作为过滤规则,而忽略全局属性。例如:全局属性为 fips=no,通过获取函数传入的属性值为 fips=yes,这时将根据 fips=yes 来进行查询,全局属性中使用的 fips=no 将被忽略。

“调度表”(dispatch table)

        调度表是由多个 <function-id, function-pointer> 配对组成的列表。function-id 由 OpenSSL 公开定义,与一组属性相关联,使用这些属性可以识别不同的具体实现。核心可以使用某一个属性及值作为过滤条件,执行查询,看看能否找到对应的函数。在代码中用以下结构体表示 <function-id, function-pointer> 配对:
typedef struct ossl_dispatch_st {
    int function_id;
    void *(*function)();
} OSSL_DISPATCH;
        调度表在代码中的存在形式是一个包含多个 OSSL_DISPATCH 类型元素的数组,这个数组最后一个元素的 function_id 值被设为 0,用来标记数组的结束位置。这种使用特殊元素值来表示数组终止位置的方式与 C 语言中使用 \0 表示字符串结束符有点类似。

        OpenSSL 3.0 在工作时其内部组件的交互过程如下图:

        工作流程大致是这样的:
1)加载提供者;加载分为隐式和显式两种,隐式加载是指使用默认的提供者或通过配置文件来指定提供者,显式加载是指在用户的应用程序中指定提供者。加载包含加载动态共享对象及初始化。加载提供者时,将执行以下操作:
      (a) 核心将模块加载到内存中;如果默认的提供者已经在内存中,则不需要执行这一步。
      (b) 为了初始化提供者,核心调用提供者的“入口点函数”(entry point function)。如果初始化成功,一个“提供者算法实现查询回调(callback)函数”会被返回给核心。
2)为了让用户的应用程序能够调用到一个密码算法实现,用户应用程序必须首先执行一个算法查询操作,即通过某个“属性”来查询相关算法的实现。具体过程是:用户应用程序通过调用“获取程序”(fetch function),发出对要使用的密码算法的“请求”(request),接下来由 EVP 接口合并全局属性与调用时临时指定的属性,用这些属性来检索将要使用的算法实施。当找到对应的算法实施后,创建并返回一个“库句柄“(library handle)给应用程序,例如 EVP_MD 和 EVP_CIPHER 类型的变量都是库句柄。搜索时,先在”调度表“(dispatch table)中查找,这是在一个内部缓存中执行的搜索过程。如果在缓存中未找到,接下来使用属性到提供者处查询。此时查询结果会被放入缓存,这样就能加速以后搜索的速度。提供者也可以选择不允许将搜索结果放入缓存。
3)用户应用程序通过 EVP API 调用算法。如果在上一步中搜索成功,应用程序就拿到了返回的库句柄,接下来调用库句柄(实质是一个函数指针),就能调用提供者中包含的算法实现、执行密码运算了。

        对于在 OpenSSL 3.0 之前版本中就已存在的 EVP_{algorithm} 形式的函数(比如EVP_sha256( ) ),整个执行过程不完全一样,区别在于未执行第(2)步中获取程序操作,因为对于这些函数,实际上在 EVP 初始化函数执行时,就完成了获取程序操作。例如在调用 EVP_sha256( ) 计算杂凑时,在将 EVP_MD_CTX 类型的变量与 EVP 初始化函数绑定的时刻,执行了获取程序操作。

        调用 EVP 层的函数,实质上是调用了提供者内部的、具有近似名称的函数,EVP函数将调用提供者内部函数的过程封装起来,使得调用提供者内部函数的过程对外不可见。例如在计算杂凑值时,通过调用 EVP_MD_fetch( ) 函数,在核心的调度表中使用消息摘要算法名称和其他属性作为关键字,查询提供者提供的那个具有近似名的函数,查询结果是一个函数指针,该指针将被放在 md 对象结构体(其类型为 EVP_MD)的一个成员中返回,应用程序使用这个函数指针来调用提供者内部的函数实现,做杂凑运算。

小结
        OpenSSL 3.0 之前的版本,可以看成是一系列功能函数的集合,这些函数只是被动地等待用户应用程序来调用它们。从OpenSSL 3.0 版开始,是参照 FIPS 140 系列标准来设计的,即设计的一个出发点是应符合对“密码模块”的安全要求。密码模块除了提供最基本的密码学运算等功能之外,还要具备启动时完整性自检和密码算法正确性自检等功能,如果自检失败就会退出,不再对外提供服务。当各种自检通过之后,密码模块才能正式向调用它的应用程序提供服务。自检通过后的工作步骤大体如下:
1)加载 OpenSSL 3.0 库文件,首先由核心加载各个提供者,提供者必须到核心处去注册自己能提供的服务项目,核心相当于一个管理员,把各位提供者能提供的服务项登记造册管理起来,即放入调度表中;
2)用户应用程序调用 OpenSSL 3.0,实际上是通过调用获取函数与核心通信,把用户应用程序的需求告诉核心,核心查询当前提供者填写的服务列表缓存,如果在缓存中找不到服务项,就再向提供者发出查询请求,提供者将查询结果发给核心,核心再把查询结果告知应用程序。
3)应用程序调用 EVP 接口,调用提供者内部的具体算法实现。
        OpenSSL 3.0 中引入的服务查询、调度表等概念,其实不是新鲜事,在微软组件模型(COM)的设计思想中,也有类似的概念。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章