Skip to content

Add ML-KEM composite KEM - #5686

Open
falko-strenzke wants to merge 15 commits into
randombit:masterfrom
falko-strenzke:falko/mlkem-composite
Open

falko-strenzke wants to merge 15 commits into
randombit:masterfrom
falko-strenzke:falko/mlkem-composite

Conversation

@falko-strenzke

@falko-strenzke falko-strenzke commented Jun 22, 2026 •

Copy link
Copy Markdown
Collaborator

Integration of ML-KEM composites according to draft-ietf-lamps-pq-composite-kem

Already solved issues

The following problem arises in the context of the MLKEM-composite implementation.
It is based on the inconsistency between these two functions defined in pk_keys.h:

      virtual std::unique_ptr<PK_Ops::Encryption> create_encryption_op(RandomNumberGenerator& rng,
                                                                       std::string_view params,
                                                                       std::string_view provider) const;

      virtual std::unique_ptr<PK_Ops::KEM_Encryption> create_kem_encryption_op(std::string_view params,
                                                                               std::string_view provider) const;

The problem is that the implementation of create_kem_encryption_op in MLKEM_Composite_PublicKey requires an RNG to internally call create_encryption_op() for RSA.
Thus inside the constructor of the MLKEM_Composite_Encapsulation_Operation, an RNG is needed to instantiate the traditional encryption operation.

It is not apparent to me why create_encryption_op() should require an RNG in the first place – and I didn't investigate. But generally, contrary to a decryption OP, it should not require randomization in setting up precomputations due to a side channel countermeasure.

The only reasonable solution that I came to is to add an optional RNG argument to the interface:

      virtual std::unique_ptr<PK_Ops::KEM_Encryption> create_kem_encryption_op(
         std::string_view params, std::string_view provider, RandomNumberGenerator* rng_may_be_null = nullptr) const;

This also requires a new function in the FFI interface. I am quite certain that this exact approach to the problem will not find acceptance. It's purpose is to make things work for now and provide a basis for the discussion of the proper solution.

The only alternative I could think of would be to let the constructor of MLKEM_Composite_Encapsulation_Operation to store the encoded public key in the instance and create the RSA encryption operation during every KEM encapsulation. This would be a both a strain on memory and performance, thus I discarded this idea.

@falko-strenzke
falko-strenzke force-pushed the falko/mlkem-composite branch from d6aaad5 to 8a7a5f7 Compare June 22, 2026 11:18
@randombit

Copy link
Copy Markdown
Owner

It is not apparent to me why create_encryption_op() should require an RNG in the first place – and I didn't investigate. But generally, contrary to a decryption OP, it should not require randomization in setting up precomputations due to a side channel countermeasure.

In principle blinding during encryption might be useful, and possibly that's why the argument is there. TBH I don't recall. It does seem to be the case that currently no implementation uses it, and since the encryption operation itself is provided an RNG any necessary blinding setup could just be deferred to the first invocation. Likely this argument could be removed entirely.

I would suggest rather than modifying the KEM interface in this way, take advantage of the fact that RSA's create_encryption_op ignores the RNG object anyway, pass a Null_RNG, catch PRNG_Unseeded and convert it into an Internal_Error.

@falko-strenzke

Copy link
Copy Markdown
Collaborator Author

I would suggest rather than modifying the KEM interface in this way, take advantage of the fact that RSA's create_encryption_op ignores the RNG object anyway, pass a Null_RNG, catch PRNG_Unseeded and convert it into an Internal_Error.

Absolutely makes sense, I applied that idea.

@falko-strenzke

Copy link
Copy Markdown
Collaborator Author

There are still CI failures apparently stemming from current master branch.

@falko-strenzke
falko-strenzke requested review from randombit and reneme and removed request for randombit June 22, 2026 15:04

@reneme reneme left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not a full review. Just some up-front comments.

Comment thread src/lib/pubkey/pk_keys.h Outdated
Comment thread doc/api_ref/pubkey.rst Outdated
Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp_parameters.cpp Outdated
Comment thread src/lib/pubkey/pk_algs.cpp
Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp_parameters.h Outdated
Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp_parameters.cpp

@reneme reneme left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Sorry for the fairly shallow high-level comments again. I hope I'll have more time to do a proper review in the next few days.

Comment thread src/lib/ffi/ffi_pkey_algs.cpp Outdated
Comment thread src/lib/ffi/ffi_pkey_algs.cpp Outdated
Comment thread src/lib/pubkey/mlkem-composite/info.txt Outdated
Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp.h Outdated
Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp.h
Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp.h Outdated
Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp_parameters.h Outdated
Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp_parameters.h Outdated
@reneme

reneme commented Jun 30, 2026 •

Copy link
Copy Markdown
Collaborator

@falko-strenzke On a general note: We should look into consolidating support for this IETF draft with the existing facilities in the pubkey/hybrid_kem and pubkey/kex_to_kem_adapter modules which are currently used in the TLS 1.3 PQC key exchange. Those provide base classes for hybrid KEMs already, and we shouldn't duplicate this logic here.

Following this idea, your MLKEM-Composite implementation should derive from Hybrid_PublicKey and Hybrid_PrivateKey. Translation from the ECDH/X25519/X448 algorithms to a KEM interface can be done conveniently using the KEX_to_KEM_Adapter. That way you should be able to avoid all dynamic casts to concrete ECDH public key classes such as ECDH_PublicKey, or X25519_PublicKey. This doesn't cover RSA-OAEP, but a similar adapter construction could be built for this.

The usage of generic adapters (for RSA and for ECDH) allow the composition to be implemented exclusively based on KEMs, without explicit handling of specific traditional algorithms. Also, such an implementation could be easily extensible to other KEMs in the future (e.g. FrodoKEM or HQC).

The existing hybrid_kem module is an internal interface and can thus be adapted as needed to create a decent foundation for both TLS's needs as well as this draft's implementation. Most notably, at the moment it is designed to be super generic by allowing arbitrary combinations of one-to-many PQ and traditional KEMs (not just specifically 1xPQ and 1xTraditional). In retrospect, this was probably a design mistake that we should fix.

I would suggest a design like so:

Screenshot 2026-06-30 at 13 28 47

excalidraw.com

I could take over the relevant work in the mentioned base classes and the TLS hybrid implementation.

@FAlbertDev

FAlbertDev commented Jul 6, 2026 •

Copy link
Copy Markdown
Collaborator

@falko-strenzke
One feature that would be very interesting for our use case is the ability to use a custom implementation of the underlying algorithms. For example, we plan to use Composite ML-KEM in combination with smart cards where the ML-KEM decapsulation and the ECDH operation are performed by the smart card. For that, we would use underlying keys of type PKCS11_EC_PrivateKey and (not yet implemented, but something like) PKCS11_ML_KEM_PrivateKey.

Therefore, it would be really nice if the interface of the composite ML-KEM private key would allow the processing of such "custom" private keys. I could imagine adding an additional constructor like:

MLKEM_Composite_PrivateKey(MLKEM_Composite_Param::id_t id, std::unique_ptr<Botan::PrivateKey> mlkem_privkey, std::unique_ptr<Botan::PrivateKey> traditional_privkey);

The information, currently obtained via dynamic casting the private key pointers, must be derived from the MLKEM_Composite_Param in this case. What do you think?

@reneme

reneme commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

@falko-strenzke See #5715 please. I'm currently working on a quick'n'dirty retrofit of your work on this branch onto the changes in #5715 to see if it provides the required flexibility to accommodate this implementation of the LAMPS draft. I'll share it with you once I have something working.

Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp.cpp Outdated
Comment thread src/lib/pubkey/mlkem-composite/info.txt
Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp.h Outdated
Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp.h Outdated
Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp.h Outdated
Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp.h Outdated
Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp.cpp Outdated
Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp.cpp Outdated
Comment thread src/lib/pubkey/mlkem-composite/info.txt
Comment thread src/lib/pubkey/mlkem-composite/mlkem_comp.cpp Outdated
@reneme

