【密码学】基于ML-DSA的门限签名(代码解读)

admin 2026-01-15 14:46:00 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文解读基于ML-DSA的门限签名Go代码,剖析短复制秘密共享、超球拒绝采样等核心算法。详解密钥生成与三轮交互协议,展示位运算优化与硬编码共享设计。该学术原型验证了后量子门限签名的可行性,为分布式信任基础设施提供技术参考。 综合评分: 82 文章分类: 安全开发,解决方案,安全建设


cover_image

【密码学】基于ML-DSA的门限签名(代码解读)

原创

Litt1eQ

Coder小Q

2026年1月15日 08:30 山东

【密码学】基于ML-DSA的门限签名(代码解读)

Bob: Alice,上次你给我讲了门限ML-DSA的理论,我现在想看看这些数学公式是怎么变成实际代码的。这个仓库里有好几千行Go代码,我有点晕。

Alice: 哈哈,从数学到代码确实是个大跨越。不过别担心,让我带你一层层剖析。这个实现其实结构很清晰,就像搭积木一样,每个模块都有明确的职责。

Bob: 那从哪里开始呢?

Alice: 我们从最核心的数据结构开始。你还记得我们说过的”短复制秘密共享”吗?在代码里,这个概念是怎么体现的呢?

Bob: 对,你说过这就像”收集套卡”,每个参与方持有多个短秘密份额。

Alice: 没错,来看看代码是怎么实现的。首先看:

type Share struct {
    s1  VecL
    s2  VecK

    // Cached values
    s1h VecL // NTT(s₁)
    s2h VecK // NTT(s₂)
}

type PrivateKey struct {
    Id uint8

    rho [32]byte
    key [32]byte
    s1  VecL
    s2  VecK
    Tr  [TRSize]byte

    shares map[uint8]*Share

    // Cached values
    A   Mat  // ExpandA(ρ)
    s1h VecL // NTT(s₁)
    s2h VecK // NTT(s₂)
}

Bob: 等等,这个shares map[uint8]*Share是什么意思?

Alice: 这就是关键,每个参与方的私钥里有一个shares映射表。键是一个8位整数,代表一个参与方集合的位掩码。比如说,如果有6个参与方,二进制101011(十进制43)就代表参与方0、1、3、5组成的集合。

Eve: 让我猜猜,这个设计是为了快速查找?假设我是参与方2,当签名集合是{0,1,2,3}时,我需要找到所有包含我的、大小为N-T+1的子集对应的秘密份额?

Alice: 完全正确,这就是短复制秘密共享的精髓。来看密钥生成的代码:

