Skip to content

add crandom.py, for our RNG, where we mix stuff with os.urandom - #10794

Draft
SomberNight wants to merge 2 commits into
spesmilo:masterfrom
SomberNight:202607_crandom
Draft

add crandom.py, for our RNG, where we mix stuff with os.urandom#10794
SomberNight wants to merge 2 commits into
spesmilo:masterfrom
SomberNight:202607_crandom

Conversation

@SomberNight

@SomberNight SomberNight commented Aug 3, 2026

Copy link
Copy Markdown
Member

(still WIP)

Currently we rely on os.urandom() ~everywhere for cryptographically secure randomness. Existing code already checks at runtime that the output of os.urandom() looks at least somewhat sane (see if it compresses with zlib) and hard-fails if it does not.

However, os.urandom could still be subtly "broken" (undetected by us) and produce bad quality output. That's the motivation of the new code here. We expect os.urandom to work well, BUT if it undetectably does not, mixing in other sources of entropy mitigates the situation somewhat.

This PR introduces a new module crandom.py that manages the RNG, and that our other code should call. Extreme care should be taken here not to make things worse than the status quo by "rolling our own" thing.

The logic is split across two modules: crandom.py and crandom_env.py.

  • The core sensitive logic (RNG mixing, extracting random bytes) is in crandom.py, which is absolutely security critical and is kept concise to ease review.
  • crandom_env.py contains secondary sources of entropy and potentially platform-specific code. It is just a companion to crandom.py and is only intended to be accessed from there. Even if all the entropy sources listed in crandom_env.py are broken, assuming os.urandom() produces high quality random, crandom.py should never produce low-quality random output. Hence crandom_env.py is much less critical tro review in depth.

This is inspired by https://github.com/bitcoin/bitcoin/blob/67efced1fc83a0b7215cc1513e7c4754fee0f12f/src/random.h#L25, which is significantly more advanced. But I wanted to (1) lessen our dependence on os.urandom(), (2) while keeping it simple.

@SomberNight SomberNight added the security 🔐 technical issue that affects security of funds label Aug 3, 2026
Currently we rely on `os.urandom()` ~everywhere for cryptographically secure randomness.
Existing code already checks at runtime that the output of `os.urandom()` looks at least somewhat sane (see if it compresses with zlib) and hard-fails if it does not.

However, `os.urandom` could still be subtly "broken" (undetected by us) and produce bad quality output. That's the motivation of the new code here. We expect `os.urandom` to work well, BUT if it undetectably does not, mixing in other sources of entropy mitigates the situation somewhat.

This PR introduces a new module `crandom.py` that manages the RNG, and that our other code should call. Extreme care should be taken here not to make things worse than the status quo by "rolling our own" thing.

The logic is split across two modules: `crandom.py` and `crandom_env.py`.
- The core sensitive logic (RNG mixing, extracting random bytes) is in `crandom.py`,
  which is absolutely security critical and is kept concise to ease review.
- `crandom_env.py` contains secondary sources of entropy and potentially platform-specific code.
  It is just a companion to `crandom.py` and is only intended to be accessed from there.
  Even if all the entropy sources listed in `crandom_env.py` are broken, assuming `os.urandom()` produces high quality random, `crandom.py` should never produce low-quality random output. Hence `crandom_env.py` is much less critical tro review in depth.

This is inspired by https://github.com/bitcoin/bitcoin/blob/67efced1fc83a0b7215cc1513e7c4754fee0f12f/src/random.h#L25, which is significantly more advanced. But I wanted to (1) lessen our dependence on `os.urandom()`, (2) while keeping it simple.
@SomberNight SomberNight added this to the 4.9.0 milestone Aug 5, 2026

@renepickhardt renepickhardt left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Disclaimer: I am no expert with randomness and security of it. So please take this feedback not as advise or proper audit. It is just some thoughts that I had while looking over this PR.

In particular not commenting on things does not mean I approve them as secure or good. It rather means I have no opinion / idea.

As a general question / comment I wonder if pulling all the data in crandom_env might not be predictable or at least bruteforcable in some scenarios.

Comment thread electrum/crandom.py

Never raises.
"""
assert nbytes >= 0, nbytes

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

also nbytes needs to by smaller equal than 32 I guess

Comment thread electrum/crandom.py
# we must not raise UnicodeError, hence "backslashreplace"
data = data.encode("utf-8", errors='backslashreplace')
with self.lock:
self._state = sha512(data + self._state)[0:32]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this also applies to other places. I am not sure about taking the first half of SHA512. Thus wouldn't it make more sense to do self._state = XOR(SHA256(data),self._state).

The - admitedly paranoid - reasoning: The hash is an additional component capable, at least in principle, of damaging a good source if the implementation or construction is defective while xor should always work?

Comment thread electrum/crandom.py
"""
with self.lock:
fresh_entropy = os.urandom(32)
h = sha512(fresh_entropy + self._state)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

see comment from feed_entropy in line 82. I think xor preserves the randomness property better if at least one source was random

Comment thread electrum/crandom.py


# Check that os.urandom works
length = len(zlib.compress(os.urandom(1000)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I am not sure about this sanity check. I think there is a non zero chance that true random output is compressable. That probability should be small however and I see what you are trying to achieve here but I am not sure if other means are more useful

Comment thread electrum/crandom.py

get_rand_bytes = _rng.get_rand_bytes
get_rand_below = _rng.get_rand_below
feed_entropy = _rng.feed_entropy

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It absolutely makes sense to expose this API. however I am not sure if users who need this API will understand that they need it and build their application accordingly. See other comment in __init__

Comment thread electrum/crandom.py
# gather ghetto-entropy:
self.rand_add_refresh() # clock
crandom_env.rand_add_static_env(self.feed_entropy)
self.rand_add_refresh() # clock again

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

as far as I understand you pull OS specific values when creating the state RNGState object (for example at startup of electrum) if users do this within a local machine and clone the local machine I believe the additional source of randomness is just copied and never updated. I understand that feed_entropy could help here but I wonder if the feeding of additional runtime components should not also occasionally happen during random number generation.

Comment thread electrum/crandom_env.py
feed(getattr(os, "ctermid", lambda: "")())
feed(os.getcwd())
feed(str(os.get_exec_path()))
feed(str(os.getgroups()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems to be supported only under linux? c.f. https://docs.python.org/3/library/os.html#os.getgroups

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

security 🔐 technical issue that affects security of funds

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants