算法注册表
面向哈希、AEAD、KEM、KDF 和签名的命名标识符注册表,以及让后量子迁移只做加法、不破坏兼容的算法可演进规则。
Label 309 中的每一项密码学选择都由取自可扩展注册表的字符串标识符来命名,例如 sha2-256、chacha20-poly1305-stream64k、EdDSA 等等。记录从不携带原始的算法编号,也不依赖任何隐含的假定,而是明确写出自己用了哪个原语,再由验证器据此查表确认。这正是算法可演进(algorithm-agile)这一不变式背后的机制:注册表可以随时间扩充,而当验证器遇到一个自己尚未实现的标识符时,它会以稳定且带类型的错误拒绝该记录,既不会崩溃,也绝不会悄悄接受自己无法核验的东西。
正是这一条规则,让向后量子算法的迁移成为只做加法的事情。新增一个标识符,只是往表里加一行,而不是给线格式(wire format)出一个新版本。旧记录依旧像以前一样能通过验证,旧验证器则会对那些它当初设计时根本无从理解的记录安全地失败关闭(fail closed)。
两条始终生效的规则
贯穿每一张注册表,有两条约束是绝对的。实现 MUST NOT 自行发明新的密码学,每一个原语都要能追溯到某个公开命名的标准。同时,所有加密 MUST 经过认证:只允许使用 AEAD 构造,绝不允许把一个未认证的密码与外挂的(或干脆缺失的)完整性校验拼在一起。
哈希
内容哈希是每条记录的首要主张,因此哈希注册表是整套体系中最吃重的一环。两个已注册的函数都产出 32 字节摘要,且符合规范的实现都 MUST 同时支持这两者。
| 标识符 | 算法 | 摘要 |
|---|---|---|
sha2-256 | SHA-256 (FIPS 180-4) | 32 B |
blake2b-256 | BLAKE2b-256 (RFC 7693) | 32 B |
为做到纵深防御,生产方 MAY 用两个函数分别对同一内容计算哈希;但单个哈希就足以构成一条有效记录。
Merkle 承诺
为了把一组有序的叶子节点承诺到单一的链上根(root)之下,Label 309 注册了一个 Merkle 承诺标识符。它是 SHA-256 二叉 Merkle 树在 IANA 注册的字符串,沿用叶子前缀(0x00)与内部节点前缀(0x01)的域分隔,以此防止叶子与节点之间发生碰撞。
| 标识符 | 算法 | 根 |
|---|---|---|
rfc9162-sha256 | RFC 9162 二叉 Merkle 树,SHA-256 | 32 B |
只有一个叶子的树承诺的是 SHA-256(0x00 ‖ leaf),而不是裸叶子本身,所以单文件证明 MUST 使用普通的哈希标识符,绝不能用只含 1 个叶子的树。
AEAD
AEAD 注册表规定由哪种内容格式在传输过程中保护密封载荷——即 enc.aead 字段。enc.scheme: 1 之下恰好注册了一个标识符,而且它是一种分段格式,而非单次(single-shot)密码。
| 标识符 | 算法 | 密钥 / 随机数 / 逐块随机数 / 标签 | 状态 |
|---|---|---|---|
chacha20-poly1305-stream64k | ChaCha20-Poly1305,64 KiB 分段 STREAM | 32 B / 24 B / 12 B / 每块 16 B | 强制 —— 线上格式 |
aes-256-gcm | AES-256-GCM | — | 保留(未来配置档) |
chacha20-poly1305-stream64k 即 ChaCha20-Poly1305(RFC 8439),采用 age v1 规范 的 64 KiB 分段 STREAM 布局:明文被切成 65536 字节的块,每块都在内容密钥之下、配一个 12 字节的逐块随机数 uint88_be(counter) ‖ final_flag(计数器从 0 起,最后一块的 final_flag 为 0x01)和一个空的逐块 AAD 加以封装,每块产出一个 16 字节标签。那 24 字节的 enc.nonce 不是 逐块随机数:它是内容密钥 HKDF 的信封唯一 salt,正是这一点让那些计数器随机数得以安全——内容密钥是一次性的,因此没有两条流会共用同一个 (key, nonce) 对,无状态生产方也从不需要在多个信封之间协调随机数。这套分段布局让验证器能在有界内存下增量地认证并释放一份大载荷,而那个末块标志让截断得以被检测。线上的拼写恰好是 chacha20-poly1305-stream64k;其他拼写 MUST NOT 被产出。完整构造见 密封 PoE。
chacha20-poly1305-stream64k 是唯一可以作为线上内容格式出现的标识符。另有一个构造 chacha20-poly1305(RFC 8439,32 字节密钥 / 12 字节随机数 / 16 字节标签)在内部用于在密封构造里包裹每位接收方各自的密钥:随机数为 12 字节全零,AAD 设为所选 KEM 的 info 标签,产出一个 48 字节的 wrap(32 字节被包裹密钥 + 16 字节标签)。它是一个构建块,而非线上标识符;任何把它写进 enc.aead 的记录都 MUST 被拒绝。aes-256-gcm 虽有命名却处于未启用状态,它为未来的某个加密配置档(enc.scheme: 2)保留,v1 验证器会拒绝任何选用它的记录。
KEM
KEM 注册表涵盖的是用于把密封载荷寻址到特定接收方的密钥封装机制。Label 309 注册了一个经典曲线 KEM,以及一个自首个版本起就启用的后量子混合机制。
| 标识符 | 算法 | 公钥 / 私钥 | 密文 / 共享密钥 |
|---|---|---|---|
x25519 | X25519 ECDH (RFC 7748) | 32 B / 32 B | 32 B / 32 B |
mlkem768x25519 | X-Wing 混合(ML-KEM-768 + X25519) | 1216 B / 32 B | 1120 B / 32 B |
mlkem768x25519 即 draft-connolly-cfrg-xwing-kem-10 中的 X-Wing 构造:它把 ML-KEM-768(FIPS 203)与 X25519(RFC 7748)配对,使得攻击者必须两者皆破才能还原出共享密钥。公钥是 ML-KEM-768 的封装密钥与 X25519 公钥拼接而成(1184 B ‖ 32 B = 1216 B);私钥则是一个 32 字节的种子,完整密钥由它推导得出。每位接收方的密文是 ML-KEM-768 密文与一个临时 X25519 公钥拼接而成(1088 B ‖ 32 B = 1120 B),两个共享密钥再由 X-Wing 的 SHA3-256 组合器(FIPS 202)组合成最终的 32 字节密钥。Label 309 把 X-Wing 当作一个黑盒 KEM 来使用,只取封装、解封装、那个 32 字节共享密钥这三样,而不依赖组合器内部哈希的任何性质。该标识符写成不含内部连字符的形式,以贴合 X-Wing 业已确立的拼写。
混合机制由加密头逐记录选定,与内容 AEAD 相互独立。由于它已经注册在册,启用后量子机密性只是挑选标识符的事情,无需等待新的线格式版本。
一条记录恰好命名一个 enc.kem,且每个槽位都使用该 KEM 的形状;形状不符的槽位是 ENC_SLOT_INVALID_SHAPE,长度不对的 epk(≠ 32 B)或 kem_ct(≠ 1120 B)是 KEM_EPK_LENGTH_MISMATCH / KEM_CT_LENGTH_MISMATCH,未注册的 enc.kem 则是 UNSUPPORTED_KEM_ALG。封装材料还必须在同一个 slots[] 内彼此互异:所有 epk 值(对 x25519)或所有 kem_ct 值(对 mlkem768x25519)都 MUST 各不相同。记录内部出现重复时,会在任何 KEM 或 AEAD 原语运行之前就以 ENC_SLOTS_DUPLICATE_KEM_MATERIAL 被拒绝,因为重复的 epk 或 kem_ct 会破坏零随机数包裹所依赖的逐槽位密钥唯一性。在任何原语之前,验证器还要给解析器的资源消耗划下边界:slots[] 超过 1024 槽这一参考上限的信封是 ENC_SLOTS_TOO_MANY,解码后的 enc 信封超过 65 536 字节是 ENC_ENVELOPE_TOO_LARGE。这两个上限都远高于约束任何诚实记录的那道约 16 KiB 的 Cardano 元数据天花板;它们由验证器强制执行,是部署期钉定的常量——而非线上字段——各部署 MAY 把它们收得更紧。
KDF
KDF 注册表命名各个密钥推导函数。hkdf-sha256 在密封构造内部推导密钥;argon2id 则对人类口令做抗暴力破解的拉伸,并带有一条强制的参数下限。
| 标识符 | 算法 | 参数 |
|---|---|---|
hkdf-sha256 | HKDF-SHA-256 (RFC 5869) | salt(可选)、info(可选)、输出长度 |
argon2id | Argon2id (RFC 9106) | 内存 ≥ 65536 KiB、迭代次数 ≥ 3、并行度 ≥ 1 |
Argon2id 的下限是规范性的:受口令保护的载荷 MUST 至少使用 64 MiB 内存、至少三次迭代、至少一条并行通道。生产方 MAY 选择更强的参数,这些参数会随记录一同传递,以便验证器复现整个推导过程。在平台支持的前提下,生产方 SHOULD 把并行度设为 p = 4——这是 RFC 9106 §4 推荐的第二套配置——而验证器 MAY 接受任意 p ≥ 1,但要受部署期天花板的约束。那些天花板是实现层面的一条 SHOULD、而非 MAY:为防范荒谬参数带来的验证器端拒绝服务,验证器 SHOULD 强制上限,并以 ENC_PASSPHRASE_PARAMS_EXCEED_POLICY 上报。这道天花板取决于硬件、并非规范性的,且 MUST NOT 与下限码 ENC_PASSPHRASE_ARGON2_PARAMS_TOO_LOW 相混淆。
只有 argon2id 是可在线上选用的:它是记录唯一可以写进 enc.passphrase.alg 的标识符。hkdf-sha256 是一个内部构建块,是种子到密钥推导、逐槽位 KEK 推导、槽集 MAC 密钥、口令承诺 MAC 密钥以及内容密钥推导背后那一步固定的提取–扩展(extract-and-expand),它不携带任何线上标识符。任何把 hkdf-sha256 写进 enc.passphrase.alg 的记录都 MUST 被拒绝:HKDF 是为高熵输入而设计的,并不用来拉伸低熵口令。
内部标签是常量,从不上线
密封构造的域分隔来自一组固定的标签字面量——HKDF 的 info 标签,以及若干 SHA-256 前缀(KEK-salt 前缀、转录前缀,以及项哈希前缀)。每一个都是 enc.scheme: 1 的常量,是精确的 ASCII,不带终止符也不带长度前缀;它们从不被序列化,也不可通过任何注册表选用。一共有十一个:
| 标签 | 作用 |
|---|---|
cardano-poe-kek-v1 | x25519 路径上逐槽位 KEK 的 HKDF info |
cardano-poe-kek-mlkem768x25519-v1 | mlkem768x25519 路径上逐槽位 KEK 的 HKDF info |
cardano-poe-x25519-kek-salt-v1 | x25519 KEK 的 HKDF salt 所用的 SHA-256 前缀 |
cardano-poe-xwing-kek-salt-v1 | mlkem768x25519 KEK 的 HKDF salt 所用的 SHA-256 前缀 |
cardano-poe-item-hashes-v1 | 项哈希摘要 hashes_hash 所用的 SHA-256 前缀 |
cardano-poe-slots-transcript-v1 | slots 转录哈希 slots_hash 所用的 SHA-256 前缀 |
cardano-poe-slots-mac-v1 | 槽集 MAC 密钥的 HKDF info |
cardano-poe-passphrase-transcript-v1 | 口令转录哈希 pw_hash 所用的 SHA-256 前缀 |
cardano-poe-passphrase-mac-v1 | 口令承诺 MAC 密钥的 HKDF info |
cardano-poe-payload-v1 | slots 路径内容密钥的 HKDF info |
cardano-poe-payload-passphrase-v1 | 口令路径内容密钥的 HKDF info |
两个 KEK salt 共享同一种带标签哈希形态——SHA-256(label ‖ enc.nonce ‖ <slot KEM material> ‖ pub_R)——只是各自带着自己那个逐 KEM 的标签。这些标签与 密钥 上的种子推导 info 字符串、以及 签名 上的记录签名域前缀都不相同:整个集合无碰撞、且互不为前缀,因此没有任何一个逐记录的密封标签会等于、或成为某个长期密钥推导标签的字节前缀,于是身份密钥推导与逐记录密钥包裹绝不会相撞。验证器 MUST 逐字节地使用每个字面量;哪怕只差一个字节,都会得到一个诚实生产方无法复现的 slots_mac、承诺或 AEAD 标签。消费每个标签的逐字节构造,参见 密封 PoE。
签名
Label 309 注册了一个签名算法。作者身份签名始终是可选的,但一旦出现,就以 COSE_Sign1(RFC 9052)的形式、采用 Ed25519 来承载。
| 标识符 | COSE 算法 | 算法 | 封装 |
|---|---|---|---|
EdDSA | -8 | Ed25519 (RFC 8032) | COSE_Sign1(RFC 9052) |
验证遵循 RFC 8032 §5.1.7 的严格要求:实现 MUST 拒绝非规范化的签名编码以及小阶点(不带余因子清除扩展)。这与各个 Cardano 钱包通用的保守接受标准一致,因此一个能在某个符合规范的实现下通过验证的签名,在所有这类实现下都能通过验证。
签名支持与内容主张相互独立。验证器若没有实现某条记录所用的签名算法,会把该签名槽位标记为不支持,并让时间戳与内容主张保持完全有效,未知的签名算法绝不会让记录本身失效。
保留标识符
有几个标识符是已命名但尚未启用的。它们标出了各方已达成共识的迁移路径,使得未来的配置档能使用稳定、预先约定好的名称,而非临时拼凑的字符串。符合规范的生产方 MUST NOT 输出它们,符合规范的验证器也 MUST 以相应的带类型错误拒绝任何使用了其中之一的记录。
| 标识符 | 算法 | 角色 |
|---|---|---|
aes-256-gcm | AES-256-GCM (NIST SP 800-38D) | 内容 AEAD |
ml-kem-768 | ML-KEM-768 (FIPS 203),独立使用 | KEM |
ml-dsa-65 | ML-DSA (FIPS 204) | 签名 |
slh-dsa-sha2-128s | SLH-DSA (FIPS 205) | 签名 |
ml-kem-768 是纯粹的后量子 KEM,与已注册的混合机制 mlkem768x25519 不同;Label 309 所采用的是那个混合机制,其原则在于:即便已经补上后量子的那一半,也应当保留一个经典回退。
算法可演进与迁移
新增一个算法是一项自成一体、只做加法的操作:为该原语援引一份公开标准,把标识符加进对应的注册表,提供一份经过审慎评审的可靠实现,并发布一份跨语言的一致性测试夹具,让各个独立实现能逐字节地达成一致。线格式版本不会改变,因为模式(schema)没有改变,发生增长的只是被识别的字符串集合。
这些后果直接源自注册表的设计:
- 旧记录依旧可验证。 它们的标识符仍在注册表里,所以每一条既有记录都和它发布的当天一模一样地通过验证。
- 旧验证器安全地失败关闭。 出现得早于某个新标识符的验证器,会以稳定的
UNSUPPORTED_*错误拒绝使用该标识符的记录,而不是去猜,根本没有悄悄接受的余地。 - 后量子支持是加法式的。 由于
mlkem768x25519已经注册在册,又由于新的 KEM 和签名都嵌入同一套机制,后量子转型只是注册表的扩充,而非破坏兼容的迁移。
线格式版本号的提升,只为真正破坏兼容的模式变更而保留,例如新增一个必填字段、移除一个字段、改变某个类型。注册表的扩充永远不在此列,而这恰恰让整套密码学目录得以演进,又始终不会让任何已发布的证明变成孤儿。