Skip to content

Commit

Permalink
add FIPS 205 stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
mjosaarinen committed Aug 20, 2024
1 parent 110b296 commit 9eddd2e
Show file tree
Hide file tree
Showing 6 changed files with 1,108 additions and 5 deletions.
44 changes: 41 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@

2024-07-01 Markku-Juhani O. Saarinen [email protected]

Updated 2024-08-18 for the release FIPS 203, FIPS 204
Updated 2024-08-20 for the release FIPS 203, FIPS 204, FIPS 205.

```
py-acvp-pqc
├── fips203.py # Python implementation of ML-KEM ("Kyber")
├── fips204.py # Python implementation of ML-DSA ("Dilithium")
├── fips205.py # Python implementation of SLH-DSA ("SPHINCS+")
├── genvals_mlkem.py # Python wrapper for ML-KEM in NIST's C# Gen/Vals
├── genvals_mldsa.py # Python wrapper for ML-DSA in NIST's C# Gen/Vals
├── genvals_slhdsa.py # Python wrapper for SLH-DSA in NIST's C# Gen/Vals
├── test_mlkem.py # Parser/tester for ML-KEM ACVP test vectors
├── test_mldsa.py # Parser/tester for ML-DSA ACVP test vectors
├── test_slhdsa.py # Parser/tester for SLH-DSA ACVP test vectors
├── ACVP-Server # (Symlink to) NIST's ACVP-Server repo for Gen/Vals
├── json-copy # Local copy from ACVP-Server/gen-val/json-files/
├── Makefile # Makefile for cleanups
Expand All @@ -26,10 +29,11 @@ You won't need the NIST C# dependencies to run the local Python implementations

* ML-KEM: [fips203.py](fips203.py) is a self-contained implementation of [FIPS 203 ML-KEM](https://doi.org/10.6028/NIST.FIPS.203) a.k.a. Kyber.
* ML-DSA: [fips204.py](fips204.py) is a self-contained implementation of [FIPS 204 ML-DSA](https://doi.org/10.6028/NIST.FIPS.204) a.k.a. Dilithium.
* SLH-DSA: [fips205.py](fips205.py) is a self-contained implementation of [FIPS 205 SLH-DSA](https://doi.org/10.6028/NIST.FIPS.205) a.k.a. SPHINCS+.
* Test vector json parsers: [test_mlkem.py](test_mlkem.py) and [test_mldsa.py](test_mldsa.py).
* Test vectors: there's a local copy of relevant json test vectors from NIST in [json-copy](json-copy). These can be synced with [https://github.com/usnistgov/ACVP-Server/tree/master/gen-val/json-files](https://github.com/usnistgov/ACVP-Server/tree/master/gen-val/json-files).

The main functions have unit tests:
The main functions have unit tests. For ML-KEM:

```
$ python3 fips203.py
Expand All @@ -40,6 +44,7 @@ ML-KEM (fips203.py) -- Total FAIL= 0
```
_( This indicates success.)_

Running the test for ML_DSA is similar:
```
$ python3 fips204.py
ML-DSA KeyGen (fips204.py): PASS= 75 FAIL= 0
Expand All @@ -50,6 +55,24 @@ ML-DSA (fips204.py) -- Total FAIL= 0

_( If you're curious why 30 test vectors are "skipped," The non-deterministic signature code is indeed non-deterministic and makes an internal call to an RBG. Hence, we're not trying to match those answers. )_

By default the output for SLH-DSA is a bit verbose, as it will take several minutes to run them all:

```
$ python3 fips205.py
SLH-DSA-SHA2-128s KeyGen/1 pass
(.. output truncated ..)
SLH-DSA-SHAKE-256f KeyGen/40 pass
SLH-DSA KeyGen (fips205.py): PASS= 40 FAIL= 0
SLH-DSA-SHA2-192s SigGen/1 pass
(.. output truncated ..)
SLH-DSA-SHAKE-128f SigGen/88 pass
SLH-DSA SigGen (fips205.py): PASS= 88 FAIL= 0 SKIP= 0
SLH-DSA-SHA2-192s SigVer/1 pass
(.. output truncated ..)
SLH-DSA-SHAKE-128f SigVer/45 pass
SLH-DSA SigVer (fips205.py): PASS= 45 FAIL= 0
SLH-DSA (fips205.py) -- Total FAIL= 0
```

# NIST Gen/Vals

Expand Down Expand Up @@ -126,7 +149,7 @@ $ source .venv/bin/activate

Note that you will have to "enter" the enviroment with `source .venv/bin/activate` to use pythonnet installed locally this way.

Anyway, we should now be able to execute our Kyber and Dilithium test programs:
Anyway, assuming that all of the DLLs are in the right places, we should be abole to run our Kyber, Dilithium, and SPHINCS+ tests:
```
(.venv) $ python3 genvals_mlkem.py
ML-KEM KeyGen (NIST Gen/Vals): PASS= 75 FAIL= 0
Expand All @@ -139,6 +162,21 @@ ML-DSA KeyGen (NIST Gen/Vals): PASS= 75 FAIL= 0
ML-DSA SigGen (NIST Gen/Vals): PASS= 30 FAIL= 0 SKIP= 30
ML-DSA SigVer (NIST Gen/Vals): PASS= 45 FAIL= 0
ML-DSA (NIST Gen/Vals) -- Total FAIL= 0
(.venv) $ $ python3 genvals_slhdsa.py
SLH-DSA-SHA2-128s KeyGen/1 pass
(.. output truncated ..)
SLH-DSA-SHAKE-256f KeyGen/40 pass
SLH-DSA KeyGen (NIST Gen/Vals): PASS= 40 FAIL= 0
SLH-DSA-SHA2-192s SigGen/1 pass
(.. output truncated ..)
SLH-DSA-SHAKE-128f SigGen/88 pass
SLH-DSA SigGen (NIST Gen/Vals): PASS= 88 FAIL= 0 SKIP= 0
SLH-DSA-SHA2-192s SigVer/1 pass
(.. output truncated ..)
SLH-DSA-SHAKE-128f SigVer/45 pass
SLH-DSA SigVer (NIST Gen/Vals): PASS= 45 FAIL= 0
SLH-DSA (NIST Gen/Vals) -- Total FAIL= 0
```
This is a success!

96 changes: 95 additions & 1 deletion fips204.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from test_mldsa import test_mldsa

# hash functions
from Crypto.Hash import SHAKE128, SHAKE256, SHA3_256, SHA3_512
from Crypto.Hash import SHAKE128, SHAKE256, SHA3_256, SHA3_512, SHA256, SHA512
ML_DSA_Q = 8380417
ML_DSA_N = 256

Expand Down Expand Up @@ -73,6 +73,100 @@ def __init__(self, param='ML-DSA-65'):
def h(self, s, l):
return SHAKE256.new(s).read(l)

# Algorithm 2, ML-DSA.Sign(sk, M, ctx)
# XXX: Not covered by test vectors.

def sign(self, sk, m, ctx, rnd_in=None, param=None):
if param != None:
self.__init__(param)

if rnd_in == None:
rnd = b'\x00'*32
else:
rnd = rnd_in

mp = ( self.integer_to_bytes(0, 1) +
self.integer_to_bytes(len(ctx), 1) + ctx + m )
sig = self.sign_internal(sk, mp, rnd)
return sig

# Algorithm 3, ML-DSA.Verify(pk, M, sigma, ctx)
# XXX: Not covered by test vectors.

def verify(self, pk, m, sig, ctx, param=None):
if param != None:
self.__init__(param)
if len(ctx) > 255:
return False
mp = ( self.integer_to_bytes(0, 1) +
self.integer_to_bytes(len(ctx), 1) + ctx + m)
return self.verify_internal(pk, mp, sig)

# Algorithm 4, HashML-DSA.Sign(sk, M, ctx, PH)
# XXX: Not covered by test vectors.

def hash_ml_dsa_sign(self, sk, m, ctx, ph, rnd_in=None, param=None):
if param != None:
self.__init__(param)
if len(ctx) > 255:
return None

if rnd_in == None:
rnd = b'\x00'*32
else:
rnd = rnd_in

if ph == 'SHA-256':
oid = bytes([ 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03,
0x04, 0x02, 0x01])
phm = SHA256.new(m).digest()
elif ph == 'SHA-512':
oid = bytes([ 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03,
0x04, 0x02, 0x03])
phm = SHA512.new(m).digest()
elif ph == 'SHAKE128':
oid = bytes([ 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03,
0x04, 0x02, 0x0B])
phm = SHAKE128.new(m).read(256 // 8)
else:
return None

mp = ( self.integer_to_bytes(1, 1) +
self.integer_to_bytes(len(ctx), 1) +
oid + phm )
sig = self.sign_internal(sk, mp, rnd)
return sig

# Algorithm 5, HashML-DSA.Verify(pk, M, sig, ctx, PH)
# Note 2024-08-20: Not covered by test vectors.

def hash_ml_dsa_verify(self, pk, m, sig, ctx, ph, param=None):
if param != None:
self.__init__(param)
if len(ctx) > 255:
return None

if ph == 'SHA-256':
oid = bytes([ 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03,
0x04, 0x02, 0x01])
phm = SHA256.new(m).digest()
elif ph == 'SHA-512':
oid = bytes([ 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03,
0x04, 0x02, 0x03])
phm = SHA512.new(m).digest()
elif ph == 'SHAKE128':
oid = bytes([ 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03,
0x04, 0x02, 0x0B])
phm = SHAKE128.new(m).read(256 // 8)
else:
return False

mp = ( self.integer_to_bytes(1, 1) +
self.integer_to_bytes(len(ctx), 1) +
oid + phm )
return self.verify_internal(pk, mp, sig)


# Algorithm 6, ML-DSA.KeyGen_internal(xi)

def keygen_internal(self, xi, param=None):
Expand Down
Loading

0 comments on commit 9eddd2e

Please sign in to comment.