mldsa 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mldsa/__init__.py +17 -0
- mldsa/mldsa.py +442 -0
- mldsa/py.typed +0 -0
- mldsa-1.0.0.dist-info/METADATA +89 -0
- mldsa-1.0.0.dist-info/RECORD +7 -0
- mldsa-1.0.0.dist-info/WHEEL +4 -0
- mldsa-1.0.0.dist-info/licenses/LICENSE +10 -0
mldsa/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Pure-Python implementation of ML-DSA (FIPS 204) signature verification."""
|
|
2
|
+
|
|
3
|
+
from .mldsa import (
|
|
4
|
+
InvalidContextError,
|
|
5
|
+
InvalidVerificationKeyError,
|
|
6
|
+
ParameterSet,
|
|
7
|
+
VerificationError,
|
|
8
|
+
VerificationKey,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"InvalidContextError",
|
|
13
|
+
"InvalidVerificationKeyError",
|
|
14
|
+
"ParameterSet",
|
|
15
|
+
"VerificationError",
|
|
16
|
+
"VerificationKey",
|
|
17
|
+
]
|
mldsa/mldsa.py
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
# mldsa-py by Filippo Valsorda is marked CC0 1.0 Universal. To view a copy of
|
|
2
|
+
# this mark, visit https://creativecommons.org/publicdomain/zero/1.0/
|
|
3
|
+
#
|
|
4
|
+
# Alternatively, you may use this source code under the terms of the 0BSD
|
|
5
|
+
# license that can be found in the LICENSE file.
|
|
6
|
+
|
|
7
|
+
"""Pure-Python implementation of ML-DSA (FIPS 204) signature verification."""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from hashlib import shake_128, shake_256
|
|
15
|
+
|
|
16
|
+
if sys.version_info >= (3, 11):
|
|
17
|
+
from typing import Self
|
|
18
|
+
else:
|
|
19
|
+
from typing_extensions import Self
|
|
20
|
+
|
|
21
|
+
if sys.version_info >= (3, 12):
|
|
22
|
+
from collections.abc import Buffer
|
|
23
|
+
from typing import override
|
|
24
|
+
else:
|
|
25
|
+
from typing_extensions import Buffer, override
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"InvalidContextError",
|
|
29
|
+
"InvalidVerificationKeyError",
|
|
30
|
+
"ParameterSet",
|
|
31
|
+
"VerificationError",
|
|
32
|
+
"VerificationKey",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
Q = 8380417
|
|
36
|
+
N = 256
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class _Parameters:
|
|
41
|
+
name: str
|
|
42
|
+
verification_key_size: int
|
|
43
|
+
signature_size: int
|
|
44
|
+
k: int
|
|
45
|
+
l: int
|
|
46
|
+
η: int
|
|
47
|
+
γ1: int
|
|
48
|
+
γ2: int
|
|
49
|
+
λ: int
|
|
50
|
+
τ: int
|
|
51
|
+
ω: int
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ParameterSet(Enum):
|
|
55
|
+
"""ML-DSA parameter sets as defined in FIPS 204."""
|
|
56
|
+
|
|
57
|
+
ML_DSA_44 = _Parameters(name="ML-DSA-44", verification_key_size=1312, signature_size=2420,
|
|
58
|
+
k=4, l=4, η=2, γ1=17, γ2=(Q - 1) // 88, λ=128, τ=39, ω=80) # fmt: skip
|
|
59
|
+
ML_DSA_65 = _Parameters(name="ML-DSA-65", verification_key_size=1952, signature_size=3309,
|
|
60
|
+
k=6, l=5, η=4, γ1=19, γ2=(Q - 1) // 32, λ=192, τ=49, ω=55) # fmt: skip
|
|
61
|
+
ML_DSA_87 = _Parameters(name="ML-DSA-87", verification_key_size=2592, signature_size=4627,
|
|
62
|
+
k=8, l=7, η=2, γ1=19, γ2=(Q - 1) // 32, λ=256, τ=60, ω=75) # fmt: skip
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def verification_key_size(self) -> int:
|
|
66
|
+
"""The encoded verification key size in bytes."""
|
|
67
|
+
return self.value.verification_key_size
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def signature_size(self) -> int:
|
|
71
|
+
"""The signature size in bytes."""
|
|
72
|
+
return self.value.signature_size
|
|
73
|
+
|
|
74
|
+
@override
|
|
75
|
+
def __str__(self) -> str:
|
|
76
|
+
"""Return the human-readable parameter set name, e.g. ``ML-DSA-44``."""
|
|
77
|
+
return self.value.name
|
|
78
|
+
|
|
79
|
+
@override
|
|
80
|
+
def __repr__(self) -> str:
|
|
81
|
+
"""Return a concise representation, e.g. ``<ParameterSet.ML_DSA_44>``."""
|
|
82
|
+
return f"<{type(self).__name__}.{self.name}>"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class VerificationError(Exception):
|
|
86
|
+
"""Raised when signature verification fails."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class InvalidVerificationKeyError(ValueError):
|
|
90
|
+
"""Raised when a verification key is invalid."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class InvalidContextError(ValueError):
|
|
94
|
+
"""Raised when a context string is invalid."""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class VerificationKey:
|
|
98
|
+
"""An ML-DSA verification key."""
|
|
99
|
+
|
|
100
|
+
_p: _Parameters
|
|
101
|
+
|
|
102
|
+
def __init__(self, pk: Buffer, /, *, parameters: ParameterSet | None = None) -> None:
|
|
103
|
+
"""Decode an ML-DSA verification key.
|
|
104
|
+
|
|
105
|
+
If *parameters* is ``None``, the parameter set is inferred from
|
|
106
|
+
the length of *pk*.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
InvalidVerificationKeyError: If the key is the wrong size or doesn't
|
|
110
|
+
match the specified parameter set.
|
|
111
|
+
"""
|
|
112
|
+
pk = memoryview(pk).cast("B")
|
|
113
|
+
if parameters is None:
|
|
114
|
+
size_to_params = {p.verification_key_size: p for p in ParameterSet}
|
|
115
|
+
if len(pk) not in size_to_params:
|
|
116
|
+
raise InvalidVerificationKeyError(f"unexpected verification key size {len(pk)}")
|
|
117
|
+
parameters = size_to_params[len(pk)]
|
|
118
|
+
self._p = parameters.value
|
|
119
|
+
|
|
120
|
+
if len(pk) != self._p.verification_key_size:
|
|
121
|
+
raise InvalidVerificationKeyError(
|
|
122
|
+
f"expected {self._p.verification_key_size} bytes, got {len(pk)}"
|
|
123
|
+
)
|
|
124
|
+
self._enc = bytes(pk)
|
|
125
|
+
self._tr = public_key_hash(pk)
|
|
126
|
+
ρ = bytes(pk[:32])
|
|
127
|
+
pkv = memoryview(pk[32:])
|
|
128
|
+
|
|
129
|
+
self._t1: list[NTTPoly] = [] # NTT(t₁ ⋅ 2ᵈ)
|
|
130
|
+
for _ in range(self._p.k):
|
|
131
|
+
self._t1.append(ntt(Poly([F(z.v << 13) for z in unpack(bytes(pkv[:320]), N, 10)])))
|
|
132
|
+
pkv = pkv[320:]
|
|
133
|
+
|
|
134
|
+
self._A: list[list[NTTPoly]] = [[] for _ in range(self._p.k)]
|
|
135
|
+
for r in range(self._p.k):
|
|
136
|
+
for s in range(self._p.l):
|
|
137
|
+
self._A[r].append(sample_ntt(ρ, s, r))
|
|
138
|
+
|
|
139
|
+
def __bytes__(self) -> bytes:
|
|
140
|
+
"""Return the encoded verification key."""
|
|
141
|
+
return self._enc
|
|
142
|
+
|
|
143
|
+
@override
|
|
144
|
+
def __repr__(self) -> str:
|
|
145
|
+
"""Return a concise representation, e.g. ``<VerificationKey ML-DSA-44>``."""
|
|
146
|
+
return f"<{type(self).__name__} {self._p.name}>"
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def parameters(self) -> ParameterSet:
|
|
150
|
+
"""The parameter set of this key."""
|
|
151
|
+
return ParameterSet(self._p)
|
|
152
|
+
|
|
153
|
+
def verify(self, signature: Buffer, message: Buffer, *, context: Buffer = b"") -> None:
|
|
154
|
+
"""Verify a signature over *message*.
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
VerificationError: If the signature is invalid.
|
|
158
|
+
InvalidContextError: If the context is too long (more than 255 bytes).
|
|
159
|
+
"""
|
|
160
|
+
signature = memoryview(signature).cast("B")
|
|
161
|
+
μ = message_hash(self._tr, message, context)
|
|
162
|
+
|
|
163
|
+
if len(signature) != self._p.signature_size:
|
|
164
|
+
raise VerificationError(
|
|
165
|
+
f"invalid signature size: expected {self._p.signature_size} bytes, got {len(signature)}"
|
|
166
|
+
)
|
|
167
|
+
ch = bytes(signature[: self._p.λ // 4])
|
|
168
|
+
sigv = memoryview(signature[self._p.λ // 4 :])
|
|
169
|
+
z: list[Poly] = []
|
|
170
|
+
for _ in range(self._p.l):
|
|
171
|
+
length = (self._p.γ1 + 1) * N // 8
|
|
172
|
+
z.append(Poly(unpack_signed(bytes(sigv[:length]), N, self._p.γ1 + 1)))
|
|
173
|
+
sigv = sigv[length:]
|
|
174
|
+
h: list[list[int]] = [[0] * N for _ in range(self._p.k)]
|
|
175
|
+
idx = 0
|
|
176
|
+
for i in range(self._p.k):
|
|
177
|
+
limit = sigv[self._p.ω + i]
|
|
178
|
+
if limit < idx or limit > self._p.ω:
|
|
179
|
+
raise VerificationError("invalid signature encoding")
|
|
180
|
+
first = idx
|
|
181
|
+
while idx < limit:
|
|
182
|
+
if idx > first and sigv[idx - 1] >= sigv[idx]:
|
|
183
|
+
raise VerificationError("invalid signature encoding")
|
|
184
|
+
h[i][sigv[idx]] = 1
|
|
185
|
+
idx += 1
|
|
186
|
+
for i in range(idx, self._p.ω):
|
|
187
|
+
if sigv[i] != 0:
|
|
188
|
+
raise VerificationError("invalid signature encoding")
|
|
189
|
+
|
|
190
|
+
c = ntt(sample_in_ball(ch, self._p))
|
|
191
|
+
|
|
192
|
+
z_hat = [ntt(x) for x in z]
|
|
193
|
+
w: list[Poly] = [] # Â ∘ NTT(z) − NTT(c) ∘ NTT(t₁ ⋅ 2ᵈ)
|
|
194
|
+
for i in range(self._p.k):
|
|
195
|
+
w_hat = NTTPoly.zero()
|
|
196
|
+
for j in range(self._p.l):
|
|
197
|
+
w_hat += z_hat[j] * self._A[i][j]
|
|
198
|
+
w_hat -= c * self._t1[i]
|
|
199
|
+
w.append(inverse_ntt(w_hat))
|
|
200
|
+
|
|
201
|
+
w1 = [use_hint(w[i], h[i], self._p) for i in range(self._p.k)]
|
|
202
|
+
|
|
203
|
+
H = shake_256()
|
|
204
|
+
H.update(μ)
|
|
205
|
+
w1_bit_length = ((Q - 1) // (2 * self._p.γ2) - 1).bit_length()
|
|
206
|
+
for i in range(self._p.k):
|
|
207
|
+
H.update(pack(w1[i], w1_bit_length))
|
|
208
|
+
if H.digest(self._p.λ // 4) != ch:
|
|
209
|
+
raise VerificationError("invalid signature")
|
|
210
|
+
|
|
211
|
+
β = self._p.τ * self._p.η
|
|
212
|
+
γ1 = 1 << self._p.γ1
|
|
213
|
+
γ1β = γ1 - β
|
|
214
|
+
|
|
215
|
+
for v in z:
|
|
216
|
+
if any(x.infinity_norm() >= γ1β for x in v.cs):
|
|
217
|
+
raise VerificationError("invalid signature")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def public_key_hash(pk: Buffer) -> bytes:
|
|
221
|
+
h = shake_256()
|
|
222
|
+
h.update(pk)
|
|
223
|
+
return h.digest(64)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def message_hash(tr: bytes, m: Buffer, ctx: Buffer) -> bytes:
|
|
227
|
+
ctx = memoryview(ctx).cast("B")
|
|
228
|
+
if len(ctx) > 255:
|
|
229
|
+
raise InvalidContextError(f"expected context of at most 255 bytes, got {len(ctx)}")
|
|
230
|
+
h = shake_256()
|
|
231
|
+
h.update(tr)
|
|
232
|
+
h.update(b"\x00")
|
|
233
|
+
h.update(bytes([len(ctx)]))
|
|
234
|
+
h.update(ctx)
|
|
235
|
+
h.update(m)
|
|
236
|
+
return h.digest(64)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class F:
|
|
240
|
+
__slots__ = ("v",)
|
|
241
|
+
|
|
242
|
+
def __init__(self, v: int) -> None:
|
|
243
|
+
assert 0 <= v < Q
|
|
244
|
+
self.v = v
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def reduce(cls, v: int) -> F:
|
|
248
|
+
return cls(v % Q)
|
|
249
|
+
|
|
250
|
+
def __add__(self, other: F) -> F:
|
|
251
|
+
return F.reduce(self.v + other.v)
|
|
252
|
+
|
|
253
|
+
def __sub__(self, other: F) -> F:
|
|
254
|
+
return F.reduce(self.v - other.v)
|
|
255
|
+
|
|
256
|
+
def __mul__(self, other: F) -> F:
|
|
257
|
+
return F.reduce(self.v * other.v)
|
|
258
|
+
|
|
259
|
+
def infinity_norm(self) -> int:
|
|
260
|
+
return self.v if self.v <= Q // 2 else Q - self.v
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def centered_mod(v: int, m: int) -> int:
|
|
264
|
+
r = v % m
|
|
265
|
+
if r > m // 2:
|
|
266
|
+
r -= m
|
|
267
|
+
return r
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def decompose(r: F, p: _Parameters) -> tuple[int, int]:
|
|
271
|
+
r0 = centered_mod(r.v, 2 * p.γ2)
|
|
272
|
+
if r.v - r0 == Q - 1:
|
|
273
|
+
return 0, r0 - 1
|
|
274
|
+
r1 = (r.v - r0) // (2 * p.γ2)
|
|
275
|
+
return r1, r0
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def unpack(buf: bytes, n: int, bit_length: int) -> list[F]:
|
|
279
|
+
assert n * bit_length == len(buf) * 8
|
|
280
|
+
res: list[F] = []
|
|
281
|
+
acc = 0
|
|
282
|
+
acc_len = 0
|
|
283
|
+
for b in buf:
|
|
284
|
+
acc |= b << acc_len
|
|
285
|
+
acc_len += 8
|
|
286
|
+
while acc_len >= bit_length:
|
|
287
|
+
res.append(F(acc & ((1 << bit_length) - 1)))
|
|
288
|
+
acc >>= bit_length
|
|
289
|
+
acc_len -= bit_length
|
|
290
|
+
return res
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def unpack_signed(buf: bytes, n: int, bit_length: int) -> list[F]:
|
|
294
|
+
b = F(1 << (bit_length - 1))
|
|
295
|
+
return [b - x for x in unpack(buf, n, bit_length)]
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def pack(cs: list[int], bit_length: int) -> bytes:
|
|
299
|
+
acc = 0
|
|
300
|
+
acc_len = 0
|
|
301
|
+
res = bytearray()
|
|
302
|
+
for c in cs:
|
|
303
|
+
acc |= c << acc_len
|
|
304
|
+
acc_len += bit_length
|
|
305
|
+
while acc_len >= 8:
|
|
306
|
+
res.append(acc & 0xFF)
|
|
307
|
+
acc >>= 8
|
|
308
|
+
acc_len -= 8
|
|
309
|
+
if acc_len > 0:
|
|
310
|
+
res.append(acc & 0xFF)
|
|
311
|
+
return bytes(res)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class Poly:
|
|
315
|
+
__slots__ = ("cs",)
|
|
316
|
+
cs: list[F]
|
|
317
|
+
|
|
318
|
+
def __init__(self, cs: list[F]) -> None:
|
|
319
|
+
assert len(cs) == N
|
|
320
|
+
self.cs = cs
|
|
321
|
+
|
|
322
|
+
def __add__(self, other: Self) -> Self:
|
|
323
|
+
if type(self) is not type(other):
|
|
324
|
+
return NotImplemented
|
|
325
|
+
return type(self)([a + b for a, b in zip(self.cs, other.cs)])
|
|
326
|
+
|
|
327
|
+
def __sub__(self, other: Self) -> Self:
|
|
328
|
+
if type(self) is not type(other):
|
|
329
|
+
return NotImplemented
|
|
330
|
+
return type(self)([a - b for a, b in zip(self.cs, other.cs)])
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class NTTPoly:
|
|
334
|
+
__slots__ = ("cs",)
|
|
335
|
+
cs: list[int] # don't use F to avoid function call overhead in hot loops
|
|
336
|
+
|
|
337
|
+
def __init__(self, cs: list[int]) -> None:
|
|
338
|
+
assert len(cs) == N
|
|
339
|
+
self.cs = cs
|
|
340
|
+
|
|
341
|
+
@classmethod
|
|
342
|
+
def zero(cls) -> Self:
|
|
343
|
+
return cls([0 for _ in range(N)])
|
|
344
|
+
|
|
345
|
+
def __iadd__(self, other: Self) -> Self:
|
|
346
|
+
if type(self) is not type(other):
|
|
347
|
+
return NotImplemented
|
|
348
|
+
for i in range(N):
|
|
349
|
+
self.cs[i] = (self.cs[i] + other.cs[i]) % Q
|
|
350
|
+
return self
|
|
351
|
+
|
|
352
|
+
def __isub__(self, other: Self) -> Self:
|
|
353
|
+
if type(self) is not type(other):
|
|
354
|
+
return NotImplemented
|
|
355
|
+
for i in range(N):
|
|
356
|
+
self.cs[i] = (self.cs[i] - other.cs[i]) % Q
|
|
357
|
+
return self
|
|
358
|
+
|
|
359
|
+
def __mul__(self, other: NTTPoly) -> NTTPoly:
|
|
360
|
+
if type(self) is not type(other):
|
|
361
|
+
return NotImplemented
|
|
362
|
+
return NTTPoly([a * b % Q for a, b in zip(self.cs, other.cs)])
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def ntt(f: Poly) -> NTTPoly:
|
|
366
|
+
m = 0
|
|
367
|
+
w = [c.v for c in f.cs]
|
|
368
|
+
for len in [128, 64, 32, 16, 8, 4, 2, 1]:
|
|
369
|
+
for start in range(0, N, 2 * len):
|
|
370
|
+
m += 1
|
|
371
|
+
zeta = ZETAS[m]
|
|
372
|
+
for j in range(start, start + len):
|
|
373
|
+
t = zeta * w[j + len] % Q
|
|
374
|
+
w[j + len] = (w[j] - t) % Q
|
|
375
|
+
w[j] = (w[j] + t) % Q
|
|
376
|
+
return NTTPoly(w)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def inverse_ntt(f: NTTPoly) -> Poly:
|
|
380
|
+
m = 255
|
|
381
|
+
w = [c for c in f.cs]
|
|
382
|
+
for len in [1, 2, 4, 8, 16, 32, 64, 128]:
|
|
383
|
+
for start in range(0, N, 2 * len):
|
|
384
|
+
zeta = ZETAS[m]
|
|
385
|
+
m -= 1
|
|
386
|
+
for j in range(start, start + len):
|
|
387
|
+
t = w[j]
|
|
388
|
+
w[j] = (t + w[j + len]) % Q
|
|
389
|
+
w[j + len] = zeta * (w[j + len] - t) % Q
|
|
390
|
+
return Poly([F(v * 8347681 % Q) for v in w])
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def sample_ntt(ρ: bytes, s: int, r: int) -> NTTPoly:
|
|
394
|
+
G = shake_128()
|
|
395
|
+
G.update(ρ)
|
|
396
|
+
G.update(bytes([s, r]))
|
|
397
|
+
buf = G.digest(894)
|
|
398
|
+
|
|
399
|
+
a: list[int] = []
|
|
400
|
+
while len(a) < N:
|
|
401
|
+
v = int.from_bytes(buf[:3], "little") & 0x7FFFFF
|
|
402
|
+
buf = buf[3:]
|
|
403
|
+
if v < Q:
|
|
404
|
+
a.append(v)
|
|
405
|
+
return NTTPoly(a)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def sample_in_ball(rho: bytes, p: _Parameters) -> Poly:
|
|
409
|
+
G = shake_256()
|
|
410
|
+
G.update(rho)
|
|
411
|
+
buf = G.digest(221)
|
|
412
|
+
s = buf[:8]
|
|
413
|
+
j = memoryview(buf)[8:]
|
|
414
|
+
|
|
415
|
+
c = [F(0) for _ in range(N)]
|
|
416
|
+
for i in range(256 - p.τ, 256):
|
|
417
|
+
while j[0] > i:
|
|
418
|
+
j = j[1:]
|
|
419
|
+
c[i] = c[j[0]]
|
|
420
|
+
bit_idx = i + p.τ - 256
|
|
421
|
+
bit = (s[bit_idx // 8] >> (bit_idx % 8)) & 1
|
|
422
|
+
c[j[0]] = F(1) if bit == 0 else F(Q - 1)
|
|
423
|
+
j = j[1:]
|
|
424
|
+
|
|
425
|
+
return Poly(c)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def use_hint(w: Poly, h: list[int], p: _Parameters) -> list[int]:
|
|
429
|
+
m = (Q - 1) // (2 * p.γ2)
|
|
430
|
+
w1: list[int] = []
|
|
431
|
+
for i in range(N):
|
|
432
|
+
r1, r0 = decompose(w.cs[i], p)
|
|
433
|
+
if h[i] == 0:
|
|
434
|
+
w1.append(r1)
|
|
435
|
+
elif r0 > 0:
|
|
436
|
+
w1.append((r1 + 1) % m)
|
|
437
|
+
else:
|
|
438
|
+
w1.append((r1 - 1) % m)
|
|
439
|
+
return w1
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
ZETAS = [1, 4808194, 3765607, 3761513, 5178923, 5496691, 5234739, 5178987, 7778734, 3542485, 2682288, 2129892, 3764867, 7375178, 557458, 7159240, 5010068, 4317364, 2663378, 6705802, 4855975, 7946292, 676590, 7044481, 5152541, 1714295, 2453983, 1460718, 7737789, 4795319, 2815639, 2283733, 3602218, 3182878, 2740543, 4793971, 5269599, 2101410, 3704823, 1159875, 394148, 928749, 1095468, 4874037, 2071829, 4361428, 3241972, 2156050, 3415069, 1759347, 7562881, 4805951, 3756790, 6444618, 6663429, 4430364, 5483103, 3192354, 556856, 3870317, 2917338, 1853806, 3345963, 1858416, 3073009, 1277625, 5744944, 3852015, 4183372, 5157610, 5258977, 8106357, 2508980, 2028118, 1937570, 4564692, 2811291, 5396636, 7270901, 4158088, 1528066, 482649, 1148858, 5418153, 7814814, 169688, 2462444, 5046034, 4213992, 4892034, 1987814, 5183169, 1736313, 235407, 5130263, 3258457, 5801164, 1787943, 5989328, 6125690, 3482206, 4197502, 7080401, 6018354, 7062739, 2461387, 3035980, 621164, 3901472, 7153756, 2925816, 3374250, 1356448, 5604662, 2683270, 5601629, 4912752, 2312838, 7727142, 7921254, 348812, 8052569, 1011223, 6026202, 4561790, 6458164, 6143691, 1744507, 1753, 6444997, 5720892, 6924527, 2660408, 6600190, 8321269, 2772600, 1182243, 87208, 636927, 4415111, 4423672, 6084020, 5095502, 4663471, 8352605, 822541, 1009365, 5926272, 6400920, 1596822, 4423473, 4620952, 6695264, 4969849, 2678278, 4611469, 4829411, 635956, 8129971, 5925040, 4234153, 6607829, 2192938, 6653329, 2387513, 4768667, 8111961, 5199961, 3747250, 2296099, 1239911, 4541938, 3195676, 2642980, 1254190, 8368000, 2998219, 141835, 8291116, 2513018, 7025525, 613238, 7070156, 6161950, 7921677, 6458423, 4040196, 4908348, 2039144, 6500539, 7561656, 6201452, 6757063, 2105286, 6006015, 6346610, 586241, 7200804, 527981, 5637006, 6903432, 1994046, 2491325, 6987258, 507927, 7192532, 7655613, 6545891, 5346675, 8041997, 2647994, 3009748, 5767564, 4148469, 749577, 4357667, 3980599, 2569011, 6764887, 1723229, 1665318, 2028038, 1163598, 5011144, 3994671, 8368538, 7009900, 3020393, 3363542, 214880, 545376, 7609976, 3105558, 7277073, 508145, 7826699, 860144, 3430436, 140244, 6866265, 6195333, 3123762, 2358373, 6187330, 5365997, 6663603, 2926054, 7987710, 8077412, 3531229, 4405932, 4606686, 1900052, 7598542, 1054478, 7648983] # fmt: skip # ruff: ignore[line-too-long]
|
mldsa/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mldsa
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: ML-DSA (FIPS 204) post-quantum signature verification (pure Python, no dependencies)
|
|
5
|
+
Author: Filippo Valsorda
|
|
6
|
+
License-Expression: CC0-1.0 OR 0BSD
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Security :: Cryptography
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Dist: typing-extensions>=4.6 ; python_full_version < '3.12'
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Project-URL: Homepage, https://github.com/FiloSottile/mldsa-py
|
|
25
|
+
Project-URL: Source, https://github.com/FiloSottile/mldsa-py
|
|
26
|
+
Project-URL: Issues, https://github.com/FiloSottile/mldsa-py/issues
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# mldsa-py
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
pip install mldsa
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This is a pure-Python production implementation of ML-DSA (FIPS 204)
|
|
36
|
+
post-quantum signature verification.
|
|
37
|
+
|
|
38
|
+
It does not provide key or signature generation, because secrets can't be
|
|
39
|
+
handled in constant-time in Python.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from mldsa import VerificationKey, VerificationError
|
|
43
|
+
|
|
44
|
+
vk = VerificationKey(verification_key_bytes)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
vk.verify(signature, message)
|
|
48
|
+
except VerificationError:
|
|
49
|
+
print("invalid signature!")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The parameter set (ML-DSA-44, ML-DSA-65, or ML-DSA-87) is inferred from the
|
|
53
|
+
verification key size, or it can be specified explicitly.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from mldsa import ParameterSet, VerificationKey
|
|
57
|
+
|
|
58
|
+
vk = VerificationKey(verification_key_bytes, parameters=ParameterSet.ML_DSA_87)
|
|
59
|
+
vk.verify(signature, message, context=b"example.com/foo token")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The non-test code is [a single-file module](https://github.com/FiloSottile/mldsa-py/blob/main/src/mldsa/mldsa.py)
|
|
63
|
+
of less than 500 lines, with no dependencies.
|
|
64
|
+
|
|
65
|
+
It works with Python 3.8 and later.
|
|
66
|
+
|
|
67
|
+
## Development
|
|
68
|
+
|
|
69
|
+
To run tests, use
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uv run ruff check
|
|
73
|
+
uv run ruff format --check
|
|
74
|
+
uv run ty check
|
|
75
|
+
uv run pytest
|
|
76
|
+
go install github.com/FiloSottile/mostly-harmless/muzoo@latest
|
|
77
|
+
muzoo -mutations tests/testdata/mutations test -- uv run pytest -x
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This project uses tests from [Wycheproof](https://github.com/C2SP/wycheproof).
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
This work is marked CC0 1.0 Universal. To view a copy of this mark, visit
|
|
85
|
+
[creativecommons.org](https://creativecommons.org/publicdomain/zero/1.0/).
|
|
86
|
+
|
|
87
|
+
Alternatively, you may use this source code under the terms of the 0BSD license
|
|
88
|
+
that can be found in the LICENSE file.
|
|
89
|
+
In short, you can do whatever you want with this code.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
mldsa/__init__.py,sha256=NaKLnkcMufb6w4j-jIBVjBi-X8mzb-ZcVslFRmj26Jc,367
|
|
2
|
+
mldsa/mldsa.py,sha256=PjX8JWhLUwVuenfN3yvXHvyuJFjP_qw5mhG54q8Xkp8,15401
|
|
3
|
+
mldsa/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
mldsa-1.0.0.dist-info/licenses/LICENSE,sha256=dLNEpDKIKDyniWs42Fm9a5EDURC7nGmFavvGCgH_ZeQ,611
|
|
5
|
+
mldsa-1.0.0.dist-info/WHEEL,sha256=iHtWm8nRfs0VRdCYVXocAWFW8ppjHL-uTJkAdZJKOBM,80
|
|
6
|
+
mldsa-1.0.0.dist-info/METADATA,sha256=eNw0EXHXSuKhSYGOfl25zqgytFAyy757DpHXx3BlyDg,2906
|
|
7
|
+
mldsa-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Permission to use, copy, modify, and/or distribute this software for
|
|
2
|
+
any purpose with or without fee is hereby granted.
|
|
3
|
+
|
|
4
|
+
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
|
|
5
|
+
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
|
|
6
|
+
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
|
|
7
|
+
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
|
|
8
|
+
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
|
9
|
+
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
|
10
|
+
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|