dilithium-py 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.
- dilithium_py/__init__.py +0 -0
- dilithium_py/dilithium/__init__.py +3 -0
- dilithium_py/dilithium/default_parameters.py +38 -0
- dilithium_py/dilithium/dilithium.py +312 -0
- dilithium_py/drbg/__init__.py +0 -0
- dilithium_py/drbg/aes256_ctr_drbg.py +134 -0
- dilithium_py/ml_dsa/__init__.py +3 -0
- dilithium_py/ml_dsa/default_parameters.py +41 -0
- dilithium_py/ml_dsa/ml_dsa.py +364 -0
- dilithium_py/modules/__init__.py +0 -0
- dilithium_py/modules/modules.py +204 -0
- dilithium_py/modules/modules_generic.py +230 -0
- dilithium_py/polynomials/__init__.py +0 -0
- dilithium_py/polynomials/polynomials.py +453 -0
- dilithium_py/polynomials/polynomials_generic.py +237 -0
- dilithium_py/shake/shake_wrapper.py +61 -0
- dilithium_py/utilities/__init__.py +0 -0
- dilithium_py/utilities/utils.py +105 -0
- dilithium_py-1.0.0.dist-info/METADATA +439 -0
- dilithium_py-1.0.0.dist-info/RECORD +22 -0
- dilithium_py-1.0.0.dist-info/WHEEL +4 -0
- dilithium_py-1.0.0.dist-info/licenses/LICENSE +21 -0
dilithium_py/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from .dilithium import Dilithium
|
|
2
|
+
|
|
3
|
+
DEFAULT_PARAMETERS = {
|
|
4
|
+
"dilithium2": {
|
|
5
|
+
"d": 13,
|
|
6
|
+
"k": 4,
|
|
7
|
+
"l": 4,
|
|
8
|
+
"eta": 2,
|
|
9
|
+
"tau": 39,
|
|
10
|
+
"omega": 80,
|
|
11
|
+
"gamma_1": 131072, # 2^17
|
|
12
|
+
"gamma_2": 95232, # (q-1)/88
|
|
13
|
+
},
|
|
14
|
+
"dilithium3": {
|
|
15
|
+
"d": 13,
|
|
16
|
+
"k": 6,
|
|
17
|
+
"l": 5,
|
|
18
|
+
"eta": 4,
|
|
19
|
+
"tau": 49,
|
|
20
|
+
"omega": 55,
|
|
21
|
+
"gamma_1": 524288, # 2^19
|
|
22
|
+
"gamma_2": 261888, # (q-1)/32
|
|
23
|
+
},
|
|
24
|
+
"dilithium5": {
|
|
25
|
+
"d": 13,
|
|
26
|
+
"k": 8,
|
|
27
|
+
"l": 7,
|
|
28
|
+
"eta": 2,
|
|
29
|
+
"tau": 60,
|
|
30
|
+
"omega": 75,
|
|
31
|
+
"gamma_1": 524288, # 2^19
|
|
32
|
+
"gamma_2": 261888, # (q-1)/32
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Dilithium2 = Dilithium(DEFAULT_PARAMETERS["dilithium2"])
|
|
37
|
+
Dilithium3 = Dilithium(DEFAULT_PARAMETERS["dilithium3"])
|
|
38
|
+
Dilithium5 = Dilithium(DEFAULT_PARAMETERS["dilithium5"])
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from ..modules.modules import ModuleDilithium
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from xoflib import shake256
|
|
6
|
+
except ImportError:
|
|
7
|
+
from ..shake.shake_wrapper import shake256
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Dilithium:
|
|
11
|
+
def __init__(self, parameter_set):
|
|
12
|
+
self.d = parameter_set["d"]
|
|
13
|
+
self.k = parameter_set["k"]
|
|
14
|
+
self.l = parameter_set["l"]
|
|
15
|
+
self.eta = parameter_set["eta"]
|
|
16
|
+
self.tau = parameter_set["tau"]
|
|
17
|
+
self.omega = parameter_set["omega"]
|
|
18
|
+
self.gamma_1 = parameter_set["gamma_1"]
|
|
19
|
+
self.gamma_2 = parameter_set["gamma_2"]
|
|
20
|
+
self.beta = self.tau * self.eta
|
|
21
|
+
|
|
22
|
+
self.M = ModuleDilithium()
|
|
23
|
+
self.R = self.M.ring
|
|
24
|
+
|
|
25
|
+
# Use system randomness by default, for deterministic randomness
|
|
26
|
+
# use the method `set_drbg_seed()`
|
|
27
|
+
self.random_bytes = os.urandom
|
|
28
|
+
|
|
29
|
+
def set_drbg_seed(self, seed):
|
|
30
|
+
"""
|
|
31
|
+
Change entropy source to a DRBG and seed it with provided value.
|
|
32
|
+
|
|
33
|
+
Setting the seed switches the entropy source from :func:`os.urandom()`
|
|
34
|
+
to an AES256 CTR DRBG.
|
|
35
|
+
|
|
36
|
+
Used for both deterministic versions of Kyber as well as testing
|
|
37
|
+
alignment with the KAT vectors
|
|
38
|
+
|
|
39
|
+
Note:
|
|
40
|
+
currently requires pycryptodome for AES impl.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
from ..drbg.aes256_ctr_drbg import AES256_CTR_DRBG
|
|
44
|
+
|
|
45
|
+
self._drbg = AES256_CTR_DRBG(seed)
|
|
46
|
+
self.random_bytes = self._drbg.random_bytes
|
|
47
|
+
except ImportError as e: # pragma: no cover
|
|
48
|
+
print(f"Error importing AES from pycryptodome: {e = }")
|
|
49
|
+
raise Warning(
|
|
50
|
+
"Cannot set DRBG seed due to missing dependencies, try installing requirements: pip -r install requirements"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
H() uses Shake256 to hash data to 32 and 64 bytes in a
|
|
55
|
+
few places in the code
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _h(input_bytes, length):
|
|
60
|
+
"""
|
|
61
|
+
H: B^* -> B^*
|
|
62
|
+
"""
|
|
63
|
+
return shake256(input_bytes).read(length)
|
|
64
|
+
|
|
65
|
+
def _expand_matrix_from_seed(self, rho):
|
|
66
|
+
"""
|
|
67
|
+
Helper function which generates a element of size
|
|
68
|
+
k x l from a seed `rho`.
|
|
69
|
+
"""
|
|
70
|
+
A_data = [[0 for _ in range(self.l)] for _ in range(self.k)]
|
|
71
|
+
for i in range(self.k):
|
|
72
|
+
for j in range(self.l):
|
|
73
|
+
A_data[i][j] = self.R.rejection_sample_ntt_poly(rho, i, j)
|
|
74
|
+
return self.M(A_data)
|
|
75
|
+
|
|
76
|
+
def _expand_vector_from_seed(self, rho_prime):
|
|
77
|
+
s1_elements = [
|
|
78
|
+
self.R.rejection_bounded_poly(rho_prime, i, self.eta) for i in range(self.l)
|
|
79
|
+
]
|
|
80
|
+
s2_elements = [
|
|
81
|
+
self.R.rejection_bounded_poly(rho_prime, i, self.eta)
|
|
82
|
+
for i in range(self.l, self.l + self.k)
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
s1 = self.M.vector(s1_elements)
|
|
86
|
+
s2 = self.M.vector(s2_elements)
|
|
87
|
+
return s1, s2
|
|
88
|
+
|
|
89
|
+
def _expand_mask_vector(self, rho_prime, kappa):
|
|
90
|
+
elements = [
|
|
91
|
+
self.R.sample_mask_polynomial(rho_prime, i, kappa, self.gamma_1)
|
|
92
|
+
for i in range(self.l)
|
|
93
|
+
]
|
|
94
|
+
return self.M.vector(elements)
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _pack_pk(rho, t1):
|
|
98
|
+
return rho + t1.bit_pack_t1()
|
|
99
|
+
|
|
100
|
+
def _pack_sk(self, rho, K, tr, s1, s2, t0):
|
|
101
|
+
s1_bytes = s1.bit_pack_s(self.eta)
|
|
102
|
+
s2_bytes = s2.bit_pack_s(self.eta)
|
|
103
|
+
t0_bytes = t0.bit_pack_t0()
|
|
104
|
+
return rho + K + tr + s1_bytes + s2_bytes + t0_bytes
|
|
105
|
+
|
|
106
|
+
def _pack_h(self, h):
|
|
107
|
+
non_zero_positions = [
|
|
108
|
+
[i for i, c in enumerate(poly.coeffs) if c == 1]
|
|
109
|
+
for row in h._data
|
|
110
|
+
for poly in row
|
|
111
|
+
]
|
|
112
|
+
packed = []
|
|
113
|
+
offsets = []
|
|
114
|
+
for positions in non_zero_positions:
|
|
115
|
+
packed.extend(positions)
|
|
116
|
+
offsets.append(len(packed))
|
|
117
|
+
|
|
118
|
+
padding_len = self.omega - offsets[-1]
|
|
119
|
+
packed.extend([0 for _ in range(padding_len)])
|
|
120
|
+
return bytes(packed + offsets)
|
|
121
|
+
|
|
122
|
+
def _pack_sig(self, c_tilde, z, h):
|
|
123
|
+
return c_tilde + z.bit_pack_z(self.gamma_1) + self._pack_h(h)
|
|
124
|
+
|
|
125
|
+
def _unpack_pk(self, pk_bytes):
|
|
126
|
+
rho, t1_bytes = pk_bytes[:32], pk_bytes[32:]
|
|
127
|
+
t1 = self.M.bit_unpack_t1(t1_bytes, self.k, 1)
|
|
128
|
+
return rho, t1
|
|
129
|
+
|
|
130
|
+
def _unpack_sk(self, sk_bytes):
|
|
131
|
+
if self.eta == 2:
|
|
132
|
+
s_bytes = 96
|
|
133
|
+
else:
|
|
134
|
+
s_bytes = 128
|
|
135
|
+
s1_len = s_bytes * self.l
|
|
136
|
+
s2_len = s_bytes * self.k
|
|
137
|
+
t0_len = 416 * self.k
|
|
138
|
+
if len(sk_bytes) != 3 * 32 + s1_len + s2_len + t0_len:
|
|
139
|
+
raise ValueError("SK packed bytes is of the wrong length")
|
|
140
|
+
|
|
141
|
+
# Split bytes between seeds and vectors
|
|
142
|
+
sk_seed_bytes, sk_vec_bytes = sk_bytes[:96], sk_bytes[96:]
|
|
143
|
+
|
|
144
|
+
# Unpack seed bytes
|
|
145
|
+
rho, K, tr = (
|
|
146
|
+
sk_seed_bytes[:32],
|
|
147
|
+
sk_seed_bytes[32:64],
|
|
148
|
+
sk_seed_bytes[64:96],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Unpack vector bytes
|
|
152
|
+
s1_bytes = sk_vec_bytes[:s1_len]
|
|
153
|
+
s2_bytes = sk_vec_bytes[s1_len : s1_len + s2_len]
|
|
154
|
+
t0_bytes = sk_vec_bytes[-t0_len:]
|
|
155
|
+
|
|
156
|
+
# Unpack bytes to vectors
|
|
157
|
+
s1 = self.M.bit_unpack_s(s1_bytes, self.l, 1, self.eta)
|
|
158
|
+
s2 = self.M.bit_unpack_s(s2_bytes, self.k, 1, self.eta)
|
|
159
|
+
t0 = self.M.bit_unpack_t0(t0_bytes, self.k, 1)
|
|
160
|
+
|
|
161
|
+
return rho, K, tr, s1, s2, t0
|
|
162
|
+
|
|
163
|
+
def _unpack_h(self, h_bytes):
|
|
164
|
+
offsets = [0] + list(h_bytes[-self.k :])
|
|
165
|
+
non_zero_positions = [
|
|
166
|
+
list(h_bytes[offsets[i] : offsets[i + 1]]) for i in range(self.k)
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
matrix = []
|
|
170
|
+
for poly_non_zero in non_zero_positions:
|
|
171
|
+
coeffs = [0 for _ in range(256)]
|
|
172
|
+
for non_zero in poly_non_zero:
|
|
173
|
+
coeffs[non_zero] = 1
|
|
174
|
+
matrix.append([self.R(coeffs)])
|
|
175
|
+
return self.M(matrix)
|
|
176
|
+
|
|
177
|
+
def _unpack_sig(self, sig_bytes):
|
|
178
|
+
c_tilde = sig_bytes[:32]
|
|
179
|
+
z_bytes = sig_bytes[32 : -(self.k + self.omega)]
|
|
180
|
+
h_bytes = sig_bytes[-(self.k + self.omega) :]
|
|
181
|
+
|
|
182
|
+
z = self.M.bit_unpack_z(z_bytes, self.l, 1, self.gamma_1)
|
|
183
|
+
h = self._unpack_h(h_bytes)
|
|
184
|
+
return c_tilde, z, h
|
|
185
|
+
|
|
186
|
+
def keygen(self):
|
|
187
|
+
"""
|
|
188
|
+
Generates a public-private keyair
|
|
189
|
+
"""
|
|
190
|
+
# Random seed
|
|
191
|
+
zeta = self.random_bytes(32)
|
|
192
|
+
|
|
193
|
+
# Expand with an XOF (SHAKE256)
|
|
194
|
+
seed_bytes = self._h(zeta, 128)
|
|
195
|
+
|
|
196
|
+
# Split bytes into suitable chunks
|
|
197
|
+
rho, rho_prime, K = seed_bytes[:32], seed_bytes[32:96], seed_bytes[96:]
|
|
198
|
+
|
|
199
|
+
# Generate matrix A ∈ R^(kxl) in the NTT domain
|
|
200
|
+
A_hat = self._expand_matrix_from_seed(rho)
|
|
201
|
+
|
|
202
|
+
# Generate the error vectors s1 ∈ R^l, s2 ∈ R^k
|
|
203
|
+
s1, s2 = self._expand_vector_from_seed(rho_prime)
|
|
204
|
+
s1_hat = s1.to_ntt()
|
|
205
|
+
|
|
206
|
+
# Matrix multiplication
|
|
207
|
+
t = (A_hat @ s1_hat).from_ntt() + s2
|
|
208
|
+
|
|
209
|
+
t1, t0 = t.power_2_round(self.d)
|
|
210
|
+
|
|
211
|
+
# Pack up the bytes
|
|
212
|
+
pk = self._pack_pk(rho, t1)
|
|
213
|
+
tr = self._h(pk, 32)
|
|
214
|
+
|
|
215
|
+
sk = self._pack_sk(rho, K, tr, s1, s2, t0)
|
|
216
|
+
return pk, sk
|
|
217
|
+
|
|
218
|
+
def sign(self, sk_bytes, m):
|
|
219
|
+
"""
|
|
220
|
+
Generates a signature for a message m from a byte-encoded private key
|
|
221
|
+
"""
|
|
222
|
+
# unpack the secret key
|
|
223
|
+
rho, K, tr, s1, s2, t0 = self._unpack_sk(sk_bytes)
|
|
224
|
+
|
|
225
|
+
# Generate matrix A ∈ R^(kxl) in the NTT domain
|
|
226
|
+
A_hat = self._expand_matrix_from_seed(rho)
|
|
227
|
+
|
|
228
|
+
# Set seeds and nonce (kappa)
|
|
229
|
+
mu = self._h(tr + m, 64)
|
|
230
|
+
kappa = 0
|
|
231
|
+
rho_prime = self._h(K + mu, 64)
|
|
232
|
+
|
|
233
|
+
# Precompute NTT representation
|
|
234
|
+
s1 = s1.to_ntt()
|
|
235
|
+
s2 = s2.to_ntt()
|
|
236
|
+
t0 = t0.to_ntt()
|
|
237
|
+
|
|
238
|
+
alpha = self.gamma_2 << 1
|
|
239
|
+
while True:
|
|
240
|
+
y = self._expand_mask_vector(rho_prime, kappa)
|
|
241
|
+
y_hat = y.to_ntt()
|
|
242
|
+
|
|
243
|
+
# increment the nonce
|
|
244
|
+
kappa += self.l
|
|
245
|
+
|
|
246
|
+
w = (A_hat @ y_hat).from_ntt()
|
|
247
|
+
|
|
248
|
+
# Extract out both the high and low bits
|
|
249
|
+
w1, w0 = w.decompose(alpha)
|
|
250
|
+
|
|
251
|
+
# Create challenge polynomial
|
|
252
|
+
w1_bytes = w1.bit_pack_w(self.gamma_2)
|
|
253
|
+
c_tilde = self._h(mu + w1_bytes, 32)
|
|
254
|
+
c = self.R.sample_in_ball(c_tilde, self.tau)
|
|
255
|
+
|
|
256
|
+
# Store c in NTT form
|
|
257
|
+
c = c.to_ntt()
|
|
258
|
+
|
|
259
|
+
z = y + (s1.scale(c)).from_ntt()
|
|
260
|
+
if z.check_norm_bound(self.gamma_1 - self.beta):
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
w0_minus_cs2 = w0 - s2.scale(c).from_ntt()
|
|
264
|
+
if w0_minus_cs2.check_norm_bound(self.gamma_2 - self.beta):
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
c_t0 = t0.scale(c).from_ntt()
|
|
268
|
+
if c_t0.check_norm_bound(self.gamma_2):
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
w0_minus_cs2_plus_ct0 = w0_minus_cs2 + c_t0
|
|
272
|
+
|
|
273
|
+
h = w0_minus_cs2_plus_ct0.make_hint_optimised(w1, alpha)
|
|
274
|
+
if h.sum_hint() > self.omega:
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
return self._pack_sig(c_tilde, z, h)
|
|
278
|
+
|
|
279
|
+
def verify(self, pk_bytes, m, sig_bytes):
|
|
280
|
+
"""
|
|
281
|
+
Verifies a signature for a message m from a byte encoded public key and
|
|
282
|
+
signature
|
|
283
|
+
"""
|
|
284
|
+
rho, t1 = self._unpack_pk(pk_bytes)
|
|
285
|
+
c_tilde, z, h = self._unpack_sig(sig_bytes)
|
|
286
|
+
|
|
287
|
+
if h.sum_hint() > self.omega:
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
if z.check_norm_bound(self.gamma_1 - self.beta):
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
A_hat = self._expand_matrix_from_seed(rho)
|
|
294
|
+
|
|
295
|
+
tr = self._h(pk_bytes, 32)
|
|
296
|
+
mu = self._h(tr + m, 64)
|
|
297
|
+
c = self.R.sample_in_ball(c_tilde, self.tau)
|
|
298
|
+
|
|
299
|
+
# Convert to NTT for computation
|
|
300
|
+
c = c.to_ntt()
|
|
301
|
+
z = z.to_ntt()
|
|
302
|
+
|
|
303
|
+
t1 = t1.scale(1 << self.d)
|
|
304
|
+
t1 = t1.to_ntt()
|
|
305
|
+
|
|
306
|
+
Az_minus_ct1 = (A_hat @ z) - t1.scale(c)
|
|
307
|
+
Az_minus_ct1 = Az_minus_ct1.from_ntt()
|
|
308
|
+
|
|
309
|
+
w_prime = h.use_hint(Az_minus_ct1, 2 * self.gamma_2)
|
|
310
|
+
w_prime_bytes = w_prime.bit_pack_w(self.gamma_2)
|
|
311
|
+
|
|
312
|
+
return c_tilde == self._h(mu + w_prime_bytes, 32)
|
|
File without changes
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from Crypto.Cipher import AES
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from ..utilities.utils import xor_bytes
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AES256_CTR_DRBG:
|
|
8
|
+
def __init__(self, seed: Optional[bytes] = None, personalization: bytes = b""):
|
|
9
|
+
"""
|
|
10
|
+
DRBG implementation based on AES-256 CTR following the document NIST SP
|
|
11
|
+
800-90A Section 10.2.1
|
|
12
|
+
|
|
13
|
+
https://csrc.nist.gov/pubs/sp/800/90/a/r1/final
|
|
14
|
+
|
|
15
|
+
Used for deterministic randomness, particularly used for comparing the
|
|
16
|
+
output of Kyber/ML-KEM against known answer tests.
|
|
17
|
+
|
|
18
|
+
:param bytes seed: 48 byte seed, if none is supplied a seed is generated
|
|
19
|
+
using ``os.urandom(48)``.
|
|
20
|
+
:param bytes personalization: optional bytes, of length at most 48 used
|
|
21
|
+
during instantiation of the DRBG
|
|
22
|
+
"""
|
|
23
|
+
self.seed_length = 48
|
|
24
|
+
self.reseed_interval = 2**48
|
|
25
|
+
self.key = bytes([0]) * 32
|
|
26
|
+
self.V = bytes([0]) * 16
|
|
27
|
+
self.entropy_input = self.__check_entropy_input(seed)
|
|
28
|
+
|
|
29
|
+
seed_material = self.__instantiate(personalization=personalization)
|
|
30
|
+
self.__ctr_drbg_update(seed_material)
|
|
31
|
+
self.reseed_ctr = 1
|
|
32
|
+
|
|
33
|
+
def __check_entropy_input(self, entropy_input: bytes) -> bytes:
|
|
34
|
+
"""
|
|
35
|
+
If no entropy given, us os.urandom, else
|
|
36
|
+
check that the input is of the right length.
|
|
37
|
+
"""
|
|
38
|
+
if entropy_input is None:
|
|
39
|
+
return os.urandom(self.seed_length)
|
|
40
|
+
elif len(entropy_input) != self.seed_length:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"The entropy input must be of length: {self.seed_length}. "
|
|
43
|
+
f"Input has length {len(entropy_input)}"
|
|
44
|
+
)
|
|
45
|
+
return entropy_input
|
|
46
|
+
|
|
47
|
+
def __instantiate(self, personalization: bytes = b"") -> bytes:
|
|
48
|
+
"""
|
|
49
|
+
Combine the input seed and optional personalisation
|
|
50
|
+
string into the seed material for the DRBG
|
|
51
|
+
|
|
52
|
+
Section 10.2.1.3.1, Page 52 (CTR_DRBG_Instantiate_algorithm)
|
|
53
|
+
"""
|
|
54
|
+
if len(personalization) > self.seed_length:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"The Personalization String must be at most length: "
|
|
57
|
+
f"{self.seed_length}. Input has length {len(personalization)}"
|
|
58
|
+
)
|
|
59
|
+
# Ensure personalization has exactly seed_length bytes
|
|
60
|
+
personalization += bytes([0]) * (self.seed_length - len(personalization))
|
|
61
|
+
# debugging
|
|
62
|
+
assert len(personalization) == self.seed_length
|
|
63
|
+
return xor_bytes(self.entropy_input, personalization)
|
|
64
|
+
|
|
65
|
+
def __increment_counter(self) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Increment the internal counter of the DRBG
|
|
68
|
+
"""
|
|
69
|
+
int_V = int.from_bytes(self.V, "big")
|
|
70
|
+
new_V = (int_V + 1) % 2**128
|
|
71
|
+
self.V = new_V.to_bytes(16, byteorder="big")
|
|
72
|
+
|
|
73
|
+
def __ctr_drbg_update(self, provided_data: bytes) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Updates the internal state of the CTR_DRBG using the
|
|
76
|
+
provided_data
|
|
77
|
+
|
|
78
|
+
Section 10.2.1.2, Page 51 (CTR_DRBG_Update)
|
|
79
|
+
"""
|
|
80
|
+
tmp = b""
|
|
81
|
+
cipher = AES.new(self.key, AES.MODE_ECB)
|
|
82
|
+
|
|
83
|
+
# Collect bytes from AES ECB
|
|
84
|
+
while len(tmp) != self.seed_length:
|
|
85
|
+
self.__increment_counter()
|
|
86
|
+
tmp += cipher.encrypt(self.V)
|
|
87
|
+
|
|
88
|
+
# Take the first 48 bytes
|
|
89
|
+
tmp = tmp[: self.seed_length]
|
|
90
|
+
tmp = xor_bytes(tmp, provided_data)
|
|
91
|
+
|
|
92
|
+
# Set the new values of key and V
|
|
93
|
+
self.key = tmp[:32]
|
|
94
|
+
self.V = tmp[32:]
|
|
95
|
+
|
|
96
|
+
def random_bytes(self, num_bytes: int, additional: Optional[bytes] = None) -> bytes:
|
|
97
|
+
"""
|
|
98
|
+
Generate pseudorandom bytes without a generating function
|
|
99
|
+
|
|
100
|
+
Section 10.2.1.5.1, Page 56 (CTR_DRBG_Generate_algorithm)
|
|
101
|
+
|
|
102
|
+
:param int num_bytes: the number of random bytes requested
|
|
103
|
+
:param bytes additional: optional bytes to be mixed into the generation
|
|
104
|
+
:return: pseudorandom bytes extracted from the DRBG of length ``num_bytes``.
|
|
105
|
+
:rtype: bytes
|
|
106
|
+
"""
|
|
107
|
+
# We don't cover this in coverage as we would need to run the counter 2^48 times
|
|
108
|
+
if self.reseed_ctr >= self.reseed_interval: # pragma: no cover
|
|
109
|
+
raise Warning("The DRBG has been exhausted! Reseed!")
|
|
110
|
+
|
|
111
|
+
# Set the optional additional information
|
|
112
|
+
if additional is None:
|
|
113
|
+
additional = bytes([0]) * self.seed_length
|
|
114
|
+
else:
|
|
115
|
+
if len(additional) > self.seed_length:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"The additional input must be of length at most: "
|
|
118
|
+
f"{self.seed_length}. Input has length {len(additional)}"
|
|
119
|
+
)
|
|
120
|
+
additional += bytes([0]) * (self.seed_length - len(additional))
|
|
121
|
+
self.__ctr_drbg_update(additional)
|
|
122
|
+
|
|
123
|
+
# Collect bytes!
|
|
124
|
+
tmp = b""
|
|
125
|
+
cipher = AES.new(self.key, AES.MODE_ECB)
|
|
126
|
+
while len(tmp) < num_bytes:
|
|
127
|
+
self.__increment_counter()
|
|
128
|
+
tmp += cipher.encrypt(self.V)
|
|
129
|
+
|
|
130
|
+
# Collect only the requested number of bits
|
|
131
|
+
output_bytes = tmp[:num_bytes]
|
|
132
|
+
self.__ctr_drbg_update(additional)
|
|
133
|
+
self.reseed_ctr += 1
|
|
134
|
+
return output_bytes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from .ml_dsa import ML_DSA
|
|
2
|
+
|
|
3
|
+
DEFAULT_PARAMETERS = {
|
|
4
|
+
"ML_DSA_44": {
|
|
5
|
+
"d": 13, # number of bits dropped from t
|
|
6
|
+
"tau": 39, # number of ±1 in c
|
|
7
|
+
"gamma_1": 131072, # coefficient range of y: 2^17
|
|
8
|
+
"gamma_2": 95232, # low order rounding range: (q-1)/88
|
|
9
|
+
"k": 4, # Dimensions of A = (k, l)
|
|
10
|
+
"l": 4, # Dimensions of A = (k, l)
|
|
11
|
+
"eta": 2, # Private key range
|
|
12
|
+
"omega": 80, # Max number of ones in hint
|
|
13
|
+
"c_tilde_bytes": 32,
|
|
14
|
+
},
|
|
15
|
+
"ML_DSA_65": {
|
|
16
|
+
"d": 13, # number of bits dropped from t
|
|
17
|
+
"tau": 49, # number of ±1 in c
|
|
18
|
+
"gamma_1": 524288, # coefficient range of y: 2^19
|
|
19
|
+
"gamma_2": 261888, # low order rounding range: (q-1)/32
|
|
20
|
+
"k": 6, # Dimensions of A = (k, l)
|
|
21
|
+
"l": 5, # Dimensions of A = (k, l)
|
|
22
|
+
"eta": 4, # Private key range
|
|
23
|
+
"omega": 55, # Max number of ones in hint
|
|
24
|
+
"c_tilde_bytes": 48,
|
|
25
|
+
},
|
|
26
|
+
"ML_DSA_87": {
|
|
27
|
+
"d": 13, # number of bits dropped from t
|
|
28
|
+
"tau": 60, # number of ±1 in c
|
|
29
|
+
"gamma_1": 524288, # coefficient range of y: 2^19
|
|
30
|
+
"gamma_2": 261888, # low order rounding range: (q-1)/32
|
|
31
|
+
"k": 8, # Dimensions of A = (k, l)
|
|
32
|
+
"l": 7, # Dimensions of A = (k, l)
|
|
33
|
+
"eta": 2, # Private key range
|
|
34
|
+
"omega": 75, # Max number of ones in hint
|
|
35
|
+
"c_tilde_bytes": 64,
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
ML_DSA_44 = ML_DSA(DEFAULT_PARAMETERS["ML_DSA_44"])
|
|
40
|
+
ML_DSA_65 = ML_DSA(DEFAULT_PARAMETERS["ML_DSA_65"])
|
|
41
|
+
ML_DSA_87 = ML_DSA(DEFAULT_PARAMETERS["ML_DSA_87"])
|