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.
- dilithium_py-1.0.0/.github/workflows/ci.yml +88 -0
- dilithium_py-1.0.0/.github/workflows/release-pypi.yml +95 -0
- dilithium_py-1.0.0/.gitignore +3 -0
- dilithium_py-1.0.0/LICENSE +21 -0
- dilithium_py-1.0.0/PKG-INFO +439 -0
- dilithium_py-1.0.0/README.md +426 -0
- dilithium_py-1.0.0/assets/ML-DSA-keyGen-FIPS204/expectedResults.json +399 -0
- dilithium_py-1.0.0/assets/ML-DSA-keyGen-FIPS204/internalProjection.json +555 -0
- dilithium_py-1.0.0/assets/ML-DSA-keyGen-FIPS204/prompt.json +330 -0
- dilithium_py-1.0.0/assets/ML-DSA-keyGen-FIPS204/registration.json +12 -0
- dilithium_py-1.0.0/assets/ML-DSA-keyGen-FIPS204/validation.json +306 -0
- dilithium_py-1.0.0/assets/ML-DSA-sigGen-FIPS204/expectedResults.json +279 -0
- dilithium_py-1.0.0/assets/ML-DSA-sigGen-FIPS204/internalProjection.json +507 -0
- dilithium_py-1.0.0/assets/ML-DSA-sigGen-FIPS204/prompt.json +387 -0
- dilithium_py-1.0.0/assets/ML-DSA-sigGen-FIPS204/registration.json +23 -0
- dilithium_py-1.0.0/assets/ML-DSA-sigGen-FIPS204/validation.json +246 -0
- dilithium_py-1.0.0/assets/ML-DSA-sigVer-FIPS204/expectedResults.json +204 -0
- dilithium_py-1.0.0/assets/ML-DSA-sigVer-FIPS204/internalProjection.json +396 -0
- dilithium_py-1.0.0/assets/ML-DSA-sigVer-FIPS204/prompt.json +258 -0
- dilithium_py-1.0.0/assets/ML-DSA-sigVer-FIPS204/registration.json +12 -0
- dilithium_py-1.0.0/assets/ML-DSA-sigVer-FIPS204/validation.json +186 -0
- dilithium_py-1.0.0/assets/PQCsignKAT_Dilithium2.rsp +902 -0
- dilithium_py-1.0.0/assets/PQCsignKAT_Dilithium3.rsp +902 -0
- dilithium_py-1.0.0/assets/PQCsignKAT_Dilithium5.rsp +902 -0
- dilithium_py-1.0.0/benchmarks/benchmark_bit_packing.py +85 -0
- dilithium_py-1.0.0/benchmarks/benchmark_dilithium.py +75 -0
- dilithium_py-1.0.0/benchmarks/benchmark_ml_dsa.py +77 -0
- dilithium_py-1.0.0/pyproject.toml +23 -0
- dilithium_py-1.0.0/pytest.ini +2 -0
- dilithium_py-1.0.0/requirements.txt +2 -0
- dilithium_py-1.0.0/src/dilithium_py/__init__.py +0 -0
- dilithium_py-1.0.0/src/dilithium_py/dilithium/__init__.py +3 -0
- dilithium_py-1.0.0/src/dilithium_py/dilithium/default_parameters.py +38 -0
- dilithium_py-1.0.0/src/dilithium_py/dilithium/dilithium.py +312 -0
- dilithium_py-1.0.0/src/dilithium_py/drbg/__init__.py +0 -0
- dilithium_py-1.0.0/src/dilithium_py/drbg/aes256_ctr_drbg.py +134 -0
- dilithium_py-1.0.0/src/dilithium_py/ml_dsa/__init__.py +3 -0
- dilithium_py-1.0.0/src/dilithium_py/ml_dsa/default_parameters.py +41 -0
- dilithium_py-1.0.0/src/dilithium_py/ml_dsa/ml_dsa.py +364 -0
- dilithium_py-1.0.0/src/dilithium_py/modules/__init__.py +0 -0
- dilithium_py-1.0.0/src/dilithium_py/modules/modules.py +204 -0
- dilithium_py-1.0.0/src/dilithium_py/modules/modules_generic.py +230 -0
- dilithium_py-1.0.0/src/dilithium_py/polynomials/__init__.py +0 -0
- dilithium_py-1.0.0/src/dilithium_py/polynomials/polynomials.py +453 -0
- dilithium_py-1.0.0/src/dilithium_py/polynomials/polynomials_generic.py +237 -0
- dilithium_py-1.0.0/src/dilithium_py/shake/shake_wrapper.py +61 -0
- dilithium_py-1.0.0/src/dilithium_py/utilities/__init__.py +0 -0
- dilithium_py-1.0.0/src/dilithium_py/utilities/utils.py +105 -0
- dilithium_py-1.0.0/tests/__init__.py +0 -0
- dilithium_py-1.0.0/tests/test_dilithium.py +182 -0
- dilithium_py-1.0.0/tests/test_drbg.py +50 -0
- dilithium_py-1.0.0/tests/test_ml_dsa.py +165 -0
- dilithium_py-1.0.0/tests/test_module_generic.py +180 -0
- dilithium_py-1.0.0/tests/test_polynomial_generic.py +156 -0
- dilithium_py-1.0.0/tests/test_shake.py +44 -0
- 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,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
|
+
[](https://github.com/GiacomoPope/dilithium-py/blob/main/LICENSE)
|
|
15
|
+
[](https://github.com/GiacomoPope/dilithium-py/actions/workflows/ci.yml)
|
|
16
|
+
[](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
|
+
```
|