// Sample the shares
honestSigners :=&nbsp;uint8((1&nbsp;<< (params.N-params.T+1)) -&nbsp;1)
for&nbsp;honestSigners < (1&nbsp;<< params.N) {
&nbsp; &nbsp;&nbsp;var&nbsp;share Share
&nbsp; &nbsp;&nbsp;var&nbsp;sSeed [64]byte
&nbsp; &nbsp; _, _ = h.Read(sSeed[:])

&nbsp; &nbsp;&nbsp;for&nbsp;j :=&nbsp;uint16(0); j < L; j++ {
&nbsp; &nbsp; &nbsp; &nbsp; PolyDeriveUniformLeqEta(&share.s1[j], &sSeed, j)
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;for&nbsp;j :=&nbsp;uint16(0); j < K; j++ {
&nbsp; &nbsp; &nbsp; &nbsp; PolyDeriveUniformLeqEta(&share.s2[j], &sSeed, j+L)
&nbsp; &nbsp; }

&nbsp; &nbsp; share.s1h = share.s1
&nbsp; &nbsp; share.s1h.NTT()
&nbsp; &nbsp; share.s2h = share.s2
&nbsp; &nbsp; share.s2h.NTT()

&nbsp; &nbsp;&nbsp;// Distribute the share
&nbsp; &nbsp;&nbsp;for&nbsp;i :=&nbsp;uint8(0); i < params.N; i++ {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(honestSigners & (1&nbsp;<< i)) !=&nbsp;0&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sks[i].shares[honestSigners] = &share
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;// ... accumulate to total secret

&nbsp; &nbsp;&nbsp;// next possible set of honest signers
&nbsp; &nbsp; c := honestSigners & -honestSigners
&nbsp; &nbsp; r := honestSigners + c
&nbsp; &nbsp; honestSigners = (((r^honestSigners) >>&nbsp;2) / c) | r
}

Bob: 哇,这段代码信息量好大。首先,honestSigners(1 << (N-T+1)) - 1开始,这是什么意思?

Alice: 这是一个巧妙的位运算技巧。假设,那么。(1 << 4) - 1 = 15 = 0b001111,代表前4个参与方。这是第一个大小为4的集合。

Bob: 然后那个神秘的循环末尾的位运算是在干什么?

Alice: 那是Gosper’s Hack——一个经典的位运算算法,用于生成下一个具有相同位数的二进制数。比如从0b001111生成0b010111,再生成0b011011,以此类推。这样就能遍历所有大小为4的子集,也就是个秘密份额。

Eve: 所以每个秘密份额都被分发给了对应子集中的所有参与方。比如秘密份额0b101011(集合)会被分发给参与方0、1、3、5。

Alice: 没错,这样设计的好处是:任意个参与方联合起来,他们能覆盖所有的秘密份额;但少于个恶意方,至少有一个秘密份额他们完全不知道。

Bob: 好,秘密分发我理解了。现在说说那个”超球拒绝采样”吧。我看到代码里有个SampleHyperball函数。

Alice: 这是整个协议中最精妙的部分之一,来看:

func&nbsp;SampleHyperball(p *FVec, radius&nbsp;float64, nu&nbsp;float64, rhop [64]byte, nonce&nbsp;uint16)&nbsp;{
&nbsp; &nbsp;&nbsp;var&nbsp;sq&nbsp;float64
&nbsp; &nbsp; samples :=&nbsp;make([]float64, common.N*(K+L) +&nbsp;2)

&nbsp; &nbsp;&nbsp;// Use SHAKE256 for cryptographic randomness
&nbsp; &nbsp; h := sha3.NewShake256()
&nbsp; &nbsp; _, _ = h.Write([]byte("H"))&nbsp;// Add a domain separator
&nbsp; &nbsp; h.Write(rhop[:])
&nbsp; &nbsp;&nbsp;// ... write nonce

&nbsp; &nbsp;&nbsp;// Generate normally distributed random numbers using Box-Muller transform
&nbsp; &nbsp;&nbsp;for&nbsp;i :=&nbsp;0; i < common.N*(K+L) +&nbsp;2; i +=&nbsp;2&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// ... convert bytes to uniform random
&nbsp; &nbsp; &nbsp; &nbsp; f1 :=&nbsp;float64(u1) / (1&nbsp;<<&nbsp;64)
&nbsp; &nbsp; &nbsp; &nbsp; f2 :=&nbsp;float64(u2) / (1&nbsp;<<&nbsp;64)

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Box-Muller transform
&nbsp; &nbsp; &nbsp; &nbsp; z1 := math.Sqrt(-2*math.Log(f1)) * math.Cos(2*math.Pi*f2)
&nbsp; &nbsp; &nbsp; &nbsp; z2 := math.Sqrt(-2*math.Log(f1)) * math.Sin(2*math.Pi*f2)

&nbsp; &nbsp; &nbsp; &nbsp; samples[i] = z1
&nbsp; &nbsp; &nbsp; &nbsp; sq += z1*z1

&nbsp; &nbsp; &nbsp; &nbsp; samples[i+1] = z2
&nbsp; &nbsp; &nbsp; &nbsp; sq += z2*z2

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;i < common.N*L {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; samples[i] *= nu
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; samples[i+1] *= nu &nbsp;// 非平衡因子
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

&nbsp; &nbsp; factor := radius / math.Sqrt(sq)
&nbsp; &nbsp;&nbsp;for&nbsp;i :=&nbsp;0; i < common.N*(K+L); i++ {
&nbsp; &nbsp; &nbsp; &nbsp; p[i] = samples[i] * factor
&nbsp; &nbsp; }
}

Bob: Box-Muller变换?这是什么魔法?

Alice: 这是一个经典的统计学算法,用于从均匀分布生成正态分布(高斯分布)。给定两个均匀随机数,通过这个公式:

就能得到两个独立的标准正态分布随机数。

Bob: 我明白了,你们先生成一堆正态分布的随机数,然后通过factor = radius / sqrt(sq)把它们缩放到超球表面上。这就像在高维空间中随机投飞镖,然后把所有飞镖都拉到球面上。但是,等等,我看到有个if i < common.N*L的判断,里面乘以了。这是什么?

Alice: 这就是非平衡拒绝采样的体现。还记得我们说过和有不同的约束吗?前个多项式对应,后个对应。通过给前面的部分乘以(通常是3),我们给更大的”自由度”,这样就能在保证安全性的同时提高成功率。

Bob: 所以这不是一个标准的超球,而是一个”拉伸”的椭球?

Alice: 准确地说,是在某些维度上拉伸的超椭球。但在数学上,我们通过适当的度量变换,仍然可以把它看作超球。来看检查函数:

func&nbsp;(v *FVec)&nbsp;Excess(r&nbsp;float64, nu&nbsp;float64)&nbsp;bool&nbsp;{
&nbsp; &nbsp;&nbsp;var&nbsp;sq&nbsp;float64
&nbsp; &nbsp;&nbsp;for&nbsp;i :=&nbsp;0; i < L + K; i++ {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;j :=&nbsp;0; j < common.N; j++ {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;i < L {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sq += v[i * common.N + j] * v[i * common.N + j] / (nu * nu)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sq += v[i * common.N + j] * v[i * common.N + j]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;return&nbsp;sq > r * r
}

Eve: 这里,前个多项式的系数除以再平方,这样就抵消了采样时乘以的效果。检查时相当于在”归一化”的空间里判断是否超出半径。

Bob: 好,现在我们有了秘密分发和超球采样。那实际的签名协议是怎么运行的呢?

Alice: 这就要看三个核心函数了:Round1Round2Round3。让我们一个个来看。

Round 1:承诺阶段

Alice: 第一轮是承诺阶段,来看代码:

func&nbsp;Round1(sk *PrivateKey, params *ThresholdParams)&nbsp;([]byte, StRound1, error)&nbsp;{
&nbsp; &nbsp;&nbsp;var&nbsp;rhop [64]byte
&nbsp; &nbsp; _, err := cryptoRand.Read(rhop[:])
&nbsp; &nbsp;&nbsp;if&nbsp;err !=&nbsp;nil&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnnil, StRound1{}, err
&nbsp; &nbsp; }

&nbsp; &nbsp; cmt :=&nbsp;make([]byte,&nbsp;32)
&nbsp; &nbsp; wbuf :=&nbsp;make([]byte,&nbsp;int(params.K) * internal.SingleCommitmentSize)

&nbsp; &nbsp; w, tmpcmtst := internal.GenThCommitment(
&nbsp; &nbsp; &nbsp; &nbsp; (*internal.PrivateKey)(sk),
&nbsp; &nbsp; &nbsp; &nbsp; rhop,
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;0,
&nbsp; &nbsp; &nbsp; &nbsp; (*internal.ThresholdParams)(params),
&nbsp; &nbsp; )
&nbsp; &nbsp; internal.PackW(w, wbuf[:])

&nbsp; &nbsp; s := sha3.NewShake256()
&nbsp; &nbsp; s.Write((*internal.PrivateKey)(sk).Tr[:])
&nbsp; &nbsp; s.Write([]byte{(*internal.PrivateKey)(sk).Id})
&nbsp; &nbsp; s.Write(wbuf)
&nbsp; &nbsp; s.Read(cmt[:])

&nbsp; &nbsp;&nbsp;return&nbsp;cmt, StRound1{wbuf, tmpcmtst},&nbsp;nil
}

Bob: 这里生成了个承诺,然后只发送哈希值cmt

Alice: 没错,这就是承诺-揭示范式。每个参与方生成个承诺(对应次并行尝试),但只发送这些承诺的哈希值。这样就防止了rushing attack——恶意参与方无法看到别人的承诺后再调整自己的。

Eve: 我注意到哈希里包含了Tr(公钥的哈希)和Id(参与方编号)。这是为了防止重放攻击吗?

Alice: 完全正确。Tr绑定了公钥,Id绑定了参与方身份。这样即使我截获了某个承诺,也无法冒充其他参与方或用于其他公钥。

Bob: 那GenThCommitment里面做了什么?

Alice: 来看:

func&nbsp;GenThCommitment(sk *PrivateKey, rhop [64]byte, nonce&nbsp;uint16, params *ThresholdParams)&nbsp;([]VecK, []FVec)&nbsp;{
&nbsp; &nbsp; ws :=&nbsp;make([]VecK, params.K)
&nbsp; &nbsp; sts :=&nbsp;make([]FVec, params.K)

&nbsp; &nbsp;&nbsp;for&nbsp;i :=&nbsp;uint16(0); i < params.K; i++ {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;r, rh VecL
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;e_ VecK

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// [THRESHOLD] Also sample an error for w
&nbsp; &nbsp; &nbsp; &nbsp; SampleHyperball(&sts[i], params.rPrime, params.nu, rhop, nonce * params.K + i)
&nbsp; &nbsp; &nbsp; &nbsp; sts[i].Round(&r, &e_)

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Set w to A y
&nbsp; &nbsp; &nbsp; &nbsp; rh = r
&nbsp; &nbsp; &nbsp; &nbsp; rh.NTT()
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;j :=&nbsp;0; j < K; j++ {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; PolyDotHat(&ws[i][j], &sk.A[j], &rh)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ws[i][j].ReduceLe2Q()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ws[i][j].InvNTT()

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// [THRESHOLD]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ws[i][j].Add(&e_[j], &ws[i][j])
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ws[i][j].ReduceLe2Q()
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; ws[i].NormalizeAssumingLe2Q()
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;return&nbsp;ws, sts
}

Bob: 我看到注释说”Also sample an error for w”,这就是你之前说的MLWE样本?

Alice: 没错。标准ML-DSA中,。但在门限版本中,,这样就是一个MLWE样本。即使拒绝采样失败需要揭示,它在计算上也不可区分于随机向量。

Eve: 这就是”Finally!”论文[1]的核心创新之一。没有这个额外的噪声,揭示被拒绝的会泄露关于秘密的信息。

Round 2:揭示承诺

Alice: 第二轮相对简单,看:

func&nbsp;Round2(sk *PrivateKey, act&nbsp;uint8, msg, ctx []byte, msgsrd1 [][]byte, strd1 *StRound1, params *ThresholdParams)&nbsp;([]byte, StRound2, error)&nbsp;{
&nbsp; &nbsp;&nbsp;// Store hashes for future use
&nbsp; &nbsp; st2 := StRound2{}
&nbsp; &nbsp; st2.hashes =&nbsp;make([][32]byte,&nbsp;len(msgsrd1))
&nbsp; &nbsp;&nbsp;for&nbsp;i, msg :=&nbsp;range&nbsp;msgsrd1 {
&nbsp; &nbsp; &nbsp; &nbsp; st2.hashes[i] = [32]byte(msg)
&nbsp; &nbsp; }

&nbsp; &nbsp; st2.mu = internal.ComputeMu((*internal.PrivateKey)(sk),&nbsp;func(w io.Writer)&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; _, _ = w.Write([]byte{0})
&nbsp; &nbsp; &nbsp; &nbsp; _, _ = w.Write([]byte{byte(len(ctx))})
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;ctx !=&nbsp;nil&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _, _ = w.Write(ctx)
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; w.Write(msg)
&nbsp; &nbsp; })
&nbsp; &nbsp; st2.act = act

&nbsp; &nbsp;&nbsp;return&nbsp;strd1.wbuf, st2,&nbsp;nil
}

Bob: 这一轮就是把第一轮生成的wbuf发出去,同时保存其他人的哈希承诺?

Alice: 对。每个参与方揭示自己的承诺,同时验证别人的承诺是否与第一轮的哈希匹配。这个验证在第三轮进行。同时,这一轮还计算了,这是消息的哈希,会用于生成挑战。

Round 3:计算响应

Bob: 第三轮应该是最复杂的吧?

Alice: 没错,这是整个协议的核心。来看代码:

func&nbsp;Round3(sk *PrivateKey, msgsrd2 [][]byte, strd1 *StRound1, strd2 *StRound2, params *ThresholdParams)&nbsp;([]byte, error)&nbsp;{
&nbsp; &nbsp; wtmp :=&nbsp;make([]internal.VecK, params.K)
&nbsp; &nbsp; wfinal :=&nbsp;make([]internal.VecK, params.K)

&nbsp; &nbsp;&nbsp;// Compute wfinal
&nbsp; &nbsp; j :=&nbsp;uint8(0)
&nbsp; &nbsp;&nbsp;for&nbsp;i :=&nbsp;0; i <&nbsp;len(msgsrd2); i++ {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;strd2.act & (1&nbsp;<< j) ==&nbsp;0&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; j++
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Check that the commitments correspond to the one hashed in round 1
&nbsp; &nbsp; &nbsp; &nbsp; s := sha3.NewShake256()
&nbsp; &nbsp; &nbsp; &nbsp; s.Write((*internal.PrivateKey)(sk).Tr[:])
&nbsp; &nbsp; &nbsp; &nbsp; s.Write([]byte{j})
&nbsp; &nbsp; &nbsp; &nbsp; s.Write(msgsrd2[i])

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;hash [32]byte
&nbsp; &nbsp; &nbsp; &nbsp; s.Read(hash[:])
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;hash != strd2.hashes[i] {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnnil, errors.New("wrong commitment")
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; internal.UnpackW(wtmp, msgsrd2[i][:])
&nbsp; &nbsp; &nbsp; &nbsp; internal.AggregateCommitments(wfinal, wtmp)

&nbsp; &nbsp; &nbsp; &nbsp; j++
&nbsp; &nbsp; }

&nbsp; &nbsp; zs := internal.ComputeResponses((*internal.PrivateKey)(sk), strd2.act, strd2.mu, wfinal, strd1.cmtst, (*internal.ThresholdParams)(params))

&nbsp; &nbsp; response :=&nbsp;make([]byte, params.ResponseSize())
&nbsp; &nbsp; internal.PackResponses(zs, response[:])
&nbsp; &nbsp;&nbsp;return&nbsp;response,&nbsp;nil
}

Bob: 首先验证承诺,然后聚合所有人的,最后计算响应?

Alice: 对,关键在ComputeResponses函数。来看代码:

func&nbsp;ComputeResponses(sk *PrivateKey, act&nbsp;uint8, mu [64]byte, wfinals []VecK, stws []FVec, params *ThresholdParams)&nbsp;[]VecL&nbsp;{
&nbsp; &nbsp;&nbsp;// ... 初始化

&nbsp; &nbsp;&nbsp;// Recover the partial secret of the current user corresponding
&nbsp; &nbsp;&nbsp;// to the signer set act
&nbsp; &nbsp; s1h, s2h := recoverShare(sk, act, params)

&nbsp; &nbsp;&nbsp;// For each commitment
&nbsp; &nbsp;&nbsp;for&nbsp;i :=&nbsp;uint16(0); i < params.K; i++ {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;z VecL
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Decompose w into w₀ and w₁
&nbsp; &nbsp; &nbsp; &nbsp; wfinals[i].Decompose(&w0, &w1)

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// c~ = H(μ ‖ w₁)
&nbsp; &nbsp; &nbsp; &nbsp; w1.PackW1(w1Packed[:])
&nbsp; &nbsp; &nbsp; &nbsp; h.Reset()
&nbsp; &nbsp; &nbsp; &nbsp; _, _ = h.Write(mu[:])
&nbsp; &nbsp; &nbsp; &nbsp; _, _ = h.Write(w1Packed[:])
&nbsp; &nbsp; &nbsp; &nbsp; _, _ = h.Read(c[:])

&nbsp; &nbsp; &nbsp; &nbsp; PolyDeriveUniformBall(&ch, c[:])
&nbsp; &nbsp; &nbsp; &nbsp; ch.NTT()

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Compute c·s₁
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;j :=&nbsp;0; j < L; j++ {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; z[j].MulHat(&ch, &s1h[j])
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; z[j].InvNTT()
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; z.Normalize()

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Compute c*s2
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;j :=&nbsp;0; j < K; j++ {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; y[j].MulHat(&ch, &s2h[j])
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; y[j].InvNTT()
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; y.Normalize()

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;zf FVec
&nbsp; &nbsp; &nbsp; &nbsp; zf.From(&z, &y)
&nbsp; &nbsp; &nbsp; &nbsp; zf.Add(&zf, &stws[i])

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;zf.Excess(params.r, params.nu) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; zf.Round(&zs[i], &y)
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;return&nbsp;zs
}

Bob: 我看到了。首先通过recoverShare恢复这个参与方在当前签名集合下的部分秘密,然后对每个承诺计算,最后用Excess检查是否通过拒绝采样。那个recoverShare函数是怎么工作的?

Alice: 这是个很有意思的函数,它使用了预先计算好的分配模式。recoverShare函数的核心是一个硬编码的分配表:

func&nbsp;recoverShare(sk *PrivateKey, act&nbsp;uint8, params *ThresholdParams)&nbsp;(s1h VecL, s2h VecK)&nbsp;{
&nbsp; &nbsp;&nbsp;// Base case
&nbsp; &nbsp;&nbsp;if&nbsp;params.T ==&nbsp;1&nbsp;|| params.T == params.N {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;u :=&nbsp;range&nbsp;sk.shares {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; s1h = sk.shares[u].s1h
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; s2h = sk.shares[u].s2h
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;// Otherwise, we rely on hardcoded sharing patterns
&nbsp; &nbsp;&nbsp;var&nbsp;sharing [][]uint8
&nbsp; &nbsp;&nbsp;// 2 3 [[5, 3], [6]]
&nbsp; &nbsp;&nbsp;// 2 4 [[13, 7], [14, 11]]
&nbsp; &nbsp;&nbsp;// 3 4 [[9, 3], [10, 6], [12, 5]]
&nbsp; &nbsp;&nbsp;// ...
&nbsp; &nbsp;&nbsp;if&nbsp;params.T ==&nbsp;2&nbsp;&& params.N ==&nbsp;3&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; sharing = [][]uint8{[]uint8{3,5}, []uint8{6}}
&nbsp; &nbsp; }&nbsp;elseif&nbsp;params.T ==&nbsp;2&nbsp;&& params.N ==&nbsp;4&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; sharing = [][]uint8{[]uint8{13,7}, []uint8{14,11}}
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;// ... 更多情况

&nbsp; &nbsp;&nbsp;// Define a permutation to cover the signing set act
&nbsp; &nbsp; perm :=&nbsp;make([]uint8, params.N)
&nbsp; &nbsp; i1 :=&nbsp;0
&nbsp; &nbsp; i2 := params.T
&nbsp; &nbsp;&nbsp;for&nbsp;j :=&nbsp;uint8(0); j < params.N; j++ {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;j == sk.Id {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; currenti = i1
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;act & (1&nbsp;<< j) !=&nbsp;0&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; perm[i1] = j
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; i1++
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; perm[i2] = j
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; i2++
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;for&nbsp;_, u :=&nbsp;range&nbsp;sharing[currenti] {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Translate the share index u to the share index u_
&nbsp; &nbsp; &nbsp; &nbsp; u_ :=&nbsp;uint8(0)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;i :=&nbsp;uint8(0); i < params.N; i++ {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;u & (1&nbsp;<< i) !=&nbsp;0&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; u_ |= (1&nbsp;<< perm[i])
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Add the share to the partial secret
&nbsp; &nbsp; &nbsp; &nbsp; s1h.Add(&s1h, &sk.shares[u_].s1h)
&nbsp; &nbsp; &nbsp; &nbsp; s2h.Add(&s2h, &sk.shares[u_].s2h)
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;return
}

Bob: 这个硬编码的表是什么意思?比如[[5, 3], [6]]

Alice: 这是一个优化的分配方案。以为例,有个秘密份额:

  • 份额3 = 0b011对应集合
  • 份额5 = 0b101对应集合
  • 份额6 = 0b110对应集合

当签名集合是时(即前个参与方):

  • 参与方0需要份额(二进制0b011和0b101)
  • 参与方1需要份额(二进制0b110)

Eve: 这个分配是为了平衡每个参与方需要使用的秘密份额数量。如果分配不均,某个参与方的部分秘密会很大,拒绝采样的成功率就会降低。

Bob: 那个置换perm是干什么的?

Alice: 这是个聪明的技巧。硬编码的表假设签名集合是前个参与方。但实际的签名集合可能是任意的,比如。置换perm把实际的签名集合映射到规范形式,然后再把份额索引映射回去。

Bob: 所以这些硬编码的表是怎么计算出来的?

Alice: 它们是通过图论算法预先计算的,在params/recover.py脚本中。目标是最小化每个参与方需要使用的份额数量,这是一个组合优化问题。

Bob: 好,现在每个参与方都计算出了自己的响应。怎么组合成最终的签名呢?

Alice: 这就是Combine函数的工作了。来看代码:

func&nbsp;Combine(pk *PublicKey, msg&nbsp;func(io.Writer),&nbsp;wfinals&nbsp;[]VecK,&nbsp;zs&nbsp;[]VecL,&nbsp;signature&nbsp;[]byte,&nbsp;params&nbsp;*ThresholdParams)&nbsp;bool&nbsp;{
&nbsp; &nbsp;&nbsp;// ... 初始化

&nbsp; &nbsp;&nbsp;// For each commitment
&nbsp; &nbsp;&nbsp;for&nbsp;i :=&nbsp;uint16(0); i < params.K; i++ {
&nbsp; &nbsp; &nbsp; &nbsp; wfinals[i].Decompose(&w0, &w1)

&nbsp; &nbsp; &nbsp; &nbsp; sig.z = zs[i]

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Ensure ‖z‖_∞ < γ1 - beta.
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;zs[i].Exceeds(Gamma1 - Beta) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; zh = zs[i]
&nbsp; &nbsp; &nbsp; &nbsp; zh.NTT()

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;j :=&nbsp;0; j < K; j++ {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; PolyDotHat(&Az[j], &pk.A[j], &zh)
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// c~ = H(μ ‖ w₁)
&nbsp; &nbsp; &nbsp; &nbsp; w1.PackW1(w1Packed[:])
&nbsp; &nbsp; &nbsp; &nbsp; h.Reset()
&nbsp; &nbsp; &nbsp; &nbsp; _, _ = h.Write(mu[:])
&nbsp; &nbsp; &nbsp; &nbsp; _, _ = h.Write(w1Packed[:])
&nbsp; &nbsp; &nbsp; &nbsp; _, _ = h.Read(sig.c[:])

&nbsp; &nbsp; &nbsp; &nbsp; PolyDeriveUniformBall(&ch, sig.c[:])
&nbsp; &nbsp; &nbsp; &nbsp; ch.NTT()

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Compute Az - 2ᵈ·c·t₁
&nbsp; &nbsp; &nbsp; &nbsp; Az2dct1.MulBy2toD(&pk.t1)
&nbsp; &nbsp; &nbsp; &nbsp; Az2dct1.NTT()
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;j :=&nbsp;0; j < K; j++ {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Az2dct1[j].MulHat(&Az2dct1[j], &ch)
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; Az2dct1.Sub(&Az, &Az2dct1)
&nbsp; &nbsp; &nbsp; &nbsp; Az2dct1.ReduceLe2Q()
&nbsp; &nbsp; &nbsp; &nbsp; Az2dct1.InvNTT()
&nbsp; &nbsp; &nbsp; &nbsp; Az2dct1.NormalizeAssumingLe2Q()

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;f VecK
&nbsp; &nbsp; &nbsp; &nbsp; f.Sub(&Az2dct1, &wfinals[i])
&nbsp; &nbsp; &nbsp; &nbsp; f.Normalize()

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Ensure ‖c*t0 - c*s2 - e_2‖_∞ < γ₂.
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;f.Exceeds(Gamma2) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Decompose and make hint
&nbsp; &nbsp; &nbsp; &nbsp; wfinals[i].Decompose(&w0, &w1)
&nbsp; &nbsp; &nbsp; &nbsp; w0pf.Add(&w0, &f)
&nbsp; &nbsp; &nbsp; &nbsp; w0pf.Normalize()
&nbsp; &nbsp; &nbsp; &nbsp; hintPop := sig.hint.MakeHint(&w0pf, &w1)

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;hintPop <= Omega {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sig.Pack(signature)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returntrue
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;returnfalse
}

Bob: 这里遍历个并行尝试,对每个尝试都检查各种约束,直到找到一个成功的?

Alice: 没错,关键的检查有三个:

  1. :确保响应不会太大
  2. :这里,确保误差在范围内
  3. :确保hint足够稀疏

Bob: 我注意到这里计算了f = Az2dct1 - wfinals[i]。这个是什么?

Alice: 这是个关键的量。回忆ML-DSA的验证等式:

但在门限版本中,由于我们添加了噪声,实际上:

而,其中。所以:

这个误差必须小于才能保证hint稀疏。

Bob: 那个MakeHint是干什么的?

Alice: Hint是ML-DSA的核心机制。验证者需要检查,但由于舍入误差,这两个值可能在某些系数上差1。Hint告诉验证者”哪些位置需要调整”。

Bob: 我看到代码里有很多硬编码的参数,比如、、。这些是怎么选的?

Alice: 这些参数是通过复杂的数学分析和实验得出的。来看代码:

func&nbsp;GetThresholdParams(t, n&nbsp;uint8)&nbsp;(*ThresholdParams, error)&nbsp;{
&nbsp; &nbsp;&nbsp;// Validate parameters
&nbsp; &nbsp;&nbsp;if&nbsp;t <&nbsp;2&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnnil, errors.New("threshold T must be 2 or more")
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;if&nbsp;t > n {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnnil, errors.New("threshold T must be less than or equal to total parties N")
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;if&nbsp;n >&nbsp;6&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnnil, errors.New("number of parties must be less than 6")
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;var&nbsp;k&nbsp;uint16
&nbsp; &nbsp;&nbsp;var&nbsp;r, rPrime&nbsp;float64
&nbsp; &nbsp; nu :=&nbsp;float64(3.)
&nbsp; &nbsp;&nbsp;if&nbsp;t ==&nbsp;2&nbsp;&& n ==&nbsp;2&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; k =&nbsp;uint16(2)
&nbsp; &nbsp; &nbsp; &nbsp; r =&nbsp;252778
&nbsp; &nbsp; &nbsp; &nbsp; rPrime =&nbsp;252833
&nbsp; &nbsp; }&nbsp;elseif&nbsp;n ==&nbsp;3&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; ks := []uint16{3,4}
&nbsp; &nbsp; &nbsp; &nbsp; rs := []float64{310060,&nbsp;246490}
&nbsp; &nbsp; &nbsp; &nbsp; rPs := []float64{310138,&nbsp;246546}
&nbsp; &nbsp; &nbsp; &nbsp; k = ks[t-2]
&nbsp; &nbsp; &nbsp; &nbsp; r = rs[t-2]
&nbsp; &nbsp; &nbsp; &nbsp; rPrime = rPs[t-2]
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;// ... 更多配置

&nbsp; &nbsp;&nbsp;return&nbsp;&ThresholdParams{
&nbsp; &nbsp; &nbsp; &nbsp; T: t, N: n, K: k,
&nbsp; &nbsp; &nbsp; &nbsp; nu: nu, r: r, rPrime: rPrime,
&nbsp; &nbsp; },&nbsp;nil
}

Bob: 我注意到固定为3,但、和随着和变化。这是为什么?

Alice: 这涉及到复杂的概率分析。是并行尝试的次数,越大成功率越高,但通信开销也越大。和是超球的半径,需要足够大以保证安全性,但又不能太大否则拒绝采样成功率会降低。

Bob: 为什么有上限限制?

Alice: 这是一个实用性的权衡。随着增加,秘密份额的数量是,增长非常快。代码中的错误信息说”less than 6″,但实际参数表中包含了的配置。比如时,有个份额;如果,就有个份额。每个参与方需要存储的秘密会急剧增加。

Eve: 而且通信轮次也会增加。在实际的网络环境中,轮次数是个关键瓶颈。

Bob: 我注意到代码里有个FVec类型用浮点数,但最终的签名都是整数。这是怎么转换的?

Alice: 这是个很有意思的细节。来看代码:

// Sets v to [s1 s2].
func&nbsp;(v *FVec)&nbsp;From(s1 *VecL, s2 *VecK)&nbsp;{
&nbsp; &nbsp;&nbsp;var&nbsp;u&nbsp;int32
&nbsp; &nbsp;&nbsp;for&nbsp;i :=&nbsp;0; i < L + K; i++ {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;j :=&nbsp;0; j < common.N; j++ {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;i < L {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; u =&nbsp;int32(s1[i][j])
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; u =&nbsp;int32(s2[i-L][j])
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// First centers u mod Q
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; u += common.Q/2
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; t := u - common.Q
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; u = t +&nbsp;int32((t >>&nbsp;31) & common.Q);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; u = u - common.Q/2

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// convert to float
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; v[i * common.N + j] =&nbsp;float64(u)
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
}

// Sets v to [s1 s2].
func&nbsp;(v *FVec)&nbsp;Round(s1 *VecL, s2 *VecK)&nbsp;{
&nbsp; &nbsp;&nbsp;var&nbsp;u&nbsp;int32
&nbsp; &nbsp;&nbsp;for&nbsp;i :=&nbsp;0; i < L + K; i++ {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;j :=&nbsp;0; j < common.N; j++ {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; u =&nbsp;int32(math.Round(v[i * common.N + j]))

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Adds +Q if it is <0
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; t := u >>&nbsp;31;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; u = u + (t & common.Q);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;i < L {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; s1[i][j] =&nbsp;uint32(u)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; s2[i-L][j] =&nbsp;uint32(u)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
}

BobFrom函数把整数转换成浮点数时,为什么要”center mod Q”?

Alice: 这是为了把模的整数映射到的范围。ML-DSA中的系数都是模的整数,但在数学上我们把它们看作范围内的整数。这个转换就是:

代码用位运算实现了这个逻辑,避免了分支。

Eve: 而Round函数则是反过来,把浮点数四舍五入成整数,然后映射回的范围。

Bob: 为什么要用浮点数?直接用整数不行吗?

Alice: 超球采样本质上是连续空间的操作,用浮点数更自然。而且Box-Muller变换生成的是实数,如果强行用整数会损失精度,影响安全性。

Bob: 好,我现在理解了代码的结构。但这个实现真的能用吗?

Alice: 这是一个学术原型实现,主要用于验证理论和性能测试。代码开头就有警告:

These implementations are academic proof-of-concept prototypes, have not received careful code review, and are not ready for production use.

Bob: 听起来还有很长的路要走。

Alice: 是的,但这个实现已经证明了门限ML-DSA是可行的。从”Finally!”的理论突破,到这个Go实现[3],我们见证了密码学从数学到代码的完整旅程。

Eve: 而且性能数据很有希望。根据论文[2],对于的配置,签名延迟只有几十毫秒,通信开销也在可接受范围内。

Bob: 那未来会怎样?

Alice: 门限ML-DSA正在成为后量子时代分布式信任的基石。从多方钱包、分布式CA,到关键基础设施的保护,它的应用场景非常广阔。而这个代码库,就是通往那个未来的第一步。

Bob: 从银行的双钥匙保管箱,到超球采样和MLWE困难问题,再到这几千行Go代码。密码学真是既抽象又具体,既优雅又实用。

Alice: 这就是密码学的魅力。它用数学的语言书写信任,用代码的形式实现安全。而我们,正站在量子时代的门槛上,见证着这一切的发生。

本次对话,就这么愉快地结束了。接下来,Alice、Bob和Eve又会遇到什么故事呢?且听下回分解。快乐的时光过得特别快,又到了说再见的时候了,咱们下次再见~

参考资料

  • [1] Rafael del Pino and Guilhem Niot. “Finally! A Compact Lattice-Based Threshold Signature”. In Proceedings of the 28th IACR International Conference on Practice and Theory of Public-Key Cryptography (PKC 2025). IACR Cryptology ePrint Archive, Report 2025/872, 2025. https://eprint.iacr.org/2025/872
  • [2] Sofía Celi, Rafael del Pino, Thomas Espitau, Guilhem Niot, and Thomas Prest. “Efficient Threshold ML-DSA”. In Proceedings of the 35th USENIX Security Symposium, 2026. IACR Cryptology ePrint Archive, Report 2026/013, 2026. https://eprint.iacr.org/2026/013
  • [3] Threshold-ML-DSA. Go implementation of threshold ML-DSA. GitHub repository. https://github.com/Threshold-ML-DSA/Threshold-ML-DSA
  • [4] NIST FIPS 204: Module-Lattice-Based Digital Signature Standard. National Institute of Standards and Technology, 2024. https://csrc.nist.gov/pubs/fips/204/final

免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:Coder小Q Litt1eQ《【密码学】基于ML-DSA的门限签名(代码解读)》

我建议删掉 网络安全文章

我建议删掉

文章总结: 该文档仅包含标题我建议删掉及作者、时间、地点等元信息,实质内容缺失或仅为图片占位符,无法提供任何技术分析、安全观点或可操作建议。 综合评分: 0 文
评论:0   参与:  0