Skip to content

Post-Quantum Cryptography (PQC) support#720

Open
karel-m wants to merge 2 commits into
developfrom
pr/pqc1
Open

Post-Quantum Cryptography (PQC) support#720
karel-m wants to merge 2 commits into
developfrom
pr/pqc1

Conversation

@karel-m

@karel-m karel-m commented Apr 12, 2026

Copy link
Copy Markdown
Member

This pull request introduces an initial proposal for Post-Quantum Cryptography (PQC) support

It adds support for the following NIST PQC standards:

  • FIPS 203: Module-Lattice-Based Key Encapsulation Mechanism (ML-KEM)
  • FIPS 204: Module-Lattice-Based Digital Signature Standard (ML-DSA)
  • FIPS 205: Stateless Hash-Based Digital Signature Standard (SLH-DSA)

Further details on the standards can be found here: https://csrc.nist.gov/projects/post-quantum-cryptography#pqc-standards

The implementation is based on the official reference implementations (all of them public domain):

I also considered adding support for NTRU Prime (as currently used for hybrid kex in OpenSSH 9), but it appears that the OpenSSH project is planning to move to ML-KEM in version 10. Therefore, I kept only the NIST PQC stuff.

A new public API has been introduced in: src/headers/tomcrypt_pqc.h The aim was to keep the interface simple/compact (unfortunately PQC itself is far from being simple/compact), while aligning with existing LTC design patterns.

At present, the test coverage is limited to basic functional checks:

  • key generation + encapsulation + decapsulation (ML-KEM)
  • key generation + signing + verification (ML-DSA, SLH-DSA)

MISSING: Official NIST test vectors still need to be identified (ACVP?) and added to the test suite.

MISSING: Interoperability with other crypto libraries was not tested at all.

MISSING: Functions for key import/export currently handle only raw data; sooner or later PKCS#8 (for privkeys) and/or SPKI/X.509 (for pubkeys) will be probably needed.

IMPORTANT: The code in this initial PR is very lightly tested and is not suitable for production use.

This change is intended as a first step towards PQC support and is open for discussion, review, and further improvements.

Checklist

  • documentation is added or updated
  • tests are added or updated

@karel-m
karel-m requested a review from sjaeckel April 12, 2026 15:17
@karel-m karel-m mentioned this pull request Apr 12, 2026
5 tasks
@karel-m

karel-m commented Apr 12, 2026

Copy link
Copy Markdown
Member Author

As for key formats, there are some very recent standards:

  • RFC 9935 (March 2026) defines ML-KEM in X.509/PKIX, including subject public keys and private keys.
  • RFC 9909 (December 2025) defines SLH-DSA in X.509/PKIX, including public keys, signatures and private keys.
  • RFC 9881 (October 2025) defines ML-DSA for X.509 certificates and CRLs, including subject public keys, signatures and private keys.

It looks doable with the infrastructure we have in LTC, but quite a lot of work. It may be worth creating a separate PR.

@VA1DER

VA1DER commented Apr 13, 2026

Copy link
Copy Markdown

I don't know what OpenSSH's plan is for hybridizing any or all of these, but sntrup761x25519 was a very, very good idea in that it is provably at least as secure as both. There are still questions about the FIPS PQ algos' classic security. I would encourage dialog with the OpenSSH project to replicate this great idea. And with Jan Mojžíš of TinySSH who was, I understand, the driver behind that idea originally. Curve448 would be a great primitive to use for this, as it has some support already in several implementations (and in OpenSSL).

Secondly, does your implementation include the 256-bit (classic-strength) versions of these? ML-KEM-1024 & ML-DSA-87?

@karel-m

karel-m commented Apr 14, 2026

Copy link
Copy Markdown
Member Author

We support all parameter sets defined in NIST FIPS 203:

  • ML-KEM-512 = LTC_MLKEM_512
  • ML-KEM-768 = LTC_MLKEM_768
  • ML-KEM-1024 = LTC_MLKEM_1024

We also support all parameter sets defined in NIST FIPS 204:

  • ML-DSA-44 = LTC_MLDSA_44
  • ML-DSA-65 = LTC_MLDSA_65
  • ML-DSA-87 = LTC_MLDSA_87

And all from NIST FIPS 205:

  • SLH-DSA-SHAKE-128s = LTC_SLHDSA_SHAKE_128S
  • SLH-DSA-SHAKE-128f = LTC_SLHDSA_SHAKE_128F
  • SLH-DSA-SHAKE-192s = LTC_SLHDSA_SHAKE_192S
  • SLH-DSA-SHAKE-192f = LTC_SLHDSA_SHAKE_192F
  • SLH-DSA-SHAKE-256s = LTC_SLHDSA_SHAKE_256S
  • SLH-DSA-SHAKE-256f = LTC_SLHDSA_SHAKE_256F
  • SLH-DSA-SHA2-128s = LTC_SLHDSA_SHA2_128S
  • SLH-DSA-SHA2-128f = LTC_SLHDSA_SHA2_128F
  • SLH-DSA-SHA2-192s = LTC_SLHDSA_SHA2_192S
  • SLH-DSA-SHA2-192f = LTC_SLHDSA_SHA2_192F
  • SLH-DSA-SHA2-256s = LTC_SLHDSA_SHA2_256S
  • SLH-DSA-SHA2-256f = LTC_SLHDSA_SHA2_256F

So yes, this includes the higher-security parameter sets such as ML-KEM-1024 and ML-DSA-87.

As for OpenSSh see https://www.openssh.org/releasenotes.html it looks as they already switched the default to mlkem768x25519-sha256 in OpenSSH 9.9.

On adding further algorithms such as NTRU Prime 761, our preference is to follow widely adopted standards. NIST remains a credible standards body (even if some people see it negatively for geopolitical reasons). In the past, we added a few algorithms that were close to becoming standards but never quite got there and that usually created extra maintenance work.

@karel-m

karel-m commented Apr 14, 2026

Copy link
Copy Markdown
Member Author

Update:

  • Added import/export functions (in the end, not that complicated) + RFC 9935, RFC 9909, RFC 9881
  • Generated a bunch of PQC keys using OpenSSL 4.0, all of which we are able to import
  • Extracted some PQC test vectors from OpenSSL 4.0

However, we are now getting some segfaults in the GitLab tests (edit: fixed)

@sjaeckel sjaeckel added this to the next milestone Apr 15, 2026
@karel-m
karel-m force-pushed the pr/pqc1 branch 2 times, most recently from 9a5be19 to f0c0895 Compare May 6, 2026 23:18
@karel-m

karel-m commented May 7, 2026

Copy link
Copy Markdown
Member Author

latest version now passes all MLKEM / MLDSA wycheproof tests

@karel-m

karel-m commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

@sjaeckel ping

@NickBrighten

Copy link
Copy Markdown

@sjaeckel sjaeckel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a deep review, I only skimmed over it. It basically looks fine, just some nitpicks here and there, but I'll do a deep review hopefully soon'ish.

Additional to those comments I'd prefer to see the different algos put in a separate folder and duplicated stuff like those oid-enum maps being de-duplicated. I guess I can spend some time next week and bring this in shape as well.

Comment thread src/headers/tomcrypt_pk.h
Comment on lines +42 to +44
LTC_PKA_MLDSA,
LTC_PKA_SLHDSA,
LTC_PKA_MLKEM,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reasoning to put them in the middle of the enum and not at the end?

Comment thread src/headers/tomcrypt_pk.h
* LibTomCrypt tagged-union for holding a Public Key
*/

#include "tomcrypt_pqc.h"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this to the top of the file.

Comment on lines +28 to +32
/* DER INTEGER decodes into an MPI, so fail cleanly when no math provider is active. */
if (ltc_mp.name == NULL) {
return CRYPT_INVALID_ARG;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put this in a separate commit.

Comment on lines +156 to +161
/* Flexi INTEGER nodes are MPI-backed, so reject no-math builds before ltc_mp_init(). */
if (ltc_mp.name == NULL) {
err = CRYPT_INVALID_ARG;
goto error;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this

Comment on lines +23 to +44
int ltc_pqc_algname_match(const char *left, const char *right)
{
char lc_r, lc_l;

if (left == NULL || right == NULL) return 0;

while ((*left != '\0') && (*right != '\0')) {
while ((*left == '-') || (*left == '_')) left++;
while ((*right == '-') || (*right == '_')) right++;
if (*left == '\0' || *right == '\0') break;
lc_r = *right;
lc_l = *left;
if ((lc_r >= 'A') && (lc_r <= 'Z')) lc_r += 32;
if ((lc_l >= 'A') && (lc_l <= 'Z')) lc_l += 32;
if (lc_l != lc_r) return 0;
left++;
right++;
}

if ((*left == '\0') && (*right == '\0')) return 1;
return 0;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a near copy of

static int s_name_match(const char *left, const char *right)
... maybe make this a private API function (not in pqc namespace) instead and use that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants