LoupVaillant / Monocypher

An easy to use, easy to deploy crypto library
https://monocypher.org
Other
580 stars 80 forks source link

Userspace RNG? #216

Closed mikejsavage closed 2 years ago

mikejsavage commented 2 years ago

I think it would be nice if Monocypher had a mechanism for constructing a userspace CSPRNG.

Kernel entropy sources tend to be quite annoying to use, since they have vaguely defined error conditions that you have no real way to recover from. e.g. MSDN docs for BCryptGenRandom:

Possible return codes include, but are not limited to, the following.

STATUS_SUCCESS: The function was successful.
STATUS_INVALID_HANDLE: The handle in the hAlgorithm parameter is not valid.
STATUS_INVALID_PARAMETER: One or more parameters are not valid. 

man getrandom on Linux:

ERRORS
       [...]
       EIO    An unspecified error occurred while trying to overwrite buffer with random data.

Even man getentropy on OpenBSD is like this: (and indeed they provide their own userspace RNG on top of it)

ERRORS
     getentropy() will succeed unless:
     [...]
     [EIO]              Too many bytes requested, or some other fatal error occurred.

Moving the RNG to userspace means that once it's initialised, it will never fail. (assuming no forks)

You can already DIY this with not much code (crypto_chacha20_stream in 2.0.7/crypto_chacha20_ctr in 3.1.2 and pthread_atfork), so the question is really whether it's a good idea to make this an officially supported thing, or just leave it for the determined.

LoupVaillant commented 2 years ago

It's a hard question.

First, though it took me a long time to realize this, The NaCl family of crypto libraries, including Monocypher, are low level. Almost every function in there is the implementation of a primitive, and not much else. Monocypher's exceptions are:

They're included because of how error prone they are: it's easy to forget to hash the shared secret of a key exchange. It's easy to botch authenticated encryption with a polynomial hash. And regular X25519 public keys are not uniformly distributed, which would defeat the purpose of using Elligator in the first place.

On the other hand, I did not include crypto_box(), because it is a straightforward combination of crypto_key_exchange() and crypto_lock() (this lead many people to believe Monocypher did not support public key encryption, I had to improve the web site to dispel that misconception).

Now a user-space RNG would quite clearly be a high-level construct. I thought of implementing fast key erasure with ChaCha20, in a way that can be made faster if you switch to vector implementations like Libsodium. (Basically maintain 512 random bytes, keep the first 32 for the next batch and use the rest to give them to users). Now Monocypher has zero dependency, so if users want to fork or thread their RNG state, they'll need to do it explicitly. So here's what I think we need:

void crypto_rng_init (crypto_rng_ctx *ctx, uint8_t seed[32]);
void crypto_rng_bytes(crypto_rng_ctx *ctx, uint8_t *out, size_t size);
void crypto_rng_spawn(crypto_rng_ctx *ctx, crypto_rng_ctx *new_ctx);

The third function just uses crypto_rng_bytes() under the hood, but I believe we need it anyway so users are less tempted to just copy RNG states around (high level language bindings could disable copy altogether). Now it's a fairly simple thing, that should require less that 30 lines of code. Possibly less than 20. Here are my reasons not to include it anyway:

I haven't got around to it, but for now, I think it is best to provide such an RNG externally. It could be a separate project, or even just a code snippet in a tutorial.

LoupVaillant commented 2 years ago

I've just realised that I already have a userspace RNG here. It's not tested yet, so be careful with that.

I also counted, adding it to Monocypher would cost 28 lines of code. Small enough to be tempting.

fscoto commented 2 years ago

The thing I worry about with userspace RNG functionality is that it's a horrendous trade-off to make. For one, you put everything at risk with every fork(2) unless you're prepared to do fork(2) handling. We can outline this issue in the manual, but we can't provide this functionality in Monocypher as it would require unportable (highly platform-specific) code. Then there is the problem about userspace state possibly leaking (be it by means of speculative execution or other memory-related bugs); fast key erasure is not enough in that case and thus periodic re-keying becomes a good practice to at least limit the damage a state leak may do. Then there is the debate about whether it should actually use ChaCha20 or ChaCha8 or ChaCha12 because of some arbitrary notion of speed that seemingly always chases RNGs around. Once you bring in a userspace RNG, I expect that people will wish to use it for things that aren't strictly cryptographic key generation, too, and then there may well be requests for additional functions such as uniform random number generation akin to arc4random_uniform(3).

The kernel entropy sources won't become any less painful to use; you're writing the same code except this time you seed a userspace RNG. At that point, you've already written a function that provides a sequence of random bytes. Any randomness suitable to seed a stream cipher is by definition also randomness suitable to be a cryptographic key directly. You may as well skip the function in the middle at that point. I agree that the error conditions are very nebulously described in the documentation cited, but in practice, none of these functions will fail with correct arguments after an initially successful call as far as I'm aware (albeit I only briefly checked OpenBSD and Linux).

Incidentally, does having more lines of code really amount to leeway for new features? If somewhere down the line, some changes in the elliptic curve code end up being an issue, you may need that space for security fixes. Having budget to spare therefore seems like a necessity to me, not a luxury. Your mileage may, of course, vary.

As an aside: getentropy() is included to be part of the next version of POSIX and thus will be some kind of portable in about a decade.

LoupVaillant commented 2 years ago

Ok, I think we have given it enough thought: best not add it to Monocypher: in addition to all the problems userspace RNG have, everything in such an RNG can rely strictly on Monocypher’s public API. Implementing inside Monocypher would not leverage any internal function, and would save a couple lines of code at best.

Thus, it is best that userspace RNGs stay outside. Let it be the separate project it is now. Perhaps reference it from the website, when I get around to provide code examples & tutorials.

I think we can close this now. Thanks @mikejsavage for the suggestion, and thanks @fscoto for laying out the drawbacks so clearly.