Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an ML-KEM implementation #2

Merged
merged 10 commits into from Mar 1, 2024
Merged

Conversation

bifurcation
Copy link
Contributor

This PR adds an implementation of ML-KEM variant of the Kyber KEM as described
in the initial public draft of FIPS 203. This implementation covers all three
parameter sets described in the specification by making heavy use of Rust
generics and the typenum crate.

In addition to self-compatibility testing, we test correctness by verifying the
test vectors supplied by NIST; see tests/nist.rs.

This PR also includes a benchmark that tests the speed of the ML-KEM-768
variant, the most commonly implemented variant. At least on my MacBook, this
implementation is competitive with / slightly slower than the fastest known implementations (units
are µs/op; only Go and ml_kem measured locally, others from this blog post):

MacOS M1 Pro ml_kem libcrux PQClean BoringSSL Circl Go
Key generation 34.487 28.336 30.387 32.051 39.783 42.900
Encapsulation 36.068 29.222 38.688 28.131 44.874 58.831
Decapsulation 42.704 31.635 43.072 33.777 50.025 70.862

@tarcieri
Copy link
Member

tarcieri commented Feb 7, 2024

cc @dsprenkels

@bifurcation
Copy link
Contributor Author

Note that the Go version linked above has a very thorough battery of tests. I think it would be a good idea to have that coverage here as well, but it can be handled as a follow-on PR.

ml-kem/.gitignore Outdated Show resolved Hide resolved
Comment on lines 84 to 122
/// A key that can be used to decapsulated an encapsulated shared key
pub trait DecapsulationKey<SharedKey, Ciphertext>: EncodedSizeUser {
/// Decapsulate the ciphertext to obtain the encapsulated shared key
fn decapsulate(&self, ciphertext: &Ciphertext) -> SharedKey;
}

/// A key that can be used to encapsulate a shared key such that it can only be decapsulated
/// by the holder of the corresponding decapsulation key
pub trait EncapsulationKey<SharedKey, Ciphertext>: EncodedSizeUser {
/// Encapsulate a fresh secret to the holder of the corresponding decapsulation key
fn encapsulate(&self, rng: &mut impl CryptoRngCore) -> (SharedKey, Ciphertext);

/// Encapsulate a specific shared key to the holder of the corresponding decapsulation key
#[cfg(feature = "deterministic")]
fn encapsulate_deterministic(&self, m: &B32) -> (SharedKey, Ciphertext);
}

/// A generic interface to a Key Encapsulation Method
pub trait KemCore {
/// The size of a shared key generated by this KEM
type SharedKeySize: ArrayLength;

/// The size of a ciphertext encapsulating a shared key
type CiphertextSize: ArrayLength;

/// A decapsulation key for this KEM
type DecapsulationKey: DecapsulationKey<SharedKey<Self>, Ciphertext<Self>> + Debug + PartialEq;

/// An encapsulation key for this KEM
type EncapsulationKey: EncapsulationKey<SharedKey<Self>, Ciphertext<Self>> + Debug + PartialEq;

/// Generate a new (decapsulation, encapsulation) key pair
fn generate(rng: &mut impl CryptoRngCore) -> (Self::DecapsulationKey, Self::EncapsulationKey);

/// Generate a new (decapsulation, encapsulation) key pair deterministically
#[cfg(feature = "deterministic")]
fn generate_deterministic(d: &B32, z: &B32)
-> (Self::DecapsulationKey, Self::EncapsulationKey);
}
Copy link
Member

@tarcieri tarcieri Feb 13, 2024

Choose a reason for hiding this comment

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

Copy link

Choose a reason for hiding this comment

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

Yup, though its tires haven't been thoroughly kicked so I'm curious if @bifurcation thinks it's an OK fit.

Also it probably needs to be upgraded to hybrid-array

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh nice re KEM traits, somehow I had missed that. Obviously should use that, was already thinking that that trait should be outside this crate.

@tarcieri if you concur re hybrid_array, happy to switch to that. (Assuming there are no roadblocks, e.g., due to some of the typenum craziness we're going here.) Might do some benchmarking to see if there are perf impacts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To be clear: I haven't taken a deep look at the KEM traits yet, will do and report back. But it's correct to have the KEM traits outside this crate.

Copy link
Member

Choose a reason for hiding this comment

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

We're migrating to hybrid-array elsewhere. It should be a drop-in replacement for generic-array. There's a migration guide here (it should mostly be find/replace): https://docs.rs/hybrid-array/0.2.0-rc.5/hybrid_array/#migrating-from-generic-array

I can also look at updating the kem crate to use hybrid-array.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On trying to build tests, I find that I need ArraySize implementations for a bunch more sizes: 480, 800, 1088, 1152, 1184, 1536, 1568, 1600, 1632, 2336, 2368, 2400, 3104, 3136, 3168. So increasing the base set size up to 4096 would cover me. Note, however, that typenum doesn't actually define U$n for all of these $n, so you might have different problems.

Given this dependency, we might want to consider migrating to hybrid-array as a follow-on to this PR.

Copy link
Member

@tarcieri tarcieri Feb 16, 2024

Choose a reason for hiding this comment

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

Yeah, that gets a bit tricky and I see typenum::Const<N> doesn't cover that range either. It will require encoding the bit pattern for those sizes, and in such a case I could probably consider e.g. multiples of 32 in the range 1024-4096 which should cover your needs at least.

Copy link

Choose a reason for hiding this comment

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

Given this dependency, we might want to consider migrating to hybrid-array as a follow-on to this PR.

Yes, agreed. Sorry for the unnecessary detour. I just started trying to migrate the crate myself and hit the same large-const issues for Saber.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No worries @rozbb! It does look a bit nicer than generic-array, so I like it as a long-term direction. It's unfortunate that the state of const generics is such that it requires all this hacking around.

Copy link
Member

Choose a reason for hiding this comment

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

This should at least be a stopgap to support the sizes you need with hybrid-array: https://github.com/RustCrypto/hybrid-array/pull/49/files

I'll see if I can come up with a better/consistent/more automated way of implementing these additional sizes.

@bifurcation
Copy link
Contributor Author

bifurcation commented Feb 16, 2024

Yup, though its tires haven't been thoroughly kicked so I'm curious if @bifurcation thinks it's an OK fit.

@rozbb I filed a bunch of comments on the interface in RustCrypto/traits#1508. Basically, I think it's usable, just could be nicer. It is correct in that it focuses on the encap/decap keys instead of trying to capture the whole system as I do here. I would probably prefer to get that fixed before implementing here.

tarcieri added a commit to RustCrypto/hybrid-array that referenced this pull request Feb 16, 2024
This adds support for sizes identified as needed for post-quantum
KEM/DSA use cases, namely the ones from this comment:

RustCrypto/KEMs#2 (comment)

These should ideally get expanded into some consistent multiples above
1024, e.g. multiples of 32, and generated in a purely automated manner
(e.g. by a script that can break down the bit representation and build
the generic syntax), but this should at least be sufficient to unblock
these use cases.

Note that `UInt<UInt<..<UTerm, B#>, B#...` aliases expressing the
explicit bits for a given number are used instead of e.g.
`<U1024 as Add<U32>>::Output` because when the latter is used it causes
similar errors for conflicting trait impls as we saw with
`typenum::U<N>` for whatever reason:

    error[E0119]: conflicting implementations of trait `traits::ArraySize` for type `UTerm`
       --> src/sizes.rs:82:13
        |
    82  |               unsafe impl ArraySize for $ty {
        |               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        |               |
        |               first implementation here
        |               conflicting implementation for `UTerm`
tarcieri added a commit to RustCrypto/hybrid-array that referenced this pull request Feb 16, 2024
This adds support for sizes identified as needed for post-quantum
KEM/DSA use cases, namely the ones from this comment:

RustCrypto/KEMs#2 (comment)

These should ideally get expanded into some consistent multiples above
1024, e.g. multiples of 32, and generated in a purely automated manner
(e.g. by a script that can break down the bit representation and build
the generic syntax), but this should at least be sufficient to unblock
these use cases.

Note that `UInt<UInt<..<UTerm, B#>, B#...` aliases expressing the
explicit bits for a given number are used instead of e.g. `<U1024 as
Add<U32>>::Output` because when the latter is used it causes similar
errors for conflicting trait impls as we saw with `typenum::U<N>` for
whatever reason:

error[E0119]: conflicting implementations of trait `traits::ArraySize`
for type `UTerm`
       --> src/sizes.rs:82:13
        |
    82  |               unsafe impl ArraySize for $ty {
        |               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        |               |
        |               first implementation here
        |               conflicting implementation for `UTerm`
@bifurcation
Copy link
Contributor Author

Latest commits address the comments so far:

  • Remove the redundant .gitignore file
  • Add a workspace-level Cargo.toml file
  • Convert to hybrid-array

I have not attempted to use the KEM traits, given RustCrypto/traits#1508

The latest commit here builds against the master branch of hybrid-array, since the required changes haven't been released to crates.io yet.

@tarcieri how would you like to proceed here?

@tarcieri
Copy link
Member

tarcieri commented Mar 1, 2024

I can cut another hybrid-array release

@rozbb
Copy link

rozbb commented Mar 1, 2024

@bifurcation also would love to get your input on RustCrypto/traits#1509 and see if that addresses your comments. It's very very minimal now. Const-size arrays aren't even needed anymore.

@tarcieri
Copy link
Member

tarcieri commented Mar 1, 2024

hybrid-array v0.2.0-rc.6 has been released: RustCrypto/hybrid-array#54

@tarcieri
Copy link
Member

tarcieri commented Mar 1, 2024

There's no current CI configuration, although I could potentially follow up with one, since I don't think it would run anyway until this PR is merged.

@bifurcation can you check the code is clean under clippy if you haven't already? I'm noticing some function names with capital letters like PRF and XOF and I'm curious if clippy would flag those

@bifurcation
Copy link
Contributor Author

Thanks for the nudge @rozbb, I had missed that PR. I sent a review. d0945d4 copy/pastes those interfaces into this repo; when that PR gets merged and released, we can switch over to it here.

Re clippy -- Yes, it runs clean. I added an #![allow(non_snake_case)] to the top to allow for variable names to follow the spec. I don't feel strongly, just think it make it easier to see how things line up if someone is reading. 4d83d5d makes a few clippy exceptions more precise. I would be open to doing the same for non_snake_case, since I agree that that's not great to have in general.

Re CI -- I think adding CI in a PR will run it on that PR. @tarcieri do you happen to have a favorite example that could be copy/pasted here?

@tarcieri
Copy link
Member

tarcieri commented Mar 1, 2024

I can add it in a separate PR (and since I'm a member, it should run it in that PR)

Copy link
Member

@tarcieri tarcieri left a comment

Choose a reason for hiding this comment

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

utACK with the caveat that my review is largely for style and consistency, and not the correctness of the implementation

@tarcieri tarcieri merged commit ea44681 into RustCrypto:master Mar 1, 2024
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.

None yet

3 participants