reneme commented Jul 8, 2026 •

Copy link
Copy Markdown
Collaborator

@falko-strenzke I took the liberty and refactored your patch (mostly mlkem_comp.cpp) based on #5715 and #5717. See here: https://github.com/reneme/botan/tree/falko/mlkem-composite (squashed to one commit and based on #5715).

This also addresses plenty of the suggestions I added above. But most notably, it gets rid of almost all low-level algorithm-specific handling by reusing existing concepts.

I'd appreciate if you could have a read-through my refactorings and pull them into this PR.

@falko-strenzke

Copy link
Copy Markdown
Collaborator Author

@falko-strenzke I took the liberty and refactored your patch (mostly mlkem_comp.cpp) based on #5715 and #5717. See here: https://github.com/reneme/botan/tree/falko/mlkem-composite (squashed to one commit and based on #5715).

This also addresses plenty of the suggestions I added above. But most notably, it gets rid of almost all low-level algorithm-specific handling by reusing existing concepts.

I'd appreciate if you could have a read-through my refactorings and pull them into this PR.

@reneme As I wrote in #5715, I am not entirely convinced of the multi-algorithm framework that you rebased the implementation onto. As a plus, it reduces the code size in mlkem_comp.cpp by one third, on the negative side, the trivial combination of two KEMs becomes somewhat harder to read due to a concept that doesn't actually bring much functionaility. In any case, I am fine with your proposal, but I would like to hear from @randombit first before pulling it in. The decision to use the multi-algorithm framework throughout the library in long term seems to be fundamental one.

@falko-strenzke

falko-strenzke commented Jul 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

@falko-strenzke One feature that would be very interesting for our use case is the ability to use a custom implementation of the underlying algorithms. For example, we plan to use Composite ML-KEM in combination with smart cards where the ML-KEM decapsulation and the ECDH operation are performed by the smart card. For that, we would use underlying keys of type PKCS11_EC_PrivateKey and (not yet implemented, but something like) PKCS11_ML_KEM_PrivateKey.

Therefore, it would be really nice if the interface of the composite ML-KEM private key would allow the processing of such "custom" private keys. I could imagine adding an additional constructor like:

MLKEM_Composite_PrivateKey(MLKEM_Composite_Param::id_t id, std::unique_ptr<Botan::PrivateKey> mlkem_privkey, std::unique_ptr<Botan::PrivateKey> traditional_privkey);

The information, currently obtained via dynamic casting the private key pointers, must be derived from the MLKEM_Composite_Param in this case. What do you think?

Well, with @reneme 's new proposal, I think that would or should be taken care of by the hybridization framework. @reneme Is that already the case?

@reneme

reneme commented Jul 9, 2026 •

Copy link
Copy Markdown
Collaborator

Is that already the case?

