当前位置:首页 > 文章列表 > Golang > Go教程 > crypto/mlkem 与传统密钥交换的迁移组合

crypto/mlkem 与传统密钥交换的迁移组合

来源:17golang原创 2026-10-10 22:41:52 0浏览 收藏

把现有 Go 协议迁移到 ML-KEM 时,比较稳妥的做法不是立刻删除传统密钥交换,也不是把两段共享秘密直接拼接后交给 AES。迁移层应先协商算法组合,再让传统 ECDH 共享秘密、ML-KEM 共享秘密和协议上下文一起经过 HKDF,最终只输出固定长度的会话密钥。

本文用传统 ECDH 表示现有密钥交换,用 crypto/mlkem 表示新增的 ML-KEM 分支。两者组合的具体线格式、协商编号和降级策略需要由你的协议定义;示例只说明密钥材料如何进入派生过程。

官方资料:https://pkg.go.dev/crypto/mlkem

为什么要用组合密钥

ML-KEM 是密钥封装机制,不是把传统公钥算法改名后的 ECDH。Go 的 crypto/mlkem 提供 ML-KEM-768 和 ML-KEM-1024,常规应用通常从 ML-KEM-768 开始评估;接收方持有解封装密钥,发送方拿到封装公钥后生成密文和共享秘密。

迁移期保留传统分支有两个现实原因:旧客户端可能还不理解 ML-KEM,协议也可能需要一段时间观察消息尺寸、握手失败率和对端兼容性。组合的安全目标是让双方都确认同一套算法选择,并且任何一条被要求的分支失败时都不生成会话密钥。

传统 ECDH 与 ML-KEM 共享秘密汇入 HKDF 的结构示意图
图1:传统密钥交换与 ML-KEM 共享秘密汇入上下文绑定和 HKDF 的静态结构示意图。

先确定 ML-KEM 的双方角色

ML-KEM 的 API 方向和许多开发者直觉不同。可以把接收方想成 Alice:她生成解封装密钥,并把封装公钥发给 Bob;Bob 用封装公钥调用 Encapsulate 得到密文和共享秘密,再把密文回传;Alice 用 Decapsulate 还原同一个共享秘密。

迁移设计时先把这条方向写进协议消息,而不是只在代码里交换一串字节。至少要明确参数集、封装公钥长度、密文长度、协商版本和上下文标识。ML-KEM-768 的共享密钥是 32 字节,但封装公钥和密文比传统曲线公钥大得多,消息上限与分片策略要单独评估。

package main

import (
    "crypto/mlkem"
    "fmt"
    "log"
)

func main() {
    // 接收方生成解封装密钥,只把封装公钥发送给对端。
    decapsulationKey, err := mlkem.GenerateKey768()
    if err != nil {
        log.Fatal(err)
    }
    encapsulationKeyBytes := decapsulationKey.EncapsulationKey().Bytes()

    // 发送方根据封装公钥创建对象,并生成共享秘密与密文。
    encapsulationKey, err := mlkem.NewEncapsulationKey768(encapsulationKeyBytes)
    if err != nil {
        log.Fatal(err)
    }
    senderSecret, ciphertext := encapsulationKey.Encapsulate()

    // 接收方用密文解封装;真实协议还要校验版本和消息上下文。
    receiverSecret, err := decapsulationKey.Decapsulate(ciphertext)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(len(senderSecret), len(receiverSecret))
}

这段代码只展示 API 的角色关系,不代表已经完成网络协议。生产代码还要限制公钥和密文长度、绑定握手上下文,并把错误当成握手失败处理。

保留传统 ECDH,但把组合写进协商

不要让服务端根据“客户端有没有发某个字段”临时猜算法。可以为组合方案分配明确的套件编号,例如 ecdh-mlkem768-v1,在握手开始时传输版本、套件编号和随机数。双方确认编号后,才分别执行传统 ECDH 与 ML-KEM。

传统分支的输入也必须属于当前握手。公钥、临时密钥、随机数和对端身份信息不能脱离上下文单独复用。尤其不要出现“ML-KEM 解封装失败就只用 ECDH 继续”的隐式降级,这会让协商结果和安全假设发生偏差。

package main

import (
    "crypto/ecdh"
    "crypto/rand"
    "log"
)

func makeECDHSecret(peerBytes []byte) ([]byte, error) {
    curve := ecdh.X25519()

    // 临时私钥只服务于当前握手,不能当成长期开销较低的静态密钥。
    privateKey, err := curve.GenerateKey(rand.Reader)
    if err != nil {
        return nil, err
    }
    peerKey, err := curve.NewPublicKey(peerBytes)
    if err != nil {
        return nil, err
    }
    sharedSecret, err := privateKey.ECDH(peerKey)
    if err != nil {
        return nil, err
    }
    return sharedSecret, nil
}

func main() {
    // 示例入口只保留错误处理位置,peerBytes 应来自已校验的握手消息。
    if _, err := makeECDHSecret(nil); err != nil {
        log.Println("ECDH 分支未完成:", err)
    }
}

示例中的空输入只用于展示错误路径,不能作为真实对端公钥。实际协议应先检查消息长度,再把公钥解析错误归类为握手失败。

用 HKDF 绑定两段秘密和上下文

组合密钥的关键不是字符串拼接,而是域分离和上下文绑定。可以把两段秘密按协议固定顺序放入输入材料,同时把套件编号、双方随机数、协议版本和身份摘要放入 info 或经过明确编码的上下文。最终只把 HKDF 输出交给后续 AEAD。

package main

import (
    "crypto/hkdf"
    "crypto/sha256"
    "fmt"
    "log"
)

func deriveSessionKey(ecdhSecret, mlkemSecret, transcript []byte) ([]byte, error) {
    // 固定标签区分本协议,避免同一份秘密材料被另一种用途误用。
    label := []byte("17golang hybrid session v1|")
    ikm := make([]byte, 0, len(label)+len(ecdhSecret)+len(mlkemSecret))
    ikm = append(ikm, label...)
    ikm = append(ikm, ecdhSecret...)
    ikm = append(ikm, mlkemSecret...)

    // transcript 必须由双方按同一编码计算,不能只放本地可控字段。
    sessionKey, err := hkdf.Key(sha256.New, ikm, transcript, "application session", 32)
    if err != nil {
        return nil, err
    }
    return sessionKey, nil
}

func main() {
    // 示例使用占位字节,真实项目应传入两条成功的握手结果。
    key, err := deriveSessionKey([]byte("ecdh-secret"), []byte("mlkem-secret"), []byte("suite|version|nonces"))
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("会话密钥长度:", len(key))
}

transcript 的编码必须稳定,例如将版本、套件、客户端随机数、服务端随机数、双方身份摘要按长度前缀编码后再哈希。不要把可变长度字段直接无分隔拼接,否则不同字段组合可能得到同一字节串。

两条分支都成功后才能进入 AEAD

在组合方案中,“其中一条可用就继续”不是兼容性,而是降级。实现上可以让握手状态同时保存 ecdhSecret、mlkemSecret 和协商上下文;只要 ECDH、ML-KEM、版本校验或上下文校验任意一项失败,就丢弃临时密钥和已生成的秘密,返回统一的握手失败。

会话密钥派生后还要进入现有的 AEAD 密钥与 nonce 管理流程。不要把 ML-KEM 密文当作 AES 密钥,也不要因为共享秘密恰好是 32 字节就跳过 KDF。对端身份认证、重放保护、密钥更新和错误响应仍由上层协议负责。

阶段应该绑定的内容失败处理
协商版本、套件编号、参数集不认识套件就拒绝,不静默切换低级别方案
密钥交换ECDH 公钥、ML-KEM 封装公钥与密文解析、长度或解封装失败都终止握手
派生两段共享秘密、握手 transcript、用途标签不输出部分成功的会话密钥
应用固定长度会话密钥、独立 nonce 计数按 AEAD 错误处理,不回退到明文

先设计协商,再替换算法

如果目标是 TLS,而不是自定义应用层协议,优先评估 Go 的 crypto/tls 内置能力。Go 1.24 起支持混合的 X25519MLKEM768,并在 Config.CurvePreferences 为 nil 时默认启用;遇到不能处理较大握手记录的旧对端时,才需要根据官方兼容性说明评估 tlsmlkem=0 等回退设置。

只有在自定义协议确实需要应用层密钥组合时,才使用 crypto/mlkem 自己编排。应用层方案应先定义套件编号、消息格式、身份绑定和失败语义,再替换底层算法。这样即使未来从 ML-KEM-768 调整到其他参数集,也只是新增协商套件,不会把旧消息解释成另一种密钥材料。

应用层组合与 crypto/tls 内置混合握手的迁移边界说明图
图2:应用层组合与 crypto/tls 内置 X25519MLKEM768 的迁移边界说明图。

迁移时最容易踩的坑

  • 把 ML-KEM 当成普通 ECDH,忽略封装公钥和密文的方向与尺寸。
  • 直接拼接两段秘密,缺少用途标签、长度编码和握手上下文。
  • ML-KEM 失败后仅凭 ECDH 继续,形成未声明的降级路径。
  • 为 TLS 自己重做一套应用层混合流程,却没有先使用标准库已有的协商能力。
  • 测试只覆盖成功路径,没有覆盖不认识套件、错误密文、截断消息和大记录兼容性。

总结

crypto/mlkem 的迁移重点不是“把一个算法替换成另一个算法”,而是把算法组合变成可协商、可绑定、可失败的协议能力。保留传统 ECDH 只是迁移手段;两段共享秘密必须共同进入 HKDF,派生过程必须绑定 transcript,并且任何要求的分支失败都不能生成会话密钥。若场景是 TLS,先使用 Go 标准库提供的 X25519MLKEM768,再考虑是否真的需要自定义组合。

相关问题

crypto/mlkem 应该优先选 ML-KEM-768 还是 ML-KEM-1024?

先按官方包文档和目标协议的安全等级、消息大小、性能与合规要求评估。一般应用先从 ML-KEM-768 做兼容性验证,不要只凭密钥长度决定参数集。

能不能只保存 ML-KEM 的共享秘密?

在纯 ML-KEM 协议中可以按协议保存派生所需材料;在组合迁移中不能因为一条分支成功就丢弃另一条分支。两条分支都完成后再派生统一会话密钥。

Go 的 crypto/tls 和 crypto/mlkem 如何选择?

TLS 握手优先使用 crypto/tls 的内置混合机制;自定义应用层协议才直接调用 crypto/mlkem,并自行定义协商、上下文绑定和失败语义。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
runtime/debug.SetMemoryLimit 与容器限制的配合runtime/debug.SetMemoryLimit 与容器限制的配合
上一篇
runtime/debug.SetMemoryLimit 与容器限制的配合
微软 All Things Open 2026 展示的开源数据库方向
下一篇
微软 All Things Open 2026 展示的开源数据库方向
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    408次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    487次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    494次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    443次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    270次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码