PEP 522 – Allow BlockingIOError in sicherheitskritischen APIs
- Autor:
- Alyssa Coghlan <ncoghlan at gmail.com>, Nathaniel J. Smith <njs at pobox.com>
- Status:
- Abgelehnt
- Typ:
- Standards Track
- Benötigt:
- 506
- Erstellt:
- 16. Juni 2016
- Python-Version:
- 3.6
- Resolution:
- Security-SIG Nachricht
Zusammenfassung
Eine Reihe von APIs in der Standardbibliothek, die Zufallswerte zurückgeben, die nominell für den Einsatz in sicherheitskritischen Operationen geeignet sind, haben derzeit einen obskuren betriebssystemabhängigen Fehlerfall, der es ihnen ermöglicht, Werte zurückzugeben, die für solche Operationen nicht geeignet sind.
Dies liegt daran, dass einige Betriebssystemkerne (insbesondere der Linux-Kernel) das Lesen von /dev/urandom zulassen, bevor die Zufallszahlengenerierung des Systems vollständig initialisiert ist, während die meisten anderen Betriebssysteme bei solchen Lesevorgängen implizit blockieren, bis die Zufallszahlengenerierung bereit ist.
Für die Low-Level-APIs os.urandom und random.SystemRandom schlägt dieser PEP vor, solche Fehler in Python 3.6 von den aktuellen stillen, schwer zu erkennenden und schwer zu debuggenden Fehlern zu leicht erkennbaren und zu debuggenden Fehlern zu ändern, indem BlockingIOError mit einer geeigneten Fehlermeldung ausgelöst wird, was Entwicklern die Möglichkeit gibt, ihren bevorzugten Ansatz zur Handhabung der Situation eindeutig anzugeben.
Für die neue High-Level-API secrets schlägt sie vor, bei Bedarf implizit zu blockieren, wann immer Zufallszahlen von diesem Modul generiert werden, sowie eine neue Funktion secrets.wait_for_system_rng() bereitzustellen, damit Code, der anderweitig die Low-Level-APIs verwendet, explizit auf die Verfügbarkeit der Zufallszahlengenerierung des Systems wartet.
Diese Änderung wirkt sich auf jedes Betriebssystem aus, das den getrandom()-Systemaufruf bietet, unabhängig davon, ob das Standardverhalten des /dev/urandom-Geräts darin besteht, potenziell vorhersagbare Ergebnisse zurückzugeben, wenn die Zufallszahlengenerierung des Systems nicht bereit ist (z. B. Linux, NetBSD), oder zu blockieren (z. B. FreeBSD, Solaris, Illumos). Betriebssysteme, die die Ausführung von Userspace-Code vor der Initialisierung der Zufallszahlengenerierung des Systems verhindern oder den getrandom()-Systemaufruf nicht anbieten, bleiben von der vorgeschlagenen Änderung völlig unberührt (z. B. Windows, Mac OS X, OpenBSD).
Die neue Ausnahme oder das blockierende Verhalten im secrets-Modul könnte potenziell in den folgenden Situationen auftreten:
- Python-Code, der diese APIs während der Linux-Systeminitialisierung aufruft
- Python-Code, der auf falsch initialisierten Linux-Systemen läuft (z. B. eingebettete Hardware ohne ausreichende Entropiequellen zur Zuführung des Zufallszahlengenerators des Systems oder Linux-VMs, die nicht so konfiguriert sind, dass sie Entropie vom VM-Host akzeptieren)
Beziehung zu anderen PEPs
Dieser PEP baut auf dem akzeptierten PEP 506 auf, der das secrets-Modul hinzufügt.
Dieser PEP konkurriert mit Victor Stinners PEP 524, der vorschlägt, os.urandom selbst implizit blockieren zu lassen, wenn die System-RNG nicht bereit ist.
Ablehnung der PEP
Für die Referenzimplementierung hat Guido diesen PEP zugunsten des bedingungslosen impliziten Blockierens im PEP 524 (der das Verhalten von CPython unter Linux an das Verhalten auf anderen Betriebssystemen anpasst) abgelehnt.
Das bedeutet, dass jede weitere Diskussion über das angemessene Standardverhalten von os.urandom() in System-Python-Installationen von Linux-Distributionen auf den jeweiligen Distro-Mailinglisten und nicht auf den Upstream-CPython-Mailinglisten stattfinden sollte.
Änderungen unabhängig von diesem PEP
Die CPython-Interpreterinitialisierung und die Initialisierung des random-Moduls wurden bereits aktualisiert, um bei Bedarf auf alternative Seed-Optionen zurückzugreifen, wenn der Zufallszahlengenerator des Systems nicht bereit ist.
Dieser PEP konkurriert nicht mit dem Vorschlag in PEP 524, eine os.getrandom()-API hinzuzufügen, um den getrandom-Systemaufruf auf Plattformen verfügbar zu machen, die ihn anbieten. Es gibt ausreichende Gründe, diese API im Rahmen der Rolle des os-Moduls als dünne Hülle um potenziell plattformabhängige Betriebssystemfunktionen hinzuzufügen, sodass sie hinzugefügt werden kann, unabhängig davon, was mit dem Standardverhalten von os.urandom() auf diesen Systemen geschieht.
Vorschlag
Ändern von os.urandom() auf Plattformen mit dem getrandom() Systemaufruf
Dieser PEP schlägt vor, dass in Python 3.6+ os.urandom() so aktualisiert wird, dass es den getrandom()-Systemaufruf im nicht-blockierenden Modus aufruft, falls verfügbar, und BlockingIOError: system random number generator is not ready; see secrets.token_bytes() auslöst, wenn der Kernel meldet, dass der Aufruf blockieren würde.
Dieses Verhalten wird sich dann auf das bestehende random.SystemRandom auswirken, das eine relativ dünne Hülle um os.urandom() bereitstellt, die der random.Random() API entspricht.
Das neue secrets-Modul, das durch PEP 506 eingeführt wurde, wird jedoch so aktualisiert, dass es die neue Ausnahme abfängt und implizit auf die Zufallszahlengenerierung des Systems wartet, wenn die Ausnahme jemals auftritt.
In allen Fällen, sobald ein Aufruf einer dieser sicherheitskritischen APIs erfolgreich ist, werden alle zukünftigen Aufrufe dieser APIs in diesem Prozess erfolgreich sein, ohne zu blockieren (sobald die Zufallszahlengenerierung des Betriebssystems nach dem Systemstart bereit ist, bleibt sie bereit).
Unter Linux und NetBSD ersetzt dies das frühere Verhalten, potenziell vorhersagbare Ergebnisse von /dev/urandom zurückzugeben.
Unter FreeBSD, Solaris und Illumos ersetzt dies das frühere Verhalten, bis die Zufallszahlengenerierung des Systems bereit ist, implizit zu blockieren. Es ist jedoch nicht klar, ob diese Betriebssysteme Userspace-Code (und damit Python) überhaupt vor der Initialisierung der Zufallszahlengenerierung des Systems ausführen lassen.
Beachten Sie, dass in allen Fällen, in denen der zugrunde liegende getrandom()-API ENOSYS meldet, anstatt eine erfolgreiche Antwort zurückzugeben oder EAGAIN zu melden, CPython weiterhin direkt auf /dev/urandom zurückgreifen wird.
Hinzufügen von secrets.wait_for_system_rng()
Eine neue Ausnahme sollte nicht hinzugefügt werden, ohne eine einfache Empfehlung, wie dieser Fehler behoben werden kann, wenn er auftritt (auch wenn das Auftreten des neuen Fehlers in der Praxis voraussichtlich selten ist). Für sicherheitskritischen Code, der tatsächlich die Low-Level-Schnittstellen zur Zufallszahlengenerierung des Systems nutzen muss (anstelle des neuen secrets-Moduls) und Live-Bug-Reports erhält, die darauf hinweisen, dass dies ein echtes Problem für die Benutzerbasis dieser speziellen Anwendung und nicht nur ein theoretisches Problem ist, wird die Empfehlung dieses PEP sein, den folgenden Snippet (direkt oder indirekt) zum __main__-Modul hinzuzufügen:
import secrets
secrets.wait_for_system_rng()
Oder, wenn Kompatibilität mit Versionen vor Python 3.6 benötigt wird
try:
import secrets
except ImportError:
pass
else:
secrets.wait_for_system_rng()
Innerhalb des secrets-Moduls selbst wird dies dann in token_bytes() verwendet, um implizit zu blockieren, wenn die neue Ausnahme auftritt
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
Andere Teile des Moduls werden dann aktualisiert, um token_bytes() als grundlegenden Baustein für die Zufallszahlengenerierung zu verwenden, anstatt os.urandom() direkt aufzurufen.
Anwendungsframeworks, die Anwendungsfälle abdecken, bei denen der Zugriff auf die Zufallszahlengenerierung des Systems fast sicher benötigt wird (z. B. Webframeworks), können sich dafür entscheiden, einen Aufruf von secrets.wait_for_system_rng() implizit in die Befehle einzubauen, die die Anwendung starten, so dass bestehende Aufrufe von os.urandom() garantiert niemals die neue Ausnahme auslösen, wenn diese Frameworks verwendet werden.
Für Fälle, in denen der Fehler bei einer Anwendung auftritt, die nicht direkt geändert werden kann, kann der folgende Befehl verwendet werden, um auf die Zufallszahlengenerierung des Systems zu warten, bevor diese Anwendung gestartet wird:
python3 -c "import secrets; secrets.wait_for_system_rng()"
Zum Beispiel könnte dieser Snippet zu einem Shell-Skript oder einem systemd ExecStartPre Hook hinzugefügt werden (und kann sich als nützlich erweisen, um zuverlässig auf die Bereitschaft der Zufallszahlengenerierung des Systems zu warten, auch wenn der nachfolgende Befehl selbst keine unter Python 3.6 laufende Anwendung ist)
Angesichts der oben vorgeschlagenen Änderungen an os.urandom() und der Aufnahme einer os.getrandom()-API auf Systemen, die sie unterstützen, wäre die vorgeschlagene Implementierung dieser Funktion:
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
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)
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:
try:
os.urandom(1)
break
except BlockingIOError:
# Only check once per millisecond
time.sleep(0.001)
Auf Systemen, auf denen es möglich ist, auf die Bereitschaft der System-RNG zu warten, wird diese Funktion ohne Busy-Loop durchgeführt, wenn os.getrandom() definiert ist, os.urandom() selbst implizit blockiert oder das Gerät /dev/random verfügbar ist. Wenn die Zufallszahlengenerierung des Systems bereit ist, wird dieser Aufruf garantiert niemals blockieren, auch wenn das /dev/random-Gerät des Systems ein Design verwendet, das es im normalen Systembetrieb intermittierend blockieren lässt.
Umfangsbeschränkungen
Für Windows- oder Mac-OS-X-Systeme sind keine Änderungen vorgesehen, da keine dieser Plattformen einen Mechanismus bietet, um Python-Code auszuführen, bevor die Zufallszahlengenerierung des Betriebssystems initialisiert wurde. Mac OS X geht so weit, einen Kernel-Panic auszulösen und den Bootvorgang abzubrechen, wenn die Zufallszahlengenerierung nicht ordnungsgemäß initialisiert werden kann (obwohl Apples Beschränkungen auf den unterstützten Hardwareplattformen dies in der Praxis äußerst unwahrscheinlich machen).
Ebenso sind keine Änderungen für andere *nix-Systeme vorgesehen, die den getrandom()-Systemaufruf nicht anbieten. Auf diesen Systemen wird os.urandom() weiterhin blockieren und auf die Initialisierung der Zufallszahlengenerierung des Systems warten.
Während andere *nix-Systeme, die eine nicht-blockierende API (außer getrandom()) zum Anfordern von Zufallszahlen für sicherheitskritische Anwendungen anbieten, potenziell ein ähnliches Update wie das für getrandom() in diesem PEP vorgeschlagene erhalten könnten, liegt ein solches Vorgehen außerhalb des Geltungsbereichs dieses speziellen Vorschlags.
Das Verhalten von Python auf älteren Versionen betroffener Plattformen, die den neuen getrandom()-Systemaufruf nicht anbieten, bleibt ebenfalls unverändert.
Begründung
Sicherstellen, dass das secrets-Modul bei Bedarf implizit blockiert
Dies geschieht, um die Verbreitung der Vorstellung zu fördern, dass die einfachste Antwort auf die Frage nach der richtigen Methode zur Generierung sicherheitskritischer Zufallszahlen lautet: „Verwenden Sie das Secrets-Modul, wenn verfügbar, andernfalls kann Ihre Anwendung unerwartet abstürzen“, anstatt des eher umständlichen „Rufen Sie immer secrets.wait_for_system_rng() auf, wenn verfügbar, andernfalls kann Ihre Anwendung unerwartet abstürzen“.
Dies geschieht auch, weil der BDFL eine höhere Toleranz für APIs hat, die unerwartet blockieren können, als für APIs, die unerwartete Ausnahmen auslösen können [11].
Auslösen von BlockingIOError in os.urandom() unter Linux
Seit mehreren Jahren lautet die Empfehlung der Sicherheits-Community, os.urandom() (oder die random.SystemRandom-Hülle) zu verwenden, wenn sicherheitskritische Operationen in Python implementiert werden.
Um die Auffindbarkeit von APIs zu verbessern und deutlicher zu machen, dass Geheimhaltung und Simulation nicht dasselbe Problem sind (obwohl beides mit Zufallszahlen zu tun hat), hat PEP 506 mehrere Einzeiler-Rezepte, die auf der Low-Level-API os.urandom() basieren, in einem neuen Modul secrets gesammelt.
Diese Empfehlung ist jedoch mit einem langjährigen Vorbehalt verbunden: Entwickler, die sicherheitskritische Software schreiben, müssen möglicherweise mindestens unter Linux und potenziell auch unter einigen anderen *BSD-Systemen warten, bis die Zufallszahlengenerierung des Betriebssystems bereit ist, bevor sie sich auf sie für sicherheitskritische Operationen verlassen. Dies geschieht im Allgemeinen nur, wenn os.urandom() sehr früh im Prozess der Systeminitialisierung gelesen wird, oder auf Systemen mit wenigen verfügbaren Entropiequellen (z. B. einige Arten von virtualisierten oder eingebetteten Systemen), aber leider sind die genauen Bedingungen, die dies auslösen, schwer vorherzusagen, und wenn es auftritt, gibt es keinen direkten Weg für den Userspace, festzustellen, dass es passiert ist, ohne betriebssystemspezifische Schnittstellen abzufragen.
Auf *BSD-Systemen (falls die jeweilige *BSD-Variante das Problem überhaupt zulässt) und potenziell auch Solaris und Illumos bedeutet das Auftreten dieser Situation, dass os.urandom() entweder darauf wartet, dass die Zufallszahlengenerierung des Systems bereit ist (das damit verbundene Symptom wäre, dass das betroffene Skript beim ersten Aufruf von os.urandom() unerwartet pausiert) oder sich genauso verhält wie unter Linux.
Unter Linux, in Python-Versionen bis einschließlich Python 3.4 und in den Wartungsversionen von Python 3.5 nach Python 3.5.2, gibt es keinen klaren Indikator für Entwickler, dass ihre Software möglicherweise nicht wie erwartet funktioniert, wenn sie früh im Linux-Bootprozess ausgeführt wird oder auf Hardware ohne gute Entropiequellen zur Zuführung des Zufallszahlengenerators des Betriebssystems: Aufgrund des Verhaltens des zugrunde liegenden /dev/urandom-Geräts gibt os.urandom() unter Linux in jedem Fall ein Ergebnis zurück, und es erfordert eine umfassende statistische Analyse, um zu zeigen, dass eine Sicherheitslücke besteht.
Wenn dagegen BlockingIOError in diesen Situationen ausgelöst wird, können Entwickler, die Python 3.6+ verwenden, einfach ihr gewünschtes Verhalten wählen:
- Warten Sie auf die System-RNG vor oder bei Anwendungsstart (sicherheitskritisch)
- Wechseln Sie zur Verwendung des Random-Moduls (nicht sicherheitskritisch)
Öffentliches machen von secrets.wait_for_system_rng()
Frühere Versionen dieses PEP schlugen eine Reihe von Rezepten für die Kapselung von os.urandom() vor, um es für sicherheitskritische Anwendungsfälle geeignet zu machen.
Die Diskussion des Vorschlags in der Security-SIG-Mailingliste führte zu der Erkenntnis [9], dass die Kernannahme, die das API-Design in diesem PEP antreibt, darin besteht, dass die Wahl zwischen dem Fehlschlagen der Anwendung aufgrund der Ausnahme, dem Warten auf die System-RNG und dem Wechsel zur Verwendung des random-Moduls anstelle von os.urandom eine anwendungs- und fallabhängige Entscheidung ist, die anwendungs- und fallabhängige Details berücksichtigen sollte.
Es gibt keine Möglichkeit für die Interpreterlaufzeit oder Support-Bibliotheken zu bestimmen, ob ein bestimmter Anwendungsfall sicherheitskritisch ist oder nicht, und obwohl es für den Anwendungsentwickler einfach ist zu entscheiden, wie eine Ausnahme einer bestimmten API behandelt werden soll, können sie eine API nicht ohne weiteres umgehen, die blockiert, wenn sie erwartet haben, dass sie nicht blockiert.
Daher wurde der PEP aktualisiert, um secrets.wait_for_system_rng() als API für Anwendungen, Skripte und Frameworks hinzuzufügen, um anzuzeigen, dass sie die System-RNG vor dem Fortfahren sicherstellen wollten, während Bibliotheksentwickler weiterhin os.urandom() aufrufen könnten, ohne sich Sorgen machen zu müssen, dass es unerwartet zu blockieren beginnt und auf die System-RNG wartet.
Bewertung der Auswirkungen auf die Abwärtskompatibilität
Ähnlich wie bei PEP 476 handelt es sich hier um einen Vorschlag, einen bisher stillen Sicherheitsfehler in eine laute Ausnahme umzuwandeln, die vom Anwendungsentwickler eine explizite Entscheidung über das gewünschte Verhalten verlangt.
Da keine Änderungen für Betriebssysteme vorgeschlagen werden, die den getrandom()-Systemaufruf nicht bieten, behält os.urandom() sein bestehendes Verhalten als nominell blockierende API, die praktisch nicht blockiert, da es schwierig ist, Python-Code so zu planen, dass er vor der Zufallszahlengenerierung des Betriebssystems ausgeführt wird. Wir glauben, dass es möglich ist, Probleme ähnlich denen, die in diesem PEP beschrieben werden, auf mindestens einigen *BSD-Varianten zu erleben, aber niemand hat dies explizit nachgewiesen. Unter Mac OS X und Windows scheint es geradezu unmöglich zu sein, einen Python-Interpreter so früh im Bootprozess zu starten.
Unter Linux und anderen Plattformen mit ähnlichem /dev/urandom-Verhalten behält os.urandom() seinen Status als garantiert nicht-blockierende API. Die Art und Weise, wie dieser Status erreicht wird, ändert sich jedoch im speziellen Fall, dass die Zufallszahlengenerierung des Betriebssystems nicht für sicherheitskritische Operationen bereit ist: Historisch gesehen gab es potenziell vorhersagbare Zufallsdaten zurück, mit diesem PEP würde es BlockingIOError auslösen.
Entwickler betroffener Anwendungen müssten dann eine der folgenden Änderungen vornehmen, um die Vorwärtskompatibilität mit Python 3.6 zu erreichen, basierend auf der Art ihrer Anwendung:
Unbetroffene Anwendungen
Die folgenden Arten von Anwendungen wären von der Änderung völlig unberührt, unabhängig davon, ob sie sicherheitskritische Operationen ausführen oder nicht:
- Anwendungen, die Linux nicht unterstützen
- Anwendungen, die nur auf Desktops oder herkömmlichen Servern ausgeführt werden
- Anwendungen, die erst ausgeführt werden, nachdem die System-RNG bereit ist (einschließlich derer, bei denen ein Anwendungsframework
secrets.wait_for_system_rng()in ihrem Namen aufruft)
Anwendungen dieser Kategorie werden die neue Ausnahme einfach nicht erleben, daher ist es für Entwickler sinnvoll, abzuwarten, ob sie Python 3.6-Kompatibilitätsfehler im Zusammenhang mit dem neuen Laufzeitverhalten erhalten, anstatt zu versuchen, präventiv festzustellen, ob sie betroffen sind.
Betroffene sicherheitskritische Anwendungen
Sicherheitskritische Anwendungen müssten entweder ihre Systemkonfiguration ändern, so dass die Anwendung erst nach der Bereitschaft der Zufallszahlengenerierung des Betriebssystems für sicherheitskritische Operationen gestartet wird, den Anwendung-Startup-Code ändern, um secrets.wait_for_system_rng() aufzurufen, oder aber zur neuen secrets.token_bytes() API wechseln.
Als Beispiel für Komponenten, die über eine systemd-Unit-Datei gestartet werden, würde der folgende Snippet die Aktivierung verzögern, bis die System-RNG bereit ist:
ExecStartPre=python3 -c “import secrets; secrets.wait_for_system_rng()”
Alternativ würde der folgende Snippet secrets.token_bytes() verwenden, falls verfügbar, und andernfalls auf os.urandom() zurückfallen:
- try
- import secrets.token_bytes as _get_random_bytes
- except ImportError
- import os.urandom as _get_random_bytes
Betroffene nicht sicherheitskritische Anwendungen
Nicht sicherheitskritische Anwendungen sollten aktualisiert werden, um das random-Modul anstelle von os.urandom zu verwenden.
def pseudorandom_bytes(num_bytes):
return random.getrandbits(num_bytes*8).to_bytes(num_bytes, "little")
Abhängig von den Details der Anwendung kann das Random-Modul andere APIs anbieten, die direkt verwendet werden können, anstatt die von der os.urandom()-API erzeugte Rohbyte-Sequenz zu emulieren.
Zusätzlicher Hintergrund
Warum jetzt vorschlagen?
Der Hauptgrund dafür ist, dass die Python 3.5.0-Version unter Verwendung des neuen Linux getrandom()-Systemaufrufs, wenn verfügbar, um einen Dateideskriptor zu vermeiden [1], und dies hatte die Nebenwirkung, die folgenden Operationen blockieren zu lassen, während auf die Zufallszahlengenerierung des Systems gewartet wurde:
os.urandom(und APIs, die davon abhängen)- Importieren des
random-Moduls - Initialisierung des randomisierten Hash-Algorithmus, der von einigen eingebauten Typen verwendet wird
Während das erste dieser Verhaltensweisen arguably wünschenswert ist (und mit dem bestehenden Verhalten von os.urandom auf anderen Betriebssystemen übereinstimmt), sind die beiden letzteren Verhaltensweisen unnötig und unerwünscht, und das letztere ist jetzt dafür bekannt, einen System-Level-Deadlock zu verursachen, wenn versucht wird, Python-Skripte während des Linux-Init-Prozesses mit Python 3.5.0 oder 3.5.1 auszuführen [2], während das zweite Probleme bei der Verwendung von virtuellen Maschinen ohne robuste konfigurierte Entropiequellen verursachen kann [3].
Da die Entkopplung dieser Verhaltensweisen in CPython eine Reihe von Implementierungsänderungen beinhalten wird, die für eine Feature-Veröffentlichung besser geeignet sind als für eine Wartungsveröffentlichung, war die relativ einfache Lösung in Python 3.5.2, alle drei auf ein Verhalten zurückzusetzen, das dem früherer Python-Versionen ähnelt: Wenn der neue Linux-Systemaufruf anzeigt, dass er blockieren wird, greift Python 3.5.2 implizit auf das direkte Lesen von /dev/urandom zurück [4].
Diese Fehlerberichte führten jedoch **auch** zu einer Reihe von Vorschlägen, **neue** APIs wie os.getrandom() [5], os.urandom_block() [6], os.pseudorandom() und os.cryptorandom() [7], oder das Hinzufügen neuer optionaler Parameter zu os.urandom() selbst [8], und dann zu versuchen, Benutzer darüber aufzuklären, wann sie diese APIs anstelle eines einfachen os.urandom()-Aufrufs verwenden sollten.
Diese Vorschläge stellen wohl Überreaktionen dar, da die Frage der zuverlässigen Beschaffung von Zufallszahlen für sicherheitskritische Arbeiten unter Linux ein relativ obskures Problem ist, das hauptsächlich für Betriebssystementwickler und Embedded-System-Programmierer von Interesse ist und möglicherweise keine Erweiterung der plattformübergreifenden APIs der Python-Standardbibliothek um neue Linux-spezifische Anliegen rechtfertigt. Dies gilt insbesondere, da das secrets-Modul bereits als Option „Benutzen Sie dies und machen Sie sich keine Sorgen um die Low-Level-Details“ für Entwickler von sicherheitskritischer Software hinzugefügt wird, die aus irgendeinem Grund nicht einmal auf höherwertige domänenspezifische APIs (wie Webframeworks) zurückgreifen können und sich auch keine Sorgen um Python-Versionen vor Python 3.6 machen müssen.
Allerdings ist es auch so, dass kostengünstige ARM-Geräte immer häufiger werden, viele davon laufen unter Linux und viele Leute schreiben Python-Anwendungen, die auf diesen Geräten laufen. Dies schafft eine Gelegenheit, ein obskures Sicherheitsproblem, das derzeit viel Wissen über Linux-Bootprozesse und nachweislich unvorhersehbare Zufallszahlengenerierung erfordert, um es zu diagnostizieren und zu beheben, und es stattdessen in eine relativ alltägliche und leicht im Internet zu findende Laufzeitausnahme zu verwandeln.
Das plattformübergreifende Verhalten von os.urandom()
Auf Betriebssystemen, die keine Linux- und NetBSD-Systeme sind, kann os.urandom() bereits darauf warten, dass die Zufallszahlengenerierung des Betriebssystems bereit ist. Dies geschieht höchstens einmal im Lebenszyklus des Prozesses, und der Aufruf ist anschließend garantiert nicht blockierend.
Linux und NetBSD sind Ausnahmen, da auch wenn die Zufallszahlengenerierung des Betriebssystems sich nicht bereit für sicherheitskritische Operationen hält, das Lesen des /dev/urandom-Geräts Zufallswerte basierend auf der verfügbaren Entropie zurückgibt.
Dieses Verhalten ist potenziell problematisch, daher hat Linux 3.17 einen neuen getrandom()-Systemaufruf eingeführt, der (unter anderem) es Aufrufern ermöglicht, entweder zu blockieren, während auf die Bereitschaft der Zufallszahlengenerierung gewartet wird, oder einen Fehler zurückzugeben, wenn die Zufallszahlengenerierung nicht bereit ist. Insbesondere unterstützt die neue API nicht das alte Verhalten, Daten zurückzugeben, die nicht für sicherheitskritische Anwendungsfälle geeignet sind.
Versionen von Python bis einschließlich Python 3.4 greifen direkt auf das Linux /dev/urandom-Gerät zu.
Python 3.5.0 und 3.5.1 (wenn auf einem System kompiliert, das den neuen Systemaufruf anbot) riefen getrandom() im blockierenden Modus auf, um die Verwendung eines Dateideskriptors für den Zugriff auf /dev/urandom zu vermeiden. Während es keine spezifischen Probleme mit dem Blockieren von os.urandom() im Benutzercode gab, **gab es** Probleme, weil CPython das blockierende Verhalten während der Interpreter-Startup und beim Importieren des random-Moduls implizit aufrief.
Anstatt zu versuchen, die SipHash-Initialisierung von der os.urandom()-Implementierung zu entkoppeln, wechselte Python 3.5.2 dazu, getrandom() im nicht-blockierenden Modus aufzurufen und auf das Lesen von /dev/urandom zurückzugreifen, wenn der Systemaufruf anzeigt, dass er blockieren wird.
Als Ergebnis des oben Genannten propagiert os.urandom() in allen Python-Versionen bis einschließlich Python 3.5 das Verhalten des zugrunde liegenden /dev/urandom-Geräts an den Python-Code.
Probleme mit dem Verhalten von /dev/urandom unter Linux
Das Python os-Modul hat sich weitgehend mit den Linux-APIs weiterentwickelt, so dass es normalerweise als wünschenswert gilt, wenn os-Modulfunktionen auf Linux eng mit dem Verhalten ihrer Linux-Betriebssystem-Gegenstücke übereinstimmen.
Allerdings stellt /dev/urandom einen Fall dar, in dem das aktuelle Verhalten als problematisch anerkannt wird, aber seine Behebung auf Kernel-Ebene einseitig gezeigt hat, dass sie einige Linux-Distributionen am Booten hindert (zumindest teilweise, da Komponenten wie Python es derzeit für nicht sicherheitskritische Zwecke früh im Systeminitialisierungsprozess verwenden).
Als Analogie betrachten Sie die folgenden beiden Funktionen:
def generate_example_password():
"""Generates passwords solely for use in code examples"""
return generate_unpredictable_password()
def generate_actual_password():
"""Generates actual passwords for use in real applications"""
return generate_unpredictable_password()
Wenn Sie die Zufallszahlengenerierung eines Betriebssystems als Methode zur Generierung von unvorhersehbaren, geheimen Passwörtern betrachten, können Sie sich die Implementierung von Linux' /dev/urandom so vorstellen:
# Oversimplified artist's conception of the kernel code
# implementing /dev/urandom
def generate_unpredictable_password():
if system_rng_is_ready:
return use_system_rng_to_generate_password()
else:
# we can't make an unpredictable password; silently return a
# potentially predictable one instead:
return "p4ssw0rd"
In diesem Szenario ist der Autor von generate_example_password zufrieden – auch wenn "p4ssw0rd" etwas häufiger vorkommt als erwartet, wird es nur in Beispielen verwendet. Der Autor von generate_actual_password hat jedoch ein Problem – wie kann er beweisen, dass seine Aufrufe von generate_unpredictable_password niemals den Pfad nehmen, der eine vorhersagbare Antwort liefert?
In der Realität ist es etwas komplizierter, da möglicherweise ein gewisses Maß an Systementropie verfügbar ist – die Fallback-Lösung könnte also eher so aussehen: return random.choice(["p4ssword", "passw0rd", "p4ssw0rd"]) oder etwas noch Variableres und somit nur statistisch vorhersagbar mit besserer Wahrscheinlichkeit, als der Autor von generate_actual_password erwartet hatte. Dies macht die Dinge jedoch nicht wirklich nachweislich sicherer; meist bedeutet es nur, dass, wenn man versucht, das Problem auf offensichtliche Weise abzufangen – if returned_password == "p4ssw0rd": raise UhOh – dies nicht funktioniert, da returned_password stattdessen p4ssword oder sogar pa55word sein könnte, oder einfach eine beliebige 64-Bit-Sequenz, die aus weniger als 2**64 Möglichkeiten ausgewählt wurde. Diese grobe Skizze vermittelt also die allgemeine Idee der Konsequenzen des „vorhersagbarer als erwartet“-Fallback-Verhaltens, auch wenn sie den Bemühungen des Linux-Kernel-Teams, die praktischen Folgen dieses Problems zu mildern, ohne auf Kompatibilitätsbrüche zurückzugreifen, völlig unfair ist.
Dieses Design wird allgemein als schlechte Idee angesehen. Soweit wir wissen, gibt es keine Anwendungsfälle, bei denen dieses Verhalten gewünscht ist. Es hat zur Verwendung unsicherer ssh-Schlüssel auf echten Systemen geführt, und viele *nix-ähnliche Systeme (einschließlich mindestens Mac OS X, OpenBSD und FreeBSD) haben ihre /dev/urandom-Implementierungen so modifiziert, dass sie niemals vorhersagbare Ausgaben liefern, entweder indem sie Lesevorgänge in diesem Fall blockieren oder indem sie einfach das Ausführen von Userspace-Programmen verweigern, bis die System-RNG initialisiert wurde. Leider konnte Linux bisher nicht folgen, da empirisch festgestellt wurde, dass die Aktivierung des blockierenden Verhaltens dazu führt, dass einige aktuell existierende Distributionen fehlschlagen (zumindest teilweise, da Komponenten wie Python es derzeit für nicht sicherheitskritische Zwecke früh im Systeminitialisierungsprozess verwenden).
Stattdessen wurde der neue getrandom()-Systemaufruf eingeführt, der es Userspace-Anwendungen **ermöglicht**, sicher auf die Zufallszahlengenerierung des Systems zuzugreifen, ohne schwer zu debuggende Deadlock-Probleme in die Initialisierungsprozesse bestehender Linux-Distributionen einzubringen.
Folgen der Verfügbarkeit von getrandom() für Python
Vor der Einführung des getrandom() Systemaufrufs war es einfach nicht praktikabel, auf den Linux-System-Zufallszahlengenerator auf eine nachweislich sichere Weise zuzugreifen. Daher waren wir gezwungen, das Lesen aus /dev/urandom als beste verfügbare Option zu akzeptieren. Da getrandom() jedoch darauf besteht, einen Fehler auszulösen oder zu blockieren, anstatt vorhersagbare Daten zurückzugeben, und darüber hinaus weitere Vorteile bietet, ist es nun die empfohlene Methode für den Zugriff auf den Kernel-RNG unter Linux, wobei das direkte Lesen von /dev/urandom in den Status „Legacy“ verschoben wird. Damit rückt Linux in dieselbe Kategorie wie andere Betriebssysteme wie Windows, das überhaupt kein /dev/urandom Gerät bereitstellt: Die beste verfügbare Option zur Implementierung von os.urandom() ist nicht mehr nur das Lesen von Bytes aus dem /dev/urandom Gerät.
Das bedeutet, dass das, was früher das Problem eines anderen war (des Linux-Kernel-Entwicklungsteams), nun das Problem von Python ist – angesichts einer Möglichkeit, zu erkennen, dass der System-RNG nicht initialisiert ist, müssen wir entscheiden, wie wir diese Situation behandeln, wenn wir versuchen, den System-RNG zu verwenden.
Er könnte einfach blockieren, wie es 3.5.0 eher unbeabsichtigt implementierte, und wie es in Victor Stinners konkurrierendem PEP vorgeschlagen wird.
# artist's impression of the CPython 3.5.0-3.5.1 behavior
def generate_unpredictable_bytes_or_block(num_bytes):
while not system_rng_is_ready:
wait
return unpredictable_bytes(num_bytes)
Oder er könnte einen Fehler auslösen, wie dieses PEP (in *einigen* Fällen) vorschlägt.
# artist's impression of the behavior proposed in this PEP
def generate_unpredictable_bytes_or_raise(num_bytes):
if system_rng_is_ready:
return unpredictable_bytes(num_bytes)
else:
raise BlockingIOError
Oder er könnte das Fallback-Verhalten von /dev/urandom explizit emulieren, wie es in 3.5.2rc1 implementiert wurde und voraussichtlich für den Rest des 3.5.x-Zyklus beibehalten wird.
# artist's impression of the CPython 3.5.2rc1+ behavior
def generate_unpredictable_bytes_or_maybe_not(num_bytes):
if system_rng_is_ready:
return unpredictable_bytes(num_bytes)
else:
return (b"p4ssw0rd" * (num_bytes // 8 + 1))[:num_bytes]
(Und die gleichen Vorbehalte gelten für diese Skizze, wie sie für die Skizze generate_unpredictable_password von /dev/urandom oben galten.)
Es gibt fünf Stellen, an denen CPython und die Standardbibliothek versuchen, den Zufallszahlengenerator des Betriebssystems zu verwenden, und somit fünf Stellen, an denen diese Entscheidung getroffen werden muss.
- Initialisierung von SipHash, das zum Schutz von
str.__hash__und ähnlichen vor DoS-Angriffen verwendet wird (wird beim Start bedingungslos aufgerufen). - Initialisierung des
randomModuls (wird aufgerufen, wennrandomimportiert wird). - Bedienung von Benutzeraufrufen an die öffentliche API
os.urandom. - Die höherrangige öffentliche API
random.SystemRandom. - Die neue öffentliche API des Moduls
secrets, die durch PEP 506 hinzugefügt wurde.
Zuvor nutzten alle diese fünf Stellen denselben zugrundeliegenden Code und trafen somit diese Entscheidung auf dieselbe Weise.
Dieses gesamte Problem wurde zuerst bemerkt, da 3.5.0 diesen zugrundeliegenden Code auf das Verhalten generate_unpredictable_bytes_or_block umstellte, und es stellt sich heraus, dass es seltene Fälle gibt, in denen Linux-Startskripte versuchten, ein Python-Programm als Teil der Systeminitialisierung auszuführen. Die Python-Startsequenz blockierte beim Versuch, SipHash zu initialisieren, und dies löste dann eine Blockade aus, da das System aufhörte, irgendetwas zu tun – einschließlich des Sammelns neuer Entropie –, bis das Python-Skript von einem externen Timer zwangsweise beendet wurde. Dies ist besonders bedauerlich, da die betreffenden Skripte niemals nicht vertrauenswürdige Eingaben verarbeiteten, so dass SipHash von vornherein nicht mit nachweislich unvorhersehbaren Zufallsdaten initialisiert werden musste. Dies motivierte die Änderung in 3.5.2rc1, das alte /dev/urandom-Verhalten in allen Fällen zu emulieren (durch Aufruf von getrandom() im nicht-blockierenden Modus und dann Rückgriff auf das Lesen von /dev/urandom, falls der Systemaufruf anzeigt, dass der /dev/urandom-Pool noch nicht vollständig initialisiert ist).
Wir wissen nicht, ob solche Probleme auch im Fedora/RHEL/CentOS-Ökosystem bestehen, da die Build-Systeme dieser Distributionen Chroots auf Servern mit älteren Betriebssystem-Kerneln verwenden, die den getrandom()-Systemaufruf nicht anbieten, was bedeutet, dass die aktuelle Build-Konfiguration von CPython die Laufzeitprüfung für diesen Systemaufruf auskompiliert [10].
Ein ähnliches Problem wurde festgestellt, da das random-Modul os.urandom als Nebeneffekt des Imports aufruft, um die Standardinstanz von random.Random() zu initialisieren.
Wir haben keine spezifischen Beschwerden bezüglich direkter Aufrufe von os.urandom() oder random.SystemRandom() bezüglich Blockaden mit 3.5.0 oder 3.5.1 erhalten – nur Problemberichte aufgrund der impliziten Blockade beim Interpreterstart und als Nebeneffekt des Imports des zufälligen Moduls.
Unabhängig von diesem PEP wurden die ersten beiden Fälle bereits so aktualisiert, dass sie niemals blockieren, unabhängig vom Verhalten von os.urandom().
Während PEP 524 vorschlägt, alle 3 der letzteren Fälle implizit blockieren zu lassen, schlägt dieses PEP diesen Ansatz nur für den letzten Fall (das Modul secrets) vor, wobei os.urandom() und random.SystemRandom() stattdessen eine Ausnahme auslösen, wenn sie erkennen, dass der zugrunde liegende Betriebssystemaufruf blockieren würde.
Referenzen
Für zusätzliche Hintergrundinformationen über die in diesem PEP und Victors konkurrierendem PEP erfassten hinaus siehe auch Victors frühere Sammlung relevanter Informationen und Links unter https://haypo-notes.readthedocs.io/summary_python_random_issue.html
Urheberrecht
Dieses Dokument wurde in den öffentlichen Bereich gestellt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0522.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT