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 SimpleKEM and FullKEM traits to kem #1559

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

incertia
Copy link

This adds a SimpleKEM trait, representing KEM models where the public and private keys from key generation are equivalent to the encapsulating and decapsulating keys. We also add a FullKEM trait, which is more general, which allows for KEM models where this is not the case, such as when trying to model authenticated X3DH as a KEM.

Motivation for this PR is mostly just promoting the DhKem trait from RustCrypto/KEMs#16 into the kem crate itself.

I think it is also good to have some code that forwards the calls from SimpleKEM::encapsulate and FullKEM::encapsulate to the actual Encapsulate<EK, SS> implementation. That way, a user can directly write KemModel::encapsulate and ensure that the types are correct, in the case that there are separate models that use very similar setups. e.g. unauthenticated vs authenticated modes of key exchange.

@tarcieri
Copy link
Member

Note that somewhat similarly shaped traits used to exist but were deliberately removed in #1509.

cc @rozbb

@incertia
Copy link
Author

incertia commented Apr 19, 2024

This is also very similar to KemCore from ml-kem

pub trait KemCore {
    type SharedKeySize: ArraySize;
    type CiphertextSize: ArraySize;
    type DecapsulationKey: Decapsulate<Ciphertext<Self>, SharedKey<Self>> + EncodedSizeUser + Debug + PartialEq;
    #[cfg(not(feature = "deterministic"))]
    type EncapsulationKey: Encapsulate<Ciphertext<Self>, SharedKey<Self>> + EncodedSizeUser + Debug + PartialEq;

    #[cfg(feature = "deterministic")]
    type EncapsulationKey: Encapsulate<Ciphertext<Self>, SharedKey<Self>> + EncapsulateDeterministic<Ciphertext<Self>, SharedKey<Self>> + EncodedSizeUser + Debug + PartialEq;

    fn generate(rng: &mut impl CryptoRngCore) -> (Self::DecapsulationKey, Self::EncapsulationKey);

    #[cfg(feature = "deterministic")]
    fn generate_deterministic(d: &B32, z: &B32) -> (Self::DecapsulationKey, Self::EncapsulationKey);
}

In particular KemCore appears to have a focus on encodability and equality which gets utilized during testing.

let dk_bytes = Encoded::<K::DecapsulationKey>::from_slice(self.dk);
assert_eq!(dk, K::DecapsulationKey::from_bytes(dk_bytes));

let ek_bytes = Encoded::<K::EncapsulationKey>::from_slice(self.ek);
assert_eq!(ek, K::EncapsulationKey::from_bytes(ek_bytes));

In the general model, this requires an adapter trait to be written for tests and included as an additional trait bound for generic tests.

pub trait SecretBytes {
    fn as_slice(&self) -> &[u8];
}

impl SecretBytes for Secret {
    fn as_slice(&self) -> &[u8] {
        self.0.as_bytes().as_slice()
    }
}

// use a generic SimpleKEM function to ensure correctness
fn test_kemtrait_basic<K: SimpleKEM>()
where
    <K as SimpleKEM>::SharedSecret: SecretBytes,
{
    let mut rng = rand::thread_rng();
    let (sk, pk) = K::random_keypair(&mut rng);
    let (ek, ss1) = K::encapsulate(&pk, &mut rng).expect("never fails");
    let ss2 = K::decapsulate(&sk, &ek).expect("never fails");

    assert_eq!(ss1.as_slice(), ss2.as_slice());
}

@rozbb
Copy link
Contributor

rozbb commented Apr 19, 2024

I agree ml-kem probably shouldn't be defining its own traits. I guess I'm not sure what functionality is needed by users. The primary addition I see in SimpleKEM is the ability to generate keypairs. As we've discovered, that small choice adds a lot of complexity. Do you have a strong take on whether that should be supported?

Thinking through alternatives: suppose a function is generic over a KEM and needs to be able to generate a fresh ephemeral keypair. Then perhaps it should be of the form

fn foo<EK, SS, E, F>(gen: F)
where
    E: Encapsulate<EK, SS>,
    F: Fn(&mut impl CryptoRngCore) -> (E, [u8; 32])
{
    let rng = rand::thread_rng();
    let (ek, dk) = gen(&mut rng);
    // ...
}

This is nice because gen can have whatever state the caller needs. In the simple Kyber case, this is totally stateless and returns a plain keypair. In the X3DH, gen can be a lambda containing an ID key and a prekey.

Thoughts?

@incertia
Copy link
Author

incertia commented Apr 19, 2024

Do you have a strong take on whether that should be supported?

I don't have an opinion per se, but the goal of the trait is so that one can precisely express what a specific KEM model does. i.e.

  1. The key generation process generates keys of PrivateKey and PublicKey
  2. The encapsulation process requires input material of type EncapsulatingKey, producing an EncapsulatedKey and SharedSecret
  3. The decapsulation process requires input material of type DecapsulatingKey and EncapsulatedKey, producing a SharedSecret