Pretty much. There are two central things to that:

  1. Have a low-level constructor that takes any keypair objects from the user,
  2. Refrain from any assumptions of the concrete key types (i.e. don't use dynamic_cast<RSA_PublicKey> for instance), because for a custom implementation that won't work.

The second point might need some work. The first point should be taken care of already.

@sehlen-bsi

sehlen-bsi commented Jul 27, 2026 •

Copy link
Copy Markdown

For the RSA composites, there seems to be the same issue as mentioned for #5451 that the RSA key sizes are not bound to the modulus size specified by the composite OID. I might be wrong here, please check!

@falko-strenzke
falko-strenzke force-pushed the falko/mlkem-composite branch 2 times, most recently from 6928fcd to 07a7949 Compare August 5, 2026 13:21
@falko-strenzke

Copy link
Copy Markdown
Collaborator Author

For the RSA composites, there seems to be the same issue as mentioned for #5451 that the RSA key sizes are not bound to the modulus size specified by the composite OID. I might be wrong here, please check!

You are correct as far as I can see, but I first want to adopt @reneme 's refactoring before addressing it.

@falko-strenzke

Copy link
Copy Markdown
Collaborator Author

@falko-strenzke I took the liberty and refactored your patch (mostly mlkem_comp.cpp) based on #5715 and #5717. See here: https://github.com/reneme/botan/tree/falko/mlkem-composite (squashed to one commit and based on #5715).
This also addresses plenty of the suggestions I added above. But most notably, it gets rid of almost all low-level algorithm-specific handling by reusing existing concepts.
I'd appreciate if you could have a read-through my refactorings and pull them into this PR.

@reneme As I wrote in #5715, I am not entirely convinced of the multi-algorithm framework that you rebased the implementation onto. As a plus, it reduces the code size in mlkem_comp.cpp by one third, on the negative side, the trivial combination of two KEMs becomes somewhat harder to read due to a concept that doesn't actually bring much functionaility. In any case, I am fine with your proposal, but I would like to hear from @randombit first before pulling it in. The decision to use the multi-algorithm framework throughout the library in long term seems to be fundamental one.

I just made a diff of our branches. After seeing many unrelated differences, I rebased on master (where I rather should have rebased onto #5715, as I saw afterwards), and still have many unrelated differences. Coud you @reneme rebase your branch on current master (4695b4a is what this PR is based on now), too , so that we have a clean diff?

@reneme

reneme commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Coud you rebase your branch on current master?

I rebased #5715 onto a8f6c6a

@falko-strenzke
falko-strenzke force-pushed the falko/mlkem-composite branch 2 times, most recently from e0bf430 to 60297e3 Compare August 6, 2026 08:55
@falko-strenzke

Copy link
Copy Markdown
Collaborator Author

Coud you rebase your branch on current master?

I rebased #5715 onto a8f6c6a

@reneme

Yes, but not https://github.com/reneme/botan/tree/falko/mlkem-composite as far as I could see..

But no problem, I now adopted the changes from that branch and should now have addressed all your review comments. Maybe you want to look over it again.

@falko-strenzke
falko-strenzke force-pushed the falko/mlkem-composite branch from 7fc3565 to 34b5f97 Compare August 6, 2026 12:34
@falko-strenzke

Copy link
Copy Markdown
Collaborator Author

For the RSA composites, there seems to be the same issue as mentioned for #5451 that the RSA key sizes are not bound to the modulus size specified by the composite OID. I might be wrong here, please check!

You are correct as far as I can see, but I first want to adopt @reneme 's refactoring before addressing it.

Now done. We have the same key size and EC group checks as for ML-DSA composites.

reneme and others added 13 commits September 16, 2026 14:01
Before, the implementation allowed for hybridization of an arbitrary number of
algorithms (n>=2). This introduced unnecessary complexity given that is pretty
unlikely that we'll see general-purpose hybridization of more than two algorithms.

Additionally, this lifts the KeyExchange-to-KeyEncapsulation adapter from the
concrete TLS 1.3 hybridization implementation into the base class so that it
may be reused by other concrete hybridization implementations. Most notably by
draft-ietf-lamps-pq-composite-kem.
Before, the Hybrid_KEM_PublicKey contained an unfortunate default c'tor
that would be silently called in the most-derived class to initialize
the class in a diamond-shape virtual inhertance relationship. This resulted
in unexpected internal states of the public key portion of the object.

Instead, by removing the default c'tor, we now force a more verbose but
easier to understand workaround on the user.
This lets user explicitly unwrap the wrapped KEX-key as necessary.
For TLS 1.3 it is currently just an implementation detail. But for the
Composite ML-KEM implementation it will have to become accessible via the
public API.
The old name 'hybrid_public_key.h' was too generic and might
become ambiguouos in the future.
use module sha3

Co-authored-by: René Meusel <github@renemeusel.de>
ecies
dh
ecdh
mlkem-composite

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we know that Composite-ML KEM will be recommended with the upcoming TR? Because, as you correctly state in the cryptodoc (sehlen-bsi/botan-docs#296), Composite ML-KEM is not fully TR compatible.

@sehlen-bsi Any insights?

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.

5 participants