OpenSSL XOF 可扩展输出函数设计解析:从单次 squeeze 到 EVP_DigestSqueeze 多段输出

发布时间:2026/9/10 12:28:52
OpenSSL XOF 可扩展输出函数设计解析:从单次 squeeze 到 EVP_DigestSqueeze 多段输出 OpenSSL XOF 可扩展输出函数设计解析从单次 squeeze 到 EVP_DigestSqueeze 多段输出【免费下载链接】opensslGeneral purpose TLS and crypto library项目地址: https://gitcode.com/GitHub_Trending/ope/openssl导读本文基于 OpenSSL 仓库中的 XOF 设计文档系统讲解可扩展输出函数Extendable Output FunctionXOF在 OpenSSL 中的演进过程从最初只支持一次 squeeze 拿到全部输出到通过新增EVP_DigestSqueeze()API 支持任意多次分段输出。你将看到海绵构造Sponge Construction中 absorb / squeeze 的语义、EVP 层的 API 设计权衡三套候选方案对比、底层SHA3_squeeze()的四种改造思路以及这些设计在 crypto/evp/digest.c、crypto/sha/sha3.c、crypto/sha/keccak1600.c 等源码中的真实落地形态可直接用于理解与使用 SHAKE、cSHAKE 等 XOF 算法。一、XOF 的定义与最小接口契约1.1 什么是 XOF可扩展输出函数XOF被定义为作用在消息上的可变长度哈希函数其输出可以被扩展到任意期望的长度。与固定输出的 SHA-2、SHA-3 不同XOF 的输出长度不再受摘要长度限制调用方可以按需挤出任意多的字节。OpenSSL 目前涉及的 XOF 包括SHAKE128 / SHAKE256FIPS 202通过 providers/implementations/digests/sha3_prov.c 中IMPLEMENT_SHAKE_functions(128/256)注册cSHAKE 家族NIST SP 800-185以cshake_keccak_128/256的形式注册是 KMAC、TupleHash 等算法的底层以及同样采用海绵结构的 Keccak 变体。1.2 最小接口伪代码设计文档给出一个 XOF 至少需要支持的调用序列xof xof.new(); xof.absorb(bytes1); xof.absorb(bytes2); xof.finalize(); out1 xof.squeeze(10); out2 xof.squeeze(1000);这段伪代码定义了三个核心动词absorb吸收输入、finalize结束吸收与squeeze挤压输出且输出可以分多次、按不同长度取出。1.3 四条基本规则文档为上述接口明确了四条约束absorb 可以被多次调用输入数据不必一次性全部喂入finalize 结束 absorb 过程其本质是补上填充字节padding并做最后一次 absorb一旦 finalize 完成除非发生 reset否则不得再调用 absorbfinalize 可以合并到第一次 squeeze 操作中完成这意味着显式 finalize并非强制要求squeeze 可以被多次调用这正是本文讨论的核心设计难点。二、OpenSSL 的现状只支持单次 squeeze设计文档明确指出OpenSSL 当前设计时的 XOF 实现只支持对 squeeze 的单一调用。这个假设同时存在于两个层面高层 APIEVP_DigestFinalXOF()底层运算SHA3_squeeze()存在通用 C 实现以及面向不同平台的手写汇编版本见 crypto/sha/asm/ 目录下十余个keccak1600-*.pl文件。该现状带来的直接后果是调用方无法像标准海绵模型那样边用边挤、按需取用必须在一次调用中指定完整的输出长度。2.1 高层 API 的单次限制从何而来查看 crypto/evp/digest.c 中EVP_DigestFinalXOF()的实现可以看到它通过一个标志位来约束调用次数若上下文中已置位EVP_MD_CTX_FLAG_FINALISED定义于 include/crypto/evp.h值为0x0800则直接报EVP_R_FINAL_ERROR正常路径下它会通过OSSL_PARAM_construct_size_t(OSSL_DIGEST_PARAM_XOFLEN, size)把期望的输出长度以参数形式传给 provider再调用ctx-digest-dfinal()一次性产出全部输出最后置位EVP_MD_CTX_FLAG_FINALISED。源码注释还透露了一段历史该函数最初写好后曾附带一个 reset 动作但后来被认为不正确而移除。这也解释了为什么它被明确定义为one shot operation一次性操作。2.2 底层 squeeze 的单次限制通用 C 版本的SHA3_squeeze()位于 crypto/sha/keccak1600.cvoid SHA3_squeeze(uint64_t A[5][5], unsigned char *out, size_t len, size_t r, int next)其设计缺陷在于除非请求的输出长度是r速率 rate即每轮 Keccak-f[1600] 之后可输出的字节数的整数倍否则函数没有能力记录当前处于状态 A 中的哪个位置——下次再调用时无从接续。同时由于最初只服务于一次性输出场景函数末尾刻意避免了多做一次KeccakF1600()置换。2.3 设计时需要权衡的约束文档列出设计变更必须满足的三点是否需要一个新 API以及变更对既有应用的影响面变更对共享同一底层代码的其他函数例如 SHAKE 与 SHA3 共用海绵核心影响应尽量小旧 provider 兼容性未更新支持该变更的旧 provider在使用新 core 发起多次 squeeze 时应返回错误而不是静默给出错误数据。三、API 层设计讨论三套候选方案围绕如何让 squeeze 支持多次调用文档给出了三套 API 层方案并逐一分析利弊。3.1 Proposal 1改造 EVP_DigestFinalXOF() 支持多次调用直接修改EVP_DigestFinalXOF(ctx, out, outlen)使其可被多次调用甚至可以提供一个别名EVP_DigestSqueeze()。核心改动只是删除那个标志位检查。优点不需要新增 API。缺点Final最终/结束这个语义名字被多次调用显得很奇怪——命名与实际行为相悖。3.2 Proposal 2最终采纳保留一次性函数新增 EVP_DigestSqueeze()保持EVP_DigestFinalXOF()为一次性函数不变新增专门处理多段输出的 APIEVP_DigestSqueeze(ctx, out, outlen)优点名字更贴切squeeze 正是海绵模型的术语既有函数完全不受影响多段输出所需的特殊逻辑不需要侵入旧代码路径既有 API 行为保持一致向后兼容文档提到至少一个其他工具库采用了同样的保留 Final 新增 Squeeze策略。缺点多了一个 API两个 API 之间的交互关系必须被明确文档化交互约束是互斥的EVP_DigestFinalXOF()之后再调用EVP_DigestSqueeze()会失败因为 Final 已声明不再有输出可取反之先EVP_DigestSqueeze()再调用EVP_DigestFinalXOF()同样失败。3.3 Proposal 3创建全新类型 EVP_XOF_MD彻底引入一个独立的EVP_XOF_MD类型其接口主要由 Init、Absorb、Squeeze 组成并可将DigestXOF标记为废弃。优点XOF 操作被完全独立出来接口语义纯净。缺点后量子Post-Quantum签名目前依赖EVP_MD对象来承载 XOF引入新类型会连带复杂化签名 API——这是否决此方案的现实原因会造成EVP_MD代码的重复尽管届时 legacy/engine 代码已全部移除。3.4 最终选择的落地证据Proposal 2 在仓库中得到了完整落地声明include/openssl/evp.h 中EVP_DigestFinalXOF()与EVP_DigestSqueeze()相邻声明实现crypto/evp/digest.c。EVP_DigestSqueeze()刻意不做 FINALISED 标志检查可多次调用其关键分支是if (ctx-digest-dsqueeze NULL) { ERR_raise(ERR_LIB_EVP, EVP_R_METHOD_NOT_SUPPORTED); return 0; } return ctx-digest-dsqueeze(ctx-algctx, md, size, size);这正是文档中旧 provider 应产生错误要求的实现——provider 通过提供dsqueeze函数指针来表示自己支持多段输出不支持的旧 provider 返回EVP_R_METHOD_NOT_SUPPORTED。EVP_DigestFinalXOF()中0x0800标志位检查依旧保留两种 API 的互斥约束与 Proposal 2 描述完全一致。四、API 命名之争Squeeze 还是 Extract文档单列一节讨论新 API 的命名。背景是目前 OpenSSL 使用的 XOF 都是海绵构造sponge construction其术语天然就是 absorb 与 squeeze但未来会出现非海绵构造的 XOF例如 BLAKE2。候选名有两个EVP_DigestSqueeze—— 与海绵模型术语一致最终选定EVP_DigestExtract—— 被否决理由是extract 与 expand 是 HKDF 的既有术语HKDF-Extract / HKDF-Expand沿用会造成概念混淆。五、围绕 XOF 的其余 API 设计除 squeeze 之外文档还明确了 Init、Absorb、Finalize、Reset、状态拷贝五类操作的形态均与海绵模型一一对应。5.1 Init普通初始化XOF 摘要的初始化与普通摘要完全相同文档给出 SHAKE256 的示例md EVP_MD_fetch(libctx, SHAKE256, propq); ctx EVP_MD_CTX_new(); EVP_DigestInit_ex2(ctx, md, NULL);5.2 Absorb多次 EVP_DigestUpdate吸收阶段通过多次EVP_DigestUpdate(ctx, in, inlen)完成。文档记录了一个讨论过的提案是否提供别名函数EVP_DigestAbsorb(ctx, in, inlen);共识是不需要——直接复用EVP_DigestUpdate即可语义已经足够清晰。5.3 Finalize并入第一次 squeeze无需显式 finalize填充与最终 absorb 被合并到第一次 squeeze 操作中完成对应规则第 3 条。5.4 Reset重新初始化重置上下文只需再次调用EVP_DigestInit_ex2(ctx, NULL, NULL);底层对应 crypto/sha/sha3.c 的ossl_sha3_reset()清零状态矩阵A、清空缓冲计数bufsz并将xof_state置回XOF_STATE_INIT。5.5 State Copy状态拷贝海绵状态可以整体复制以便在同一份输入上派生多条独立输出流EVP_MD_CTX_copy_ex(ctx, newctx);这在确定性派生如从同一消息生成多个独立密钥/随机流场景下非常实用。provider 层的对应实现是 sha3_prov.c 的keccak_dupctx()直接按字节拷贝整个KECCAK1600_CTX。六、底层 squeeze 改造四种方案的技术权衡解决了 API 层之后更关键的是底层SHA3_squeeze()如何在多次调用时保持正确性。文档给出四种方案它们的取舍最终塑造了今天的实现。6.1 方案一为状态 A 增加位置追踪参数修改SHA3_squeeze()增加一个输入/输出参数用于记录状态 A 内部的当前读取位置。优点C 代码改动最小仅多传一个参数且没有额外的缓冲结果内存拷贝缺点参考 C 实现中包含大量if分支逻辑复杂该逻辑还必须在每种汇编实现中重写而不同架构下状态 A 的内部格式如比特去交错 BitDeinterleave 的处理各不相同汇编改动量大且彼此不同通用 SHA3 路径若不复制代码会变慢。6.2 方案二保留 SHA3_squeeze()在 final 层缓冲调用不修改SHA3_squeeze()本体在更高层final把多次输出请求缓冲拼接。优点改动主要集中在 C 代码缺点由于SHA3_squeeze()的一次性本质缓冲层仍需直接调用KeccakF1600()这要求把汇编版KeccakF1600()暴露为公共接口——而它本不打算暴露因为状态 A 的内部格式在不同平台架构上可能不同内部缓冲区的清理时机何时清空难以界定。6.3 方案三一次性挤出全部输出再丢弃前缀对原始吸收数据执行一次完整 squeeze然后丢弃输出缓冲的前半部分。优点实现最简单缺点极度低效——每次小请求都要重算整个海绵且本质上只是权宜之计hack并非真正的解决方案。6.4 方案四最终采纳给 SHA3_squeeze() 增加布尔参数在方案二思路上修正轻微修改SHA3_squeeze()传入一个布尔值用于在多段调用时正确控制KeccakF1600()的触发时机。优点C 实现相当简单状态数据继续作为不透明 blobopaque保存无需暴露内部格式当outlen较大时SHA3_squeeze()可以直接使用调用方的输出缓冲减少中间拷贝。缺点需要小幅修改汇编代码以传递该布尔值并正确处理KeccakF1600()调用对不足一个块r字节的零头输出需要使用memcpy将部分结果暂存于缓冲。七、最终实现两层配合如何落地7.1 底层next 参数与状态机方案四的产物就是 crypto/sha/keccak1600.c 中带注释的SHA3_squeeze()/* * SHA3_squeeze may be called after SHA3_absorb to generate |out| hash value of * |len| bytes. * If multiple SHA3_squeeze calls are required the output length |len| must be a * multiple of the blocksize, with |next| being 0 on the first call and 1 on * subsequent calls. It is the callers responsibility to buffer the results. * When only a single call to SHA3_squeeze() is required, |len| can be any size * and |next| must be 0. */要点多次调用时len必须是块大小的整数倍next首次为 0、后续为 1调用方更高层负责缓冲不足一个块的剩余字节核心逻辑if (next) KeccakF1600(A);解决了多次调用之间需要推进海绵置换的问题——这正是文档方案四描述的布尔参数。7.2 中层C 通用版的多段 squeeze 缓冲策略crypto/sha/sha3.c 的ossl_shake_squeeze_default()实现了缓冲 分块策略其注释明确说明思路不去大改SHA3_squeeze()的汇编而是利用其既有约束——每次只请求块大小的整数倍小于一个块的请求则申请一个块并缓存剩余部分。其执行步骤为首次调用时完成吸收收尾补 10*1 填充ctx-buf[num] ctx-pad; ctx-buf[bsz - 1] | 0x80;执行最后一次SHA3_absorb()并将next置 0消耗上次 squeeze 缓存的零头字节ctx-buf尾部整块部分直接写入输出缓冲SHA3_squeeze(ctx-A, out, len, bsz, next)零头部分再多 squeeze 一个块进内部缓冲拷出所需字节剩余部分留待下次ctx-bufsz bsz - outlen。这一设计完美呼应了文档方案二缓冲调用与方案四布尔参数的结合——C 层缓冲 底层布尔参数两层各司其职。7.3 状态机XOF_STATE 四态流转整个流程由 include/internal/sha3.h 定义的四个状态驱动#define XOF_STATE_INIT 0 #define XOF_STATE_ABSORB 1 #define XOF_STATE_FINAL 2 #define XOF_STATE_SQUEEZE 3ossl_sha3_absorb()仅在 INIT 或 ABSORB 态接受输入吸收满块后转入 ABSORBossl_sha3_final()在 FINAL 或 SQUEEZE 态拒绝调用ossl_sha3_squeeze()在 FINAL 态拒绝调用成功 squeeze 后置为 SQUEEZE 态。状态机保证了文档四条规则在底层被强制执行finalize 后不可 absorb、squeeze 可重复但不可回退。7.4 provider 层dsqueeze 分发sha3_prov.c 的shake_squeeze()是 provider 侧的处理函数通过OSSL_FUNC_DIGEST_SQUEEZE分发项见PROV_FUNC_SHAKE_DIGEST宏注册于同一文件的 490-500 行接入EVP_DigestSqueeze()的dsqueeze调用链。注意其执行前提if (ctx-meth.squeeze NULL) return 0;——平台方法表PROV_SHA3_METHOD未提供 squeeze 能力的算法如普通 SHA3-256在这里直接拒绝。值得注意的还有平台差异SHAKE 的 provider 实现在 sha3_prov.cS390X 路径与 crypto/sha/sha3.cS390X 路径各有一份shake_squeeze_s390x它们直接借助 s390x 的klmd指令完成多段输出x86_64、ARM 等平台则走通用 C 缓冲路径或各自的SHA3_squeeze汇编如 keccak1600-x86_64.pl 中带next参数的SHA3_squeeze符号。这正是文档反复强调的不同架构汇编格式不同的真实写照。八、测试与验证仓库测试层面对该设计有直接覆盖test/evp_test.c 支持以XOF yes关键字声明测试向量通过EVP_DigestFinalXOF()校验同一文件 test/evp_test.c 中通过OSSL_PARAM_construct_size_t(OSSL_DIGEST_PARAM_XOFLEN, ...)构造参数印证了EVP_DigestFinalXOF()用参数传递输出长度的实现细节test/evp_test.c 对EVP_DigestFinalXOF()输出与预期向量逐字节比对任何多段 squeeze 引发的状态机错误都会在这里暴露。结合 doc/man7/EVP_DigestInit.pod 与 doc/man3/EVP_DigestFinalXOF.pod 等手册页读者可进一步获得完整的参数表如OSSL_DIGEST_PARAM_XOFLEN与调用约束说明。九、设计决策小结决策点结论落地位置API 方案保留一次性EVP_DigestFinalXOF()新增EVP_DigestSqueeze()crypto/evp/digest.cAPI 命名Squeeze而非Extract避开 HKDF 术语冲突include/openssl/evp.hAbsorb 别名不新增EVP_DigestAbsorb()沿用EVP_DigestUpdatecrypto/evp/digest.c底层方案SHA3_squeeze()增加next布尔参数 C 层块缓冲crypto/sha/keccak1600.c、crypto/sha/sha3.c旧 provider 兼容未提供dsqueeze的 provider 返回EVP_R_METHOD_NOT_SUPPORTEDcrypto/evp/digest.c状态约束四态状态机强制 absorb/finalize/squeeze 时序include/internal/sha3.h这套设计的关键启示在于对外 API 的语义纯净与向后兼容通过对内增加一个布尔参数和一层缓冲逻辑实现——既没有破坏既有一次性调用者的行为也没有暴露平台相关的海绵状态内部格式同时为未来非海绵构造的 XOF如 BLAKE2预留了命名空间。对于需要使用 SHAKE 派生任意长度密钥流、或者在后量子签名方案中按需挤压输出字节的开发者EVP_DigestSqueeze()与配套的状态拷贝、重置 API 构成了一个完整且可组合的编程模型。【免费下载链接】opensslGeneral purpose TLS and crypto library项目地址: https://gitcode.com/GitHub_Trending/ope/openssl创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考