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.
File without changes
@@ -0,0 +1,3 @@
1
+ from .default_parameters import Dilithium2, Dilithium3, Dilithium5
2
+
3
+ __all__ = ["Dilithium2", "Dilithium3", "Dilithium5"]
@@ -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,3 @@
1
+ from .default_parameters import ML_DSA_44, ML_DSA_65, ML_DSA_87
2
+
3
+ __all__ = ["ML_DSA_44", "ML_DSA_65", "ML_DSA_87"]
@@ -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"])