文章总结: 本文解读基于ML-DSA的门限签名Go代码,剖析短复制秘密共享、超球拒绝采样等核心算法。详解密钥生成与三轮交互协议,展示位运算优化与硬编码共享设计。该学术原型验证了后量子门限签名的可行性,为分布式信任基础设施提供技术参考。 综合评分: 82 文章分类: 安全开发,解决方案,安全建设
【密码学】基于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 := uint8((1 << (params.N-params.T+1)) - 1)
for honestSigners < (1 << params.N) {
var share Share
var sSeed [64]byte
_, _ = h.Read(sSeed[:])
for j := uint16(0); j < L; j++ {
PolyDeriveUniformLeqEta(&share.s1[j], &sSeed, j)
}
for j := uint16(0); j < K; j++ {
PolyDeriveUniformLeqEta(&share.s2[j], &sSeed, j+L)
}
share.s1h = share.s1
share.s1h.NTT()
share.s2h = share.s2
share.s2h.NTT()
// Distribute the share
for i := uint8(0); i < params.N; i++ {
if (honestSigners & (1 << i)) != 0 {
sks[i].shares[honestSigners] = &share
}
}
// ... accumulate to total secret
// next possible set of honest signers
c := honestSigners & -honestSigners
r := honestSigners + c
honestSigners = (((r^honestSigners) >> 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 SampleHyperball(p *FVec, radius float64, nu float64, rhop [64]byte, nonce uint16) {
var sq float64
samples := make([]float64, common.N*(K+L) + 2)
// Use SHAKE256 for cryptographic randomness
h := sha3.NewShake256()
_, _ = h.Write([]byte("H")) // Add a domain separator
h.Write(rhop[:])
// ... write nonce
// Generate normally distributed random numbers using Box-Muller transform
for i := 0; i < common.N*(K+L) + 2; i += 2 {
// ... convert bytes to uniform random
f1 := float64(u1) / (1 << 64)
f2 := float64(u2) / (1 << 64)
// Box-Muller transform
z1 := math.Sqrt(-2*math.Log(f1)) * math.Cos(2*math.Pi*f2)
z2 := math.Sqrt(-2*math.Log(f1)) * math.Sin(2*math.Pi*f2)
samples[i] = z1
sq += z1*z1
samples[i+1] = z2
sq += z2*z2
if i < common.N*L {
samples[i] *= nu
samples[i+1] *= nu // 非平衡因子
}
}
factor := radius / math.Sqrt(sq)
for i := 0; i < common.N*(K+L); i++ {
p[i] = samples[i] * factor
}
}
Bob: Box-Muller变换?这是什么魔法?
Alice: 这是一个经典的统计学算法,用于从均匀分布生成正态分布(高斯分布)。给定两个均匀随机数,通过这个公式:
就能得到两个独立的标准正态分布随机数。
Bob: 我明白了,你们先生成一堆正态分布的随机数,然后通过factor = radius / sqrt(sq)把它们缩放到超球表面上。这就像在高维空间中随机投飞镖,然后把所有飞镖都拉到球面上。但是,等等,我看到有个if i < common.N*L的判断,里面乘以了。这是什么?
Alice: 这就是非平衡拒绝采样的体现。还记得我们说过和有不同的约束吗?前个多项式对应,后个对应。通过给前面的部分乘以(通常是3),我们给更大的”自由度”,这样就能在保证安全性的同时提高成功率。
Bob: 所以这不是一个标准的超球,而是一个”拉伸”的椭球?
Alice: 准确地说,是在某些维度上拉伸的超椭球。但在数学上,我们通过适当的度量变换,仍然可以把它看作超球。来看检查函数:
func (v *FVec) Excess(r float64, nu float64) bool {
var sq float64
for i := 0; i < L + K; i++ {
for j := 0; j < common.N; j++ {
if i < L {
sq += v[i * common.N + j] * v[i * common.N + j] / (nu * nu)
} else {
sq += v[i * common.N + j] * v[i * common.N + j]
}
}
}
return sq > r * r
}
Eve: 这里,前个多项式的系数除以再平方,这样就抵消了采样时乘以的效果。检查时相当于在”归一化”的空间里判断是否超出半径。
Bob: 好,现在我们有了秘密分发和超球采样。那实际的签名协议是怎么运行的呢?
Alice: 这就要看三个核心函数了:Round1、Round2、Round3。让我们一个个来看。
Round 1:承诺阶段
Alice: 第一轮是承诺阶段,来看代码:
func Round1(sk *PrivateKey, params *ThresholdParams) ([]byte, StRound1, error) {
var rhop [64]byte
_, err := cryptoRand.Read(rhop[:])
if err != nil {
returnnil, StRound1{}, err
}
cmt := make([]byte, 32)
wbuf := make([]byte, int(params.K) * internal.SingleCommitmentSize)
w, tmpcmtst := internal.GenThCommitment(
(*internal.PrivateKey)(sk),
rhop,
0,
(*internal.ThresholdParams)(params),
)
internal.PackW(w, wbuf[:])
s := sha3.NewShake256()
s.Write((*internal.PrivateKey)(sk).Tr[:])
s.Write([]byte{(*internal.PrivateKey)(sk).Id})
s.Write(wbuf)
s.Read(cmt[:])
return cmt, StRound1{wbuf, tmpcmtst}, nil
}
Bob: 这里生成了个承诺,然后只发送哈希值cmt?
Alice: 没错,这就是承诺-揭示范式。每个参与方生成个承诺(对应次并行尝试),但只发送这些承诺的哈希值。这样就防止了rushing attack——恶意参与方无法看到别人的承诺后再调整自己的。
Eve: 我注意到哈希里包含了Tr(公钥的哈希)和Id(参与方编号)。这是为了防止重放攻击吗?
Alice: 完全正确。Tr绑定了公钥,Id绑定了参与方身份。这样即使我截获了某个承诺,也无法冒充其他参与方或用于其他公钥。
Bob: 那GenThCommitment里面做了什么?
Alice: 来看:
func GenThCommitment(sk *PrivateKey, rhop [64]byte, nonce uint16, params *ThresholdParams) ([]VecK, []FVec) {
ws := make([]VecK, params.K)
sts := make([]FVec, params.K)
for i := uint16(0); i < params.K; i++ {
var r, rh VecL
var e_ VecK
// [THRESHOLD] Also sample an error for w
SampleHyperball(&sts[i], params.rPrime, params.nu, rhop, nonce * params.K + i)
sts[i].Round(&r, &e_)
// Set w to A y
rh = r
rh.NTT()
for j := 0; j < K; j++ {
PolyDotHat(&ws[i][j], &sk.A[j], &rh)
ws[i][j].ReduceLe2Q()
ws[i][j].InvNTT()
// [THRESHOLD]
ws[i][j].Add(&e_[j], &ws[i][j])
ws[i][j].ReduceLe2Q()
}
ws[i].NormalizeAssumingLe2Q()
}
return ws, sts
}
Bob: 我看到注释说”Also sample an error for w”,这就是你之前说的MLWE样本?
Alice: 没错。标准ML-DSA中,。但在门限版本中,,这样就是一个MLWE样本。即使拒绝采样失败需要揭示,它在计算上也不可区分于随机向量。
Eve: 这就是”Finally!”论文[1]的核心创新之一。没有这个额外的噪声,揭示被拒绝的会泄露关于秘密的信息。
Round 2:揭示承诺
Alice: 第二轮相对简单,看:
func Round2(sk *PrivateKey, act uint8, msg, ctx []byte, msgsrd1 [][]byte, strd1 *StRound1, params *ThresholdParams) ([]byte, StRound2, error) {
// Store hashes for future use
st2 := StRound2{}
st2.hashes = make([][32]byte, len(msgsrd1))
for i, msg := range msgsrd1 {
st2.hashes[i] = [32]byte(msg)
}
st2.mu = internal.ComputeMu((*internal.PrivateKey)(sk), func(w io.Writer) {
_, _ = w.Write([]byte{0})
_, _ = w.Write([]byte{byte(len(ctx))})
if ctx != nil {
_, _ = w.Write(ctx)
}
w.Write(msg)
})
st2.act = act
return strd1.wbuf, st2, nil
}
Bob: 这一轮就是把第一轮生成的wbuf发出去,同时保存其他人的哈希承诺?
Alice: 对。每个参与方揭示自己的承诺,同时验证别人的承诺是否与第一轮的哈希匹配。这个验证在第三轮进行。同时,这一轮还计算了,这是消息的哈希,会用于生成挑战。
Round 3:计算响应
Bob: 第三轮应该是最复杂的吧?
Alice: 没错,这是整个协议的核心。来看代码:
func Round3(sk *PrivateKey, msgsrd2 [][]byte, strd1 *StRound1, strd2 *StRound2, params *ThresholdParams) ([]byte, error) {
wtmp := make([]internal.VecK, params.K)
wfinal := make([]internal.VecK, params.K)
// Compute wfinal
j := uint8(0)
for i := 0; i < len(msgsrd2); i++ {
for strd2.act & (1 << j) == 0 {
j++
}
// Check that the commitments correspond to the one hashed in round 1
s := sha3.NewShake256()
s.Write((*internal.PrivateKey)(sk).Tr[:])
s.Write([]byte{j})
s.Write(msgsrd2[i])
var hash [32]byte
s.Read(hash[:])
if hash != strd2.hashes[i] {
returnnil, errors.New("wrong commitment")
}
internal.UnpackW(wtmp, msgsrd2[i][:])
internal.AggregateCommitments(wfinal, wtmp)
j++
}
zs := internal.ComputeResponses((*internal.PrivateKey)(sk), strd2.act, strd2.mu, wfinal, strd1.cmtst, (*internal.ThresholdParams)(params))
response := make([]byte, params.ResponseSize())
internal.PackResponses(zs, response[:])
return response, nil
}
Bob: 首先验证承诺,然后聚合所有人的,最后计算响应?
Alice: 对,关键在ComputeResponses函数。来看代码:
func ComputeResponses(sk *PrivateKey, act uint8, mu [64]byte, wfinals []VecK, stws []FVec, params *ThresholdParams) []VecL {
// ... 初始化
// Recover the partial secret of the current user corresponding
// to the signer set act
s1h, s2h := recoverShare(sk, act, params)
// For each commitment
for i := uint16(0); i < params.K; i++ {
var z VecL
// Decompose w into w₀ and w₁
wfinals[i].Decompose(&w0, &w1)
// c~ = H(μ ‖ w₁)
w1.PackW1(w1Packed[:])
h.Reset()
_, _ = h.Write(mu[:])
_, _ = h.Write(w1Packed[:])
_, _ = h.Read(c[:])
PolyDeriveUniformBall(&ch, c[:])
ch.NTT()
// Compute c·s₁
for j := 0; j < L; j++ {
z[j].MulHat(&ch, &s1h[j])
z[j].InvNTT()
}
z.Normalize()
// Compute c*s2
for j := 0; j < K; j++ {
y[j].MulHat(&ch, &s2h[j])
y[j].InvNTT()
}
y.Normalize()
var zf FVec
zf.From(&z, &y)
zf.Add(&zf, &stws[i])
if zf.Excess(params.r, params.nu) {
continue
}
zf.Round(&zs[i], &y)
}
return zs
}
Bob: 我看到了。首先通过recoverShare恢复这个参与方在当前签名集合下的部分秘密,然后对每个承诺计算,最后用Excess检查是否通过拒绝采样。那个recoverShare函数是怎么工作的?
Alice: 这是个很有意思的函数,它使用了预先计算好的分配模式。recoverShare函数的核心是一个硬编码的分配表:
func recoverShare(sk *PrivateKey, act uint8, params *ThresholdParams) (s1h VecL, s2h VecK) {
// Base case
if params.T == 1 || params.T == params.N {
for u := range sk.shares {
s1h = sk.shares[u].s1h
s2h = sk.shares[u].s2h
return
}
}
// Otherwise, we rely on hardcoded sharing patterns
var sharing [][]uint8
// 2 3 [[5, 3], [6]]
// 2 4 [[13, 7], [14, 11]]
// 3 4 [[9, 3], [10, 6], [12, 5]]
// ...
if params.T == 2 && params.N == 3 {
sharing = [][]uint8{[]uint8{3,5}, []uint8{6}}
} elseif params.T == 2 && params.N == 4 {
sharing = [][]uint8{[]uint8{13,7}, []uint8{14,11}}
}
// ... 更多情况
// Define a permutation to cover the signing set act
perm := make([]uint8, params.N)
i1 := 0
i2 := params.T
for j := uint8(0); j < params.N; j++ {
if j == sk.Id {
currenti = i1
}
if act & (1 << j) != 0 {
perm[i1] = j
i1++
} else {
perm[i2] = j
i2++
}
}
for _, u := range sharing[currenti] {
// Translate the share index u to the share index u_
u_ := uint8(0)
for i := uint8(0); i < params.N; i++ {
if u & (1 << i) != 0 {
u_ |= (1 << perm[i])
}
}
// Add the share to the partial secret
s1h.Add(&s1h, &sk.shares[u_].s1h)
s2h.Add(&s2h, &sk.shares[u_].s2h)
}
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 Combine(pk *PublicKey, msg func(io.Writer), wfinals []VecK, zs []VecL, signature []byte, params *ThresholdParams) bool {
// ... 初始化
// For each commitment
for i := uint16(0); i < params.K; i++ {
wfinals[i].Decompose(&w0, &w1)
sig.z = zs[i]
// Ensure ‖z‖_∞ < γ1 - beta.
if zs[i].Exceeds(Gamma1 - Beta) {
continue
}
zh = zs[i]
zh.NTT()
for j := 0; j < K; j++ {
PolyDotHat(&Az[j], &pk.A[j], &zh)
}
// c~ = H(μ ‖ w₁)
w1.PackW1(w1Packed[:])
h.Reset()
_, _ = h.Write(mu[:])
_, _ = h.Write(w1Packed[:])
_, _ = h.Read(sig.c[:])
PolyDeriveUniformBall(&ch, sig.c[:])
ch.NTT()
// Compute Az - 2ᵈ·c·t₁
Az2dct1.MulBy2toD(&pk.t1)
Az2dct1.NTT()
for j := 0; j < K; j++ {
Az2dct1[j].MulHat(&Az2dct1[j], &ch)
}
Az2dct1.Sub(&Az, &Az2dct1)
Az2dct1.ReduceLe2Q()
Az2dct1.InvNTT()
Az2dct1.NormalizeAssumingLe2Q()
var f VecK
f.Sub(&Az2dct1, &wfinals[i])
f.Normalize()
// Ensure ‖c*t0 - c*s2 - e_2‖_∞ < γ₂.
if f.Exceeds(Gamma2) {
continue
}
// Decompose and make hint
wfinals[i].Decompose(&w0, &w1)
w0pf.Add(&w0, &f)
w0pf.Normalize()
hintPop := sig.hint.MakeHint(&w0pf, &w1)
if hintPop <= Omega {
sig.Pack(signature)
returntrue
}
}
returnfalse
}
Bob: 这里遍历个并行尝试,对每个尝试都检查各种约束,直到找到一个成功的?
Alice: 没错,关键的检查有三个:
- :确保响应不会太大
- :这里,确保误差在范围内
- :确保hint足够稀疏
Bob: 我注意到这里计算了f = Az2dct1 - wfinals[i]。这个是什么?
Alice: 这是个关键的量。回忆ML-DSA的验证等式:
但在门限版本中,由于我们添加了噪声,实际上:
而,其中。所以:
这个误差必须小于才能保证hint稀疏。
Bob: 那个MakeHint是干什么的?
Alice: Hint是ML-DSA的核心机制。验证者需要检查,但由于舍入误差,这两个值可能在某些系数上差1。Hint告诉验证者”哪些位置需要调整”。
Bob: 我看到代码里有很多硬编码的参数,比如、、。这些是怎么选的?
Alice: 这些参数是通过复杂的数学分析和实验得出的。来看代码:
func GetThresholdParams(t, n uint8) (*ThresholdParams, error) {
// Validate parameters
if t < 2 {
returnnil, errors.New("threshold T must be 2 or more")
}
if t > n {
returnnil, errors.New("threshold T must be less than or equal to total parties N")
}
if n > 6 {
returnnil, errors.New("number of parties must be less than 6")
}
var k uint16
var r, rPrime float64
nu := float64(3.)
if t == 2 && n == 2 {
k = uint16(2)
r = 252778
rPrime = 252833
} elseif n == 3 {
ks := []uint16{3,4}
rs := []float64{310060, 246490}
rPs := []float64{310138, 246546}
k = ks[t-2]
r = rs[t-2]
rPrime = rPs[t-2]
}
// ... 更多配置
return &ThresholdParams{
T: t, N: n, K: k,
nu: nu, r: r, rPrime: rPrime,
}, nil
}
Bob: 我注意到固定为3,但、和随着和变化。这是为什么?
Alice: 这涉及到复杂的概率分析。是并行尝试的次数,越大成功率越高,但通信开销也越大。和是超球的半径,需要足够大以保证安全性,但又不能太大否则拒绝采样成功率会降低。
Bob: 为什么有上限限制?
Alice: 这是一个实用性的权衡。随着增加,秘密份额的数量是,增长非常快。代码中的错误信息说”less than 6″,但实际参数表中包含了的配置。比如时,有个份额;如果,就有个份额。每个参与方需要存储的秘密会急剧增加。
Eve: 而且通信轮次也会增加。在实际的网络环境中,轮次数是个关键瓶颈。
Bob: 我注意到代码里有个FVec类型用浮点数,但最终的签名都是整数。这是怎么转换的?
Alice: 这是个很有意思的细节。来看代码:
// Sets v to [s1 s2].
func (v *FVec) From(s1 *VecL, s2 *VecK) {
var u int32
for i := 0; i < L + K; i++ {
for j := 0; j < common.N; j++ {
if i < L {
u = int32(s1[i][j])
} else {
u = int32(s2[i-L][j])
}
// First centers u mod Q
u += common.Q/2
t := u - common.Q
u = t + int32((t >> 31) & common.Q);
u = u - common.Q/2
// convert to float
v[i * common.N + j] = float64(u)
}
}
}
// Sets v to [s1 s2].
func (v *FVec) Round(s1 *VecL, s2 *VecK) {
var u int32
for i := 0; i < L + K; i++ {
for j := 0; j < common.N; j++ {
u = int32(math.Round(v[i * common.N + j]))
// Adds +Q if it is <0
t := u >> 31;
u = u + (t & common.Q);
if i < L {
s1[i][j] = uint32(u)
} else {
s2[i-L][j] = uint32(u)
}
}
}
}
Bob: From函数把整数转换成浮点数时,为什么要”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的门限签名(代码解读)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论