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 +125 -0
- mlkem-0.0.1/README.md +118 -0
- mlkem-0.0.1/mlkem/__init__.py +0 -0
- mlkem-0.0.1/mlkem/auxiliary/__init__.py +0 -0
- mlkem-0.0.1/mlkem/auxiliary/crypto.py +48 -0
- mlkem-0.0.1/mlkem/auxiliary/general.py +112 -0
- mlkem-0.0.1/mlkem/auxiliary/ntt.py +128 -0
- mlkem-0.0.1/mlkem/auxiliary/sampling.py +47 -0
- mlkem-0.0.1/mlkem/data_types.py +18 -0
- mlkem-0.0.1/mlkem/fast_k_pke.py +179 -0
- mlkem-0.0.1/mlkem/k_pke.py +239 -0
- mlkem-0.0.1/mlkem/math/__init__.py +0 -0
- mlkem-0.0.1/mlkem/math/constants.py +2 -0
- mlkem-0.0.1/mlkem/math/fastmathmodule.c +717 -0
- mlkem-0.0.1/mlkem/math/field.py +67 -0
- mlkem-0.0.1/mlkem/math/matrix.py +171 -0
- mlkem-0.0.1/mlkem/math/polynomial_ring.py +160 -0
- mlkem-0.0.1/mlkem/ml_kem.py +82 -0
- mlkem-0.0.1/mlkem/parameter_set.py +18 -0
- mlkem-0.0.1/mlkem.egg-info/PKG-INFO +125 -0
- mlkem-0.0.1/mlkem.egg-info/SOURCES.txt +28 -0
- mlkem-0.0.1/mlkem.egg-info/dependency_links.txt +1 -0
- mlkem-0.0.1/mlkem.egg-info/top_level.txt +1 -0
- mlkem-0.0.1/pyproject.toml +37 -0
- mlkem-0.0.1/setup.cfg +4 -0
- mlkem-0.0.1/setup.py +11 -0
- mlkem-0.0.1/tests/test_decaps.py +425 -0
- mlkem-0.0.1/tests/test_encaps.py +506 -0
- mlkem-0.0.1/tests/test_key_gen.py +506 -0
- mlkem-0.0.1/tests/test_ml_kem.py +15 -0
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
|