-
Notifications
You must be signed in to change notification settings - Fork 433
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
Make RngCore infallible? #1418
Comments
As far as I am aware, most Rust cryptographic libraries already do not use our traits, and probably shouldn't either. They could still use our generators with an adapter over their own traits if required. Saying "but it would be less flexible" is a weak argument. As said above, our PRNGs could still be used in test code (via an adapter). |
Sorry for being snarky, but you haven't even taken a look at RustCrypto crates before writing this? For example, see docs for the |
Re:
By being bounded by
Because it's outside of its responsibility. Its goal is to provide access to a "default" system entropy source, not to abstract over various IO-based RNGs. |
Having separate traits for fallible vs infallible RNGs sounds like a reasonable improvement |
The fact that there is a use-case for an error-reporting byte-generating CSPRNG trait shouldn't surprise anyone here. The question is, should that be part of the (And given that
Make an argument for why it should be part of The only one I can think of is that it is useful for CSPRNGs to implement both |
Well, if you want to completely remove the cryptographic use cases from
Because it literally declares "random number generation traits" in the first line of its documentation. What exact problem are you trying to solve? The I don't remember a single issue which was caused by As for splitting fallibility into a separate trait, I don't see much reason to do that. As I wrote in the |
Okay. I do see that a Should it just be this? pub trait FallibleRng {
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error>;
} What relationship should it have to impl<R: RngCore + ?Sized> FallibleRng for R easily; is this enough? There is a minor argument for not including in
I propose that if we add a new
This is acceptable in generic code which does not care about fallibility or which forwards this error type intact. It can be a nuisance to deal with in generic code which must wrap and forward errors (in that simpler solutions are often possible when the error type is known). Code which wants only infallible RNGs is fine with either this solution or my proposal above, though having to specify Code which wants to be generic over RNGs and be able to handle errors properly requires a fixed specification of errors (at least as a conversion target), which is what Object-safe code cannot be generic; the error type must be explicitly specified in
I don't think there's even an unstable feature for associated constants in object-safe traits? There is certainly interest. But even if this was supported, it would need to be fixed in the vtable, thus effectively, we would have the following incompatible object-safe traits:
Since no one needs the "definitely not a crypto RNG" assertion, the current |
At a guess, most users of object-safe RNGs will not want to deal with fallibility, thus it may be fine to go with this: pub trait FallibleRng {
type Error: Into<getrandom::Error>;
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error>;
/// Convert to an InfallibleRng which panics on error
fn as_infallible(self) -> PanicOnError<Self> where Self: Sized {
PanicOnError(self)
}
}
pub struct PanicOnError<R: FallibleRng>(R);
impl<R: FallibleRng> InfallibleRng for PanicOnError<R> { .. } Also, should we rename what's left of |
For old discussions on this topic, see: #225 . I do not see mention of using multiple traits for fallible vs infallible RNGs and can't recall why we didn't investigate this approach. |
Huh, I thought that it returns reseeding errors from
Yes, this is why I wrote that we probably should wait for trait aliases before doing it. I don't quite understand why you want to introduce a separate trait with blanket impl instead of using super-traits: pub trait RngCore {
fn next_u32(&mut self) -> u32;
fn next_u64(&mut self) -> u64;
fn fill_bytes(&mut self, dst: &mut [u8]);
}
pub trait CryptoRng: RngCore {
type Error;
// maybe change name to something like `fill_crypto_bytes`?
fn try_fill_bytes(&mut self) -> Result<(), Self::Error>;
} This way I don't think it's important for Your |
That's what I meant. Relevant past issue (from you): #768
The culprit is I don't think we ever bothered detected cycles. Certainly not on small PRNGs (where it would probably require a copy of the state). I think ChaCha at one point incremented the nonce if the counter ever overflowed, but later we decided not to do that.
Under the limitation that We did briefly consider a separate fallible RNG trait.
I mean, |
@dhardy I do like those names: If you want a blanket impl, perhaps you could have a blanket impl of That would be similar to how the blanket impl of |
@tarcieri that blanket impl is what I had in mind above, but it doesn't work with @newpavlov's suggestion to put |
It seems like |
Another consideration is whether fallible RNGs should implement If we didn't have this, it might make sense to have
In my opinion the sensible type of fallibility to consider here is that initialisation might fail. For this, we'd have: Otherwise the only possible error is a cycle, but as said above we don't report that anyway. |
True. After initialization is complete |
Do you recall which methods of @aticu (see #1143): same question. .. because these are apparently cases where an object-safe |
@dhardy for a fallible RNG trait, there are more than just initialization errors to consider. I think such a trait should make it possible to abstract over hardware TRNGs, which being hardware devices can error at any time (e.g. communication errors or other hardware faults are always possible). To get an infallible RNG from such a fallible RNG, you can use the TRNG to seed a CSPRNG. |
@tarcieri |
Why? I can easily imagine someone using |
It seems we want at least:
This could be satisfied with: pub trait RngCore { /* non-try methods */ }
pub trait CryptoRng: RngCore {}
pub trait TryCryptoRng: CryptoRng {
type Error;
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error>;
} Possible issues:
Also note: maybe we shouldn't call |
I would like to drop in here and express my support for the use of the never type to make the trait(s) fallible by default, but allow implementations to be infallible via |
I think it's better to use something like this: use core::fmt;
pub trait RngCore {
// non-try methods
}
pub trait CryptoRng: RngCore {
type Error: fmt::Debug + fmt::Display;
fn fill_bytes(&mut self, dst: &mut [u8]) {
self.try_fill_bytes(dst).unwrap()
}
fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error>;
}
/// "Erased" RNG error.
pub struct CryptoRngError;
/// Object-safe version of `CryptoRng`
pub trait DynCryptoRng {
// May panic
fn fill_bytes(&mut self, dest: &mut [u8]);
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), CryptoRngError>;
}
// We also may seal this trait
impl<R: CryptoRng> DynCryptoRng for R {
fn fill_bytes(&mut self, dst: &mut [u8]) {
<Self as CryptoRng>::fill_bytes(self, dst)
}
fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), CryptoRngError> {
<Self as CryptoRng>::try_fill_bytes(self, dst).map_err(|err| {
log::error!("Crypto RNG error: {err}");
CryptoRngError
})
}
} This way the object safe trait will keep ability to return "erased" errors.
RNG is a traditional terminology used in cryptography, so I don't think we need to play with the naming here. |
All infallible RNGs can trivially implement the fallible RNG trait. Fallible RNGs can only implement the infallible trait by fixing the error-handling system. This implies that the inheritance here is the wrong way around: we should instead have If we don't force all fallible RNGs to implement The "wrap to implement fallible" also requires an explicit opt-in. |
I agree with you on the theoretical level, but incorporating it into API design leads to something like this (I still think that the error should be an associated type): use core::{fmt, convert::Infallible};
pub trait RngCore {
fn next_u32(&mut self) -> u32;
fn next_u64(&mut self) -> u64;
fn fill_bytes(&mut self, dst: &mut [u8]);
}
pub trait CryptoRng: RngCore {}
pub trait TryRngCore {
type Error: fmt::Debug + fmt::Display;
fn try_next_u32(&mut self) -> Result<u32, Self::Error>;
fn try_next_u64(&mut self) -> Result<u64, Self::Error>;
fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error>;
fn panic_on_err(self) -> PanicOnErr<Self>
where Self: Sized
{
PanicOnErr(self)
}
}
pub trait TryCryptoRng: TryRngCore {}
impl<R: TryRngCore<Error = Infallible>> RngCore for R {
fn next_u32(&mut self) -> u32 { self.try_next_u32().unwrap() }
fn next_u64(&mut self) -> u64 { self.try_next_u64().unwrap() }
fn fill_bytes(&mut self, dst: &mut [u8]) { self.try_fill_bytes(dst).unwrap() }
}
pub struct PanicOnErr<R: TryRngCore>{
pub inner: R,
}
impl<R: TryRngCore> RngCore for PanicOnErr<R> {
fn next_u32(&mut self) -> u32 { self.inner.try_next_u32().unwrap() }
fn next_u64(&mut self) -> u64 { self.inner.try_next_u64().unwrap() }
fn fill_bytes(&mut self, dst: &mut [u8]) { self.inner.try_fill_bytes(dst).unwrap() }
}
impl<R: TryCryptoRng> CryptoRng for PanicOnErr<R> {}
/// "Erased" crypto RNG error.
pub struct CryptoRngError;
/// Object-safe version of `TryCryptoRng`
pub trait DynTryCryptoRng {
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), CryptoRngError>;
}
impl<R: TryCryptoRng> DynTryCryptoRng for R {
fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), CryptoRngError> {
<Self as TryRngCore>::try_fill_bytes(self, dst).map_err(|err| {
log::error!("Crypto RNG error: {err}");
CryptoRngError
})
}
} This... would work? But the question is whether such complexity is warranted. We could simplify it a bit, by removing |
It's been quite a while since then, but I think I used I haven't read the discussion, but I hope this helps! |
@newpavlov I like that design, even if it is a bit complex. Caveat: Question: do we actually need |
I think I was just using fill-bytes, but ultimately it was like, I needed to pass this to other APIs that require |
@cbeck88 note that you can pass a |
if memory serves, the code sample was like,
If we didn't have I can try to find links to the patch we came up with if you are interested, but I think that was the gist of it. |
As I wrote above, it's optional and similar application-specific traits can be easily defined in user crates. Ideally, we would use something like this, but alas. |
@cbeck88 as I understand it, your requirements are an RNG which:
The remaining question in my mind is whether your case cares about fallibility (error reporting) at all — if not then |
Summary
Remove
RngCore::try_fill_bytes
and recommend that the trait only be implemented for infallible RNGs.Details
While, in theory, RNGs like ChaCha or even PCG could detect the end of a cycle and report an error, our current RNGs don't do this.
BlockRng
also does not have any consideration for RNG errors. Meanwhile,OsRng
is our only remaining fallible RNG.A potential external fallible RNG is a hardware RNG token. Plugging one of these into code expecting an
RngCore
would therefore be more difficult, requiring a fallback, hang or panic-on-error. It is, however, questionable if such a generator should ever implementRngCore
.Users of
try_fill_bytes
:RngReadAdapter
Fill::try_fill
; this is only to push error handling up to the caller, and arguably should be replaced with an infallible variantHence, for the rand library, the only real loss would be
OsRng
.What this doesn't solve
Cryptographic applications requiring a byte RNG with support for error handling. An existing alternative is
rustls::crypto::SecureRandom
. Another, more application specific trait, iscipher::StreamCipherCore
.There is no reason that a crate can't simultaneously provide impls of more than one of these traits, so impls can be shared.
There is a missing link here: applications which are generic over a
SecureRandom
no longer have access torand
's functionality (without some error-handling adapter), and so on. This doesn't seem to bad though; in particular (a) adapters are possible (even if they must panic) and (b) as far as I am aware, applications of these different traits usually don't need functionality from multiple of these libraries.(Note: it is also possible to use a custom trait like
SecureRandomRng: SecureRandom + RngCore
in case generic code does need to use multiple of these libraries.)Motivation
Simplicity and focus.
We recently had #1412 in order to improve the situation for infallible code. Ultimately, though,
rand
is a crate for stochastic algorithms like shuffling, bounded uniform sampling and sampling from a Gaussian. Its generators can be used to generate keys or a book of random data and this won't change aside from the inability to plug in a fallible generator with proper error handling, but arguably when you need this error handling you should already be using another library anyway.Alternatives
We could encode fallibility into
RngCore
(#1412 implements one method and mentions another in its comments).We could also provide a
FallibleRng
with thetry_fill_bytes
method, however this begs questions like why not move it up togetrandom
or why not useSecureRandom
instead?Final word
The rand library has long been a compromise between many competing demands. The only specific functionality we would lose here is the ability to forward errors from byte-generating RNGs. Fallible RNGs must currently panic (or hang) on error when implementing any of the other three
RngCore
methods, which is a strong indication that something is wrong with the current design.CC
@newpavlov @vks @tarcieri @ctz @josephlr
The text was updated successfully, but these errors were encountered: