mlkem 0.0.1__tar.gz

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.
mlkem-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.2
2
+ Name: mlkem
3
+ Version: 0.0.1
4
+ Summary: A module-latice-based key-encapsulation mechanism (ML-KEM). Implements the FIPS-203 standard.
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+
8
+ # Module-Lattice-Based Key-Encapsulation Mechanism (ML-KEM)
9
+ An implementation of the module-lattice-based key encapsulation mechanism (ML-KEM)
10
+ as described in [FIPS-203](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.203.pdf).
11
+ At this time the package is in alpha and _SHOULD NOT_ be considered for real-world
12
+ cryptographic applications.
13
+
14
+ # Usage
15
+
16
+ The package includes includes a pure python implementation of the K-PKE function
17
+ (`mlkem.k_pke.K_PKE`) and an implementation that leverages C extensions
18
+ (`mlkem.fast_k_pke.Fast_K_PKE`). The implementations have interchangeable interfaces
19
+ and can be selected in their wrapper class `mlkem.ml_kem.ML_KEM` by setting the
20
+ `fast` param to `True` for C extensions and `False` for pure python e.g.
21
+
22
+ ```python
23
+ from mlkem.ml_kem import ML_KEM
24
+ from mlkem.parameter_set import ParameterSet
25
+
26
+ ML_KEM(ParameterSet.ML_KEM_768, fast=True) # C extensions
27
+ ML_KEM(ParameterSet.ML_KEM_768, fast=False) # Pure python
28
+ ```
29
+
30
+ Both implementations are self contained and portable (assuming you have 8 bits per byte
31
+ on your system) with no dependencies on third party libraries in either the C or python
32
+ code.
33
+
34
+ NIST recommends the ML-KEM-768 parameter set, which offers 192 bit security. ML-KEM-512
35
+ and ML-KEM-1024 are also available, which provide 128 and 256 bit security respectively.
36
+ ML-KEM-768 is used by default in this package. Thus, the two instantiations below are
37
+ equivalent -
38
+
39
+ ```python
40
+ from mlkem.ml_kem import ML_KEM
41
+ from mlkem.parameter_set import ParameterSet
42
+
43
+ ML_KEM()
44
+ ML_KEM(ParameterSet.ML_KEM_768, fast=True)
45
+ ```
46
+
47
+ The interface follows the one defined in section 7 of the standard for the functions KeyGen,
48
+ Encaps and Decaps.
49
+
50
+ ```python
51
+ from mlkem.ml_kem import ML_KEM
52
+
53
+ ml_kem = ML_KEM()
54
+ ek, dk = ml_kem.key_gen() # encapsulation and decapsulation key
55
+ k, c = ml_kem.encaps(ek) # shared secret key and ciphertext
56
+ k_ = ml_kem.decaps(dk, c) # shared secret key
57
+ ```
58
+
59
+ In a less contrived scenario, Alice might run KeyGen and send the encapsulation key
60
+ to Bob. Bob would then run Encaps and generate a shared secret key and a ciphertext.
61
+ Bob would send the ciphertext to Alice, who would derive the shared secret key from the
62
+ ciphertext. Alice and Bob can then use the shared secret key to generate additional
63
+ secret material by passing it to a KDF, use the shared secret to directly key a symmetric
64
+ cipher like AES, etc.
65
+
66
+ # Development
67
+
68
+ As a prerequisite, `uv` is required for this project
69
+
70
+ pip install uv
71
+
72
+ Build the C extensions
73
+
74
+ uv run python setup.py build_ext --inplace
75
+
76
+ Run the test suite
77
+
78
+ uv run pytest
79
+
80
+ Build the docs
81
+
82
+ uv run make -C docs html
83
+
84
+ # Performance
85
+
86
+ The performance of the C extensions is _significantly_ faster. The python
87
+ implementation is primarily included for those that wish to explore and debug the algorithm.
88
+ Performance against the NIST
89
+ [keygen](https://github.com/usnistgov/ACVP-Server/tree/master/gen-val/json-files/ML-KEM-keyGen-FIPS203) and
90
+ [encap/decap](https://github.com/usnistgov/ACVP-Server/tree/master/gen-val/json-files/ML-KEM-encapDecap-FIPS203)
91
+ test vectors can be seen below -
92
+
93
+ #### C Extensions
94
+ ```
95
+ uv run pytest -k "key_gen or encaps or decaps"
96
+ ================================================ test session starts ================================================
97
+ platform darwin -- Python 3.11.11, pytest-8.3.4, pluggy-1.5.0
98
+ rootdir: /Users/antonku/dev/github/mlkem
99
+ configfile: pyproject.toml
100
+ plugins: cov-6.0.0
101
+ collected 261 items / 36 deselected / 225 selected
102
+
103
+ tests/test_decaps.py ........................................................................... [ 33%]
104
+ tests/test_encaps.py ........................................................................... [ 66%]
105
+ tests/test_key_gen.py ........................................................................... [100%]
106
+
107
+ ======================================== 225 passed, 36 deselected in 0.23s =========================================
108
+ ```
109
+
110
+ #### Pure Python
111
+ ```
112
+ uv run pytest -k "key_gen or encaps or decaps" 1 ↵
113
+ ================================================ test session starts ================================================
114
+ platform darwin -- Python 3.11.11, pytest-8.3.4, pluggy-1.5.0
115
+ rootdir: /Users/antonku/dev/github/mlkem
116
+ configfile: pyproject.toml
117
+ plugins: cov-6.0.0
118
+ collected 261 items / 36 deselected / 225 selected
119
+
120
+ tests/test_decaps.py ........................................................................... [ 33%]
121
+ tests/test_encaps.py ........................................................................... [ 66%]
122
+ tests/test_key_gen.py ........................................................................... [100%]
123
+
124
+ ======================================== 225 passed, 36 deselected in 4.42s =========================================
125
+ ```
mlkem-0.0.1/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # Module-Lattice-Based Key-Encapsulation Mechanism (ML-KEM)
2
+ An implementation of the module-lattice-based key encapsulation mechanism (ML-KEM)
3
+ as described in [FIPS-203](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.203.pdf).
4
+ At this time the package is in alpha and _SHOULD NOT_ be considered for real-world
5
+ cryptographic applications.
6
+
7
+ # Usage
8
+
9
+ The package includes includes a pure python implementation of the K-PKE function
10
+ (`mlkem.k_pke.K_PKE`) and an implementation that leverages C extensions
11
+ (`mlkem.fast_k_pke.Fast_K_PKE`). The implementations have interchangeable interfaces
12
+ and can be selected in their wrapper class `mlkem.ml_kem.ML_KEM` by setting the
13
+ `fast` param to `True` for C extensions and `False` for pure python e.g.
14
+
15
+ ```python
16
+ from mlkem.ml_kem import ML_KEM
17
+ from mlkem.parameter_set import ParameterSet
18
+
19
+ ML_KEM(ParameterSet.ML_KEM_768, fast=True) # C extensions
20
+ ML_KEM(ParameterSet.ML_KEM_768, fast=False) # Pure python
21
+ ```
22
+
23
+ Both implementations are self contained and portable (assuming you have 8 bits per byte
24
+ on your system) with no dependencies on third party libraries in either the C or python
25
+ code.
26
+
27
+ NIST recommends the ML-KEM-768 parameter set, which offers 192 bit security. ML-KEM-512
28
+ and ML-KEM-1024 are also available, which provide 128 and 256 bit security respectively.
29
+ ML-KEM-768 is used by default in this package. Thus, the two instantiations below are
30
+ equivalent -
31
+
32
+ ```python
33
+ from mlkem.ml_kem import ML_KEM
34
+ from mlkem.parameter_set import ParameterSet
35
+
36
+ ML_KEM()
37
+ ML_KEM(ParameterSet.ML_KEM_768, fast=True)
38
+ ```
39
+
40
+ The interface follows the one defined in section 7 of the standard for the functions KeyGen,
41
+ Encaps and Decaps.
42
+
43
+ ```python
44
+ from mlkem.ml_kem import ML_KEM
45
+
46
+ ml_kem = ML_KEM()
47
+ ek, dk = ml_kem.key_gen() # encapsulation and decapsulation key
48
+ k, c = ml_kem.encaps(ek) # shared secret key and ciphertext
49
+ k_ = ml_kem.decaps(dk, c) # shared secret key
50
+ ```
51
+
52
+ In a less contrived scenario, Alice might run KeyGen and send the encapsulation key
53
+ to Bob. Bob would then run Encaps and generate a shared secret key and a ciphertext.
54
+ Bob would send the ciphertext to Alice, who would derive the shared secret key from the
55
+ ciphertext. Alice and Bob can then use the shared secret key to generate additional
56
+ secret material by passing it to a KDF, use the shared secret to directly key a symmetric
57
+ cipher like AES, etc.
58
+
59
+ # Development
60
+
61
+ As a prerequisite, `uv` is required for this project
62
+
63
+ pip install uv
64
+
65
+ Build the C extensions
66
+
67
+ uv run python setup.py build_ext --inplace
68
+
69
+ Run the test suite
70
+
71
+ uv run pytest
72
+
73
+ Build the docs
74
+
75
+ uv run make -C docs html
76
+
77
+ # Performance
78
+
79
+ The performance of the C extensions is _significantly_ faster. The python
80
+ implementation is primarily included for those that wish to explore and debug the algorithm.
81
+ Performance against the NIST
82
+ [keygen](https://github.com/usnistgov/ACVP-Server/tree/master/gen-val/json-files/ML-KEM-keyGen-FIPS203) and
83
+ [encap/decap](https://github.com/usnistgov/ACVP-Server/tree/master/gen-val/json-files/ML-KEM-encapDecap-FIPS203)
84
+ test vectors can be seen below -
85
+
86
+ #### C Extensions
87
+ ```
88
+ uv run pytest -k "key_gen or encaps or decaps"
89
+ ================================================ test session starts ================================================
90
+ platform darwin -- Python 3.11.11, pytest-8.3.4, pluggy-1.5.0
91
+ rootdir: /Users/antonku/dev/github/mlkem
92
+ configfile: pyproject.toml
93
+ plugins: cov-6.0.0
94
+ collected 261 items / 36 deselected / 225 selected
95
+
96
+ tests/test_decaps.py ........................................................................... [ 33%]
97
+ tests/test_encaps.py ........................................................................... [ 66%]
98
+ tests/test_key_gen.py ........................................................................... [100%]
99
+
100
+ ======================================== 225 passed, 36 deselected in 0.23s =========================================
101
+ ```
102
+
103
+ #### Pure Python
104
+ ```
105
+ uv run pytest -k "key_gen or encaps or decaps" 1 ↵
106
+ ================================================ test session starts ================================================
107
+ platform darwin -- Python 3.11.11, pytest-8.3.4, pluggy-1.5.0
108
+ rootdir: /Users/antonku/dev/github/mlkem
109
+ configfile: pyproject.toml
110
+ plugins: cov-6.0.0
111
+ collected 261 items / 36 deselected / 225 selected
112
+
113
+ tests/test_decaps.py ........................................................................... [ 33%]
114
+ tests/test_encaps.py ........................................................................... [ 66%]
115
+ tests/test_key_gen.py ........................................................................... [100%]
116
+
117
+ ======================================== 225 passed, 36 deselected in 4.42s =========================================
118
+ ```
File without changes
File without changes
@@ -0,0 +1,48 @@
1
+ from hashlib import sha3_256, sha3_512, shake_128, shake_256
2
+
3
+
4
+ def prf(eta: int, s: bytes, b: bytes) -> bytes:
5
+ if eta not in {2, 3}:
6
+ raise ValueError(f"eta must be 2 or 3 (got {eta})")
7
+ if len(s) != 32:
8
+ raise ValueError(f"len(s) must be 32 (got {len(s)})")
9
+ if len(b) != 1:
10
+ raise ValueError(f"len(b) must be 1 (got {len(b)})")
11
+
12
+ # length passed to digest is byte length, so omit factor of 8 from spec (which uses bit length)
13
+ return shake_256(s + b).digest(64 * eta)
14
+
15
+
16
+ def h(s: bytes) -> bytes:
17
+ return sha3_256(s).digest()
18
+
19
+
20
+ def j(s: bytes) -> bytes:
21
+ # length passed to digest is byte length, so omit factor of 8 from spec (which uses bit length)
22
+ return shake_256(s).digest(32)
23
+
24
+
25
+ def g(c: bytes) -> tuple[bytes, bytes]:
26
+ ab = sha3_512(c).digest()
27
+ return ab[:32], ab[32:]
28
+
29
+
30
+ class XOF:
31
+ def __init__(self) -> None:
32
+ # https://cryptojedi.org/papers/terminate-20230516.pdf
33
+ self.chunk_size = 840
34
+ self.shake = shake_128()
35
+ self.data = b""
36
+ self.idx = 0
37
+
38
+ def absorb(self, string: bytes) -> None:
39
+ self.shake.update(string)
40
+ self.data += self.shake.digest(self.chunk_size)
41
+
42
+ def squeeze(self, length: int) -> bytes:
43
+ while self.idx + length > len(self.data):
44
+ self.data += self.shake.digest(self.chunk_size)
45
+
46
+ result = self.data[self.idx : self.idx + length]
47
+ self.idx += length
48
+ return result
@@ -0,0 +1,112 @@
1
+ from mlkem.math.constants import n, q
2
+ from mlkem.math.field import Zm
3
+
4
+ BITS_IN_BYTE = 8
5
+ MAX_D = q.bit_length()
6
+
7
+
8
+ def bits_to_bytes(bits: list[int]) -> list[int]:
9
+ """Converts a bit array (of a length that is a multiple of 8) into an array of bytes.
10
+
11
+ Bytes are represented as unsigned numbers in the range [0, 255]. Bits are either 0 or 1.
12
+
13
+ Args:
14
+ | bits (:type:`list[int]`): The bit array (of a length that is a multiple of 8).
15
+
16
+ Returns:
17
+ :type:`list[int]`: The array of bytes equivalent to the bit array.
18
+ """
19
+ length = len(bits)
20
+ if length % BITS_IN_BYTE != 0:
21
+ raise ValueError(
22
+ f"Bit array must have a length that is a multiple of 8 (got {length})."
23
+ )
24
+
25
+ result = [0 for _ in range(length // BITS_IN_BYTE)]
26
+ for i in range(length):
27
+ bitval = bits[i] * (1 << (i % BITS_IN_BYTE))
28
+ result[i // BITS_IN_BYTE] = result[i // BITS_IN_BYTE] + bitval
29
+
30
+ return result
31
+
32
+
33
+ def bytes_to_bits(byts: list[int]) -> list[int]:
34
+ """Converts a byte array into an array of bits.
35
+
36
+ Bytes are represented as unsigned numbers in the range [0, 255]. Bits are either 0 or 1.
37
+
38
+ Args:
39
+ | byts (:type:`list[int]`): The byte array.
40
+
41
+ Returns:
42
+ :type:`list[int]`: The array of bits equivalent to the byte array.
43
+ """
44
+ c = byts.copy()
45
+
46
+ result: list[int] = []
47
+ for i in range(len(c)):
48
+ for _ in range(BITS_IN_BYTE):
49
+ result.append(c[i] & 1)
50
+ c[i] //= 2
51
+
52
+ return result
53
+
54
+
55
+ def _round_fraction(x: int, y: int) -> int:
56
+ """Round the fraction x/y to the nearest integer."""
57
+ return (2 * x + y) // (2 * y)
58
+
59
+
60
+ def compress(d: int, x: Zm) -> Zm:
61
+ if not d < MAX_D:
62
+ raise ValueError(f"d must be less than {MAX_D} (got {d}).")
63
+ if x.m != q:
64
+ raise ValueError(f"Element being compressed must be in Z_q (got Z_{x.m}).")
65
+
66
+ m = 1 << d
67
+ val = _round_fraction(m * x.val, q) % m
68
+ return Zm(val, m)
69
+
70
+
71
+ def decompress(d: int, y: Zm) -> Zm:
72
+ if not d < MAX_D:
73
+ raise ValueError(f"d must be less than {MAX_D} (got {d}).")
74
+
75
+ m = 1 << d
76
+ val = _round_fraction(q * y.val, m)
77
+ return Zm(val, q)
78
+
79
+
80
+ def byte_encode(d: int, f: list[Zm]) -> bytes:
81
+ if len(f) != n:
82
+ raise ValueError(f"f must have {n} elements (got {len(f)}).")
83
+
84
+ if d > MAX_D or d < 1:
85
+ raise ValueError(f"d may not be greater than {MAX_D} or less than 1 (got {d}).")
86
+
87
+ b = [0 for _ in range(n * d)]
88
+ for i in range(n):
89
+ a = f[i].val
90
+
91
+ for j in range(d):
92
+ x = a & 1
93
+ b[i * d + j] = x
94
+ a = (a - x) // 2
95
+
96
+ return bytes(bits_to_bytes(b))
97
+
98
+
99
+ def byte_decode(d: int, b: bytes) -> list[Zm]:
100
+ if d > MAX_D or d < 1:
101
+ raise ValueError(f"d may not be greater than {MAX_D} or less than 1 (got {d}).")
102
+
103
+ m = q if d == MAX_D else 1 << d
104
+ bits = bytes_to_bits([x for x in b])
105
+
106
+ f = []
107
+ for i in range(n):
108
+ fi = sum([bits[i * d + j] * (1 << j) for j in range(d)])
109
+ fi_m = Zm(fi, m)
110
+ f.append(fi_m)
111
+
112
+ return f
@@ -0,0 +1,128 @@
1
+ from mlkem.math.constants import n, q
2
+ from mlkem.math.field import Zm
3
+ from mlkem.math.polynomial_ring import PolynomialRing, RingRepresentation
4
+
5
+ # see Appendix A - Precomputed Values for the NTT
6
+ ZETA_LOOKUP = [
7
+ Zm(x, q) for x in
8
+ [
9
+ 1, 1729, 2580, 3289, 2642, 630, 1897, 848,
10
+ 1062, 1919, 193, 797, 2786, 3260, 569, 1746,
11
+ 296, 2447, 1339, 1476, 3046, 56, 2240, 1333,
12
+ 1426, 2094, 535, 2882, 2393, 2879, 1974, 821,
13
+ 289, 331, 3253, 1756, 1197, 2304, 2277, 2055,
14
+ 650, 1977, 2513, 632, 2865, 33, 1320, 1915,
15
+ 2319, 1435, 807, 452, 1438, 2868, 1534, 2402,
16
+ 2647, 2617, 1481, 648, 2474, 3110, 1227, 910,
17
+ 17, 2761, 583, 2649, 1637, 723, 2288, 1100,
18
+ 1409, 2662, 3281, 233, 756, 2156, 3015, 3050,
19
+ 1703, 1651, 2789, 1789, 1847, 952, 1461, 2687,
20
+ 939, 2308, 2437, 2388, 733, 2337, 268, 641,
21
+ 1584, 2298, 2037, 3220, 375, 2549, 2090, 1645,
22
+ 1063, 319, 2773, 757, 2099, 561, 2466, 2594,
23
+ 2804, 1092, 403, 1026, 1143, 2150, 2775, 886,
24
+ 1722, 1212, 1874, 1029, 2110, 2935, 885, 2154
25
+ ]
26
+ ] # fmt: skip
27
+
28
+ GAMMA_LOOKUP = [
29
+ Zm(x, q) for x in
30
+ [
31
+ 17, -17, 2761, -2761, 583, -583, 2649, -2649,
32
+ 1637, -1637, 723, -723, 2288, -2288, 1100, -1100,
33
+ 1409, -1409, 2662, -2662, 3281, -3281, 233, -233,
34
+ 756, -756, 2156, -2156, 3015, -3015, 3050, -3050,
35
+ 1703, -1703, 1651, -1651, 2789, -2789, 1789, -1789,
36
+ 1847, -1847, 952, -952, 1461, -1461, 2687, -2687,
37
+ 939, -939, 2308, -2308, 2437, -2437, 2388, -2388,
38
+ 733, -733, 2337, -2337, 268, -268, 641, -641,
39
+ 1584, -1584, 2298, -2298, 2037, -2037, 3220, -3220,
40
+ 375, -375, 2549, -2549, 2090, -2090, 1645, -1645,
41
+ 1063, -1063, 319, -319, 2773, -2773, 757, -757,
42
+ 2099, -2099, 561, -561, 2466, -2466, 2594, -2594,
43
+ 2804, -2804, 1092, -1092, 403, -403, 1026, -1026,
44
+ 1143, -1143, 2150, -2150, 2775, -2775, 886, -886,
45
+ 1722, -1722, 1212, -1212, 1874, -1874, 1029, -1029,
46
+ 2110, -2110, 2935, -2935, 885, -885, 2154, -2154
47
+ ]
48
+ ] # fmt: skip
49
+
50
+
51
+ def ntt(f: PolynomialRing) -> PolynomialRing:
52
+ if f.representation != RingRepresentation.STANDARD:
53
+ raise ValueError(
54
+ "NTT can only be applied to polynomials in standard representation."
55
+ )
56
+
57
+ f_ = PolynomialRing(f.coefficients, RingRepresentation.NTT)
58
+ i = 1
59
+ length = 128
60
+
61
+ while length >= 2:
62
+ for start in range(0, n, 2 * length):
63
+ zeta = ZETA_LOOKUP[i]
64
+ i += 1
65
+
66
+ for j in range(start, start + length):
67
+ t = zeta * f_[j + length]
68
+ f_[j + length] = f_[j] - t
69
+ f_[j] += t
70
+
71
+ length //= 2
72
+
73
+ return f_
74
+
75
+
76
+ def ntt_inv(f_: PolynomialRing) -> PolynomialRing:
77
+ if f_.representation != RingRepresentation.NTT:
78
+ raise ValueError(
79
+ "Inverse NTT can only be applied to polynomials in NTT representation."
80
+ )
81
+
82
+ f = PolynomialRing(f_.coefficients, RingRepresentation.STANDARD)
83
+ i = 127
84
+ length = 2
85
+
86
+ while length <= 128:
87
+ for start in range(0, n, 2 * length):
88
+ zeta = ZETA_LOOKUP[i]
89
+ i -= 1
90
+
91
+ for j in range(start, start + length):
92
+ t = f[j]
93
+ f[j] = t + f[j + length]
94
+ f[j + length] = zeta * (f[j + length] - t)
95
+
96
+ length *= 2
97
+
98
+ scale = Zm(3303, q) # 3303 = 128^{-1} mod q
99
+ for i, fi in enumerate(f.coefficients):
100
+ f[i] = fi * scale
101
+
102
+ return f
103
+
104
+
105
+ def multiply_ntt(f_: PolynomialRing, g_: PolynomialRing) -> PolynomialRing:
106
+ if (
107
+ f_.representation != RingRepresentation.NTT
108
+ or g_.representation != RingRepresentation.NTT
109
+ ):
110
+ raise ValueError(
111
+ "NTT multiplication can only be applied to polynomials in NTT representation."
112
+ )
113
+
114
+ h_ = PolynomialRing(representation=RingRepresentation.NTT)
115
+
116
+ for i in range(128):
117
+ gamma = GAMMA_LOOKUP[i]
118
+ h_[2 * i], h_[2 * i + 1] = _base_case_multiply(
119
+ f_[2 * i], f_[2 * i + 1], g_[2 * i], g_[2 * i + 1], gamma
120
+ )
121
+
122
+ return h_
123
+
124
+
125
+ def _base_case_multiply(a0: Zm, a1: Zm, b0: Zm, b1: Zm, gamma: Zm) -> tuple[Zm, Zm]:
126
+ c0 = a0 * b0 + a1 * b1 * gamma
127
+ c1 = a0 * b1 + a1 * b0
128
+ return c0, c1
@@ -0,0 +1,47 @@
1
+ from mlkem.auxiliary.crypto import XOF
2
+ from mlkem.auxiliary.general import bytes_to_bits
3
+ from mlkem.math.constants import n, q
4
+ from mlkem.math.field import Zm
5
+ from mlkem.math.polynomial_ring import PolynomialRing, RingRepresentation
6
+
7
+
8
+ def sample_ntt(b: bytes) -> PolynomialRing:
9
+ if len(b) != 34:
10
+ raise ValueError(
11
+ f"Input must be 34 bytes (32-byte seed and two indices). Got {len(b)}."
12
+ )
13
+
14
+ a = PolynomialRing(representation=RingRepresentation.NTT)
15
+ xof = XOF()
16
+ xof.absorb(b)
17
+
18
+ j = 0
19
+ while j < n:
20
+ c = xof.squeeze(3)
21
+ d1 = c[0] + n * (c[1] % 16)
22
+ d2 = c[1] // 16 + 16 * c[2]
23
+
24
+ if d1 < q:
25
+ a[j] = Zm(d1, q)
26
+ j += 1
27
+
28
+ if d2 < q and j < n:
29
+ a[j] = Zm(d2, q)
30
+ j += 1
31
+
32
+ return a
33
+
34
+
35
+ def sample_poly_cbd(eta: int, b: bytes) -> PolynomialRing:
36
+ if len(b) != 64 * eta:
37
+ raise ValueError(f"Input must be {64 * eta} bytes (got {len(b)}).")
38
+
39
+ f = PolynomialRing(representation=RingRepresentation.STANDARD)
40
+ bits = bytes_to_bits([x for x in b])
41
+
42
+ for i in range(n):
43
+ x = sum([bits[2 * i * eta + j] for j in range(eta)])
44
+ y = sum([bits[2 * i * eta + eta + j] for j in range(eta)])
45
+ f[i] = Zm(x - y, q)
46
+
47
+ return f
@@ -0,0 +1,18 @@
1
+ from abc import abstractmethod
2
+ from typing import Protocol, Self
3
+
4
+
5
+ # the entries of a matrix must have a type supporting addition
6
+ # define that interface via this protocol
7
+ class Field(Protocol):
8
+ @abstractmethod
9
+ def __add__(self, other: Self) -> Self:
10
+ pass
11
+
12
+ @abstractmethod
13
+ def __sub__(self, other: Self) -> Self:
14
+ pass
15
+
16
+ @abstractmethod
17
+ def __mul__(self, other: Self) -> Self:
18
+ pass