3
votes

Referring to documentation for os and secrets:

os.getrandom(size, flags=0)

Get up to size random bytes. The function can return less bytes than requested.

getrandom() relies on entropy gathered from device drivers and other sources of environmental noise.

So does this mean it's from /dev/random?

On Linux, if the getrandom() syscall is available, it is used in blocking mode: block until the system urandom entropy pool is initialized (128 bits of entropy are collected by the kernel).

So to ensure kernel CSPRNG with bad internal state is never used I should use os.getrandom()? Since the function can return less bytes than requested, should I run the application level CSPRNG as something like

def rng():
    r = bytearray()
    while len(r) < 32:
        r += os.getrandom(1)
    return bytes(r)

to ensure maximum security? I explicitly want all systems that do not support blocking until urandom entropy pool is initialized to be unable to run the program and system that support it, to wait. This is because the software must be secure even if it's run from a live CDs that has zero entropy at start.

Or does the blocking mean if I do os.getrandom(32), the program waits if necessary, forever until the 32 bytes are collected?

The flags argument is a bit mask that can contain zero or more of the following values ORed together: os.GRND_RANDOM and GRND_NONBLOCK.

Can someone please ELI5 how this works?


os.urandom(size)

On Linux, if the getrandom() syscall is available, it is used in blocking mode: block until the system urandom entropy pool is initialized (128 bits of entropy are collected by the kernel).

So the urandom quietly falls back to non-blocking CSPRNG that doesn't know it's internal seeding state in older Linux kernel versions?

Changed in version 3.6.0: On Linux, getrandom() is now used in blocking mode to increase the security.

Does this have to do with os.getrandom()? Is it a lower level call? Are the two the same?


os.GRND_NONBLOCK

By default, when reading from /dev/random, getrandom() blocks if no random bytes are available, and when reading from /dev/urandom, it blocks if the entropy pool has not yet been initialized.

So it's the 0-flag in os.getrandom(size, flag=0)?

os.GRND_RANDOM

If this bit is set, then random bytes are drawn from the /dev/random pool instead of the /dev/urandom pool.

What does ORing the os.getrandom() flags mean? How does the os.getrandom(flags=1) tell if I meant to enable os.GRND_NONBLOCK or os.GRND_RANDOM. Or do I need to set it before like this:

os.GRND_RANDOM = 1
os.getrandom(32) # or use the rng() defined above

secrets module

The secrets module is used for generating cryptographically strong random numbers suitable for managing data such as passwords, account authentication, security tokens, and related secrets.

The only clear way to generate random bytes is

secrets.token_bytes(32)

The secrets module provides access to the most secure source of randomness that your operating system provides.

So that should mean it's os.getrandom with fallback to os.urandom? So it's not a good choice if you desire 'graceful exit if internal state can not be evaluated'?

To be secure against brute-force attacks, tokens need to have sufficient randomness. Unfortunately, what is considered sufficient will necessarily increase as computers get more powerful and able to make more guesses in a shorter period. As of 2015, it is believed that 32 bytes (256 bits) of randomness is sufficient for the typical use-case expected for the secrets module.

Yet the blocking stops at 128 bits of internal state, not 256. Most symmetric ciphers have 256-bit versions for a reason.

So I should probably make sure the /dev/random is used in blocking mode to ensure internal state has reached 256 bits by the time the key is generated?

So tl;dr

What's the most secure way in Python3.6 to generate a 256-bit key on a Linux (3.17 or newer) live distro that has zero entropy in kernel CSPRNG internal state at the start of my program's execution?

2

2 Answers

4
votes

After doing some research, I can answer my own question.

os.getrandom is a wrapper for getrandom() syscall offered in Linux Kernel 3.17 and newer. The flag is a number (0, 1, 2 or 3) that corresponds to bitmask in following way:

GETRANDOM with ChaCha20 DRNG

os.getrandom(32, flags=0)

GRND_NONBLOCK =  0  (=Block until the ChaCha20 DRNG seed level reaches 256 bits)
GRND_RANDOM   = 0   (=Use ChaCha20 DRNG)
              = 00  (=flag 0)

This is a good default to use with all Python 3.6 programs on all platforms (including live distros) when no backwards compatibility with Python 3.5 and pre-3.17 kernels is needed.

The PEP 524 is incorrect when it claims

On Linux, getrandom(0) blocks until the kernel initialized urandom with 128 bits of entropy.

According to page 84 of the BSI report, the 128-bit limit is used during boot time for callers of kernel module's get_random_bytes() function, if the code was made to properly wait for the triggering of the add_random_ready_callback() function. (Not waiting means get_random_bytes() might return insecure random numbers.) According to page 112

When reaching the state of being fully seeded and thus having the ChaCha20 DRNG seeded with 256 bits of entropy -- the getrandom system call unblocks and generates random numbers.

So, GETRANDOM() never returns random numbers until the ChaCha20 DRNG is fully seeded.

os.getrandom(32, flags=1)

GRND_NONBLOCK =  1  (=If the ChaCha20 DRNG is not fully seeded, raise BlockingIOError instead of blocking)
GRND_RANDOM   = 0   (=Use ChaCha20 DRNG)
              = 01  (=flag 1)

Useful if the application needs to do other tasks while it waits for the ChaCha20 DRNG to be fully seeded. The ChaCha20 DRNG is almost always fully seeded during boot time, so flags=0 is most likely a better choice. Needs the try-except logic around it.

GETRANDOM with blocking_pool

The blocking_pool is also accessible via the /dev/random device file. The pool was designed with the idea in mind that entropy runs out. This idea applies only when trying to create one-time pads (that strive for information theoretic security). The quality of entropy in blocking_pool for that purpose is not clear, and the performance is really bad. For every other use, properly seeded DRNG is enough.

The only situation where blocking_pool might be more secure is with pre-4.17 kernels that have the CONFIG_RANDOM_TRUST_CPU flag set during compile time, and if the CPU HWRNG happened to have a backdoor. Since in that case the ChaCha20 DRNG is initially seeded with RDSEED/RDRAND instruction, bad CPU HWRNG would be a problem. However, according to page page 134 of the BSI report:

[As of kernel version 4.17] The Linux-RNG now considers the ChaCha20 DRNG fully seeded after it received 128 bit of entropy from the noise sources. Previously it was sufficient that it received at least 256 interrupts.

Thus the ChaCha20 DRNG wouldn't be considered fully seeded until entropy is also mixed from input_pool, that pools and mixes random events from all LRNG noise sources together.

By using os.getrandom() with flags 2 or 3, the entropy comes from blocking_pool, that receives entropy from input_pool, that in turn receives entropy from several additional noise sources. The ChaCha20 DRNG is reseeded also from the input_pool, thus the CPU RNG does not have permanent control over the DRNG state. Once this happens, ChaCha20 DRNG is as secure as blocking_pool.

os.getrandom(32, flags=2)

GRND_NONBLOCK =  0  (=Return 32 bytes or less if entropy counter of blocking_pool is low. Block if no entropy is available.)
GRND_RANDOM   = 1   (=Use blocking_pool)
              = 10  (=flag 2)

This needs an external loop that runs the function and stores returned bytes into a buffer until the buffer size is 32 bytes. The major problem here is due to the blocking behavior of the blocking_pool, obtaining the bytes needed might take a very long time, especially if other programs are also requesting random numbers from the same syscall or /dev/random. Another issue is loop that uses os.getrandom(32, flags=2) spends more time idle waiting for random bytes than it would with flag 3 (see below).

os.getrandom(32, flags=3)

GRND_NONBLOCK =  1  (=return 32 bytes or less if entropy counter of blocking_pool is low. If no entropy is available, raise BlockingIOError instead of blocking).
GRND_RANDOM   = 1   (=use blocking_pool)
              = 11  (=flag 3)

Useful if the application needs to do other tasks while it waits for blocking_pool to have some amount of entropy. Needs the try-except logic around it plus an external loop that runs the function and stores returned bytes into a buffer until the buffer size is 32 bytes.

Other

open('/dev/urandom', 'rb').read(32)

To ensure backwards compatibility, unlike GETRANDOM() with ChaCha20 DRNG, reading from /dev/urandom device file never blocks. There is no guarantee for the quality of random numbers, which is bad. This is the least recommended option.

os.urandom(32)

os.urandom(n) provides best effort security:

Python3.6

On Linux 3.17 and newer, os.urandom(32) is the equivalent of os.getrandom(32, flags=0). On older kernels it quietly falls back to the equivalent of open('/dev/urandom', 'rb').read(32) which is not good.

os.getrandom(32, flags=0) should be preferred as it can not fall back to insecure mode.

Python3.5 and earlier

Always the equivalent of open('/dev/urandom', 'rb').read(32) which is not good. As os.getrandom() is not available, Python3.5 should not be used.

secrets.token_bytes(32) (Python 3.6 only)

