news 2026/9/10 12:28:06

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 Function,XOF)在 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 / SHAKE256(FIPS 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 四条基本规则

文档为上述接口明确了四条约束:

  1. absorb 可以被多次调用:输入数据不必一次性全部喂入;
  2. finalize 结束 absorb 过程:其本质是补上填充字节(padding)并做最后一次 absorb;一旦 finalize 完成,除非发生 reset,否则不得再调用 absorb;
  3. finalize 可以合并到第一次 squeeze 操作中完成:这意味着"显式 finalize"并非强制要求;
  4. 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.c:

void 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()为一次性函数不变,新增专门处理多段输出的 API:

EVP_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_SUPPORTEDEVP_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 实现相当简单;
    • 状态数据继续作为不透明 blob(opaque)保存,无需暴露内部格式;
    • 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()的汇编,而是利用其既有约束——每次只请求块大小的整数倍;小于一个块的请求则申请一个块并缓存剩余部分。其执行步骤为:

  1. 首次调用时完成吸收收尾:补 10*1 填充(ctx->buf[num] = ctx->pad; ctx->buf[bsz - 1] |= 0x80;),执行最后一次SHA3_absorb(),并将next置 0;
  2. 消耗上次 squeeze 缓存的零头字节ctx->buf尾部);
  3. 整块部分直接写入输出缓冲SHA3_squeeze(ctx->A, out, len, bsz, next)
  4. 零头部分:再多 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 3
  • ossl_sha3_absorb()仅在 INIT 或 ABSORB 态接受输入,吸收满块后转入 ABSORB;
  • ossl_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.c(S390X 路径)与 crypto/sha/sha3.c(S390X 路径)各有一份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.c
API 命名Squeeze而非Extract(避开 HKDF 术语冲突)include/openssl/evp.h
Absorb 别名不新增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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 12:26:18

SQL Server数据库自动清理双模式设计与实现

1. 项目概述:数据库自动清理的双模式设计 在数据处理领域,数据库膨胀是个永恒话题。我维护的某个生产系统曾因未及时清理历史数据,导致单表体积达到惊人的120GB,查询性能断崖式下跌。这个惨痛教训促使我设计了一套基于C#和SQL Ser…

作者头像 李华
网站建设 2026/9/10 12:25:07

图像配准偏移计算:互相关与归一化互相关方法详解

简介:面向图像处理初学者、科研人员与工程开发者,这份MATLAB示例包聚焦图像互相关、最大互相关与相关图像配准,通过自动计算两幅图像在所有位移下的相似度,找出互相关函数的峰值位置,从而得到x/y方向的配准偏移量&…

作者头像 李华
网站建设 2026/9/10 12:24:48

W25Q512芯片如何使用(通用篇)

首先拿到一颗芯片首先要获取它的数据手册,一般来讲获取手册的方法就是搜索立创,然后寻找你需要的芯片,这里不在过多的赘述。作为嵌入式工程师,通常会遇到一般分为两种情况,一种就是自己需要设计板子,完成硬…

作者头像 李华
网站建设 2026/9/10 12:21:05

4步搭好APIJSON测试体系:JUnit实战与覆盖率提升指南

4步搭好APIJSON测试体系:JUnit实战与覆盖率提升指南 【免费下载链接】APIJSON 🏆 Real-Time no-code, powerful and secure ORM 🚀 providing APIs and Docs without coding by Backend, and Frontend(Client) can customize response JSONs …

作者头像 李华