丰富的线上&线下活动,深入探索云世界
做任务,得社区积分和周边
最真实的开发者用云体验
让每位学生受益于普惠算力
让创作激发创新
资深技术专家手把手带教
遇见技术追梦人
技术交流,直击现场
海量开发者使用工具、手册,免费下载
极速、全面、稳定、安全的开源镜像
开发手册、白皮书、案例集等实战精华
为开发者定制的Chrome浏览器插件
Tongsuo-8.4.0是基于OpenSSL-3.0.3开发,所以本文对Tongsuo开发者同样适用,内容丰富,值得一读!
本文概述了OpenSSL3.0的设计,这是在1.1.1版本之后的OpenSSL的下一个版本。假设读者熟悉名为"OpenSSL战略架构"的文档,并对OpenSSL1.1.x有一定的实际操作经验。
OpenSSL3.0版本对大多数现有应用程序影响很小;几乎所有良好行为的应用程序只需重新编译即可。
OpenSSL3.0中的大多数变更是为了进行内部架构重组,以便实现一个长期可支持的密码学框架,从而更好地将算法实现与算法API分离。这些结构性变更还支持更易维护的OpenSSLFIPS密码模块3.0。
在OpenSSL3.0中,目前标记为弃用(deprecated)的API不会被移除。
在OpenSSL3.0中,许多额外的低级函数将被标记为弃用(deprecated)的API。
OpenSSL3.0将同时支持应用程序同时具有处于FIPS模式(使用OpenSSLFIPS密码模块3.0)和非FIPS模式的TLS连接。
以下术语按字母顺序在本文中使用,简要定义如下:
架构应具备以下特性:
本文档中有许多关于"EVPAPI"的引用。这指的是"应用级别"的操作,例如公钥签名、生成摘要等。这些函数包括EVP_DigestSign、EVP_Digest、EVP_MAC_init等。EVPAPI还封装了执行这些服务所使用的密码对象,例如EVP_PKEY、EVP_CIPHER、EVP_MD、EVP_MAC等等。Provider为后者集合实现了后端功能。这些对象的实例可以根据应用程序的需求隐式或显式地绑定到Provider上。这在下面的Provider设计部分将详细讨论。
架构具有以下特点:
OpenSSL架构中的概念组件概述如下图所示。请注意,图中组件的存在并不意味着该组件是公共API或用于直接访问或使用的最终用户的组件。
新的组件(在先前架构中不存在)如下所示:
上述概念组件视图中描述的各个组件在物理上打包为:
OpenSSL3.0将提供多种不同的打包选项(例如一个名为libcrypto的单一库,包含除FIPSProvider之外的所有内容,以及所有Provider作为单独的可动态加载模块)。
哪些可动态加载模块被注册、使用或可用可以在运行时进行配置,以下图示根据物理打包描述了架构:
本版本引入的物理打包如下:
我们不会试图阻止用户出错,但我们会考虑典型用户的使用情况和“安全性”。默认情况下,将构建并安装FIPSProvider。
我们将能够执行安全检查,以检测用户是否以对FIPS有影响的方式修改了源代码,并在未提供覆盖选项的情况下(尽力而为)阻止构建FIPSProvider。
我们需要确保存在一种机制,使最终用户能够确定他们对FIPS模块的使用是否符合正式验证的允许用途。
FIPS模块的版本将与基础OpenSSL版本号对齐,验证时的版本号取决于该版本。并非所有OpenSSL发布版本都需要更新FIPS模块。因此,当发布新的FIPS模块版本时,其版本号可能存在间隔或跳跃,与之前版本相比。
最初的计划是使用Provider的封装来构建引擎(Engines),以便在将ENGINE指针传递给某些函数时,使其像往常一样工作,并在充当默认实现时作为Provider。在开发过程中的调查显示,这种方法存在问题的边缘情况。目前的解决方法是,当进行EVP调用时,目前存在两个代码路径。对于引擎(engine)的支持,使用“遗留密钥”的旧代码。长期计划是从代码库中删除引擎(engine)和遗留代码路径。一旦删除引擎(engine),任何作为引擎(engine)编写的内容都需要重新编写为Provider。
Core具有以下特点:
Provider具有以下特点:
接下来的小节描述了应用程序使用的流程,以加载Provider、获取算法实现并使用它为例。此外,本节详细描述了算法、属性和参数的命名方式,以及如何处理算法查询、注册和初始化算法,以及如何加载Provider。
为了使应用程序能够使用算法,首先必须通过算法查询来“获取(fetch)”其实现。我们的设计目标是能够支持显式(事先)获取算法和在使用时获取算法的方式。默认情况下,我们希望在使用时进行获取(例如使用EVP_sha256()),这样算法通常会在“init”函数期间进行获取,并绑定到上下文对象(通常命名为ctx)。显式获取选项将通过新的API调用实现(例如EVP_MD_fetch())。
上述图示展示了显式获取算法的方法。具体步骤如下:
方法调度表是一个由<函数ID,函数指针>对组成的列表,其中函数ID是OpenSSL公开定义并已知的,同时还包括一组用于标识每个特定实现的属性。Core可以根据属性查询找到相应的调度表,以供适用的操作使用。这种方法允许Provider灵活地传递函数引用,以便OpenSSL代码可以动态创建其方法结构。
关于EVP_{algorithm}()函数的返回值,目前应用程序可以做出的假设是:
对于应用程序直接使用显式获取(而不是使用现有的EVP_{algorithm}()函数)的情况,语义将有所不同:
将提供新的API来测试可以用于显式获取对象和静态变体对象的相等性,这些API将使得可以比较算法标识本身或具体的算法实现。
库上下文是一个不透明的结构,用于保存库的“全局”数据,OpenSSL将提供这样的结构,仅限于Core必须保留的全局数据,未来的扩展可能会包括其他现有的全局数据,应用程序可以创建和销毁一个或多个库上下文,所有后续与Core的交互都将在其中进行,如果应用程序不创建并提供自己的库上下文,则将使用内部的默认上下文。
OPENSSL_CTX*OPENSSL_CTX_new();voidOPENSSL_CTX_free(OPENSSL_CTX*ctx);
库上下文可以传递给显式获取函数。如果将NULL传递给它们,将使用内部默认上下文。
可以分配多个库上下文,这意味着任何Provider模块可能会被初始化多次,这使得应用程序既可以直接链接到libcrypto并加载所需的Provider,又可以链接到使用其自己Provider模块的其他库,而二者是相互独立的。
算法、参数和属性需要命名,为了确保一致性,并使外部Provider实现者能够以一致的方式定义新名称,将建立一个推荐或已使用名称的注册表。它将与源代码分开维护。
需要能够定义名称的别名,因为在某些情况下,对同一事物存在多个名称(例如,对于具有通用名称和NIST名称的椭圆曲线)的上下文。
算法实现(包括加密和非加密)将具有一些属性,用于从可用的实现中选择一个实现。在3.0版本中,定义了两个属性:
有效的输入及其含义如下:
属性字符串
定义中的含义
查询中的含义
default
是默认实现
请求默认实现
default=yes
default=no
不是默认实现
请求非默认实现
fips
此实现已通过FIPS验证
请求经过FIPS验证的实现
fips=yes
fips=no
此实现未通过FIPS验证
请求未经过FIPS验证的实现
在所有情况下,属性名称将被定义为可打印的ASCII字符,并且不区分大小写,属性值可以带引号或不带引号,不带引号的值也必须是可打印的ASCII字符,并且不区分大小写,引号中的值仅以原始字节比较的方式进行相等性测试。
在开发此版本的过程中,可能会定义其他属性,一个可能的候选是provider,表示提供实现的Provider名称。另一个可能性是engine,表示此算法由伪装为Provider的OpenSSL1.1.1动态加载的引擎实现。
将有一个内置的全局属性查询字符串,其值为"default"。
算法实现的选择基于属性。
Provider在其提供的算法上设置属性,应用程序在算法选择过程中设置要用作筛选条件的属性查询。
可以在以下位置指定获取算法实现所需的属性:
属性将在算法查找过程中使用(参数规范的属性值)。
属性集将以解析为每个指定属性(关键字)的属性的单个值的方式进行评估。关键字评估的优先顺序如下:
在开发过程中,可能会定义其他属性设置方法和评估方法。
默认情况下,OpenSSL3.0将自动加载配置文件(其中包含全局属性和其他设置),而无需显式的应用程序API调用,这将在libcrypto中发生。请注意,在OpenSSL1.1.1中,配置文件仅在默认(自动)初始化libssl时自动加载。
对于仅由Core控制的内容,我们将使用宏来命名它们,使用数字作为索引值,分配的索引值是递增的,即对于任何新的操作或函数,将选择下一个可用的数字。
每种算法类型(例如EVP_MD、EVP_CIPHER等)都有一个可用的“fetch”函数(例如EVP_MD_fetch()、EVP_CIPHER_fetch()),算法实现是通过其名称和属性来识别的。
如果多个实现与传递的名称和属性完全匹配,其中之一将在检索时返回,但具体返回哪个实现是不确定的,此外,并不能保证每次都返回相同的匹配实现。
算法查询将与其结果一起被缓存。
下列这些算法查询缓存都可以清除:
为了处理全局属性和传递给特定调用(例如获取调用)的属性,全局属性查询设置将与传递的属性设置合并,除非存在冲突,具体规则如下:
全局设置
传递的设置
最终查询结果
-fips
fipsisnotspecified
Provider可以是内置的或可动态加载的模块。
如果在第一次获取(隐式或显式)时尚未加载任何Provider,则会自动加载内置的默认Provider。
请注意,Provider可能针对libcrypto当前版本之前的旧版本CoreAPI进行编写,例如,用户可以运行与OpenSSL主版本不同的FIPSProvider模块版本,这意味着CoreAPI必须保持稳定和向后兼容(就像任何其他公共API一样)。
OpenSSL构建的所有命令行应用程序都将获得一个-providerxxx选项,用于加载Provider,该选项可以在命令行上多次指定(可以始终加载多个Provider),并且如果Provider在特定操作中未使用(例如,在进行SHA256摘要时加载仅提供AES的Provider),并不会导致错误。
动态Provider模块在UNIX类型操作系统上是.so文件,在Windows类型操作系统上是.dll文件,或者在其他操作系统上对应的文件类型。默认情况下,它们将被安装在一个众所周知的目录中。
Provider模块的加载可以通过以下几种方式进行:
其中一些方法可以进行组合使用。
Provider模块可以通过完整路径指定,因此即使它不位于众所周知的目录中,也可以加载。
Core加载Provider模块后,会调用Provider模块的入口点函数。
一个Provider模块必须具有以下众所周知的入口点函数:
intOSSL_provider_init(constOSSL_PROVIDER*provider,constOSSL_DISPATCH*in,constOSSL_DISPATCH**outvoid**provider_ctx);如果动态加载的对象中不存在该入口点,则它不是一个有效的模块,加载会失败。
in是核心传递给Provider的函数数组。
out是Provider传递回Core的Provider函数数组。
provider_ctx(在本文档的其他地方可能会缩写为provctx)是Provider可选创建的对象,用于自身使用(存储它需要安全保留的数据),这个指针将传递回适当的Provider函数。
typedefstructossl_dispatch_st{intfunction_id;void*(*function)();}OSSL_DISPATCH;function_id标识特定的函数,function是指向该函数的指针。这些函数的数组以function_id设置为0来终止。
Provider模块可以链接或者不链接到libcrypto,如果没有链接,则它将无法直接访问任何libcrypto函数,所有与libcrypto的基本通信将通过Core提供的回调函数进行。重要的是,由特定Provider分配的内存应由相同的Provider来释放,同样,libcrypto中分配的内存应由libcrypto释放。
API将指定一组众所周知的回调函数编号,在后续发布中,可以根据需要添加更多的函数编号,而不会破坏向后兼容性。
/*FunctionsprovidedbytheCoretotheprovider*/#defineOSSL_FUNC_ERR_PUT_ERROR1#defineOSSL_FUNC_GET_PARAMS2/*FunctionsprovidedbytheprovidertotheCore*/#defineOSSL_FUNC_PROVIDER_QUERY_OPERATION3#defineOSSL_FUNC_PROVIDER_TEARDOWN4Core将设置一个众所周知的回调函数数组:
staticOSSL_DISPATCHcore_callbacks[]={{OSSL_FUNC_ERR_PUT_ERROR,ERR_put_error},/*intossl_get_params(OSSL_PROVIDER*prov,OSSL_PARAMparams[]);*/{OSSL_FUNC_GET_PARAMS,ossl_get_params,}/*...andmore*/};这只是核心可能决定传递给Provider的一些函数之一。根据需要,我们还可以传递用于日志记录、测试、仪表等方面的函数。
一旦模块加载完成并找到了众所周知的入口点,Core就可以调用初始化入口点:
Provider注册回调只能在OSSL_provider_init()调用成功后执行。
一个算法提供一组操作(功能、特性等),这些操作通过函数调用,例如,RSA算法提供签名和加密(两个操作),通过init、update、final函数进行签名,以及init、update、final函数进行加密,函数集由上层EVP代码的实现确定。
操作通过唯一的编号进行标识,例如:
#defineOSSL_OP_DIGEST1#defineOSSL_OP_SYM_ENCRYPT2#defineOSSL_OP_SEAL3#defineOSSL_OP_DIGEST_SIGN4#defineOSSL_OP_SIGN5#defineOSSL_OP_ASYM_KEYGEN6#defineOSSL_OP_ASYM_PARAMGEN7#defineOSSL_OP_ASYM_ENCRYPT8#defineOSSL_OP_ASYM_SIGN9#defineOSSL_OP_ASYM_DERIVE10要使Provider中的算法可供libcrypto使用,它必须注册一个操作查询回调函数,该函数根据操作标识返回一个实现描述符数组:
<算法名称,属性定义字符串,实现的OSSL_DISPATCH*>
因此,例如,如果给定的操作是OSSL_OP_DIGEST,此查询回调将返回其所有摘要的列表。
算法通过字符串进行标识。
Core库以函数表的形式提供了一组服务供Provider使用。
为了实现一个操作,可能需要定义多个函数回调,每个函数将通过数字函数标识进行标识,对于操作和函数的组合,每个标识都是唯一的,即为摘要操作的init函数分配的编号不能用于其他操作的init函数,它们将有自己的唯一编号。例如,对于摘要操作,需要以下这些函数:
#defineOSSL_OP_DIGEST_NEWCTX_FUNC1#defineOSSL_OP_DIGEST_INIT_FUNC2#defineOSSL_OP_DIGEST_UPDATE_FUNC3#defineOSSL_OP_DIGEST_FINAL_FUNC4#defineOSSL_OP_DIGEST_FREECTX_FUNC5typedefvoid*(*OSSL_OP_digest_newctx_fn)(void*provctx);typedefint(*OSSL_OP_digest_init_fn)(void*ctx);typedefint(*OSSL_OP_digest_update_fn)(void*ctx,void*data,size_tlen);typedefint(*OSSL_OP_digest_final_fn)(void*ctx,void*md,size_tmdsize,size_t*outlen);typedefvoid(*OSSL_OP_digest_freectx_fn)(void*ctx);对于无法处理多部分操作的设备,还建议使用多合一版本:
#defineOSSL_OP_DIGEST_FUNC6typedefint(*OSSL_OP_digest)(void*provctx,constvoid*data,size_tlen,unsignedchar*md,size_tmdsize,size_t*outlen);然后,Provider定义包含每个算法实现的函数集的数组,并为每个操作定义一个算法描述符数组,算法描述符在前面提到过,并且可以公开定义如下:
typedefstructossl_algorithm_st{constchar*name;constchar*properties;OSSL_DISPATCH*impl;}OSSL_ALGORITHM;例如(这只是一个示例,Provider可以按照自己的方式组织这些内容,重要的是算法查询函数(如下面的fips_query_operation)返回的内容),FIPS模块可以定义如下数组来表示SHA1算法:
staticOSSL_DISPATCHfips_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}};staticconstcharprop_fips[]="fips";staticconstOSSL_ALGORITHMfips_digests[]={{"sha1",prop_fips,fips_sha1_callbacks},{"SHA-1",prop_fips,fips_sha1_callbacks},/*aliasfor"sha1"*/{NULL,NULL,NULL}};FIPSProvider初始化模块入口点函数可能如下所示:
为了说明这个过程是如何工作的,下面的代码是使用OpenSSL1.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);在OpenSSL3.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的算法。
例如,考虑这样的情况:应用程序希望使用FIPSProvider的某些算法,但在某些情况下仍然使用默认算法。可以以不同的方式实现,例如:
与OpenSSL3.0.0之前版本编写的代码相比,如果您只需要FIPS实现,您只需要像这样进行一些更改:
intmain(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算法,与pre-3.0.0OpenSSL的代码相比,应用程序可能会进行以下更改:
intmain(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算法,应用程序代码可能如下所示(与3.0.0之前版本的OpenSSL代码相比):
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);/*<===*/在这个版本中,我们没有在“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算法的示例。
下一个图示展示了稍微复杂一些的情景,即使用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模块本身不会有"FIPS模式",可以使用FIPSProvider的OpenSSL将具有与FIPS-Module-2.0.0兼容的"模式"概念。
版本将为FIPS-Module-3.0。
任何后续的修订版本将以与先前发布类似的方式进行标记,例如3.0.x。
对于更改通知或重新验证,FIPS模块的版本号将更新以匹配当前的OpenSSL库版本。
可以使用一个脚本来对C源代码进行标记化处理,就像C预处理器一样,但还要教会它忽略源代码中的某些部分:
(提醒:C预处理器可以将所有非换行空白字符合并,并在每个标记之间留下标准的单个空格,对于此目的,注释被视为空白字符)
标记化处理的结果可以通过校验和进行处理,该校验和存储在与源代码文件相对应的文件中,并最终进行版本控制。
该过程大致如下(并非完全相同,这只是一个代码示例,用于展示整个过程):
forfin$(FIPS_SOURCES);doperl./util/fips-tokenize$f|opensslsha256-rdone|opensslsha256-hex-outfips.checksum还会有一些机制来提醒我们有关变化的信息,以便我们可以采取适当的措施。例如:
gitdiff--quietfips.checksum||\(gitrev-parseHEAD>fips.commit;scream)关于scream的具体操作尚待确定。
更新fips.checksum应该作为正常的makeupdate的一部分进行,这是通常用于更改和检查版本控制文件的方法,OpenSSL的持续集成已经运行了此命令,以确保没有遗漏任何内容,并且如果有内容被更改,将中断构建过程,运行makeupdate也是正常的OpenSSL发布流程的一部分。
有两种可能的情况:
已验证的校验和列表将在其他地方列出(稍后确定具体位置)。
对于每个FIPSProvider的源文件,我们计算该文件的校验和,并将其与fips.checksum中收集的校验和进行比对,如果存在不匹配,将拒绝编译。
FIPS模块仅包含经过FIPS验证的密码算法,任何FIPS模式的“切换逻辑”将位于FIPS模块边界之外-这将由“fips”属性处理。
以下的FIPSAPI将继续可供应用程序使用(为了保持一致性,使用了与1.1.1版本中相同的名称):
确保当前全局属性设置中设置了“fips=yes”(当on!=0时),或者未设置“fips”(当on==0时)。这还将尝试使用属性“fips=yes”获取HMAC-SHA256算法,并确保它成功返回。
如果当前的全局属性字符串包含属性“fips=yes”(或“fips”),则返回1,否则返回0。
我们可以检查当前是否有提供FIPS算法的Provider可用,并稍微以不同的方式处理。
如果FIPS_mode()返回true,则运行KATs。
完整性测试将不在此处涵盖,如果我们决定提供它,它将是一个单独的函数。
成功时返回1,失败或没有OpenSSLFIPSProvider时返回0。
注意:这些函数只能在OpenSSLFIPSProvider的上下文中运行,而不能在其他任何FIPSProvider的上下文中运行。这些是过时的遗留接口,应使用EVP_set_default_alg_properties()函数进行非遗留配置。
有两个隐含的角色-密码官(CO)和用户。这两个角色都支持相同的服务,唯一的区别是CO负责安装软件,该模块不应支持用户身份验证(对于1级而言不是必需的),所有这些都可以在安全策略中解释,而无需编写具体的代码。
需要定义一个状态机。
我们将需要以下状态:
触发自检失败的方式包括:
将提供一个内部API来设置上述情况的失败状态。
在状态机中未显示的状态以虚线表示。进入和离开错误状态的边缘以虚线表示,以表明不希望遍历这些边缘。
状态模型由这些状态组成:
状态之间的边缘关系如下:
如果可能的话,我们应该尽量在运行状态下注册算法,任何进入运行状态的转换都应该允许注册/缓存密码算法,而任何进入错误或关闭状态的转换都应该清除libcrypto中的所有缓存算法,通过采用这种方法,我们可以避免在所有密码工厂函数中检查状态,这样可以避免为自我测试(手动启动时)提供特殊情况访问权限,同时阻止外部调用者的访问。
FIPS模块提供以下服务。
这些服务仅在运行状态下运行,在任何其他状态下尝试访问服务将返回错误,如果自检失败,则任何尝试访问任何服务的操作都应返回错误。
自测试包括上电自测试(POST)和运行时测试(例如,确保熵不会重复作为RNG的输入)。
POST包括模块完整性检查(每次运行使用FIPS的应用程序时运行)以及算法KAT(可以在安装时运行一次)。
POST测试在调用FIPS模块的OSSL_provider_init()入口点时运行。
为了按照正确的顺序实现完整性测试和KAT,模块需要访问以下数据项:
新的OpenSSL“fips”应用程序将提供安装(运行KAT并输出配置文件数据)和检查(检查配置文件中的值是否有效)的功能。
模块的默认入口点(DEP),即Linux库中的“.init”函数,将设置一个模块变量(可能是状态变量),在OSSL_provider_init()中将检查此变量,并且如果设置了(通常会设置),则验证文件中的值。这个两步过程满足了FIPS要求,即DEP确保测试运行,但允许我们在正常运行时初始化模块的其余部分时实现这些测试。
作为构建过程的一部分,必须将FIPS模块的完整性校验和保存到文件中,这可以通过脚本完成,它只是使用已知固定密钥对整个FIPS模块文件进行HMAC_SHA256运算的结果,如果库已签名,则必须在应用签名之后计算校验和。
至少具有112位的固定密钥将嵌入到FIPS模块中,用于所有HMAC完整性操作,这个密钥也将提供给外部构建脚本。
出于测试目的,即使其中一个或多个测试失败,所有活动的POST测试也会运行。
完整性校验和将在安装过程中保存到一个单独的文件中,默认情况下,该文件将与FIPS模块本身位于相同的位置,但可以配置为位于不同的位置。
KAT的目的是对密码模块进行健康检查,以识别在电源周期之间的模块的严重故障或变化,而不是验证实现是否正确。
注意:完整性测试使用HMAC-SHA-256,因此不需要单独进行HMAC测试。
为了方便修改和更改运行的自测试,自测试应该是数据驱动的,POST测试在注册任何方法之前运行,但方法表仍然可以间接使用,仍然需要较低级别的API来设置密钥(参数、公钥/私钥),密钥加载代码应该在一个单独的函数中进行隔离。
需要一个初始化方法来为高级函数设置所需的依赖项,例如在执行基本调用之前可能需要调用set_cpuid。
应为摘要、密码、签名、DRBG、KDF、HMAC提供不同类型的自测试API。
传递给这些测试的参数是KAT数据。
允许具有至少112位安全强度的算法。
对于签名验证,为了遗留目的,允许使用安全强度至少为80且低于112的算法。
这两个值可以在FIPS模块中定义和执行,也可以在安全策略文档中更简单地处理。
可以通过公共API定义这些最小值,并允许设置它们。
还应添加目标安全强度的概念,该值将在密钥生成算法中使用,这些算法通过其标准指定了目标安全强度参数。
这些标准包含密钥协商协议,为了测试这些协议,加密模块需要包含以下低级原语。
尚未完成的工作是将其整合到FIPS模块中,OpenSSLFIPSProvider将具有强制密钥大小限制的逻辑;
if(target_strength<112||target_strength>256||BN_security_bits(nbits) 对于KASDH参数,支持两种类型: (其中g=2,q=(p-1)/2,priv=[1,q-1],pub=[2,p-2]) TLS:(ffdhe2048,ffdhe3072,ffdhe4096,ffdhe6144,ffdhe8192) IKE:(modp-2048,modp-3072,modp-4096,modp-6144,modp-8192) 只有上述安全素数可以进行验证-其他任何素数都应该失败。 安全素数可用于至少112位的安全强度,可能需要进行FIPS特定的检查以验证群组。 如果需要同时支持两种类型,则需要不同的密钥验证代码。 现有的DH_Check()需要进行FIPS特定的检查来验证批准的类型。 密钥生成对于两者来说是相同的(安全强度和私钥的最大位长度是输入)。 现有代码需要修改,以便在init()阶段未设置IV时生成IV,然后可以使用do_cipher()方法在需要时生成IV。 intaes_gcm_cipher(){..../*oldcodejustreturned-1ifiv_setwaszero*/if(!gctx->iv_set){if(ctx->encrypt){if(!aes_gcm_iv_generate(gctx,0))return-1;}else{return-1;}}}}生成代码如下所示: #defineAES_GCM_IV_GENERATE(gctx,offset)\if(!gctx->iv_set){\intsz=gctx->ivlen-offset;\if(sz<=0)\return-1;\/*Mustbeatleast96bits*/\if(gctx->ivlen<12)\return-1;\/*UseDRBGtogeneraterandomiv*/\if(RAND_bytes(gctx->iv+offset,sz)<=0)\return-1;\gctx->iv_set=1;\}生成的IV可以通过EVP_CIPHER_CTX_iv()方法获取,因此不需要使用ctrlid。 理想情况下,在FIPS模式下尝试设置GCMIV参数将导致错误。实际上,可能仍然有一些应用程序需要设置IV,因此建议将其指定为安全策略项。 当不再需要临界安全参数(CSP)时,我们必须将其全部设置为零,这可能会在不同的上下文中发生: OpenSSLFIPS模块将包含其自己的标准OPENSSL_cleanse()函数的副本来执行清零操作,这是使用特定于平台的汇编语言实现的。 在旧的FIPS模块中存在以下API,可能需要重新添加: 除了下面描述的熵之外,还需要考虑以下几点: 对于所有平台,操作系统将提供熵。对于某些平台,还可以使用内置的硬件随机数生成器,但这将引入额外的验证需求。 对于类UNIX系统,将使用系统调用getrandom、getentropy或随机设备/dev/random作为熵源,优先考虑使用系统调用。可以替代/dev/random的其他强随机设备包括:/dev/srandom和/dev/hwrng。请注意,/dev/urandom、/dev/prandom、/dev/wrandom和/dev/arandom在没有额外的证明的情况下不能用于FIPS操作。 在Windows上,将使用BCryptGenRandom或CryptGenRandom作为熵源。 在VMS上,将使用各种系统状态信息作为熵源。请注意,这将需要证明和分析以证明源的质量。 使用后,初始数据块必须被清零和丢弃。 一旦进入FIPS模块提供的算法,在任何其他的加密操作中我们必须仍然保持在FIPS模块内部。根据FIPS规则,允许一个FIPS模块使用另一个FIPS模块。然而,在3.0设计中,为了简化起见,我们假设不允许这样做。例如,EVP_DigestSign*实现同时使用签名算法和摘要算法,我们不允许其中一个算法来自FIPS模块,另一个来自其他Provider。 所有Provider在初始化时都被分配一个唯一的OSSL_PROVIDER对象,当FIPS模块被要求使用某个算法时,它会验证该算法的实现OSSL_PROVIDER对象是否与自己的OSSL_PROVIDER对象相同(即传递给OSSL_provider_init的对象),例如,考虑使用RSA和SHA256的EVP_DigestSign*的情况,两个算法都会通过Core在FIPS模块外部进行查找,RSA签名算法是第一个入口点,"init"调用将被传递给要使用的SHA256算法的引用,FIPS模块的实现将检查与其被要求使用的SHA256实现关联的OSSL_PROVIDER对象是否也在FIPS模块边界内,如果不是,则"init"操作将失败。下面的图示从FIPS模块的角度说明了这个操作。 请注意,在FIPS模块内部,我们使用了EVP的概念(如EVP_MD_CTX、EVP_PKEY_CTX等)来实现这一点,这些是libcrypto中EVP实现的副本,FIPS模块没有与libcrypto进行链接,这是为了确保完整的操作都在FIPS模块的边界内进行,而不调用外部的代码。 ASN.1DER(DistinguishedEncodingRules)用于: FIPS模块不会包含ASN.1DER编码器/解析器的副本,也不会要求任何Provider对由OpenSSL实现的算法执行ASN.1序列化/反序列化。 用于RSAPKCS#1填充的编码摘要OID将预先生成(与旧FIPS模块使用SHA_DATA宏相同)或根据需要使用简单函数生成,这个函数仅为PKCS#1填充支持的小型摘要集合生成编码的OID,这些摘要OID在“OID树”下的一个公共节点下,验证填充时将获取预期摘要的编码OID,并将其字节与填充中的字节进行比较;不需要进行DER解析/解码。 密码学实现(crypto/evp/e_*.c和大部分crypto/evp/m_*.c,以及任何定义EVP_CIPHER、EVP_MD、EVP_PKEY_METHOD、EVP_MAC或EVP_KDF的代码)必须移出evp目录,它们最终将成为一个或两个Provider的一部分,因此它们应该位于特定Provider的子目录中。 将创建一个新的目录providers/,用于存放特定Provider的代码。providers/build.info定义了哪些源文件在哪些Provider模块中使用。 FIPSProvider模块和默认Provider将共享相同的源代码,在不同的条件下,例如不同的#include路径或定义的宏不同(后者需要在构建系统中添加支持)。下面是一个示例build.info文件,实现了这一点: PROVIDERS=p_fipsp_defaultSOURCE[p_fips]=foo.cINCLUDE[p_fips]=include/fipsSOURCE[p_default]=foo.cINCLUDE[p_default]=include/default或者,使用宏: PROVIDERS=p_fipsp_defaultSOURCE[p_fips]=foo.cDEFINE[p_fips]=FIPS_MODESOURCE[p_default]=foo.c注意:一些关键字还不是build.info语言的一部分。 我们需要对编译时包含FIPS特定代码的方法进行一致处理,并在某些情况下排除FIPS不允许的代码。 构建系统将通过使用-DFIPS_MODE编译FIPSProvider对象文件,以及不带命令行定义的来自相同源的默认Provider对象文件来支持此操作。 对于运行时检查,将需要检查TLS连接是否处于FIPS模式,这可以通过以通用方式检查与特定SSL_CTX或SSL对象关联的属性查询字符串来完成,以查看是否设置了“fips”属性。 需要进行以下类型的测试: 任何需要返回中间值(例如CAVS密钥生成)以显示信息(自检状态)或更改FIPS模块代码的正常流程的特殊情况代码(例如自检失败或在提供固定随机值的密钥生成循环中失败),将通过将回调函数嵌入到FIPS模块代码中进行控制。 建议将这些回调代码以条件编译的方式编入模块中中,因为其中一些值不应该被返回(例如,FIPS模块不应该输出密钥生成中的中间值)。 针对需要使用固定rand_bytes的测试,将对rand_bytes()进行重写。 应用程序可以选择提供一个回调函数,用于处理从FIPS模块接收到的值(如果需要,可以注册多个回调函数)。 可选的应用程序回调函数的形式如下: staticintfips_test_callback(constchar*type,void*arg){return1;}回调函数的返回值可用于控制FIPS模块代码中的特殊情况的流程。 类型由FIPS模块钩子传入,FIPS模块中的每个不同的钩子应具有唯一的类型,类型决定了参数arg的内容(可以是结构体(例如中间值)、名称或整数)。 FIPS模块中的回调函数的形式如下: MY_STRUCTdata;/*valuesthatneedtobereturnedtotheapplication*/data.i=1;.....if(FIPS_test_cb!=NULL)FIPS_test_cb(FIPS_TEST_CB_RSA_KEYGEN_GET,(void*)&data);POST故障测试和日志记录为了支持多个测试的失败,所有测试将始终运行而不提前退出(只是标记失败),在所有测试完成后,将返回失败状态。 用于日志记录或失败的参数将为: struct{constchar*desc;constchar*state;constchar*fail_reason;};其中: CAVS测试将由实验室执行。 然而,每个CAVS测试文件也可以进行抽样,并添加到单元测试中,这意味着可以将单个测试的文件数据转换为单元测试内的二进制数据。 (DRBG_ctr是已经实现了此功能的示例)。 这将确保以下内容: 如果与实验室之间有良好的沟通,我们可以跳过此步骤,但如果实验室发现缺少对内部访问器的访问,可能需要在代码中添加一些额外的回调钩子。 在某些地方,低级API结构被赋值给EVP_PKEY对象,对公共的EVP_PKEY的影响是它必须保留指向可能的低级结构的指针,并且在libcrypto内部必须知道该低级结构的类型,每当具有这种指针的EVP_PKEY用于任何计算时,都必须检查低级结构是否发生了变化,并将其数据转换为可以与新的Provider一起使用的参数。 确定如何检查低级结构的内容是否发生了变化的具体机制有待确定,一种可能的方法是在低级结构中设置一个dirty计数器,并在EVP_PKEY结构中复制一个副本,每当低级结构发生变化时(例如,RSA_set0_key等函数必须执行递增操作),每当EVP_PKEY用于计算时,就会将其副本与低级脏计数器进行比较,如果它们不同,则EVP_PKEY的Provider参数将使用低级结构的数据进行修改。 (另一个想法是通过遗留函数在EVP_PKEY中放置一个回调函数,如果检测到低级别的更改,则更新参数) 在OpenSSL1.1.x中有可以轻松创建各种EVP方法结构的功能,可以像这样找到: 一些被认为是“遗留”的算法(例如IDEA)且具有当前的EVP_CIPHER、EVP_MD、EVP_PKEY_METHOD、EVP_MAC或EVP_KDF实现将移至一个名为"Legacy"的Provider模块,而不是我们的默认Provider模块。 以下算法的方法将成为"Legacy"Provider模块中的调度表: (注意:这不是一个详尽无遗的列表,尽管对目前来说相当完整) 整个ENGINEAPI将在此版本之后的主要发布中被弃用和移除,到那时,人们必须学会如何创建Provider模块。与此同时,它将被转变为一个工具,帮助实现者从ENGINE模块实现过渡到Provider模块实现。 由于算法构造函数将被更改为构造调度表,因此ENGINE类型将变为一组调度表,并且ENGINE构造函数的功能将更改为将它们收集到给定的ENGINE中。 通过这种注册方式注册的调度表将添加一个名为"engine"的属性,并将ENGINE标识作为Provider名称属性。这将使ENGINE_by_id和类似功能能够找到正确的Provider。 ENGINE模块的入口点bind_engine将被替换为Provider模块的入口点,并且宏IMPLEMENT_DYNAMIC_BIND_FN将被更改为构造此类入口点。此入口点将创建一个类似Provider的ENGINE结构,调用绑定函数,该函数将使用与之前相同的方法创建函数将其填充到调度表中,然后使用与以前相同的方法设置函数将所有这些收集到的调度表在ENGINE结构中注册,就像任何Provider模块一样。 与此版本的其余部分一样,我们的目标是源代码级的兼容性。 在OpenSSL1.1.x及更早版本中,可以使用诸如ENGINE_get_default_RSA和ENGINE_get_RSA等函数,将ENGINE提供的方法挂钩以替代内置于libcrypto中的函数,前者无需修改,而后者将被更改为根据附加到引擎的相应调度表创建旧式方法(例如RSA_METHOD)。 属性定义和查询具有明确定义的语法。本节将以eBNF和铁路图的形式介绍该语法。几乎是eBNF,但在某些地方使用了正则表达式扩展。 PropertyName::=[A-Z][A-Z0-9_]*('.'[A-Z][A-Z0-9_]*)*附录2-参数传递Core或Provider对象应该对外部所有内容保持不透明,然而,我们仍然需要能够以统一的方式从中获取参数或向其传递参数。因此,我们需要一个中间的非透明结构来支持此操作。 传递的数据类型需要保持简单: 传递值给模块的任何参数都需要包含以下项: 用于请求模块的值的任何参数都需要包含以下项: 这两个结构非常相似,可以表示为同一个结构: typedefstructossl_param_st{constchar*key;unsignedchardata_type;/*declarewhatkindofcontentissentorexpected*/void*buffer;/*valuebeingpassedinorout*/size_tbuffer_size;/*buffersize*/size_t*return_size;/*OPTIONAL:addresstocontentsize*/}OSSL_PARAM;使用示例: /*passingparameterstoamodule*/unsignedchar*rsa_n=/*memoryallocation*/#if__BYTE_ORDER==__LITTLE_ENDIANsize_trsa_n_size=BN_bn2lebinpad(N,rsa_n,BN_num_bytes(rsa_n));#elsesize_trsa_n_size=BN_bn2bin(N,rsa_n);#endifstructOSSL_PARAMrsa_params[]={{RSA_N,OSSL_PARAM_INTEGER,rsa_n,rsa_n_size,NULL},{0,0,0,0,0},};EVP_set_params(pkey,rsa_params);/*requestingparametersfromamodule*/size_trsa_n_buffer_size=BITS/2/8+1;unsignedchar*rsa_n_buffer=OPENSSL_malloc(rsa_n_size);size_trsa_n_size=0;OSSL_PARAMrsa_params[]={{RSA_N,OSSL_PARAM_INTEGER,rsa_n_buffer,rsa_n_buffer_size,&rsa_n_size},{0,0,0,0,0},};EVP_get_params(pkey,rsa_params);/**Note:wecouldalsohaveactrlfunctionality:*EVP_ctrl(pkey,EVP_CTRL_SET_PARAMS,rsa_params);*EVP_ctrl(pkey,EVP_CTRL_GET_PARAMS,rsa_params);**ThiswouldallowothercontrolsusingthesameAPI.*Foraddedflexibility,thesignaturecouldbesomethinglike:**intEVP_ctrl(EVP_CTX*ctx,intcmd,...);*/数据类型该规范支持以下参数类型: #defineOSSL_PARAM_INTEGER1#defineOSSL_PARAM_UNSIGNED_INTEGER2#defineOSSL_PARAM_UTF8_STRING3#defineOSSL_PARAM_OCTET_STRING4/**Thisoneiscombinedwithoneoftheabove,i.e.togetastringpointer:*OSSL_PARAM_POINTER|OSSL_PARAM_UTF8_STRING*/#defineOSSL_PARAM_POINTER0x80实现细节确定缓冲区的大小在请求参数值时,调用者可以选择在一个或多个参数结构中将buffer赋值为NULL。被调用的getter应该通过填充return_size指向的大小并返回来回应这样的请求,此时调用者可以分配适当大小的缓冲区,并进行第二次调用,此时getter可以毫无问题地填充缓冲区。 如果程序员希望,return_size可以指向同一个OSSL_PARAM中的buffer_size。 配置值对于Provider来说是有兴趣的!然而,仅仅在Core和Provider之间传递一个CONF指针可能是不可行的,尽管它当前是一个非透明的结构。 另一种方法是将CONF/NCONF值转换为参数,其中的灵感来自于git配置值的命名方式。 让我们首先设想一个与当前ENGINE配置模块类似的Provider配置: [provider_section]#Configureprovidernamed"foo"foo=foo_section#Configureprovidernamed"bar"bar=bar_section[foo_section]provider_id=myfoomodule_path=/usr/lib/openssl/providers/foo.soselftests=foo_selftest_sectionalgorithms=RSA,DSA,DH[foo_selftest_section]doodah=1cookie=0对于Provider"foo",Core侧的Provider结构可以响应以下请求的参数keys: 请注意,section名称本身从未出现在参数key中,而是出现在导致该section的key中。这是因为OpenSSL允许任意命名的section名称。 要求 标准 说明 TDES CBC TDES仅支持解密(从2020年开始)和禁止解密(从2025年开始)。