Wrapper for os.urandom(). Default length of keys is 32 bytes (256 bits). On Linux 3.17 and newer, secrets.token_bytes(32) is the equivalent of os.getrandom(32, flags=0). On older kernels it quietly falls back to the equivalent of open('/dev/urandom', 'rb').read(32) which is not good.

Again, os.getrandom(32, flags=0) should be preferred as it can not fall back to insecure mode.

tl;dr

Use os.getrandom(32, flags=0).

What about other RNG sources, random, SystemRandom() etc?

import random
random.<anything>()

is never safe for creating passwords, cryptographic keys etc.

import random
sys_rand = random.SystemRandom()

is safe for cryptographic use WITH EXCEPTIONS!

sys_rand.sample()

Generating a random password with sys_rand.sample(list_of_password_chars, counts=password_length) is not safe because to quote the documentation, the sample() method is used for "random sampling without replacement". This means that each consequtive character in the password is guaranteed not to contain any of the previous characters. This will lead to passwords that are not uniformly random.

sys_rand.choices()

The sample() method was used for random sampling without replacement. The choices() method is used for random sampling with replacement. However, the to quote the documentation on choices,

The algorithm used by choices() uses floating point arithmetic for internal consistency and speed. The algorithm used by choice() defaults to integer arithmetic with repeated selections to avoid small biases from round-off error.

The floating point arithmetic choices() method uses thus introduces cryptographically non-negligible biases to the sampled passwords. Thus, random.choices() must not be used for password/key generation!

sys_random.choice()

As per the previously quoted piece of documentation, the sys_random.choice() method uses integer arithmetic as opposed to floating point arithmetic, thus generating passwords/keys with repeated calls to sys_random.choice() is therefore safe.

secrets.choice()

The secrets.choice() is a wrapper for sys_random.choice(), and can be used interchangeably with random.SystemRandom().choice(): they are the same thing.

The recipe for best practice to generate a passphrase with secrets.choice() is

import secrets
# On standard Linux systems, use a convenient dictionary file.
# Other platforms may need to provide their own word-list.
with open('/usr/share/dict/words') as f:
    words = [word.strip() for word in f]
    passphrase = ' '.join(secrets.choice(words) for i in range(4))

How can I ensure the generated passphrase meets some security level, e.g. 128 bits?

Here's a recipe for that

import math
import secrets

def generate_passphrase() -> str:
    
    PASSWORD_MIN_BIT_STRENGTH = 128  # Set desired minimum bit strength here

    with open('/usr/share/dict/words') as f:
        wordlist = [word.strip() for word in f]

    word_space = len(wordlist)
    word_count = math.ceil(math.log(2 ** PASSWORD_MIN_BIT_STRENGTH, word_space))

    passphrase = ' '.join(secrets.choice(wordlist) for _ in range(word_count))

    # pwd_bit_strength = math.floor(math.log2(word_space ** word_count))
    # print(f"Generated {pwd_bit_strength}-bit passphrase.")

    return passphrase
0
votes

As @maqp suggested...

Using os.getrandom(32, flags=0) is logical choice unless you're using the new secrets AND the Linux kernel (3.17 and newer) does NOT fall back to open('dev/urandom', 'rb').read(32).

Workaround, Secrets on Python 3.5.x

I installed Secrets for Python 2 even though running Python 3 and at a glance Secrets is working in Python 3.5.2 environment. Perhaps if I get time, or someone else does they can learn whether this one falls back, I suppose if Linux kernel is below certain version it may occur.

pip install python2-secrets

Once completed you can import secrets just like you would have with the Python 3 flavor.

Or just make sure to use Linux Kernel 3.17 & newer. Knowing one's kernel is always good practice, but in reality we count on smart people like maqp to find & share these things. Great job.

Were we complacent... having false sense of security?*

1st, 2nd, 4th... where is the outrage? It's not about 'your' security, that would be selfish to assume. It's the ability to spy on those who represent you in government, those who have skeletons & weaknesses (humans). Be sure to correct those selfish ones that say, "I got nothing to hide".

How Bad Was It?

The strength of encryption increases exponentially as length of key increases, therefore is it reasonable to assume that a reduction by of at least half, say 256 down to 128 would equate to decrease in strength by factors of tens, hundreds, thousands or more? Did it make big-bro job pretty easy, or just a tiny bit easier, am leaning towards saying the former.

The Glass Half Full?

Oh well, at least Linux is open source & we can see the insides for the most part. We still need chip hackers to find secret stuff, and the chips & hardware drivers are where you'll probably find stuff that will keep you from sleeping at night.