登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go crypto/mlkem 解封装无效密文时怎样处理错误

来源:17golang原创

时间:2026-11-05 09:18:09 195浏览 收藏

使用 crypto/mlkem 的 Decapsulate 时,最容易误判的是“无效密文”只有一种结果。实际上要先分长度:密文长度不符合参数集时,方法返回 error;长度正确但内容无效时,方法不会返回错误,而是生成一个与发送方不匹配的共享密钥。调用方不能只写 if err == nil 就把消息当成可信数据。

实用的处理方式是:在协议入口先按参数集拒绝明显的长度错误;对通过长度检查的密文正常解封装,但只把共享密钥交给带认证的上层协议,最终由认证结果决定是否接受消息。

先分清两类无效密文

Go 官方文档对 Decapsulate 的描述很明确:密文不是正确长度时返回错误;密文长度正确但其他内容无效时不返回错误,而是产生一个不会与发送方匹配的共享密钥。这是 ML-KEM 的接口语义,不是 Go 把错误吞掉了。

Go crypto mlkem 密文长度错误与同长度无效密文的分流说明图
图1:结构说明图,展示 ML-KEM 密文先按参数集检查长度,再分流到 error 或共享密钥路径;不是运行结果截图。

因此可以把输入分成两层:

  • 长度错误:例如截断、拼接,或者发送方和接收方使用了不同的参数集。此时应立即返回错误,不要继续使用返回值。
  • 同长度内容错误:字节数符合要求,但密文被篡改、来自错误的封装密钥,或与当前解封装私钥不对应。此时 err 可能为 nil,但共享密钥不能单独证明消息可信。

把长度检查放在协议入口

ML-KEM-768 和 ML-KEM-1024 有不同的密文长度。标准库提供了 CiphertextSize768 和 CiphertextSize1024 常量,适合用来表达协议参数,而不是在业务代码中散落数字。

package kemdemo

import (
    "crypto/mlkem"
    "errors"
)

var errBadCiphertextLength = errors.New("密文长度不符合 ML-KEM-768 参数集")

func decapsulate768(dk *mlkem.DecapsulationKey768, ciphertext []byte) ([]byte, error) {
    // 先拒绝截断或拼接后的密文,避免把长度问题混进协议后续处理。
    if len(ciphertext) != mlkem.CiphertextSize768 {
        return nil, errBadCiphertextLength
    }

    // 长度正确只说明输入能进入 ML-KEM 解封装,不代表消息已经通过认证。
    sharedKey, err := dk.Decapsulate(ciphertext)
    if err != nil {
        return nil, err
    }
    return sharedKey, nil
}

这里的显式长度判断主要有两个作用。第一,错误信息可以在协议边界被统一处理;第二,参数集变更时,代码中的常量会提醒维护者同步调整。它不能替代 Decapsulate 自身的检查,所以调用方法仍然要保留 err 分支。

同长度无效密文为什么不能只看 error

如果密文长度正确但内容被改动,Decapsulate 仍可能返回一个 32 字节共享密钥,同时 err == nil。这个密钥与发送方计算出的密钥不一致,后续用它解密带认证的数据时,认证标签应该失败。

func decapsulateAndOpen(dk *mlkem.DecapsulationKey768, ciphertext []byte, open func([]byte, []byte) ([]byte, error)) ([]byte, error) {
    // 长度检查只处理格式边界,不能把它当成消息真实性判断。
    if len(ciphertext) != mlkem.CiphertextSize768 {
        return nil, errBadCiphertextLength
    }

    sharedKey, err := dk.Decapsulate(ciphertext)
    if err != nil {
        return nil, err
    }

    // open 代表上层带认证的解密或验签流程;认证失败就拒绝消息。
    plaintext, err := open(sharedKey, ciphertext)
    if err != nil {
        return nil, errors.New("消息认证失败")
    }
    return plaintext, nil
}

示例中的 open 是协议适配点,不是 crypto/mlkem 提供的函数。真实系统应使用经过设计和审查的带认证协议,并明确 nonce、关联数据、重放保护等边界。文章这里只强调一条调用原则:解封装成功不等于业务消息已验证。

Go crypto mlkem 共享密钥交给上层认证判断的边界说明图
图2:关系说明图,展示同长度无效密文不会自动产生 error,可信消息需要由上层认证结果决定;不是运行结果截图。

把密钥材料和错误细节隔离

共享密钥是敏感材料,拿到后应尽快交给后续密码操作,减少在日志、结构体字段和长生命周期缓存中的停留时间。不要为了排错把共享密钥、完整密文或私钥序列化到日志中。

对外响应也不必暴露“长度错误”还是“同长度认证失败”的全部内部细节。服务端可以在内部记录分类后的原因,在协议层返回统一的解封装失败或消息认证失败;是否区分响应,要结合协议的可观察性和攻击面决定。

func handleCiphertext(dk *mlkem.DecapsulationKey768, ciphertext []byte, open func([]byte) error) error {
    // 只保留最小作用域,避免共享密钥意外进入长期对象。
    if len(ciphertext) != mlkem.CiphertextSize768 {
        return errBadCiphertextLength
    }

    sharedKey, err := dk.Decapsulate(ciphertext)
    if err != nil {
        return err
    }

    // 由上层认证流程决定是否接受消息,不能仅凭 err == nil 放行。
    if err := open(sharedKey); err != nil {
        return errors.New("消息认证失败")
    }
    return nil
}

如果业务需要区分可观测指标,可以在内部把长度错误、解封装错误和上层认证失败分别计数,但日志内容应保持最小化。尤其不要把“共享密钥不匹配”理解成可以直接比较双方密钥:接收方通常拿不到发送方的密钥,正确的判断点是后续认证协议。

实现时的判断清单

检查项建议原因
参数集固定使用 768 或 1024 的对应类型与常量避免把不同参数集的密文混用
密文长度入口先检查,调用后仍处理 error区分格式边界和 API 失败
同长度无效不能用 err == nil 作为放行条件接口可能返回与发送方不同的共享密钥
消息可信度交给带认证的上层协议判断解封装与消息认证是两层职责
日志与内存不记录密钥,缩短共享密钥作用域减少敏感材料泄露面

常见问题

密文长度正确,为什么还需要认证?

长度只描述字节数,不证明密文由对应的封装密钥产生。ML-KEM 对同长度无效密文的处理不会返回 error,所以必须由后续带认证的消息协议给出最终接受或拒绝结果。

可以直接判断共享密钥是否为空吗?

不应把“非空”当成成功依据。正确长度的无效密文也可能得到共享密钥,调用方应处理 error,再把密钥交给上层认证流程。

768 和 1024 的处理代码能混用吗?

不能只替换长度数字。应让解封装密钥类型、密文长度常量和协议参数一起切换,避免使用 768 的密文调用 1024 的解封装路径。

总结

处理 Go crypto/mlkem 无效密文时,最重要的不是把所有情况都改造成 error,而是尊重接口的两层语义:错误长度立即拒绝;同长度内容无效时,不能凭 err == nil 放行,必须让上层认证协议决定消息是否可信。把长度、解封装、认证和日志隔离开,代码才不会把 API 成功误当成协议成功。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>