Skip to content

kem: remove associated Error types#2216

Merged
tarcieri merged 1 commit intomasterfrom
kem/remove-associated-error-types
Jan 22, 2026
Merged

kem: remove associated Error types#2216
tarcieri merged 1 commit intomasterfrom
kem/remove-associated-error-types

Conversation

@tarcieri
Copy link
Member

@tarcieri tarcieri commented Jan 22, 2026

All of our kem implementations either use type Error = Infallible or use the error type exclusively for handling RNG errors.

That's good, because having an error case for decapsulation introduces a potential sidechannel, which can be eliminated by instead using implicit rejection that returns a pseudorandom rejection symbol as its output.

This removes the Error types and makes Decapsulate::decapsulate infallible in order to close the potential sidechannel having fallible decapsulation provides.

Encapsulate::encapsulate_with_rng now only uses the Result for handling RNG errors and returns R::Error, which should hopefully help mitigate the concerns in #2214.

For end users, Encapsulate::encapsulate now provides infallible encapsulation using the system RNG.

All of our `kem` implementations either use `type Error = Infallible` or
use the error type exclusively for handling RNG errors.

That's good, because having an error case for decapsulation introduces a
potential sidechannel, which can be eliminated by instead using implicit
rejection that returns a pseudorandom rejection symbol as its output.

This removes the `Error` types and makes `Decapsulate::decapsulate`
infallible in order to close the potential sidechannel having fallible
decapsulation provides.

`Encapsulate::encapsulate_with_rng` now only uses the `Result` for
handling RNG errors and returns `R::Error`, which should hopefully help
mitigate the concerns in #2214.
@tarcieri tarcieri force-pushed the kem/remove-associated-error-types branch from 85bd8c7 to 34c2d30 Compare January 22, 2026 18:45
@rozbb
Copy link
Contributor

rozbb commented Jan 22, 2026

This seems reasonable to me. Fwiw, implicit rejection now seems to be more of a byproduct of analysis rather than a real security mitigation. Future KEMs may explicitly reject during decapsulation. But we can cross that bridge when we get there.

Do you have opinions on removing TryCryptoRng entirely? I know it's a hot take so I wanted to see what your thoughts were. imo its upsides in maintainability win against the downsides of strictness

@tarcieri
Copy link
Member Author

tarcieri commented Jan 22, 2026

Fwiw, implicit rejection now seems to be more of a byproduct of analysis rather than a real security mitigation. Future KEMs may explicitly reject during decapsulation. But we can cross that bridge when we get there.

I'm curious how you avoid adaptive chosen ciphertext attacks if there is a timing sidechannel around rejection. It doesn't seem like that's the sort of thing that paper is concerning itself with though, skimming it the conclusions seem to be mostly in regard to the security proof.

I imagine with future KEMs there's going to be some kind of risk/benefit analysis in regard to implementing explicit rejection which will involve quite a bit of debate, so I agree we can cross that bridge when we get there.

Do you have opinions on removing TryCryptoRng entirely? I know it's a hot take so I wanted to see what your thoughts were. imo its upsides in maintainability win against the downsides of strictness

We've generally tried to support both CryptoRng and TryCryptoRng elsewhere, along with getrandom-based APIs.

Currently this supports an infallible getrandom-based API and TryCryptoRng, but not a fallible getrandom-based API or CryptoRng, so as-is it's somewhat inconsistent with other things.

I'm going to go ahead and merge this and maybe we can circle back on CryptoRng, but this should at least make the TryCryptoRng error handling easier.

@tarcieri tarcieri merged commit 0dabd78 into master Jan 22, 2026
10 checks passed
@tarcieri tarcieri deleted the kem/remove-associated-error-types branch January 22, 2026 19:25
@rozbb
Copy link
Contributor

rozbb commented Jan 22, 2026

I'm going to go ahead and merge this and maybe we can circle back on CryptoRng, but this should at least make the TryCryptoRng error handling easier.

Sounds good!

I'm curious how you avoid adaptive chosen ciphertext attacks if there is a timing sidechannel around rejection.

There's no side-channel. Say a KEM does Fujisaki-Okamoto with rejection, ie, it checks that the ciphertext is well-formed via re-computation, and rejects if not. In this setting, decapsulation fails if and only if the encapsulator deviated from the encapsulation algorithm. The encapsulator already knows they deviated, so that's not an interesting bit of information. What is leaky here?

@tarcieri
Copy link
Member Author

Rejecting a malformed ciphertext seems reasonably OK, but perhaps that could be a function of EK which happens in advance?

Sidebar: I guess the one remaining gap in the API right now is serializing/deserializing EK

@rozbb
Copy link
Contributor

rozbb commented Jan 22, 2026

Rejecting a malformed ciphertext seems reasonably OK, but perhaps that could be a function of EK which happens in advance?

You can only see if they deviated from the encapsulation algorithm if you have the decapsulation key. So this has to happen after deserialization.

I guess the one remaining gap in the API right now is serializing/deserializing EK

Yeah, it was tough to get something that encompassed all the aspects of a KEM and also made sense and also wasn't unwieldy. So far I haven't needed (de)serialization routines as a trait, so I don't have intuition for what should be covered.

@tarcieri
Copy link
Member Author

@rozbb I see. Well I guess the good news is Decapuslate doesn't need to deal with RNG errors, so having an associated Error type for that wouldn't be the end of the world

@tarcieri
Copy link
Member Author

We can always add a TryDecapsulate trait to handle the fallible case.

tarcieri added a commit that referenced this pull request Jan 22, 2026
Some terminology:
- `EK`: encapsulated key, i.e. ciphertext, not to be confused with
  "encapsulation key", the public key
- `SS`: shared secret, output of the decapsulator when given a
  ciphertext a.k.a. encapsulated key

`EK` and `SK` were previously generic parameters on the `Encapsulate`
and `Decapsulate` traits, however KEMs don't benefit from overlapping
impls and have relatively fixed notions of what these types should be.

This commit replaces them with new type aliases `Ciphertext` and
`SharedSecret`:

- `Ciphertext<K>`: type alias for `Array<u8, K::CiphertextSize>`, a.k.a.
  "encapsulated key", where `Array` is from `hybrid-array`.
- `SharedSecret<K>`: type alias for `Array<u8, K::SharedSecretSize>`

This means consumers of the traits always use bytestrings, which should
hopefully make it dramatically simpler to implement things generically
across KEMs.

The `K` generic parameter above is for types which impl a new `Kem`
trait which defines two associated `ArraySize`s:
- `Kem::CiphertextSize`: size of the ciphertext
- `Kem::SharedSecretSize`: size of the shared secret

This was split out into its own trait so the `Ciphertext<K>` and
`SharedSecret<K>` type aliases work with either encapsulators or
decapsulators.

Next, `Decapsulate` was split into three(!) traits to handle fallible
decapsulation:

- `Decapsulate`: what we had before with the `Decapsulate::Encapsulator`
  associated type extracted into a supertrait `Decapsulator`, which it
  now bounds on. It has a provided `decapsulate_slice` method which
  returns `core::array::TryFromSliceError` in the event the provided
  slice does not match `CiphertextSize`
- `TryDecapsulate`: fallible equivalent of `Decapsulate`, kind of like
  what we had prior to #2216, with an associated `Error` type and
  with a `try_decapsulate` method that returns a result. It also bounds
  on `Decapsulator` as its supertrait.
- `Decapsulator`: common supertrait of `Decapsulate` and
  `TryDecapsulate` which defines the associated `Encapsulator` and
  provides the `Decapsulator::encapsulator` method for retrieving it.

A blanket impl of `TryDecapsulate` is provided for types which impl
`Decapsulate` which uses `Infallible` as the error type, so any type
which impls `Decapsulate` can be used as a `TryDecapsulate`-bounded
argument. Likewise `Decapsulate` carries a
`TryDecapsulate<Error = Infallible>` bound which is satisfied by the
blanket impl but also enforces this property.

The reason we need to reintroduce fallible decapsulation is `dhkem`:
when I was scanning over our KEMs repo looking at the error types,
`dhkem` has `Error = Infallible`, but this hides that it was using
`elliptic_curve::PublicKey<C>` as its "encapsulated key" / ciphertext
type, which is a well-typed wrapper for a valid curve point.

With this type now being a raw byte slice, `dhkem` needs to handle
decoding the curve point in the `TryDecapsulate` impl, and if the point
fails to decode return an error (there are ways we could pseudorandomly
select a different point in constant time to compute a rejection symbol,
but having it return an error in this case seems like a straightforward
way to start).

Closes #2219
tarcieri added a commit that referenced this pull request Jan 22, 2026
Some terminology:
- `EK`: encapsulated key, i.e. ciphertext, not to be confused with
  "encapsulation key", the public key
- `SS`: shared secret, output of the decapsulator when given a
  ciphertext a.k.a. encapsulated key

`EK` and `SK` were previously generic parameters on the `Encapsulate`
and `Decapsulate` traits, however KEMs don't benefit from overlapping
impls and have relatively fixed notions of what these types should be.

This commit replaces them with new type aliases `Ciphertext` and
`SharedSecret`:

- `Ciphertext<K>`: type alias for `Array<u8, K::CiphertextSize>`, a.k.a.
  "encapsulated key", where `Array` is from `hybrid-array`.
- `SharedSecret<K>`: type alias for `Array<u8, K::SharedSecretSize>`

This means consumers of the traits always use bytestrings, which should
hopefully make it dramatically simpler to implement things generically
across KEMs.

The `K` generic parameter above is for types which impl a new `Kem`
trait which defines two associated `ArraySize`s:
- `Kem::CiphertextSize`: size of the ciphertext
- `Kem::SharedSecretSize`: size of the shared secret

This was split out into its own trait so the `Ciphertext<K>` and
`SharedSecret<K>` type aliases work with either encapsulators or
decapsulators.

Next, `Decapsulate` was split into three(!) traits to handle fallible
decapsulation:

- `Decapsulate`: what we had before with the `Decapsulate::Encapsulator`
  associated type extracted into a supertrait `Decapsulator`, which it
  now bounds on. It has a provided `decapsulate_slice` method which
  returns `core::array::TryFromSliceError` in the event the provided
  slice does not match `CiphertextSize`
- `TryDecapsulate`: fallible equivalent of `Decapsulate`, kind of like
  what we had prior to #2216, with an associated `Error` type and
  with a `try_decapsulate` method that returns a result. It also bounds
  on `Decapsulator` as its supertrait.
- `Decapsulator`: common supertrait of `Decapsulate` and
  `TryDecapsulate` which defines the associated `Encapsulator` and
  provides the `Decapsulator::encapsulator` method for retrieving it.

A blanket impl of `TryDecapsulate` is provided for types which impl
`Decapsulate` which uses `Infallible` as the error type, so any type
which impls `Decapsulate` can be used as a `TryDecapsulate`-bounded
argument. Likewise `Decapsulate` carries a
`TryDecapsulate<Error = Infallible>` bound which is satisfied by the
blanket impl but also enforces this property.

The reason we need to reintroduce fallible decapsulation is `dhkem`:
when I was scanning over our KEMs repo looking at the error types,
`dhkem` has `Error = Infallible`, but this hides that it was using
`elliptic_curve::PublicKey<C>` as its "encapsulated key" / ciphertext
type, which is a well-typed wrapper for a valid curve point.

With this type now being a raw byte slice, `dhkem` needs to handle
decoding the curve point in the `TryDecapsulate` impl, and if the point
fails to decode return an error (there are ways we could pseudorandomly
select a different point in constant time to compute a rejection symbol,
but having it return an error in this case seems like a straightforward
way to start).

Closes #2219
tarcieri added a commit to RustCrypto/KEMs that referenced this pull request Jan 23, 2026
Companion PR to RustCrypto/traits#2216

The traits were changed to be infallible, since all of the current
implementations are, and it's a pleasure to work with.

However, things get trickier if we want a byte-oriented interface for
ciphertexts/shared secrets as is proposed in RustCrypto/traits#2220
since `dhkem` needs decapsulation to handle point validation, which is
fallible.

Everything except `dhkem` can support infallible decapsulation, though.
tarcieri added a commit to RustCrypto/KEMs that referenced this pull request Jan 23, 2026
Companion PR to RustCrypto/traits#2216

The traits were changed to be infallible, since all of the current
implementations are, and it's a pleasure to work with.

However, things get trickier if we want a byte-oriented interface for
ciphertexts/shared secrets as is proposed in RustCrypto/traits#2220
since `dhkem` needs decapsulation to handle point validation, which is
fallible.

Everything except `dhkem` can support infallible decapsulation, though.
tarcieri added a commit that referenced this pull request Jan 23, 2026
Some terminology:
- `EK`: encapsulated key, i.e. ciphertext, not to be confused with
  "encapsulation key", the public key
- `SS`: shared secret, output of the decapsulator when given a
  ciphertext a.k.a. encapsulated key

`EK` and `SK` were previously generic parameters on the `Encapsulate`
and `Decapsulate` traits, however KEMs don't benefit from overlapping
impls and have relatively fixed notions of what these types should be.

This commit replaces them with new type aliases `Ciphertext` and
`SharedSecret`:

- `Ciphertext<K>`: type alias for `Array<u8, K::CiphertextSize>`, a.k.a.
  "encapsulated key", where `Array` is from `hybrid-array`.
- `SharedSecret<K>`: type alias for `Array<u8, K::SharedSecretSize>`

This means consumers of the traits always use bytestrings, which should
hopefully make it dramatically simpler to implement things generically
across KEMs.

The `K` generic parameter above is for types which impl a new `Kem`
trait which defines two associated `ArraySize`s:
- `Kem::CiphertextSize`: size of the ciphertext
- `Kem::SharedSecretSize`: size of the shared secret

This was split out into its own trait so the `Ciphertext<K>` and
`SharedSecret<K>` type aliases work with either encapsulators or
decapsulators.

Next, `Decapsulate` was split into three(!) traits to handle fallible
decapsulation:

- `Decapsulate`: what we had before with the `Decapsulate::Encapsulator`
  associated type extracted into a supertrait `Decapsulator`, which it
  now bounds on. It has a provided `decapsulate_slice` method which
  returns `core::array::TryFromSliceError` in the event the provided
  slice does not match `CiphertextSize`
- `TryDecapsulate`: fallible equivalent of `Decapsulate`, kind of like
  what we had prior to #2216, with an associated `Error` type and
  with a `try_decapsulate` method that returns a result. It also bounds
  on `Decapsulator` as its supertrait.
- `Decapsulator`: common supertrait of `Decapsulate` and
  `TryDecapsulate` which defines the associated `Encapsulator` and
  provides the `Decapsulator::encapsulator` method for retrieving it.

A blanket impl of `TryDecapsulate` is provided for types which impl
`Decapsulate` which uses `Infallible` as the error type, so any type
which impls `Decapsulate` can be used as a `TryDecapsulate`-bounded
argument. Likewise `Decapsulate` carries a
`TryDecapsulate<Error = Infallible>` bound which is satisfied by the
blanket impl but also enforces this property.

The reason we need to reintroduce fallible decapsulation is `dhkem`:
when I was scanning over our KEMs repo looking at the error types,
`dhkem` has `Error = Infallible`, but this hides that it was using
`elliptic_curve::PublicKey<C>` as its "encapsulated key" / ciphertext
type, which is a well-typed wrapper for a valid curve point.

With this type now being a raw byte slice, `dhkem` needs to handle
decoding the curve point in the `TryDecapsulate` impl, and if the point
fails to decode return an error (there are ways we could pseudorandomly
select a different point in constant time to compute a rejection symbol,
but having it return an error in this case seems like a straightforward
way to start).

Closes #2219
tarcieri added a commit to RustCrypto/KEMs that referenced this pull request Jan 23, 2026
Companion PR to RustCrypto/traits#2216

The traits were changed to be infallible, since all of the current
implementations are, and it's a pleasure to work with.

However, things get trickier if we want a byte-oriented interface for
ciphertexts/shared secrets as is proposed in RustCrypto/traits#2220
since `dhkem` needs decapsulation to handle point validation, which is
fallible.

Everything except `dhkem` can support infallible decapsulation, though.
tarcieri added a commit to RustCrypto/KEMs that referenced this pull request Jan 23, 2026
Companion PR to RustCrypto/traits#2216

The traits were changed to be infallible, since all of the current
implementations are, and it's a pleasure to work with.

However, things get trickier if we want a byte-oriented interface for
ciphertexts/shared secrets as is proposed in RustCrypto/traits#2220
since `dhkem` needs decapsulation to handle point validation, which is
fallible.

Everything except `dhkem` can support infallible decapsulation, though.
tarcieri added a commit that referenced this pull request Jan 23, 2026
Some terminology:
- `EK`: encapsulated key, i.e. ciphertext, not to be confused with
  "encapsulation key", the public key
- `SS`: shared secret, output of the decapsulator when given a
  ciphertext a.k.a. encapsulated key

`EK` and `SK` were previously generic parameters on the `Encapsulate`
and `Decapsulate` traits, however KEMs don't benefit from overlapping
impls and have relatively fixed notions of what these types should be.

This commit replaces them with new type aliases `Ciphertext` and
`SharedSecret`:

- `Ciphertext<K>`: type alias for `Array<u8, K::CiphertextSize>`, a.k.a.
  "encapsulated key", where `Array` is from `hybrid-array`.
- `SharedSecret<K>`: type alias for `Array<u8, K::SharedSecretSize>`

This means consumers of the traits always use bytestrings, which should
hopefully make it dramatically simpler to implement things generically
across KEMs.

The `K` generic parameter above is for types which impl a new `Kem`
trait which defines two associated `ArraySize`s:
- `Kem::CiphertextSize`: size of the ciphertext
- `Kem::SharedSecretSize`: size of the shared secret

This was split out into its own trait so the `Ciphertext<K>` and
`SharedSecret<K>` type aliases work with either encapsulators or
decapsulators.

Next, `Decapsulate` was split into three(!) traits to handle fallible
decapsulation:

- `Decapsulate`: what we had before with the `Decapsulate::Encapsulator`
  associated type extracted into a supertrait `Decapsulator`, which it
  now bounds on. It has a provided `decapsulate_slice` method which
  returns `core::array::TryFromSliceError` in the event the provided
  slice does not match `CiphertextSize`
- `TryDecapsulate`: fallible equivalent of `Decapsulate`, kind of like
  what we had prior to #2216, with an associated `Error` type and
  with a `try_decapsulate` method that returns a result. It also bounds
  on `Decapsulator` as its supertrait.
- `Decapsulator`: common supertrait of `Decapsulate` and
  `TryDecapsulate` which defines the associated `Encapsulator` and
  provides the `Decapsulator::encapsulator` method for retrieving it.

A blanket impl of `TryDecapsulate` is provided for types which impl
`Decapsulate` which uses `Infallible` as the error type, so any type
which impls `Decapsulate` can be used as a `TryDecapsulate`-bounded
argument. Likewise `Decapsulate` carries a
`TryDecapsulate<Error = Infallible>` bound which is satisfied by the
blanket impl but also enforces this property.

The reason we need to reintroduce fallible decapsulation is `dhkem`:
when I was scanning over our KEMs repo looking at the error types,
`dhkem` has `Error = Infallible`, but this hides that it was using
`elliptic_curve::PublicKey<C>` as its "encapsulated key" / ciphertext
type, which is a well-typed wrapper for a valid curve point.

With this type now being a raw byte slice, `dhkem` needs to handle
decoding the curve point in the `TryDecapsulate` impl, and if the point
fails to decode return an error (there are ways we could pseudorandomly
select a different point in constant time to compute a rejection symbol,
but having it return an error in this case seems like a straightforward
way to start).

Closes #2219
tarcieri added a commit that referenced this pull request Jan 23, 2026
Some terminology:
- `EK`: encapsulated key, i.e. ciphertext, not to be confused with
"encapsulation key", the public key
- `SS`: shared secret, output of the decapsulator when given a
ciphertext a.k.a. encapsulated key

`EK` and `SK` were previously generic parameters on the `Encapsulate`
and `Decapsulate` traits, however KEMs don't benefit from overlapping
impls and have relatively fixed notions of what these types should be.

This commit replaces them with new type aliases `Ciphertext` and
`SharedSecret`:

- `Ciphertext<K>`: type alias for `Array<u8, K::CiphertextSize>`, a.k.a.
"encapsulated key", where `Array` is from `hybrid-array`.
- `SharedSecret<K>`: type alias for `Array<u8, K::SharedSecretSize>`

This means consumers of the traits always use bytestrings, which should
hopefully make it dramatically simpler to implement things generically
across KEMs.

The `K` generic parameter above is for types which impl a new
`KemParams` trait which defines two associated `ArraySize`s:
- `KemParams::CiphertextSize`: size of the ciphertext
- `KemParams::SharedSecretSize`: size of the shared secret

This was split out into its own trait so the `Ciphertext<K>` and
`SharedSecret<K>` type aliases work with either encapsulators or
decapsulators.

Next, `Decapsulate` was split into three(!) traits to handle fallible
decapsulation:

- `Decapsulate`: what we had before with the `Decapsulate::Encapsulator`
associated type extracted into a supertrait `Decapsulator`, which it now
bounds on. It has a provided `decapsulate_slice` method which returns
`core::array::TryFromSliceError` in the event the provided slice does
not match `CiphertextSize`
- `TryDecapsulate`: fallible equivalent of `Decapsulate`, kind of like
what we had prior to #2216, with an associated `Error` type and with a
`try_decapsulate` method that returns a result. It also bounds on
`Decapsulator` as its supertrait.
- `Decapsulator`: common supertrait of `Decapsulate` and
`TryDecapsulate` which defines the associated `Encapsulator` and
provides the `Decapsulator::encapsulator` method for retrieving it.

A blanket impl of `TryDecapsulate` is provided for types which impl
`Decapsulate` which uses `Infallible` as the error type, so any type
which impls `Decapsulate` can be used as a `TryDecapsulate`-bounded
argument. Likewise `Decapsulate` carries a `TryDecapsulate<Error =
Infallible>` bound which is satisfied by the blanket impl but also
enforces this property.

The reason we need to reintroduce fallible decapsulation is `dhkem`:
when I was scanning over our KEMs repo looking at the error types,
`dhkem` has `Error = Infallible`, but this hides that it was using
`elliptic_curve::PublicKey<C>` as its "encapsulated key" / ciphertext
type, which is a well-typed wrapper for a valid curve point.

With this type now being a raw byte slice, `dhkem` needs to handle
decoding the curve point in the `TryDecapsulate` impl, and if the point
fails to decode return an error (there are ways we could pseudorandomly
select a different point in constant time to compute a rejection symbol,
but having it return an error in this case seems like a straightforward
way to start).

Closes #2219
tarcieri added a commit that referenced this pull request Jan 24, 2026
Switches from `TryCryptoRng` back to `CryptoRng` for
`encapsulate_with_rng`.

We originally switched to #2049 with the rationale that the whole trait
was fallible anyway, so we might as well handle the RNG errors. But then
in #2216 we made the rest of the trait infallible, only using
fallibility for the RNG.

`Decapsulate` is also now fully infallible, but for cases where we need
to handle errors there's a `TryDecapsulate` trait. Prospectively we
could do the same thing here, and have a fallible `TryEncapsulate` trait
that uses `TryCryptoRng` and handles RNG errors. This PR doesn't attempt
to add one because it has some trait design issues around how we convert
RNG errors into KEM-specific error types.

Closes #2214
tarcieri added a commit that referenced this pull request Jan 24, 2026
Switches from `TryCryptoRng` back to `CryptoRng` for
`encapsulate_with_rng`.

We originally switched to #2049 with the rationale that the whole trait
was fallible anyway, so we might as well handle the RNG errors. But then
in #2216 we made the rest of the trait infallible, only using
fallibility for the RNG.

`Decapsulate` is also now fully infallible, but for cases where we need
to handle errors there's a `TryDecapsulate` trait. Prospectively we
could do the same thing here, and have a fallible `TryEncapsulate` trait
that uses `TryCryptoRng` and handles RNG errors. This PR doesn't attempt
to add one because it has some trait design issues around how we convert
RNG errors into KEM-specific error types.

Closes #2214 (and see also that issue for the problems around error type
conversions)
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.

2 participants