Skip to content

Commit 4b4e6bb

Browse files
authored
Merge pull request #77 from GiacomoPope/drbg_doc
clean up drbg with documentation
2 parents ab4f388 + befa134 commit 4b4e6bb

File tree

5 files changed

+67
-21
lines changed

5 files changed

+67
-21
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ have made a child class `PolynomialRingKyber(PolynomialRing)` which has the
205205
following additional methods:
206206

207207
- `PolynomialRingKyber`
208-
- `parse(bytes)` takes $3n$ bytes and produces a random polynomial in $R_q$
208+
- `ntt_sample(bytes)` takes $3n$ bytes and produces a random polynomial in $R_q$
209209
- `decode(bytes, l)` takes $\ell n$ bits and produces a polynomial in $R_q$
210210
- `cbd(beta, eta)` takes $\eta \cdot n / 4$ bytes and produces a polynomial in
211211
$R_q$ with coefficents taken from a centered binomial distribution

src/kyber_py/drbg/aes256_ctr_drbg.py

+56-16
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,38 @@
11
import os
22
from ..utilities.utils import xor_bytes
33
from Crypto.Cipher import AES
4+
from typing import Optional
45

56

67
class AES256_CTR_DRBG:
7-
def __init__(self, seed=None, personalization=b""):
8+
def __init__(
9+
self, seed: Optional[bytes] = None, personalization: bytes = b""
10+
):
11+
"""
12+
DRBG implementation based on AES-256 CTR following the document NIST SP
13+
800-90A Section 10.2.1
14+
15+
https://csrc.nist.gov/pubs/sp/800/90/a/r1/final
16+
17+
Used for deterministic randomness, particularly used for comparing the
18+
output of Kyber/ML-KEM against known answer tests.
19+
20+
:param bytes seed: 48 byte seed, if none is supplied a seed is generated
21+
using ``os.urandom(48)``.
22+
:param bytes personalization: optional bytes, of length at most 48 used
23+
during instantiation of the DRBG
24+
"""
825
self.seed_length = 48
926
self.reseed_interval = 2**48
1027
self.key = bytes([0]) * 32
1128
self.V = bytes([0]) * 16
1229
self.entropy_input = self.__check_entropy_input(seed)
1330

1431
seed_material = self.__instantiate(personalization=personalization)
15-
self.ctr_drbg_update(seed_material)
32+
self.__ctr_drbg_update(seed_material)
1633
self.reseed_ctr = 1
1734

18-
def __check_entropy_input(self, entropy_input):
35+
def __check_entropy_input(self, entropy_input: bytes) -> bytes:
1936
"""
2037
If no entropy given, us os.urandom, else
2138
check that the input is of the right length.
@@ -29,32 +46,44 @@ def __check_entropy_input(self, entropy_input):
2946
)
3047
return entropy_input
3148

32-
def __instantiate(self, personalization=b""):
49+
def __instantiate(self, personalization: bytes = b"") -> bytes:
3350
"""
3451
Combine the input seed and optional personalisation
3552
string into the seed material for the DRBG
53+
54+
Section 10.2.1.3.1, Page 52 (CTR_DRBG_Instantiate_algorithm)
3655
"""
3756
if len(personalization) > self.seed_length:
3857
raise ValueError(
3958
f"The Personalization String must be at most length: "
4059
f"{self.seed_length}. Input has length {len(personalization)}"
4160
)
42-
elif len(personalization) < self.seed_length:
43-
personalization += bytes([0]) * (
44-
self.seed_length - len(personalization)
45-
)
61+
# Ensure personalization has exactly seed_length bytes
62+
personalization += bytes([0]) * (
63+
self.seed_length - len(personalization)
64+
)
4665
# debugging
4766
assert len(personalization) == self.seed_length
4867
return xor_bytes(self.entropy_input, personalization)
4968

50-
def __increment_counter(self):
69+
def __increment_counter(self) -> None:
70+
"""
71+
Increment the internal counter of the DRBG
72+
"""
5173
int_V = int.from_bytes(self.V, "big")
52-
new_V = (int_V + 1) % 2 ** (8 * 16)
74+
new_V = (int_V + 1) % 2**128
5375
self.V = new_V.to_bytes(16, byteorder="big")
5476

55-
def ctr_drbg_update(self, provided_data):
77+
def __ctr_drbg_update(self, provided_data: bytes) -> None:
78+
"""
79+
Updates the internal state of the CTR_DRBG using the
80+
provided_data
81+
82+
Section 10.2.1.2, Page 51 (CTR_DRBG_Update)
83+
"""
5684
tmp = b""
5785
cipher = AES.new(self.key, AES.MODE_ECB)
86+
5887
# Collect bytes from AES ECB
5988
while len(tmp) != self.seed_length:
6089
self.__increment_counter()
@@ -68,7 +97,19 @@ def ctr_drbg_update(self, provided_data):
6897
self.key = tmp[:32]
6998
self.V = tmp[32:]
7099

71-
def random_bytes(self, num_bytes, additional=None):
100+
def random_bytes(
101+
self, num_bytes: int, additional: Optional[bytes] = None
102+
) -> bytes:
103+
"""
104+
Generate pseudorandom bytes without a generating function
105+
106+
Section 10.2.1.5.1, Page 56 (CTR_DRBG_Generate_algorithm)
107+
108+
:param int num_bytes: the number of random bytes requested
109+
:param bytes additional: optional bytes to be mixed into the generation
110+
:return: pseudorandom bytes extracted from the DRBG of length ``num_bytes``.
111+
:rtype: bytes
112+
"""
72113
# We don't cover this in coverage as we would need to run the counter 2^48 times
73114
if self.reseed_ctr >= self.reseed_interval: # pragma: no cover
74115
raise Warning("The DRBG has been exhausted! Reseed!")
@@ -82,9 +123,8 @@ def random_bytes(self, num_bytes, additional=None):
82123
f"The additional input must be of length at most: "
83124
f"{self.seed_length}. Input has length {len(additional)}"
84125
)
85-
elif len(additional) < self.seed_length:
86-
additional += bytes([0]) * (self.seed_length - len(additional))
87-
self.ctr_drbg_update(additional)
126+
additional += bytes([0]) * (self.seed_length - len(additional))
127+
self.__ctr_drbg_update(additional)
88128

89129
# Collect bytes!
90130
tmp = b""
@@ -95,6 +135,6 @@ def random_bytes(self, num_bytes, additional=None):
95135

96136
# Collect only the requested number of bits
97137
output_bytes = tmp[:num_bytes]
98-
self.ctr_drbg_update(additional)
138+
self.__ctr_drbg_update(additional)
99139
self.reseed_ctr += 1
100140
return output_bytes

src/kyber_py/kyber/kyber.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def _generate_matrix_from_seed(self, rho, transpose=False):
139139
for i in range(self.k):
140140
for j in range(self.k):
141141
input_bytes = self._xof(rho, bytes([j]), bytes([i]))
142-
A_data[i][j] = self.R.parse(input_bytes, is_ntt=True)
142+
A_data[i][j] = self.R.ntt_sample(input_bytes)
143143
A_hat = self.M(A_data, transpose=transpose)
144144
return A_hat
145145

src/kyber_py/ml_kem/ml_kem.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def _generate_matrix_from_seed(self, rho, transpose=False):
117117
for i in range(self.k):
118118
for j in range(self.k):
119119
xof_bytes = self._xof(rho, bytes([j]), bytes([i]))
120-
A_data[i][j] = self.R.parse(xof_bytes, is_ntt=True)
120+
A_data[i][j] = self.R.ntt_sample(xof_bytes)
121121
A_hat = self.M(A_data, transpose=transpose)
122122
return A_hat
123123

src/kyber_py/polynomials/polynomials.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@ def _br(i, k):
2929
bin_i = bin(i & (2**k - 1))[2:].zfill(k)
3030
return int(bin_i[::-1], 2)
3131

32-
def parse(self, input_bytes, is_ntt=False):
32+
def ntt_sample(self, input_bytes):
3333
"""
3434
Algorithm 1 (Parse)
3535
https://pq-crystals.org/kyber/data/kyber-specification-round3-20210804.pdf
3636
37+
Algorithm 6 (Sample NTT)
38+
FIPS 203-ipd
39+
3740
Parse: B^* -> R
3841
"""
3942
i, j = 0, 0
@@ -51,13 +54,16 @@ def parse(self, input_bytes, is_ntt=False):
5154
j = j + 1
5255

5356
i = i + 3
54-
return self(coefficients, is_ntt=is_ntt)
57+
return self(coefficients, is_ntt=True)
5558

5659
def cbd(self, input_bytes, eta, is_ntt=False):
5760
"""
5861
Algorithm 2 (Centered Binomial Distribution)
5962
https://pq-crystals.org/kyber/data/kyber-specification-round3-20210804.pdf
6063
64+
Algorithm 6 (Sample Poly CBD)
65+
FIPS 203-ipd
66+
6167
Expects a byte array of length (eta * deg / 4)
6268
For Kyber, this is 64 eta.
6369
"""

0 commit comments

Comments
 (0)