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:
parent
a919235364
commit
5392cf9fb8
297
pep-0522.txt
297
pep-0522.txt
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue