Skip to content

Conversation

@jschneider-bensch
Copy link
Collaborator

@jschneider-bensch jschneider-bensch commented Sep 30, 2025

This PR implements an RFC 9180-style DHKEM(X25519, HKDF-SHA256), sans Auth, on top of the ECDH trait in libcrux_curve25519 (only that one, for now).

The implementation is behind a kem-api feature, since users may want to implement their own KEM on top of the ECDH trait, e.g. with a different KDF.

There are some things I'm not quite satisfied with:

  • While it is possible to make the arrayref::Kem implementation generic over some const SHARED_SECRET_LEN: usize, it is not possible to make the macro-based implementation of slice::Kem generic. For this reason, I've fixed the SHARED_SECRET_LEN == 32 in both for now.
  • The implementation is not really specific to X25519, except for hardcoded (at the moment) KEM identifiers taken from RFC 9180. For this reason, I would really like to avoid duplicating it almost exactly in p256. Some things I've thought about/tried:
    • Blanket impl in libcrux_traits: Does not work because, libcrux_hkdf depends on libcrux_traits via libcrux_sha2. ❌
    • Blanket impl in libcrux_kem: I think this does not work because of the orphan rule, since both traits are foreign. ❌
    • Yet another macro in libcrux_traits::kem: Maybe this could work, but I dislike it, because it makes it harder to understand how things work, and because the macro would have to assume a crate using it has the HKDF dependency, which is not present in libcrux_traits (forbidden there, actually!).❓
  • I've also had to touch the XWing implementation in libcrux-kem, since it was using the KEM trait API for X25519, assuming it gave you the raw ECDH shared secret. Instead it now uses the ECDH API, which one the one side is what you want to use there. On the other hand the usage of EcdhArrayref is a bit inconvienient I found. (Errors handling can also be improved.)

@jschneider-bensch
Copy link
Collaborator Author

Rebased onto jonas/ecdh-keygen to use generate_pair in Xwing.

@jschneider-bensch jschneider-bensch changed the base branch from main to jonas/ecdh-keygen September 30, 2025 14:58
@keks
Copy link
Member

keks commented Sep 30, 2025

I think the approach here makes sense!

Regarding how to abstract this: Since the blanket impl (had it worked) would likely have lived in libcrux-ecdh, maybe that would be a good place for the impl macro, and then we call it everywhere where we also implement ECDH? I agree it doesn't make this more readable but (a) I don't have an idea for how we could get it to work with blanked impls and (b) at least personally, I don't find macros that much worse than blanket impls from a readability perspective :)

Regarding the extract-then-expand function: Is that exactly one of the HPKE key schedules? Maybe we could call it that.

Base automatically changed from jonas/ecdh-keygen to main October 1, 2025 06:29
let ek_x: &mut [u8; X25519_EK_LEN] = ek_x.try_into().unwrap();

let (dk_m, dk_x) = dk.split_at_mut(MLKEM768_DK_LEN);
let dk_m: &mut [u8; MLKEM768_DK_LEN] = dk_m.try_into().unwrap();
Copy link
Member

Choose a reason for hiding this comment

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

These have been here before, but we really shouldn't unwrap here. The first one would be safe if we checked the length of the thing we split. But the second one would need checking as well.

libcrux-hkdf = { version = "=0.0.3", path = "../libcrux-hkdf", optional = true }

[features]
kem-api = ["libcrux-hkdf"]
Copy link
Member

Choose a reason for hiding this comment

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

Do we have naming convention for these? It could also just be kem. I'm fine either way.

.map_err(|_| libcrux_traits::kem::arrayref::EncapsError::Unknown)?;

let mut derived_ecdh = [0u8; EK_LEN];
<X25519 as EcdhArrayref<DK_LEN, DK_LEN, EK_LEN>>::derive_ecdh(
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't X25519:: be enough? Same for the other calls.


pub const SHARED_SECRET_LEN: usize = 32;

impl libcrux_traits::kem::arrayref::Kem<DK_LEN, EK_LEN, EK_LEN, SHARED_SECRET_LEN, DK_LEN, DK_LEN>
Copy link
Member

Choose a reason for hiding this comment

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

If we import arrayref this entire thing gets a little more readable.

.map_err(|_| libcrux_traits::kem::arrayref::EncapsError::InvalidEncapsKey)?;

extract_and_expand(ct, ss, ek, derived_ecdh)
.map_err(|_| libcrux_traits::kem::arrayref::EncapsError::Unknown)
Copy link
Member

Choose a reason for hiding this comment

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

Why do we do additional things with the secret? This will make this API unusable in most usecases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You mean, why do we do the HKDF?
The KEM implementation we had before gave out the raw DH shared secret, but we warn against doing that in libcrux-ecdh. This here provides specifically an implementation of DHKEM(X25519, HKDF-SHA256) under the KEM traits, which requires that round of HKDF to make the shared secret safe to use. If you need the raw DH shared secret, like in XWing for example, you can use the ECDH API.

Or do you mean something else?

Copy link
Member

Choose a reason for hiding this comment

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

But the point of having a KEM API for ECDH as well, is to make it possible to have a uniform way of calling KEMs, if that's an ECC based one or a PQ one. Like we do for example in Bertie. That won't be possible with this API.
When should people use this then?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Okay, I think we had a misunderstanding of what this should achieve then.
For me, this was a way to deal with the warning I linked above and provide a DHKEM implementation under the KEM API. It would be used when you need a DHKEM.

The previous KEM API did what you want already, it just did so without using the ECDH trait. Doing that on top of the ECDH trait should be easy, however: Just remove the HKDF-related bits here. In fact, removing the HKDF bits makes it possible to have a blanket impl in libcrux-traits.

Shall I do that instead?

@jschneider-bensch
Copy link
Collaborator Author

As discussed offline, this doesn't quite go in the right direction. We do want the EC KEMs to output the raw secret for uniformity of use with PQ-KEMs in protocol implementations.

Perhaps this can be salvaged into an easy fix for users that shouldn't use the raw secret, but it's not an immediate priority at the moment.

@jschneider-bensch jschneider-bensch marked this pull request as draft October 1, 2025 10:05
@github-actions
Copy link

github-actions bot commented Dec 1, 2025

This PR has been marked as stale due to a lack of activity for 60 days. If you believe this pull request is still relevant, please provide an update or comment to keep it open. Otherwise, it will be closed in 7 days.

@github-actions github-actions bot added the stale label Dec 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement KEM traits based on ECDH API for curve25519, p256

3 participants