dilithium-py 1.0.0__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.
Files changed (56) hide show
  1. dilithium_py-1.0.0/.github/workflows/ci.yml +88 -0
  2. dilithium_py-1.0.0/.github/workflows/release-pypi.yml +95 -0
  3. dilithium_py-1.0.0/.gitignore +3 -0
  4. dilithium_py-1.0.0/LICENSE +21 -0
  5. dilithium_py-1.0.0/PKG-INFO +439 -0
  6. dilithium_py-1.0.0/README.md +426 -0
  7. dilithium_py-1.0.0/assets/ML-DSA-keyGen-FIPS204/expectedResults.json +399 -0
  8. dilithium_py-1.0.0/assets/ML-DSA-keyGen-FIPS204/internalProjection.json +555 -0
  9. dilithium_py-1.0.0/assets/ML-DSA-keyGen-FIPS204/prompt.json +330 -0
  10. dilithium_py-1.0.0/assets/ML-DSA-keyGen-FIPS204/registration.json +12 -0
  11. dilithium_py-1.0.0/assets/ML-DSA-keyGen-FIPS204/validation.json +306 -0
  12. dilithium_py-1.0.0/assets/ML-DSA-sigGen-FIPS204/expectedResults.json +279 -0
  13. dilithium_py-1.0.0/assets/ML-DSA-sigGen-FIPS204/internalProjection.json +507 -0
  14. dilithium_py-1.0.0/assets/ML-DSA-sigGen-FIPS204/prompt.json +387 -0
  15. dilithium_py-1.0.0/assets/ML-DSA-sigGen-FIPS204/registration.json +23 -0
  16. dilithium_py-1.0.0/assets/ML-DSA-sigGen-FIPS204/validation.json +246 -0
  17. dilithium_py-1.0.0/assets/ML-DSA-sigVer-FIPS204/expectedResults.json +204 -0
  18. dilithium_py-1.0.0/assets/ML-DSA-sigVer-FIPS204/internalProjection.json +396 -0
  19. dilithium_py-1.0.0/assets/ML-DSA-sigVer-FIPS204/prompt.json +258 -0
  20. dilithium_py-1.0.0/assets/ML-DSA-sigVer-FIPS204/registration.json +12 -0
  21. dilithium_py-1.0.0/assets/ML-DSA-sigVer-FIPS204/validation.json +186 -0
  22. dilithium_py-1.0.0/assets/PQCsignKAT_Dilithium2.rsp +902 -0
  23. dilithium_py-1.0.0/assets/PQCsignKAT_Dilithium3.rsp +902 -0
  24. dilithium_py-1.0.0/assets/PQCsignKAT_Dilithium5.rsp +902 -0
  25. dilithium_py-1.0.0/benchmarks/benchmark_bit_packing.py +85 -0
  26. dilithium_py-1.0.0/benchmarks/benchmark_dilithium.py +75 -0
  27. dilithium_py-1.0.0/benchmarks/benchmark_ml_dsa.py +77 -0
  28. dilithium_py-1.0.0/pyproject.toml +23 -0
  29. dilithium_py-1.0.0/pytest.ini +2 -0
  30. dilithium_py-1.0.0/requirements.txt +2 -0
  31. dilithium_py-1.0.0/src/dilithium_py/__init__.py +0 -0
  32. dilithium_py-1.0.0/src/dilithium_py/dilithium/__init__.py +3 -0
  33. dilithium_py-1.0.0/src/dilithium_py/dilithium/default_parameters.py +38 -0
  34. dilithium_py-1.0.0/src/dilithium_py/dilithium/dilithium.py +312 -0
  35. dilithium_py-1.0.0/src/dilithium_py/drbg/__init__.py +0 -0
  36. dilithium_py-1.0.0/src/dilithium_py/drbg/aes256_ctr_drbg.py +134 -0
  37. dilithium_py-1.0.0/src/dilithium_py/ml_dsa/__init__.py +3 -0
  38. dilithium_py-1.0.0/src/dilithium_py/ml_dsa/default_parameters.py +41 -0
  39. dilithium_py-1.0.0/src/dilithium_py/ml_dsa/ml_dsa.py +364 -0
  40. dilithium_py-1.0.0/src/dilithium_py/modules/__init__.py +0 -0
  41. dilithium_py-1.0.0/src/dilithium_py/modules/modules.py +204 -0
  42. dilithium_py-1.0.0/src/dilithium_py/modules/modules_generic.py +230 -0
  43. dilithium_py-1.0.0/src/dilithium_py/polynomials/__init__.py +0 -0
  44. dilithium_py-1.0.0/src/dilithium_py/polynomials/polynomials.py +453 -0
  45. dilithium_py-1.0.0/src/dilithium_py/polynomials/polynomials_generic.py +237 -0
  46. dilithium_py-1.0.0/src/dilithium_py/shake/shake_wrapper.py +61 -0
  47. dilithium_py-1.0.0/src/dilithium_py/utilities/__init__.py +0 -0
  48. dilithium_py-1.0.0/src/dilithium_py/utilities/utils.py +105 -0
  49. dilithium_py-1.0.0/tests/__init__.py +0 -0
  50. dilithium_py-1.0.0/tests/test_dilithium.py +182 -0
  51. dilithium_py-1.0.0/tests/test_drbg.py +50 -0
  52. dilithium_py-1.0.0/tests/test_ml_dsa.py +165 -0
  53. dilithium_py-1.0.0/tests/test_module_generic.py +180 -0
  54. dilithium_py-1.0.0/tests/test_polynomial_generic.py +156 -0
  55. dilithium_py-1.0.0/tests/test_shake.py +44 -0
  56. dilithium_py-1.0.0/tests/test_utils.py +21 -0
@@ -0,0 +1,88 @@
1
+ name: GitHub CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+
9
+ jobs:
10
+ code-format:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ with:
15
+ fetch-depth: 10
16
+ - name: Verify git status
17
+ run: |
18
+ git status
19
+ git remote -v
20
+ - name: Set up Python ${{ matrix.python-version }}
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: "3.12"
24
+ - name: Display Python version
25
+ run: python -c "import sys; print(sys.version)"
26
+ - name: Display installed python package versions
27
+ run: |
28
+ pip list || :
29
+ - name: Install build dependencies
30
+ run: |
31
+ pip install -r requirements.txt
32
+ - name: Install black
33
+ run: |
34
+ pip install black==24.4.2
35
+ - name: Display installed python package versions
36
+ run: |
37
+ pip list || :
38
+ - name: Check formatting
39
+ run: |
40
+ black --check .
41
+
42
+ test:
43
+ runs-on: ${{ matrix.os }}
44
+ strategy:
45
+ fail-fast: false
46
+ matrix:
47
+ include:
48
+ - name: py3.9
49
+ os: ubuntu-latest
50
+ python-version: "3.9"
51
+ - name: py3.10
52
+ os: ubuntu-latest
53
+ python-version: "3.10"
54
+ - name: py3.11
55
+ os: ubuntu-latest
56
+ python-version: "3.11"
57
+ - name: py3.12
58
+ os: ubuntu-latest
59
+ python-version: "3.12"
60
+ steps:
61
+ - uses: actions/checkout@v4
62
+ with:
63
+ fetch-depth: 10
64
+ - name: Verify git status
65
+ run: |
66
+ git status
67
+ git remote -v
68
+ - name: Set up Python ${{ matrix.python-version }}
69
+ uses: actions/setup-python@v5
70
+ with:
71
+ python-version: ${{ matrix.python-version }}
72
+ - name: Display Python version
73
+ run: python -c "import sys; print(sys.version)"
74
+ - name: Display installed python package versions
75
+ run: |
76
+ pip list || :
77
+ - name: Install pytest
78
+ run: |
79
+ pip install pytest
80
+ - name: Install build dependencies
81
+ run: |
82
+ pip install -r requirements.txt
83
+ - name: Display installed python package versions
84
+ run: |
85
+ pip list || :
86
+ - name: Run unit tests
87
+ run: |
88
+ PYTHONPATH=src pytest
@@ -0,0 +1,95 @@
1
+ name: publish dilithium-py to pypi
2
+
3
+ on: push
4
+
5
+ jobs:
6
+ build:
7
+ name: Build distribution
8
+ runs-on: ubuntu-latest
9
+
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ with:
13
+ persist-credentials: false
14
+ - name: Set up Python
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.x"
18
+ - name: Install pypa/build
19
+ run: >-
20
+ python3 -m
21
+ pip install
22
+ build
23
+ --user
24
+ - name: Build a binary wheel and a source tarball
25
+ run: python3 -m build
26
+ - name: Store the distribution packages
27
+ uses: actions/upload-artifact@v4
28
+ with:
29
+ name: python-package-distributions
30
+ path: dist/
31
+
32
+ publish-to-pypi:
33
+ name: >-
34
+ Publish Python distribution to PyPI
35
+ if: github.ref_type == 'tag' # only publish to PyPI on tag pushes
36
+ needs:
37
+ - build
38
+ runs-on: ubuntu-latest
39
+ environment:
40
+ name: pypi
41
+ url: https://pypi.org/project/dilithium-py/${{ github.ref_name }}
42
+ permissions:
43
+ id-token: write # IMPORTANT: mandatory for trusted publishing
44
+
45
+ steps:
46
+ - name: Download all the dists
47
+ uses: actions/download-artifact@v4
48
+ with:
49
+ name: python-package-distributions
50
+ path: dist/
51
+ - name: Publish distribution to PyPI
52
+ uses: pypa/gh-action-pypi-publish@release/v1
53
+
54
+ github-release:
55
+ name: >-
56
+ Sign the Python distribution with Sigstore
57
+ and upload them to GitHub Release
58
+ needs:
59
+ - publish-to-pypi
60
+ runs-on: ubuntu-latest
61
+
62
+ permissions:
63
+ contents: write # IMPORTANT: mandatory for making GitHub Releases
64
+ id-token: write # IMPORTANT: mandatory for sigstore
65
+
66
+ steps:
67
+ - name: Download all the dists
68
+ uses: actions/download-artifact@v4
69
+ with:
70
+ name: python-package-distributions
71
+ path: dist/
72
+ - name: Sign the dists with Sigstore
73
+ uses: sigstore/gh-action-sigstore-python@v3.0.0
74
+ with:
75
+ inputs: >-
76
+ ./dist/*.tar.gz
77
+ ./dist/*.whl
78
+ - name: Create GitHub Release
79
+ env:
80
+ GITHUB_TOKEN: ${{ github.token }}
81
+ run: >-
82
+ gh release create
83
+ "$GITHUB_REF_NAME"
84
+ --repo "$GITHUB_REPOSITORY"
85
+ --notes ""
86
+ - name: Upload artifact and their signatures to GitHub Release
87
+ env:
88
+ GITHUB_TOKEN: ${{ github.token }}
89
+ # Upload to GitHub Release using the `gh` CLI.
90
+ # `dist/` contains the built packages, and the
91
+ # sigstore-produced signatures and certificates.
92
+ run: >-
93
+ gh release upload
94
+ "$GITHUB_REF_NAME" dist/**
95
+ --repo "$GITHUB_REPOSITORY"
@@ -0,0 +1,3 @@
1
+ **/.DS_Store
2
+ __pycache__
3
+ .pytest_cache
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Giacomo Pope
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,439 @@
1
+ Metadata-Version: 2.4
2
+ Name: dilithium-py
3
+ Version: 1.0.0
4
+ Summary: A pure python implementation of ML-DSA (FIPS 204)
5
+ Project-URL: Homepage, https://github.com/GiacomoPope/dilithium-py
6
+ Project-URL: Issues, https://github.com/GiacomoPope/dilithium-py/issues
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Security :: Cryptography
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+
14
+ [![License MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://github.com/GiacomoPope/dilithium-py/blob/main/LICENSE)
15
+ [![GitHub CI](https://github.com/GiacomoPope/dilithium-py/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/GiacomoPope/dilithium-py/actions/workflows/ci.yml)
16
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
17
+
18
+ # CRYSTALS-Dilithium Python Implementation
19
+
20
+ > [!CAUTION]
21
+ > :warning: **Under no circumstances should this be used for cryptographic
22
+ applications.** :warning:
23
+ >
24
+ > This is an educational resource and has not been designed to be secure
25
+ > against any form of side-channel attack. The intended use of this project
26
+ > is for learning and experimenting with ML-DSA and Dilithium
27
+
28
+ This repository contains a pure python implementation of both:
29
+
30
+ 1. **ML-DSA** the NIST Module-Lattice-Based Digital Signature Standard following
31
+ the [FIPS 204](https://csrc.nist.gov/pubs/fips/204/final) based off the Dilithium
32
+ submission to the NIST post-quantum project.
33
+ 1. **CRYSTALS-Dilithium**: following (at the time of writing) the most recent
34
+ [specification](https://pq-crystals.org/dilithium/data/dilithium-specification-round3-20210208.pdf) (v3.1)
35
+
36
+ **Note**: This project has followed
37
+ [`kyber-py`](https://github.com/GiacomoPope/kyber-py) which is a pure-python
38
+ implementation of CRYSTALS-Kyber and ML-KEM and reuses a lot of code.
39
+
40
+ ## Disclaimer
41
+
42
+ I have written `dilithium-py` as a way to learn about the way protocol works,
43
+ and to try and create a clean, well commented implementation which people can
44
+ learn from.
45
+
46
+ This code is not constant time, or written to be performant. Rather, it was
47
+ written so that reading though the pseudocode of the
48
+ [specification](https://pq-crystals.org/dilithium/data/dilithium-specification-round3-20210208.pdf)
49
+ closely matches the code which we use within `dilithium.py` and supporting files.
50
+
51
+ ## History of this Repository
52
+
53
+ This work started by simply implementing Dilithium for fun, however after NIST
54
+ picked Dilithium to standardise as ML-DSA, the repository grew and now includes
55
+ both implementations of Dilithium and ML-DSA. I assume as this repository ages,
56
+ the Dilithium implementation will get less useful and the ML-DSA one will be the
57
+ focus, but for historical reasons we will include both. If only so that people
58
+ can study the differences which NIST introduced during the standardisation of
59
+ the protocol.
60
+
61
+ ### KATs
62
+
63
+ This implementation passes all the KAT vectors for `dilithium` and `ml_dsa`. For more information see the unit tests in [`test_ml_dsa.py`](tests/test_ml_dsa.py) and [`test_dilithium.py`](tests/test_dilithium.py)
64
+
65
+ The KAT files were either downloaded or generated:
66
+
67
+ 1. For **ML-DSA**, the KAT files were download from the GitHub repository
68
+ [usnistgov/ACVP-Server/](https://github.com/usnistgov/ACVP-Server/releases/tag/v1.1.0.35) release 1.1.0.35, and are included in `assets/ML-DSA-*` directories.
69
+ 2. For **Dilithium**, the KAT files were generated from the projects [GitHub
70
+ repository](https://github.com/pq-crystals/dilithium/) and are included in
71
+ `assets/PQCsignKAT_*.rsp`
72
+
73
+ ### Generating KAT files for Dilithium
74
+
75
+ This implementation is based off the most recent specification (v3.1).
76
+ There were
77
+ [breaking changes](https://github.com/pq-crystals/dilithium/commit/e989e691ae3d3f5933d012ab074bdc413ebc6fad)
78
+ to the KAT files submitted to NIST when Dilithium was updated to 3.1, so the
79
+ NIST KAT files will not match our code.
80
+
81
+ To deal with this, we generated our own KAT files from the
82
+ [reference implementation](https://github.com/pq-crystals/dilithium/releases/tag/v3.1)
83
+ for version 3.1. These are the files inside [assets](assets/).
84
+
85
+ ### Dependencies
86
+
87
+ Originally, as with `kyber-py`, this project was planned to have zero
88
+ dependencies, however like `kyber-py`, to pass the KATs, I need a
89
+ deterministic CSRNG. The reference implementation uses
90
+ AES256 CTR DRBG. I have implemented this in [`ase256_ctr_drbg.py`](src/dilithium_py/drbg/ase256_ctr_drbg.py).
91
+ However, I have not implemented AES itself, instead I import this from `pycryptodome`.
92
+
93
+ To install dependencies, run `pip install -r requirements.txt`.
94
+
95
+ If you're happy to use system randomness (`os.urandom`) then you don't need
96
+ this dependency.
97
+
98
+ #### `xoflib`
99
+
100
+ There is an additional optional dependency of
101
+ [`xoflib`](https://github.com/GiacomoPope/xoflib) which is a python package with
102
+ bindings to many Rust implementations of eXtendable-Output Functions (XOFx). The
103
+ creation of this package was inspired by this repository as Dilithium needs a streaming API from the shake XOFs which `hashlib` doesn't support.
104
+
105
+ `xoflib` can be installed by running `pip install xoflib` or by installing from requirements as above.
106
+
107
+ If you do not wish to install this dependency, then we include a small
108
+ [`shake_wrapper`](src/dilithium_py/shake/shake_wrapper.py) to mimic `xoflib` but
109
+ with a much higher memory consumption due to the limitations of `hashlib`.
110
+
111
+ ## Using dilithium-py
112
+
113
+ ### ML DSA
114
+
115
+ There are three functions exposed on the `ML_DSA` class which are intended
116
+ for use:
117
+
118
+ - `ML_DSA.keygen()`: generate a bit-packed keypair `(pk, sk)`
119
+ - `ML_DSA.sign(sk, msg)`: generate a bit-packed signature `sig`
120
+ from the message `msg` and bit-packed secret key `sk`.
121
+ - `ML_DSA.verify(pk, msg, sig)`: verify that the bit-packed `sig` is
122
+ valid for a given message `msg` and bit-packed public key `pk`.
123
+
124
+ To use `ML_DSA()`, it must be initialised with a dictionary of the
125
+ protocol parameters. An example can be seen in `DEFAULT_PARAMETERS` in
126
+ the file [`ml_dsa.py`](src/dilithium_py/ml_dsa/default_parameters.py)
127
+
128
+ Additionally, the class has been initialised with these default parameters,
129
+ so you can simply import the NIST level you want to play with:
130
+
131
+ #### Example
132
+
133
+ ```python
134
+ >>> from dilithium_py.ml_dsa import ML_DSA_44
135
+ >>>
136
+ >>> # Example of signing
137
+ >>> pk, sk = ML_DSA_44.keygen()
138
+ >>> msg = b"Your message signed by ML_DSA"
139
+ >>> sig = ML_DSA_44.sign(sk, msg)
140
+ >>> assert ML_DSA_44.verify(pk, msg, sig)
141
+ >>>
142
+ >>> # Verification will fail with the wrong msg or pk
143
+ >>> assert not ML_DSA_44.verify(pk, b"", sig)
144
+ >>> pk_new, sk_new = ML_DSA_44.keygen()
145
+ >>> assert not ML_DSA_44.verify(pk_new, msg, sig)
146
+ ```
147
+
148
+ The above example would also work with the other NIST levels
149
+ `ML_DSA_65` and `ML_DSA_87`.
150
+
151
+ ### Benchmarks
152
+
153
+ Some very rough benchmarks to give an idea about performance:
154
+
155
+ | 1000 Iterations | `ML_DSA_44` | `ML_DSA_65` | `ML_DSA_87` |
156
+ |--------------------------|--------------|--------------|--------------|
157
+ | `KeyGen()` Median Time | 6 ms | 10 ms | 14 ms |
158
+ | `Sign()` Median Time | 29 ms | 49 ms | 59 ms |
159
+ | `Sign()` Average Time | 36 ms | 62 ms | 75 ms |
160
+ | `Verify()` Median Time | 8 ms | 11 ms | 17 ms |
161
+
162
+ All times recorded using a Intel Core i7-9750H CPU averaged over 1000 calls.
163
+
164
+ ### Dilithium
165
+
166
+ There are three functions exposed on the `Dilithium` class which are intended
167
+ for use:
168
+
169
+ - `Dilithium.keygen()`: generate a bit-packed keypair `(pk, sk)`
170
+ - `Dilithium.sign(sk, msg)`: generate a bit-packed signature `sig`
171
+ from the message `msg` and bit-packed secret key `sk`.
172
+ - `Dilithium.verify(pk, msg, sig)`: verify that the bit-packed `sig` is
173
+ valid for a given message `msg` and bit-packed public key `pk`.
174
+
175
+ To use `Dilithium()`, it must be initialised with a dictionary of the
176
+ protocol parameters. An example can be seen in `DEFAULT_PARAMETERS` in
177
+ the file [`dilithium.py`](src/dilithium_py/dilithium/default_parameters.py)
178
+
179
+ Additionally, the class has been initialised with these default parameters,
180
+ so you can simply import the NIST level you want to play with:
181
+
182
+ #### Example
183
+
184
+ ```python
185
+ >>> from dilithium_py.dilithium import Dilithium2
186
+ >>>
187
+ >>> # Example of signing
188
+ >>> pk, sk = Dilithium2.keygen()
189
+ >>> msg = b"Your message signed by Dilithium"
190
+ >>> sig = Dilithium2.sign(sk, msg)
191
+ >>> assert Dilithium2.verify(pk, msg, sig)
192
+ >>>
193
+ >>> # Verification will fail with the wrong msg or pk
194
+ >>> assert not Dilithium2.verify(pk, b"", sig)
195
+ >>> pk_new, sk_new = Dilithium2.keygen()
196
+ >>> assert not Dilithium2.verify(pk_new, msg, sig)
197
+ ```
198
+
199
+ The above example would also work with the other NIST levels
200
+ `Dilithium3` and `Dilithium5`.
201
+
202
+ ### Benchmarks
203
+
204
+ Some very rough benchmarks to give an idea about performance:
205
+
206
+ | 1000 Iterations | `Dilithium2` | `Dilithium3` | `Dilithium5` |
207
+ |--------------------------|---------------|--------------|--------------|
208
+ | `KeyGen()` Median Time | 6 ms | 9 ms | 15 ms |
209
+ | `Sign()` Median Time | 27 ms | 46 ms | 58 ms |
210
+ | `Sign()` Average Time | 35 ms | 58 ms | 72 ms |
211
+ | `Verify()` Median Time | 7 ms | 11 ms | 18 ms |
212
+
213
+ All times recorded using a Intel Core i7-9750H CPU averaged over 1000 calls.
214
+
215
+ ## Discussion of Implementation
216
+
217
+ ### Optimising decomposition and making hints
218
+
219
+ You may notice that ML DSA has marginally slower signing than the reported
220
+ Dilithium times included above. This is because the ML DSA implementation
221
+ follows the NIST spec while the Dilithium implementation includes an
222
+ optimisation of decomposition and hint generation. Details of this are given in
223
+ Section 5.1 of the [Dilithium
224
+ specification](https://pq-crystals.org/dilithium/data/dilithium-specification-round3-20210208.pdf).
225
+ We discuss it informally below and would like to thank Keegan Ryan for helping
226
+ understand the differences between the two hint generation functions.
227
+
228
+ When it comes to implementing the optimisation, not only are slightly different
229
+ vectors used during the computation, but the generation of the hint $\mathbf{h}$
230
+ coefficients themselves is subtly different.
231
+
232
+ For the NIST specification the hint is generated by considering the vectors
233
+ $-c\mathbf{t}_0$ and $\mathbf{w} -c\mathbf{s}_1 + -c\mathbf{t}_0$ and each
234
+ coefficient of each polynomial within $\mathbf{h}$ is computed by checking if
235
+ the top bits will change when the coefficients `r` and `r + z` are added
236
+ together. This is computed using algorithm 39 from FIPS 204:
237
+
238
+ ```py
239
+ def make_hint(z, r, a, q):
240
+ """
241
+ Check whether the top bit of z will change when r is added
242
+ """
243
+ r1 = high_bits(r, a, q)
244
+ v1 = high_bits(r + z, a, q)
245
+ return int(r1 != v1)
246
+ ```
247
+
248
+ This function is used pairwise for every coefficient of every polynomial in the
249
+ two vectors: $-c\mathbf{t}_0$ and $\mathbf{w} -c\mathbf{s}_1 + -c\mathbf{t}_0$.
250
+
251
+ For the Dilithium optimisation, rather than computing only the high bits of
252
+ $\mathbf{w}$ as $\mathbf{w}_1$, for the same cost, one can compute both the high
253
+ and low bits denoted $\mathbf{w}_1$ and $\mathbf{w}_0$. Then, the hint can be
254
+ constructed from $\mathbf{w}_0$ (and a further call to low bits for
255
+ $\mathbf{r}_0$ in line 21 of Algorithm 7 of FIPS 204 can be avoided). Precisely, the hint is generated from the two vectors $\mathbf{w}_0 -c\mathbf{s}_1 + -c\mathbf{t}_0$ and $\mathbf{w}_1$.
256
+
257
+ As the inputs to the hint generation are now used from the decomposition, where
258
+ top bits have already been removed, the `make_hint()` function has to check
259
+ whether high bits are set in the result of `low_bits(r) + z`, which is computed
260
+ using the following function:
261
+
262
+ ```py
263
+ def make_hint_optimised(z, r, a, q):
264
+ """
265
+ Optimised version of the above used when the low bits w0 are extracted from
266
+ `w = (A_hat @ y_hat).from_ntt()` during signing
267
+ """
268
+ gamma2 = a >> 1
269
+ if z <= gamma2 or z > (q - gamma2) or (z == (q - gamma2) and r == 0):
270
+ return 0
271
+ return 1
272
+ ```
273
+
274
+ In particular, when `z = q-1`, `make_hint()` will return `1`, while the `make_hint_optimised()` returns `0`.
275
+
276
+ As this optimisation is present in most implementations, this has caused a
277
+ confusion about whether `make_hint()` is correct as the output is different to
278
+ `make_hint_optimised()`.
279
+
280
+ It's important to realise the output of these make hints functions is different
281
+ with the same input vectors, but the hint vector will be identical in the cases that:
282
+
283
+ - `make_hint()` is used with $-c\mathbf{t}_0$ and $\mathbf{w} -c\mathbf{s}_1 + -c\mathbf{t}_0$
284
+ - `make_hint_optimised()` is used with $\mathbf{w}_0 -c\mathbf{s}_1 + -c\mathbf{t}_0$ and $\mathbf{w}_1$
285
+
286
+ This means that for an implementation one can pick either the NIST documented
287
+ algorithm or the optimisation in 5.1 of Dilithium and the resulting signatures
288
+ will be identical (indeed, you can use either method and have all KAT vectors
289
+ pass.)
290
+
291
+ ### Polynomials
292
+
293
+ The file [`polynomials.py`](src/dilithium_py/polynomials/polynomials_generic.py) contains the classes
294
+ `PolynomialRing` and
295
+ `Polynomial`. This implements the univariate polynomial ring
296
+
297
+ $$
298
+ R_q = \mathbb{F}_q[X] /(X^n + 1)
299
+ $$
300
+
301
+ The implementation is inspired by `SageMath` and you can create the
302
+ ring $R_{11} = \mathbb{F}_{11}[X] /(X^8 + 1)$ in the following way:
303
+
304
+ #### Example
305
+
306
+ ```python
307
+ >>> R = PolynomialRing(11, 8)
308
+ >>> x = R.gen()
309
+ >>> f = 3*x**3 + 4*x**7
310
+ >>> g = R.random_element(); g
311
+ 5 + x^2 + 5*x^3 + 4*x^4 + x^5 + 3*x^6 + 8*x^7
312
+ >>> f*g
313
+ 8 + 9*x + 10*x^3 + 7*x^4 + 2*x^5 + 5*x^6 + 10*x^7
314
+ >>> f + f
315
+ 6*x^3 + 8*x^7
316
+ >>> g - g
317
+ 0
318
+ ```
319
+
320
+ ### Modules
321
+
322
+ The file [`modules.py`](src/dilithium_py/modules/modules_generic.py) contains the classes `Module` and `Matrix`.
323
+ A module is a generalisation of a vector space, where the field
324
+ of scalars is replaced with a ring. In the case of Dilithium, we
325
+ need the module with the ring $R_q$ as described above.
326
+
327
+ `Matrix` allows elements of the module to be of size $m \times n$
328
+ For Dilithium, we need vectors of length $k$ and $l$ and a matrix
329
+ of size $l \times k$.
330
+
331
+ As an example of the operations we can perform with out `Module`
332
+ lets revisit the ring from the previous example:
333
+
334
+ #### Example
335
+
336
+ ```python
337
+ >>> R = PolynomialRing(11, 8)
338
+ >>> x = R.gen()
339
+ >>>
340
+ >>> M = Module(R)
341
+ >>> # We create a matrix by feeding the coefficients to M
342
+ >>> A = M([[x + 3*x**2, 4 + 3*x**7], [3*x**3 + 9*x**7, x**4]])
343
+ >>> A
344
+ [ x + 3*x^2, 4 + 3*x^7]
345
+ [3*x^3 + 9*x^7, x^4]
346
+ >>> # We can add and subtract matricies of the same size
347
+ >>> A + A
348
+ [ 2*x + 6*x^2, 8 + 6*x^7]
349
+ [6*x^3 + 7*x^7, 2*x^4]
350
+ >>> A - A
351
+ [0, 0]
352
+ [0, 0]
353
+ >>> # A vector can be constructed by a list of coefficents
354
+ >>> v = M([3*x**5, x])
355
+ >>> v
356
+ [3*x^5, x]
357
+ >>> # We can compute the transpose
358
+ >>> v.transpose()
359
+ [3*x^5]
360
+ [ x]
361
+ >>> v + v
362
+ [6*x^5, 2*x]
363
+ >>> # We can also compute the transpose in place
364
+ >>> v.transpose_self()
365
+ [3*x^5]
366
+ [ x]
367
+ >>> v + v
368
+ [6*x^5]
369
+ [ 2*x]
370
+ >>> # Matrix multiplication follows python standards and is denoted by @
371
+ >>> A @ v
372
+ [8 + 4*x + 3*x^6 + 9*x^7]
373
+ [ 2 + 6*x^4 + x^5]
374
+ ```
375
+
376
+ ### Number Theoretic Transform
377
+
378
+ We can transform polynomials to NTT form and from NTT form
379
+ with `poly.to_ntt()` and `poly.from_ntt()`.
380
+
381
+ When we perform operations between polynomials, `(+, -, *)`
382
+ either both or neither must be in NTT form.
383
+
384
+ ```py
385
+ >>> f = R.random_element()
386
+ >>> f == f.to_ntt().from_ntt()
387
+ True
388
+ >>> g = R.random_element()
389
+ >>> h = f*g
390
+ >>> h == (f.to_ntt() * g.to_ntt()).from_ntt()
391
+ True
392
+ ```
393
+
394
+ While writing this README, performing multiplication of of polynomials
395
+ in NTT form is about 100x faster when working with the ring used by
396
+ Dilithium.
397
+
398
+ ```py
399
+ >>> # Lets work in the ring we use for Dilithium
400
+ >>> R = Dilithium2.R
401
+ >>> # Generate some random elements
402
+ >>> f = R.random_element()
403
+ >>> g = R.random_element()
404
+ >>> # Takes about 10 seconds to perform 1000 multiplications
405
+ >>> timeit.timeit("f*g", globals=globals(), number=1000)
406
+ 9.621509193995735
407
+ >>> # Now lets convert to NTT and try again
408
+ >>> f.to_ntt()
409
+ >>> g.to_ntt()
410
+ >>> # Now it only takes ~0.1s to perform 1000 multiplications!
411
+ >>> timeit.timeit("f*g", globals=globals(), number=1000)
412
+ 0.12979038299818058
413
+ ```
414
+
415
+ These functions extend to modules
416
+
417
+ ```py
418
+ >>> M = Dilithium2.M
419
+ >>> R = Dilithium2.R
420
+ >>> v = M([R.random_element(), R.random_element()])
421
+ >>> u = M([R.random_element(), R.random_element()]).transpose()
422
+ >>> A = u @ v
423
+ >>> A == (u.to_ntt() @ v.to_ntt()).from_ntt()
424
+ True
425
+ ```
426
+
427
+ As operations on the module are just operations between elements,
428
+ we expect a similar 100x speed up when working in NTT form:
429
+
430
+ ```py
431
+ >>> u = M([R.random_element(), R.random_element()]).transpose()
432
+ >>> v = M([R.random_element(), R.random_element()])
433
+ >>> timeit.timeit("u@v", globals=globals(), number=1000)
434
+ 38.39359304799291
435
+ >>> u = u.to_ntt()
436
+ >>> v = v.to_ntt()
437
+ >>> timeit.timeit("u@v", globals=globals(), number=1000)
438
+ 0.495470915993792
439
+ ```