As for how these types are specified, it is ultimately up to the user. The X3DH test includes an example of how FullKEM would be written in the style of the test itself.

impl FullKEM for X3Dh {
    type PrivateKey = X3DhPrivkeyBundle;
    type PublicKey = X3DhPubkeyBundle;
    type DecapsulatingKey = DecapContext;
    type EncapsulatingKey = EncapContext;
    type EncapsulatedKey = EphemeralKey;
    type SharedSecret = SharedSecret;

    fn random_keypair(_: &mut impl CryptoRngCore) -> (Self::PrivateKey, Self::PublicKey) {
        let sk = Self::PrivateKey::gen();
        let pk = sk.as_pubkeys();

        (sk, pk)
    }
}

fn test_kemtrait_x3dh() {
    let mut rng = rand::thread_rng();

    let sk_ident_a = IdentityKey::default();
    let pk_ident_a = sk_ident_a.strip();
    let (sk_bundle_b, pk_bundle_b) = X3Dh::random_keypair(&mut rng);

    let encap_context = EncapContext(pk_bundle_b, sk_ident_a);
    let decap_context = DecapContext(sk_bundle_b, pk_ident_a);

    // Now do an authenticated encap
    let (encapped_key, ss1) = X3Dh::encapsulate(&encap_context, &mut rng).unwrap();
    let ss2 = X3Dh::decapsulate(&decap_context, &encapped_key).unwrap();

    assert_eq!(ss1, ss2);
}

The primary motivation for these traits is to develop the general TLS KEM combiner so we can ultimately do impl<K1: FullKEM, K2: FullKEM> FullKEM for TlsKemCombiner<K1, K2> but realistically the only real work that needs to be done is implementing Encapsulate<EK, SS> and Decapsulate<EK, SS> for TlsKemCombinerEK<EK1, EK2>. The only downside to this approach is finding the correct type for everything could become troublesome.

@rozbb
Copy link
Contributor

rozbb commented Apr 20, 2024

I see what you mean. Though I might be missing the point on the example API and impl for X3DH you give.

If you defined a function that generically created a keypair and did some operations, it's very unlikely that it would generate an identity key AND a prekey AND an ephemeral key (more likely: keep the identity key, pick a prekey from a set, and generate a fresh ephemeral key). For that reason, it seems like there's no concrete use case for a random_keypair(rng) function that returns a privkey bundle. Either the function should take more state (pre-existing long-term keys) or it should return only a subcomponent of the bundle. Hence why I suggest the lambda-based keygen method.

@incertia
Copy link
Author

I do agree that key generation can probably be moved outside of the trait. I think the main reason I included it is due to it being in KemCore and trying to do something similar. We can probably wait for @tarcieri and @bifurcation to chime in before settling on what the trait should be exactly.

@tarcieri
Copy link
Member

I think having a trait for capturing these details is good.

I'm unclear why we need two traits and why they can't build on each other. It feels like a lot of duplication.

The names should follow RFC430, i.e. *Kem. It would probably be good to just define a Kem trait as opposed to having a prefix like Simple*.

@bifurcation
Copy link

My initial impression is that this seems like a regression over the simplification we did in #1509. It looks to me like the only details this interface exposes is key generation, which has not been exposed in, e.g., the traits in the signature crate. If we're going to expose key generation, I would do only that, something like:

trait KemKeyGenerate {
    type EncapKey: Encapsulator<EK>,
    type DecapKey: Decapsulator<EK>,
    fn generate(rng: &mut CryptoRng) -> Result<(Self::DecapKey, Self::EncapKey), Error>;
}

Having encapsulate and decapsulate on the reified KEM just seems duplicative.

If we're copying patterns from ml-kem, the error is probably in that crate.

@incertia
Copy link
Author

I'm unclear why we need two traits and why they can't build on each other. It feels like a lot of duplication.

For KEMs in particular it seemed very weird to me why the original traits also split Encapsulator and Decapsulator. In particular, for KEMs, these actions are closely tied together and it would make sense to bundle these into a single trait.

Having encapsulate and decapsulate on the reified KEM just seems duplicative.

I think the point here is similar to above. In practice, KEMs are essentially specified by their encpsulate and decapsulate process so it seems more natural to write DhKem<X25519>::encapsulate(pk) rather than pk.encapsulate() where pk: PublicKey<X25519>.

Perhaps the best design here is to unify Encapsulate and Decapsulate into Kem and then introduce another FromCryptoRng or other similarly named trait that exposes fn random(rng: &mut impl CryptoRngCore) -> Self, since this seems to be a reoccuring pattern for many types provided by RustCrypto.

@bifurcation
Copy link

For KEMs in particular it seemed very weird to me why the original traits also split Encapsulator and Decapsulator. In particular, for KEMs, these actions are closely tied together and it would make sense to bundle these into a single trait.

Why is this any different from a signature algorithm that specifies both sign and verify? These are reflected in Signer and Verifier traits in this repo. Just because they're in different traits doesn't mean they can't be implemented together, say in the same file.

To put it differently, think of what application code needs to have in order to do something. With the Verify / Encapsulate approach, all you need is a public key. With the approach in this PR, you need the public key and a handle to the KEM.

@tarcieri
Copy link
Member

@incertia having a ZST to hang the overall scheme off of seems fine to me, but I would think it would only have associated types for the Encapsulator and Decapsulator so you can look them up ala DhKem::<X25519>::Encapsulator

@bifurcation
Copy link

@tarcieri what value would that add over say dhkem::Encapsulator<X25519>?

@tarcieri
Copy link
Member

Alternatively, Encapsulate could have an associated Decapsulator, which would be a bit closer to how things are in kem v0.2

@tarcieri
Copy link
Member

tarcieri commented Apr 24, 2024

@bifurcation being able to write generic code that can locate the type which performs decapsulation, similar to signature::Keypair.

Edit: whoops, it would probably make more sense to be able to look up the associated encapsulator for a given decapsulator, but hopefully you get the idea

@incertia
Copy link
Author

only have associated types for the Encapsulator and Decapsulator so you can look them up ala DhKem::<X25519>::Encapsulator

This also makes sense

@bifurcation
Copy link

kem::Keypair then? Basically, seems like we can follow the good example of signature here, unless there are reasons to be different.

@tarcieri
Copy link
Member

As a more concrete example of a signature-shaped API for this sort of thing, rsa defines the following traits:

(sidebar: it would be nice to support RSA-KEM eventually)

@tarcieri
Copy link
Member

tarcieri commented Apr 24, 2024

@bifurcation kem::Keypair sounds fine to me, and unlike signature where there's a proliferation of possible signer traits, it could probably be bounded on Decapsulate, defining an associated Encapsulator or thereabouts (and a method for retrieving it)

@rozbb
Copy link
Contributor

rozbb commented May 7, 2024

Ok, to sketch out at bit: we have as a starting point

pub trait signature::Keypair {
    type VerifyingKey: Clone;
    fn verifying_key(&self) -> Self::VerifyingKey;
}

Replacing everything with the appropriate types, we get

pub trait kem::Keypair<EK, SS> {
    type EncapsulationKey: Encapsulation<EK, SS>;
    fn encapsulation_key(&self) -> Self::EncapsulationKey;
}

Did we want to support getting the decap key from this? If so, do we know why the signature::Keypair trait doesn't support that?

@tarcieri
Copy link
Member

tarcieri commented May 7, 2024

Did we want to support getting the decap key from this? If so, do we know why the signature::Keypair trait doesn't support that?

@rozbb in signature, the Keypair trait is intended to be impl'd on signing key types themselves.

The reason signature::Keypair doesn't have a bound on Signer is because there are multiple *Signer traits depending on the nature of the underlying signature algorithm.

As I mentioned in this earlier, since that complication doesn't exist here, kem::Keypair could be bounded on Decapsulate.

@rozbb
Copy link
Contributor

rozbb commented May 7, 2024

I see. So iteration 2:

pub trait kem::Keypair<EK, SS> {
    type EncapsulationKey: Encapsulation<EK, SS>;
    type DecapsulationKey: Decapsulation<EK, SS>;
    fn encapsulation_key(&self) -> Self::EncapsulationKey;
    fn decapsulation_key(&self) -> Self::DecapsulationKey;
}

Actually, by this logic, why can't we just do:

struct kem::Keypair<EK, SS, E, D>
where
    E: Encapsulation<EK, SS>,
    D: Decapsulation<EK, SS>,
{
    pub encap_key: E,
    pub decap_key: D,
}

@tarcieri
Copy link
Member

tarcieri commented May 7, 2024

@rozbb of those, the struct looks better to me.

Either are a bit different from signature, which allows computing the public key from the secret key.

@incertia
Copy link
Author

incertia commented May 7, 2024

Actually, by this logic, why can't we just do:

struct kem::Keypair<EK, SS, E, D>
where
    E: Encapsulation<EK, SS>,
    D: Decapsulation<EK, SS>,
{
    pub encap_key: E,
    pub decap_key: D,
}

following in this vein, perhaps dhkem will define something like type DhKemKeypair<C: Curve> = kem::Keypair<PublicKey<C>, SharedSecret<C>, DhKemEncapsulator<C>, DhKemDecapsulator<C>>?

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

4 participants