Skip to content

Commit 22f0d83

Browse files
authored
Merge pull request #9 from GiacomoPope/external_mu
add support for external mu following lamps ietf draft
2 parents c54fee7 + f5cbf53 commit 22f0d83

File tree

5 files changed

+116
-12
lines changed

5 files changed

+116
-12
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,27 @@ All times recorded using a Intel Core i7-9750H CPU averaged over 1000 calls.
210210

211211
## Discussion of Implementation
212212

213+
### External Mu
214+
215+
Within FIPS 204, there is the option when signing for the value $\mu = H(H(\textsf{pk}) || M')$ to be computed outside of the main signing algorithm and instead be passed into the signature as explicit input. Notice that $\mu$ is formed from only public data, and allows signing a fixed sized (hashed) message (64 bytes) rather than an arbitrary sized message $M'$.
216+
217+
An API which signs given $\mu$ rather than a message $m$ is known as "external mu ML-DSA" and is a popular choice over Hash-ML-DSA due to the fact that both "pure" and external mu ML-DSA can be verified with the same method, where as HASH-ML-DSA necessarily requires a separate verification function leading to complications.
218+
219+
Following Appendix D of the [lamps dilithium signature draft](https://datatracker.ietf.org/doc/html/draft-ietf-lamps-dilithium-certificates-07) we additionally offer the external mu API by exposing two additional methods.
220+
221+
```py
222+
>>> from dilithium_py.ml_dsa import ML_DSA_44
223+
>>>
224+
>>> # Example of signing with external mu
225+
>>> pk, sk = ML_DSA_44.keygen()
226+
>>> msg = b"Your message signed by ML_DSA"
227+
>>> mu = ML_DSA_44.prehash_external_mu(pk, msg)
228+
>>> sig = ML_DSA_44.sign_external_mu(sk, mu)
229+
>>> assert ML_DSA_44.verify(pk, msg, sig)
230+
```
231+
232+
The method `prehash_external_mu(pk, m)` takes as input the public data and computes the prehash `mu`. This is then passed to a new signing API which anticipates $\mu$ instead of the message itself. To verify this signature, we can use the regular method for verification.
233+
213234
### Optimising decomposition and making hints
214235

215236
You may notice that ML DSA has marginally slower signing than the reported

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "dilithium-py"
7-
version = "1.0.2"
7+
version = "1.1.0"
88
requires-python = ">= 3.9"
99
description = "A pure python implementation of ML-DSA (FIPS 204)"
1010
readme = "README.md"

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setup(
44
name="dilithium-py",
5-
version="1.0.2",
5+
version="1.1.0",
66
python_requires=">=3.9",
77
description="A pure python implementation of ML-DSA (FIPS 204)",
88
long_description=open("README.md").read(),

src/dilithium_py/ml_dsa/ml_dsa.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,10 +215,13 @@ def _keygen_internal(self, zeta):
215215

216216
return pk, sk
217217

218-
def _sign_internal(self, sk_bytes, m, rnd):
218+
def _sign_internal(self, sk_bytes, m, rnd, external_mu=False):
219219
"""
220220
Deterministic algorithm to generate a signature for a formatted message
221221
M' following Algorithm 7 (FIPS 204)
222+
223+
When `external_mu` is `True`, the message `m` is interpreted instead as
224+
the pre-hashed message `mu = prehash_external_mu()`
222225
"""
223226
# unpack the secret key
224227
rho, K, tr, s1, s2, t0 = self._unpack_sk(sk_bytes)
@@ -232,7 +235,10 @@ def _sign_internal(self, sk_bytes, m, rnd):
232235
A_hat = self._expand_matrix_from_seed(rho)
233236

234237
# Set seeds and nonce (kappa)
235-
mu = self._h(tr + m, 64)
238+
if external_mu:
239+
mu = m
240+
else:
241+
mu = self._h(tr + m, 64)
236242
rho_prime = self._h(K + rnd + mu, 64)
237243

238244
kappa = 0
@@ -379,3 +385,52 @@ def verify(self, pk_bytes, m, sig_bytes, ctx=b""):
379385
m_prime = bytes([0]) + bytes([len(ctx)]) + ctx + m
380386

381387
return self._verify_internal(pk_bytes, m_prime, sig_bytes)
388+
389+
"""
390+
The following external mu functions are not in FIPS 204, but are in
391+
Appendix D of the following IETF draft and are included for experimentation
392+
for researchers and engineers
393+
394+
https://datatracker.ietf.org/doc/html/draft-ietf-lamps-dilithium-certificates-07
395+
"""
396+
397+
def prehash_external_mu(self, pk_bytes, m, ctx=b""):
398+
"""
399+
Prehash the message `m` with context `ctx` together with
400+
the public key. For use with `sign_external_mu()`
401+
"""
402+
# Ensure the length of the context is as expected
403+
if len(ctx) > 255:
404+
raise ValueError(
405+
f"ctx bytes must have length at most 255, ctx has length {len(ctx) = }"
406+
)
407+
408+
# Format the message using the context
409+
m_prime = bytes([0]) + bytes([len(ctx)]) + ctx + m
410+
411+
# Compute mu by hashing the public key into the message
412+
tr = self._h(pk_bytes, 64)
413+
mu = self._h(tr + m_prime, 64)
414+
415+
return mu
416+
417+
def sign_external_mu(self, sk_bytes, mu, deterministic=False):
418+
"""
419+
Generates an ML-DSA signature of a message given the prehash
420+
mu = H(H(pk), M')
421+
"""
422+
# Ensure the length of the context is as expected
423+
if len(mu) != 64:
424+
raise ValueError(
425+
f"mu bytes must have length 64, mu has length {len(mu) = }"
426+
)
427+
428+
if deterministic:
429+
rnd = bytes([0] * 32)
430+
else:
431+
rnd = self.random_bytes(32)
432+
433+
# Compute the signature given external mu, we set the external_mu
434+
# to True
435+
sig_bytes = self._sign_internal(sk_bytes, mu, rnd, external_mu=True)
436+
return sig_bytes

tests/test_ml_dsa.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,39 @@ class TestMLDSA(unittest.TestCase):
1313
def generic_test_ml_dsa(self, ML_DSA, count=5):
1414
for _ in range(count):
1515
msg = b"Signed by ML_DSA" + os.urandom(16)
16+
ctx = os.urandom(128)
1617

1718
# Perform signature process
1819
pk, sk = ML_DSA.keygen()
19-
sig = ML_DSA.sign(sk, msg)
20-
check_verify = ML_DSA.verify(pk, msg, sig)
20+
sig = ML_DSA.sign(sk, msg, ctx=ctx)
21+
check_verify = ML_DSA.verify(pk, msg, sig, ctx=ctx)
22+
23+
# Sign with external_mu instead
24+
external_mu = ML_DSA.prehash_external_mu(pk, msg, ctx=ctx)
25+
sig_external_mu = ML_DSA.sign_external_mu(sk, external_mu)
26+
check_external_mu = ML_DSA.verify(pk, msg, sig_external_mu, ctx=ctx)
2127

2228
# Generate some fail cases
2329
pk_bad, _ = ML_DSA.keygen()
24-
check_wrong_pk = ML_DSA.verify(pk_bad, msg, sig)
25-
check_wrong_msg = ML_DSA.verify(pk, b"", sig)
30+
check_wrong_pk = ML_DSA.verify(pk_bad, msg, sig, ctx=ctx)
31+
check_wrong_msg = ML_DSA.verify(pk, b"", sig, ctx=ctx)
32+
check_no_ctx = ML_DSA.verify(pk, msg, sig)
2633

2734
# Check that signature works
2835
self.assertTrue(check_verify)
2936

37+
# Check that external_mu also works
38+
self.assertTrue(check_external_mu)
39+
3040
# Check changing the key breaks verify
3141
self.assertFalse(check_wrong_pk)
3242

3343
# Check changing the message breaks verify
3444
self.assertFalse(check_wrong_msg)
3545

46+
# Check removing the context breaks verify
47+
self.assertFalse(check_no_ctx)
48+
3649
def test_ml_dsa_44(self):
3750
self.generic_test_ml_dsa(ML_DSA_44)
3851

@@ -52,26 +65,41 @@ class TestMLDSADeterministic(unittest.TestCase):
5265
def generic_test_ml_dsa(self, ML_DSA, count=5):
5366
for _ in range(count):
5467
msg = b"Signed by ML_DSA" + os.urandom(16)
68+
ctx = os.urandom(128)
5569

5670
# Perform signature process
5771
pk, sk = ML_DSA.keygen()
58-
sig = ML_DSA.sign(sk, msg, deterministic=True)
59-
check_verify = ML_DSA.verify(pk, msg, sig)
72+
sig = ML_DSA.sign(sk, msg, ctx=ctx, deterministic=True)
73+
check_verify = ML_DSA.verify(pk, msg, sig, ctx=ctx)
74+
75+
# Sign with external_mu instead
76+
external_mu = ML_DSA.prehash_external_mu(pk, msg, ctx=ctx)
77+
sig_external_mu = ML_DSA.sign_external_mu(
78+
sk, external_mu, deterministic=True
79+
)
80+
check_external_mu = ML_DSA.verify(pk, msg, sig_external_mu, ctx=ctx)
6081

6182
# Generate some fail cases
6283
pk_bad, _ = ML_DSA.keygen()
63-
check_wrong_pk = ML_DSA.verify(pk_bad, msg, sig)
64-
check_wrong_msg = ML_DSA.verify(pk, b"", sig)
84+
check_wrong_pk = ML_DSA.verify(pk_bad, msg, sig, ctx=ctx)
85+
check_wrong_msg = ML_DSA.verify(pk, b"", sig, ctx=ctx)
86+
check_no_ctx = ML_DSA.verify(pk, msg, sig)
6587

6688
# Check that signature works
6789
self.assertTrue(check_verify)
6890

91+
# Check that external_mu also works
92+
self.assertTrue(check_external_mu)
93+
6994
# Check changing the key breaks verify
7095
self.assertFalse(check_wrong_pk)
7196

7297
# Check changing the message breaks verify
7398
self.assertFalse(check_wrong_msg)
7499

100+
# Check removing the context breaks verify
101+
self.assertFalse(check_no_ctx)
102+
75103
def test_ml_dsa_44(self):
76104
self.generic_test_ml_dsa(ML_DSA_44)
77105

0 commit comments

Comments
 (0)