PEP 522: Make secrets API block implicitly

- the PEP now proposes that the secrets module block
  instead of raising BlockingIOError
- this makes secrets.token_bytes a blocking counterpart
  to os.urandom that's a dropin replacement
- drop discussions of behaviour of interpreter startup
  and random module import as already addressed
- note os.getrandom() is useful regardless of outcome
  of this PEP
This commit is contained in:
Nick Coghlan 2016-08-06 20:01:41 +10:00
parent a919235364
commit 5392cf9fb8
1 changed files with 145 additions and 152 deletions

View File

@ -24,12 +24,19 @@ permitting reads from ``/dev/urandom`` before the system random number
generator is fully initialized, whereas most other operating systems will
implicitly block on such reads until the random number generator is ready.
This PEP proposes changing such failures in Python 3.6 from the current silent,
For the lower level ``os.urandom`` and ``random.SystemRandom`` APIs, this PEP
proposes changing such failures in Python 3.6 from the current silent,
hard to detect, and hard to debug, errors to easily detected and debugged errors
by raising ``BlockingIOError`` with a suitable error message, allowing
developers the opportunity to unambiguously specify their preferred approach
for handling the situation.
For the new high level ``secrets`` API, it proposes to block implicitly if
needed whenever random number is generated by that module, as well as to
expose a new ``secrets.wait_for_system_rng()`` function to allow code otherwise
using the low level APIs to explicitly wait for the system random number
generator to be available.
This change will impact any operating system that offers the ``getrandom()``
system call, regardless of whether the default behaviour of the
``/dev/urandom`` device is to return potentially predictable results when the
@ -39,19 +46,8 @@ userspace code prior to the initialization of the system random number
generator, or do not offer the ``getrandom()`` syscall, will be entirely
unaffected by the proposed change (e.g. Windows, Mac OS X, OpenBSD).
The APIs affected by this change would be:
* ``os.urandom``
* ``random.SystemRandom``
* the new ``secrets`` module added by PEP 506
A new ``secrets.wait_for_system_rng()`` API would be added to allow affected
applications, frameworks and scripts that encounter the new exception to
readily opt-in to blocking behaviour if they so choose, either by modifying
the affected application directly, or by running a preceding
``python3 -c "import secrets; secrets.wait_for_system_rng()"`` command.
The new exception would potentially be encountered in the following situations:
The new exception or the blocking behaviour in the ``secrets`` module would
potentially be encountered in the following situations:
* Python code calling these APIs during Linux system initialization
* Python code running on improperly initialized Linux systems (e.g. embedded
@ -59,19 +55,28 @@ The new exception would potentially be encountered in the following situations:
generator, or Linux VMs that aren't configured to accept entropy from the
VM host)
CPython interpreter initialization and ``random`` module initialization would
also be updated to gracefully fall back to alternative seeding options if the
system random number generator is not ready.
Relationship with other PEPs
============================
This PEP depends on the Accepted PEP 506, which adds the ``secrets`` module.
This PEP competes with Victor Stinner's `currently unnumbered proposal
<http://haypo-notes.readthedocs.io/pep_random.html>`_ to make
``os.urandom`` implicitly block when the system RNG is not ready.
This PEP competes with Victor Stinner's PEP 524, which proposes to make
``os.urandom`` itself implicitly block when the system RNG is not ready.
Changes independent of this PEP
===============================
CPython interpreter initialization and ``random`` module initialization have
already been updated to gracefully fall back to alternative seeding options if
the system random number generator is not ready.
This PEP does not compete with the proposal in PEP 524 to add an
``os.getrandom()`` API to expose the ``getrandom`` syscall on platforms that
offer it. There is sufficient motive for adding that API in the ``os`` module's
role as a thin wrapper around potentially platform dependent operating system
features that it can be added regardless of what happens to the default
behaviour of ``os.urandom()`` on these systems.
Proposal
@ -81,18 +86,22 @@ Changing ``os.urandom()`` on platforms with the getrandom() system call
-----------------------------------------------------------------------
This PEP proposes that in Python 3.6+, ``os.urandom()`` be updated to call
the ``getrandom()`` syscall in non-blocking mode if available and
raise ``BlockingIOError: system random number generator is not ready`` if
the kernel reports that the call would block.
the ``getrandom()`` syscall in non-blocking mode if available and raise
``BlockingIOError: system random number generator is not ready; see 'secrets.token_bytes()'``
if the kernel reports that the call would block.
This behaviour will then propagate through to higher level standard library
APIs that depend on ``os.urandom`` (specifically ``random.SystemRandom`` and
the new ``secrets`` module introduced by PEP 506).
This behaviour will then propagate through to the existing
``random.SystemRandom``, which provides a relatively thin wrapper around
``os.urandom()`` that matches the ``random.Random()`` API.
However, the new ``secrets`` module introduced by PEP 506 will be updated to
catch the new exception and implicitly wait for the system random number
generator if the exception is ever encountered.
In all cases, as soon as a call to one of these security sensitive APIs
succeeds, all future calls to these APIs in that process will succeed (once
the operating system random number generator is ready after system boot, it
remains ready).
succeeds, all future calls to these APIs in that process will succeed
without blocking (once the operating system random number generator is ready
after system boot, it remains ready).
On Linux and NetBSD, this will replace the previous behaviour of returning
potentially predictable results read from ``/dev/urandom``.
@ -102,9 +111,9 @@ implicitly blocking until the system random number generator is ready. However,
it is not clear if these operating systems actually allow userspace code (and
hence Python) to run before the system random number generator is ready.
Note that in all cases, if calling the ``getrandom()`` API reports ``ENOSYS``
rather than returning a successful response or reporting ``EAGAIN``, CPython
will continue to fall back to reading from ``/dev/urandom`` directly.
Note that in all cases, if calling the underlying ``getrandom()`` API reports
``ENOSYS`` rather than returning a successful response or reporting ``EAGAIN``,
CPython will continue to fall back to reading from ``/dev/urandom`` directly.
Adding ``secrets.wait_for_system_rng()``
@ -113,7 +122,8 @@ Adding ``secrets.wait_for_system_rng()``
A new exception shouldn't be added without a straightforward recommendation
for how to resolve that error when encountered (however rare encountering
the new error is expected to be in practice). For security sensitive code that
actually does need to use the system random number generator, and does receive
actually does need to use the lower level interfaces to the system random
number generator (rather than the new ``secrets`` module), and does receive
live bug reports indicating this is a real problem for the userbase of that
particular application rather than a theoretical one, this PEP's recommendation
will be to add the following snippet (directly or indirectly) to the
@ -131,10 +141,29 @@ Or, if compatibility with versions prior to Python 3.6 is needed::
else:
secrets.wait_for_system_rng()
Within the ``secrets`` module itself, this will then be used in
``token_bytes()`` to block implicitly if the new exception is encountered::
def token_bytes(nbytes=None):
if nbytes is None:
nbytes = DEFAULT_ENTROPY
try:
result = os.urandom(nbytes)
except BlockingIOError:
wait_for_system_rng()
result = os.urandom(nbytes)
return result
Other parts of the module will then be updated to use ``token_bytes()`` as
their basic random number generation building block, rather than calling
``os.urandom()`` directly.
Application frameworks covering use cases where access to the system random
number generator is almost certain to be needed (e.g. web frameworks) may
choose to incorporate this step implicitly into the commands that start the
application.
choose to incorporate a call to ``secrets.wait_for_system_rng()`` implicitly
into the commands that start the application such that existing calls to
``os.urandom()`` will be guaranteed to never raise the new exception when using
those frameworks.
For cases where the error is encountered for an application which cannot be
modified directly, then the following command can be used to wait for the
@ -147,62 +176,50 @@ For example, this snippet could be added to a shell script or a systemd
system random number generator to be ready, even if the subsequent command
is not itself an application running under Python 3.6)
Given the changes proposed to ``os.urandom()`` above, the suggested
Given the changes proposed to ``os.urandom()`` above, and the inclusion of
an ``os.getrandom()`` API on systems that support it, the suggested
implementation of this function would be::
def wait_for_system_rng():
"""Block waiting for system random number generator to be ready"""
# If the system RNG is already seeded, don't wait at all
try:
os.urandom(1)
if hasattr(os, "getrandom"):
# os.getrandom() always blocks waiting for the system RNG by default
def wait_for_system_rng():
"""Block waiting for system random number generator to be ready"""
os.getrandom(1)
return
except BlockingIOError:
pass
# Avoid the below busy loop if possible
try:
block_on_system_rng = open("/dev/random", "rb")
except FileNotFoundError:
pass
else:
with block_on_system_rng:
block_on_system_rng.read(1)
# Busy loop until the system RNG is ready
while True:
else:
# As far as we know, other platforms will never get BlockingIOError
# below but the implementation makes pessimistic assumptions
def wait_for_system_rng():
"""Block waiting for system random number generator to be ready"""
# If the system RNG is already seeded, don't wait at all
try:
os.urandom(1)
break
return
except BlockingIOError:
# Only check once per millisecond
time.sleep(0.001)
pass
# Avoid the below busy loop if possible
try:
block_on_system_rng = open("/dev/random", "rb")
except FileNotFoundError:
pass
else:
with block_on_system_rng:
block_on_system_rng.read(1)
# Busy loop until the system RNG is ready
while True:
try:
os.urandom(1)
break
except BlockingIOError:
# Only check once per millisecond
time.sleep(0.001)
On systems where it is possible to wait for the system RNG to be ready, this
function will do so without a busy loop if either ``os.urandom()``
itself implicitly blocks, or the ``/dev/random`` device is available. If the
system random number generator is ready, this call is guaranteed to never
block, even if the system's ``/dev/random`` device uses a design that permits
it to block intermittently during normal system operation.
Related changes
---------------
Currently, SipHash initialization and ``random`` module initialization
both gather random bytes using the same code that underlies
``os.urandom``. This PEP proposes to modify these so that in situations where
``os.urandom`` would raise a ``BlockingIOError``, they automatically
fall back on potentially more predictable sources of randomness.
In the SipHash case, this will also print a warning message to ``stderr``
indicating that that particular Python process should not be used to process
untrusted data: "Python reverted to potentially predictable hash
initialization. Avoid handling untrusted data in this process.". This
warning would NOT be displayed when hash randomization is explicitly disabled
or set to a known value via ``PYTHONHASHSEED``.
To transparently accommodate a potential future where Linux adopts the same
"potentially blocking during system initialization" ``/dev/urandom`` behaviour
used by other \*nix systems, this fallback source of randomness will *not* be
the ``/dev/urandom`` device.
function will do so without a busy loop if ``os.getrandom()`` is defined,
``os.urandom()`` itself implicitly blocks, or the ``/dev/random`` device is
available. If the system random number generator is ready, this call is
guaranteed to never block, even if the system's ``/dev/random`` device uses
a design that permits it to block intermittently during normal system operation.
Limitations on scope
@ -232,6 +249,21 @@ the new ``getrandom()`` syscall will also remain unchanged.
Rationale
=========
Ensuring the ``secrets`` module implicitly blocks when needed
-------------------------------------------------------------
This is done to help encourage the meme that arises for folks that want the
simplest possible answer to the right way to generate security sensitive random
numbers to be "Use the secrets module when available or your application might
crash unexpectedly", rather than the more boilerplate heavy "Always call
secrets.wait_for_system_rng() when available or your application might crash
unexpectedly".
It's also done due to the BDFL having a higher tolerance for APIs that might
block unexpectedly than he does for APIs that might throw an unexpected
exception [11]_.
Raising ``BlockingIOError`` in ``os.urandom()`` on Linux
--------------------------------------------------------
@ -278,8 +310,8 @@ developers using Python 3.6+ can easily choose their desired behaviour:
2. Switch to using the random module (non-security sensitive)
Adding ``secrets.wait_for_system_rng()``
----------------------------------------
Making ``secrets.wait_for_system_rng()`` public
-----------------------------------------------
Earlier versions of this PEP proposed a number of recipes for wrapping
``os.urandom()`` to make it suitable for use in security sensitive use cases.
@ -305,58 +337,6 @@ developers could continue to call ``os.urandom()`` without worrying that it
might unexpectedly start blocking waiting for the system RNG to be available.
Issuing a warning for potentially predictable internal hash initialization
--------------------------------------------------------------------------
The challenge for internal hash initialization is that it might be very
important to initialize SipHash with a reliably unpredictable random seed
(for processes that are exposed to potentially hostile input) or it might be
totally unimportant (for processes that never have to deal with untrusted data).
The Python runtime has no way to know which case a given invocation involves,
which means that if we allow SipHash initialization to block or error out,
then our intended security enhancement may break code that is already safe
and working fine, which is unacceptable -- especially since we are reasonably
confident that most Python invocations that might run during Linux system
initialization fall into this category (exposure to untrusted input tends to
involve network access, which typically isn't brought up until after the system
random number generator is initialized).
However, at the same time, since Python has no way to know whether any given
invocation needs to handle untrusted data, when the default SipHash
initialization fails this *might* indicate a genuine security problem, which
should not be allowed to pass silently.
Accordingly, if internal hash initialization needs to fall back to a potentially
predictable seed due to the system random number generator not being ready, it
will also emit a warning message on ``stderr`` to say that the system random
number generator is not available and that processing potentially hostile
untrusted data should be avoided.
Allowing potentially predictable ``random`` module initialization
-----------------------------------------------------------------
Other than for ``random.SystemRandom`` (which is a relatively thin
wrapper around ``os.urandom``), the ``random`` module has long documented
that the numbers it generates are not suitable for use in security sensitive
operations. Instead, the use of the system random number generator to seed the
default Mersenne Twister instance is primarily aimed at ensuring Python isn't
biased towards any particular starting states for simulation use cases.
However, this seeding approach has also turned out to be beneficial as a harm
mitigation measure for code that is using the ``random`` module inappropriately.
Since a single call to ``os.urandom()`` is cheap once the system random
number generator has been initialized it makes sense to retain that as the
default behaviour, but there's no need to issue a warning when falling back to
a potentially more predictable alternative when necessary (in such cases,
a warning will typically already have been issued as part of interpreter
startup, as the only way for the call when importing the random module to
fail without the implicit call during interpreter startup also failing if for
the latter to have been skipped by entirely disabling the hash randomization
mechanism).
Backwards Compatibility Impact Assessment
=========================================
@ -394,7 +374,9 @@ regardless of whether or not they perform security sensitive operations:
- applications that don't support Linux
- applications that are only run on desktops or conventional servers
- applications that are only run after the system RNG is ready
- applications that are only run after the system RNG is ready (including
those where an application framework calls ``secrets.wait_for_system_rng()``
on their behalf)
Applications in this category simply won't encounter the new exception, so it
will be reasonable for developers to wait and see if they receive
@ -407,14 +389,23 @@ Affected security sensitive applications
Security sensitive applications would need to either change their system
configuration so the application is only started after the operating system
random number generator is ready for security sensitive operations, or else
change the application startup code to invoke ``secrets.wait_for_system_rng()``
random number generator is ready for security sensitive operations, change the
application startup code to invoke ``secrets.wait_for_system_rng()``, or
else switch to using the new ``secrets.token_bytes()`` API.
As an example for components started via a systemd unit file, the following
snippet would delay activation until the system RNG was ready:
ExecStartPre=python3 -c "import secrets; secrets.wait_for_system_rng()"
Alternatively, the following snippet will use ``secrets.token_bytes()`` if
available, and fall back to ``os.urandom()`` otherwise:
try:
import secrets.token_bytes as _get_random_bytes
except ImportError:
import os.urandom as _get_random_bytes
Affected non-security sensitive applications
--------------------------------------------
@ -671,8 +662,8 @@ decision has to be made:
* the higher level ``random.SystemRandom`` public API
* the new ``secrets`` module public API added by PEP 506
Currently, these five places all use the same underlying code, and
thus make this decision in the same way.
Previously, these five places all used the same underlying code, and
thus made this decision in the same way.
This whole problem was first noticed because 3.5.0 switched that
underlying code to the ``generate_unpredictable_bytes_or_block`` behavior,
@ -705,15 +696,14 @@ We have not received any specific complaints regarding direct calls to
only problem reports due to the implicit blocking on interpreter startup and
as a side-effect of importing the random module.
Accordingly, this PEP proposes providing consistent shared behaviour for the
latter three cases (ensuring that their behaviour is unequivocally suitable for
all security sensitive operations), while updating the first two cases to
account for that behavioural change.
Independently of this PEP, the first two cases have already been updated to
never block, regardless of the behaviour of ``os.urandom()``.
This approach should mean that the vast majority of Python users never need to
even be aware that this change was made, while those few whom it affects will
receive an exception at runtime that they can look up online and find suitable
guidance on addressing.
Where PEP 524 proposes to make all 3 of the latter cases block implicitly,
this PEP proposes that approach only for the last case (the ``secrets``)
module, with ``os.urandom()`` and ``random.SystemRandom()`` instead raising
an exception when they detect that the underlying operating system call
would block.
References
@ -751,6 +741,9 @@ References
.. [10] Does the HAVE_GETRANDOM_SYSCALL config setting make sense?
(https://mail.python.org/pipermail/security-sig/2016-June/000060.html)
.. [11] Take a decision for os.urandom() in Python 3.6
(https://mail.python.org/pipermail/security-sig/2016-August/000084.htm)
For additional background details beyond those captured in this PEP and Victor's
competing PEP, also see Victor's prior collection of relevant information and