Skip to content

Conversation

@tarcieri
Copy link
Member

@tarcieri tarcieri commented Nov 21, 2025

Adds a trait providing fallible key initialization, similar to the existing KeyInit trait, but designed to handle the case that not all bytestrings of a given length represent valid keys.

This is primarily useful in the context of public-key cryptography, e.g. scalars representing elliptic curve private keys.

The API and method names are duplicated from KeyInit. It is assumed that KeyInit and TryKeyInit have an either-or relationship, i.e. types will not impl both KeyInit and TryKeyInit, and consumers of code which is generic over these traits will not be attempting to abstract over the KeyInit/TryKeyInit distinction, but one or the other will make sense in a given context (e.g. symmetric cryptography uses KeyInit, ECC uses TryKeyInit)

Key generation methods are provided which generically implement rejection sampling as a reasonabe baseline for unbiased random sampling of potential keys. As a provided method, implementers of TryKeyInit can override it if they can provide more efficient means of generating keys.

Closes #1897

Adds a trait providing fallible key initialization, similar to the
existing `KeyInit` trait, but designed to handle the case that not all
bytestrings of a given length represent valid keys.

This is primarily useful in the context of public-key cryptography, e.g.
scalars representing elliptic curve private keys.

The API and method names are duplicated from `KeyInit`. It is assumed
that `KeyInit` and `TryKeyInit` have an either-or relationship, i.e.
types will not impl both `KeyInit` and `TryKeyInit`, and consumers of
code which is generic over these traits will not be attempting to
abstract over the `KeyInit`/`TryKeyInit` distinction, but one or the
other will make sense in a given context (e.g. symmetric cryptography
uses `KeyInit`, ECC uses `TryKeyInit`)

Key generation methods are provided which generically implement
rejection sampling as a reasonabe baseline for unbiased random sampling
of potential keys. As a provided method, implementers of `TryKeyInit`
can override it if they can provide more efficient means of generating
keys.
@tarcieri tarcieri requested a review from newpavlov November 21, 2025 21:03
@tarcieri
Copy link
Member Author

KeyInit and TryKeyInit have an either-or relationship, i.e. types will not impl both KeyInit and TryKeyInit [...] ECC uses TryKeyInit

There is a counterexample to this: the Ed25519 signature system uses a seed to derive the private scalar as the result of SHA-512 acting as a KDF, along with "clamping" to ensure the scalar is in range. So Ed25519 keys could use KeyInit.

But for ECDSA keys, or secp256k1 Schnorr, we'd want TryKeyInit. So perhaps having a way to abstract over the distinction should be considered.

(There is also a similar concern for X25519 static keys)

Comment on lines +352 to +362
fn generate_key() -> Result<Key<Self>, getrandom::Error> {
// Use rejection sampling to find a key which initializes successfully in an unbiased manner
loop {
let mut key = Key::<Self>::default();
getrandom::fill(&mut key)?;

if Self::new(&key).is_ok() {
return Ok(key);
}
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

re: #2060 implementing random bytestring generation in hybrid-array (which I still think is an independently good idea) won't suffice for these use cases, since they need algorithm-specific logic to select valid keys.

The approach in this PR is able to provide a generic unbiased key generation implementation which can have algorithm-specific overrides to improve efficiency.

I think we could potentially drop the keygen for KeyInit and point to hybrid-array for such cases, but having generic keygen for TryKeyInit is still independently useful, IMO.

Copy link
Member

@newpavlov newpavlov Nov 23, 2025

Choose a reason for hiding this comment

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

On the first glance, it certainly could be useful to have unbiased generation as part of the trait. Especially for algorithms which do not use all most significant bits in the key. Using naive rejection sampling can be really inefficient in such cases.

But looking at the current code, I can not say I am happy with it... Not only it forces us to keep the rand_core/getrandom features in crypto-common, but it also could be inefficient. For example, let key = Alg::generate_key()?; let alg = Alg::new(&key).unwrap(); results in an unnecessary key validation as part of the new method (which is not guaranteed to be optimized out by the compiler), but also forces users to use unwrap or handle unreachable error.

How about introducing an associated type for "validated" key? Something like this:

pub trait TryKeyInit: KeySizeUser + Sized {
    type ValidatedKey: Into<Key<Self>> + TryFrom<Key<Self>, Error = InvalidKey>;
    fn new_from_validated(key: &Self::ValidatedKey) -> Self;
    fn new(key: &Key<Self>) -> Result<Self, InvalidKey> {
        key.try_into().map(Self::new_from_validated)
    }
}

This way we will be able to move key generation code to the validated key type without burdening the TryKeyInit trait. I haven't thought much about method names, so feel free to rename them.

Copy link
Member Author

Choose a reason for hiding this comment

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

let key = Alg::generate_key()?; let alg = Alg::new(&key).unwrap(); results in an unnecessary key validation as part of the new method

This problem does not exist with the Generate/FromCryptoRng traits, it's inhereted by following the existing pattern of KeyInit, which is why I suggested separating the key generation traits out of KeyInit (#2060) and implemented an alternative which can generate random keys of any kind which you receive fully initialized (#2096), which fully addresses this concern.

Copy link
Member

Choose a reason for hiding this comment

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

Are you sure that we do not need the TryKeyInit with "validated" type? It would be orthogonal to #2096 and we even could add Generate bound on the associated type.

Copy link
Member Author

Choose a reason for hiding this comment

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

Except for micro-optimizing the case where keygen gives you back a "validated" bytestring rather than Self, I don't see a use case for it.

How else would you initialize a "validated" type other than fully initializing the Self and then throwing it away? And at that point, why not keep Self around, rather than the validated bytestring?

Copy link
Member

@newpavlov newpavlov Nov 23, 2025

Choose a reason for hiding this comment

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

Validated byte string may be smaller than Self. You also could have alternative ways for generating it (e.g. using deterministic rejection sampling).

@tarcieri
Copy link
Member Author

With #2096 I think the RNG code in this PR can be substantially improved.

It would be nice to retain a generic implementation of rejection sampling though. Perhaps it could work with a blanket impl and a marker trait, e.g. pub trait RejectionSamplingRng: TryKeyInit

@tarcieri
Copy link
Member Author

I'm just going to close this as it's very messy and I'm unhappy with it

@tarcieri tarcieri closed this Nov 23, 2025
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.

crypto-common: fallible alternative to KeyInit

3 participants