[ANN] NistyPQC.jl

NistyPQC.jl provides pure Julia implementations of the post-quantum cryptography algorithms selected in NIST’s ongoing PQC Standardization Project.

So far, four winners have been selected:

  • ML-KEM aka Kyber
  • ML-DSA aka Dilithium
  • SLH-DSA aka SPHINCS+
  • FN-DSA aka Falcon

The package also contains the two candidate algorithms

  • BIKE and
  • Classic McEliece.

I tried to follow the official specifications as closely as possible. All implementations are self-contained in the sense that the only external dependencies are hash functions.

More details can be found in the documentation.

Of course, feedback is very welcome. :smiley:

8 Likes

Congrats on the massive undertaking :partying_face:

It is quite interesting to see how you have modularised code. in https://github.com/erich-9/NistyPQC.jl/blob/main/src/SLHDSA/SLHDSA.jl, you have used @eval to generate modules which look exciting/amusing. In CryptoGroups.jl I put the cryptographic parameters within a type parameter and then explicitly write every function signature with where. That though has a hurdle that type parameters need to be bitstypes which complicates things. This looks like an interesting alternative to consider in future refactoring.

Some minor things I noticed that you were using @inbounds. Does it improve performance in your situation? The cost of making out-of-bounds access and sacrificing memory safety unnoticed seems unbalanced. I also noticed the use of @eval in get_hashers function in https://github.com/erich-9/NistyPQC.jl/blob/main/src/SLHDSA/Parameters.jl. Perhaps you can put that code outside evaluating for all possible n or refactor to use n and possibly omega as a type parameter for a hasher which is constructing.

I also noticed the use of default_rng(), which is unsafe. A better alternative is Random.RandomDevice(), which provides random numbers from the operating system. I use it to generate a global seed and to generate CSPRG within the function deterministically as I have done that within generate_key in CryptoSignatures.jl. It would actually be good to CSPRG in a separate package from CryptoGroups.jl and also implement one used within a FIPS standard. If you are interested in that kind of thing let me know.

I am also curious about your motivation. Are you planning to use NistyPQC.jl in other projects and develop other cryptographic libraries in Julia?

4 Likes

I am also curious about your motivation. Are you planning to use NistyPQC.jl in other projects and develop other cryptographic libraries in Julia?

For now I just want to better understand NIST’s PQC algorithms. Hence, my decision to implement them from scratch. Julia seemed a good choice to do that, so as not to get lost in implementation details.

It is quite interesting to see how you have modularised code. […] I put the cryptographic parameters within a type parameter and then explicitly write every function signature with where. That though has a hurdle that type parameters need to be bitstypes which complicates things.

I also started out with packing parameters into types. But as you hint at, dealing with the more complex parameters for some of the algorithms (like floating-point numbers, hash functions etc.) then easily results in cluttered code. In contrast, the “amusing” metaprogramming solution makes the code look more concise by avoiding convoluted type annotations. On the downside, it probably increases compile time and compiled size. But I don’t consider this to be an issue. Moreover, currently many methods will be defined separately once for each parameter set, even in cases where resolving parameters at compile time gives no significant gain in performance. However, for readability and consistency among the algorithms, I deliberately didn’t factor them out to be shared among the modules for different parameter sets.

All that said, I’m open for suggestions how to modularize in a different, perhaps more Julian way.

Some minor things I noticed that you were using @inbounds. Does it improve performance in your situation?

It does. Anyway, since the implementations are not meant to be highly optimized for speed, I will follow your suggestion and reactivate bounds checking in the future.

I also noticed the use of @eval in get_hashers function in NistyPQC.jl/src/SLHDSA/Parameters.jl at main · erich-9/NistyPQC.jl · GitHub. Perhaps you can put that code outside evaluating for all possible n or refactor to use n and possibly omega as a type parameter for a hasher which is constructing.

Each of the files Parameters.jl merely automates setting up the different variants of the respective algorithm and computes derived parameters which typically are hard-coded in real-world implementations. So I don’t bother too much about them.

But I agree that get_hashers is one of the functions that deserves an overhaul. For example switching from hash functions to hash contexts here might be a good idea to improve performance a bit. In fact, SLH-DSA is one of the slowest algorithms due to the high cost of the many hashing operations.

I also noticed the use of default_rng(), which is unsafe.

I’d like to stick with Julia’s default. So this was a deliberate choice. The RNG used by the package can easily be changed with NistyPQC.set_rng.

I’m well aware that default_rng() is not cryptographically secure. But then the package is not intended to be used thoughtlessly for security relevant applications anyway.

It would actually be good to CSPRG in a separate package from CryptoGroups.jl and also implement one used within a FIPS standard. If you are interested in that kind of thing let me know.

Yes, definitely interested. As a matter of fact, for testing I needed CTR_DRBG with AES256 to check against “known answer tests”. Since I didn’t find a Julia package providing it, I wrote my own ad-hoc implementation here: test/Utilities/NistDRBG.jl


Finally, thank you very much for taking a look at the code and for your feedback!

I’ll check out your CryptoGroups.jl project later. :slight_smile:

2 Likes