Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 524 – Make os.urandom() blocking on Linux

Autor:
Victor Stinner <vstinner at python.org>
Status:
Final
Typ:
Standards Track
Erstellt:
20-Jun-2016
Python-Version:
3.6

Inhaltsverzeichnis

Zusammenfassung

Ändern Sie os.urandom(), sodass es unter Linux 3.17 und neuer blockiert, bis das OS urandom initialisiert ist, um die Sicherheit zu erhöhen.

Fügen Sie auch eine neue Funktion os.getrandom() hinzu (für Linux und Solaris), um wählen zu können, wie damit umgegangen wird, wenn os.urandom() unter Linux blockiert.

Der Fehler

Ursprünglicher Fehler

Python 3.5.0 wurde mit der neuen getrandom()-Syscall erweitert, die in Linux 3.17 und Solaris 11.3 eingeführt wurde. Das Problem ist, dass Benutzer begannen zu beanstanden, dass Python 3.5 beim Start unter Linux in virtuellen Maschinen und eingebetteten Geräten blockiert: siehe Issues #25420 und #26839.

Unter Linux blockiert getrandom(0), bis der Kernel urandom mit 128 Bit Entropie initialisiert hat. Issue #25420 beschreibt eine Linux-Build-Plattform, die bei import random blockiert. Issue #26839 beschreibt ein kurzes Python-Skript zur Berechnung eines MD5-Hashs, systemd-cron, ein Skript, das sehr früh im Init-Prozess aufgerufen wird. Die Systeminitialisierung blockiert wegen dieses Skripts, das auf getrandom(0) blockiert, um Python zu initialisieren.

Die Python-Initialisierung erfordert zufällige Bytes zur Implementierung einer Gegenmaßnahme gegen Hash-Denial-of-Service (Hash DoS), siehe

Der Import des Moduls random erstellt eine Instanz von random.Random: random._inst. Unter Python 3.5 liest der Konstruktor von random.Random 2500 Bytes aus os.urandom(), um einen Mersenne-Twister-RNG (Zufallszahlengenerator) zu seeden.

Andere Plattformen können von diesem Fehler betroffen sein, aber praktisch gesehen verwenden nur Linux-Systeme Python-Skripte zur Systeminitialisierung.

Status in Python 3.5.2

Python 3.5.2 verhält sich wie Python 2.7 und Python 3.4. Wenn das System-urandom nicht initialisiert ist, blockiert der Start nicht, aber os.urandom() kann geringwertige Entropie zurückgeben (auch wenn sie nicht leicht zu erraten ist).

Anwendungsfälle

Die folgenden Anwendungsfälle helfen bei der Wahl des richtigen Kompromisses zwischen Sicherheit und Praktikabilität.

Anwendungsfall 1: Init-Skript

Verwenden Sie ein Python 3-Skript zur Initialisierung des Systems, wie z. B. systemd-cron. Wenn das Skript blockiert, ist auch die Systeminitialisierung blockiert. Issue #26839 ist ein gutes Beispiel für diesen Anwendungsfall.

Anwendungsfall 1.1: Kein Geheimnis benötigt

Wenn das Init-Skript kein sicheres Geheimnis generieren muss, wird dieser Anwendungsfall in Python 3.5.2 bereits korrekt behandelt: Der Python-Start blockiert nicht mehr durch das System-urandom.

Anwendungsfall 1.2: Sicheres Geheimnis erforderlich

Wenn das Init-Skript ein sicheres Geheimnis generieren muss, gibt es keine sichere Lösung.

Auf schwache Entropie zurückzufallen ist nicht akzeptabel, es würde die Sicherheit des Programms verringern.

Python kann selbst keine sichere Entropie erzeugen, es kann nur warten, bis das System-urandom initialisiert ist. Aber in diesem Anwendungsfall ist die gesamte Systeminitialisierung durch dieses Skript blockiert, so dass das System nicht bootet.

Die wirkliche Antwort ist, dass die Systeminitialisierung nicht durch ein solches Skript blockiert werden darf. Es ist in Ordnung, das Skript sehr früh in der Systeminitialisierung zu starten, aber das Skript darf einige Sekunden blockieren, bis es in der Lage ist, das Geheimnis zu generieren.

Zur Erinnerung: In einigen Fällen erfolgt die Initialisierung des System-urandom nie, und Programme, die auf System-urandom warten, blockieren daher ewig.

Anwendungsfall 2: Webserver

Starten Sie einen Python 3-Webserver, der Webseiten über HTTP und HTTPS bereitstellt. Der Server wird so schnell wie möglich gestartet.

Das erste Ziel des Hash-DoS-Angriffs war der Webserver: Es ist wichtig, dass das Hash-Geheimnis von einem Angreifer nicht leicht erraten werden kann.

Wenn das Bereitstellen einer Webseite ein Geheimnis zum Erstellen eines Cookies, einer Verschlüsselungsschlüssel usw. benötigt, muss das Geheimnis mit guter Entropie erstellt werden: Wiederum muss es schwer sein, das Geheimnis zu erraten.

Ein Webserver benötigt Sicherheit. Wenn eine Wahl getroffen werden muss zwischen Sicherheit und dem Betrieb des Servers mit schwacher Entropie, hat die Sicherheit Vorrang. Wenn keine gute Entropie vorhanden ist: Der Server muss blockieren oder mit einem Fehler fehlschlagen.

Die Frage ist, ob es sinnvoll ist, einen Webserver auf einem Host zu starten, bevor das System-urandom initialisiert ist.

Die Issues #25420 und #26839 beschränken sich auf den Python-Start, nicht auf die Generierung eines Geheimnisses vor der Initialisierung des System-urandom.

System-urandom korrigieren

Entropie von der Festplatte beim Booten laden

Das Sammeln von Entropie kann mehrere Minuten dauern. Um die Systeminitialisierung zu beschleunigen, speichern Betriebssysteme Entropie beim Herunterfahren auf der Festplatte und laden die Entropie beim Booten wieder von der Festplatte.

Wenn ein System mindestens einmal genügend Entropie sammelt, wird das System-urandom schnell initialisiert, sobald die Entropie von der Festplatte geladen ist.

Virtuelle Maschinen

Virtuelle Maschinen haben keinen direkten Zugriff auf die Hardware und somit weniger Entropiequellen als Bare-Metal-Systeme. Eine Lösung ist die Hinzufügung eines virtio-rng-Geräts, um Entropie vom Host an die virtuelle Maschine zu übergeben.

Eingebettete Geräte

Eine Lösung für eingebettete Geräte ist der Anschluss eines Hardware-RNG.

Zum Beispiel verfügt der Raspberry Pi über einen Hardware-RNG, der aber standardmäßig nicht verwendet wird. Siehe: Hardware RNG auf dem Raspberry Pi.

Denial-of-Service beim Lesen von Zufallswerten

Nicht /dev/random verwenden, sondern /dev/urandom

Das Gerät /dev/random sollte nur für sehr spezielle Anwendungsfälle verwendet werden. Das Lesen von /dev/random unter Linux blockiert wahrscheinlich. Benutzer mögen es nicht, wenn eine Anwendung länger als 5 Sekunden blockiert, um ein Geheimnis zu generieren. Dies ist nur für spezielle Fälle zu erwarten, wie z. B. die explizite Generierung eines Verschlüsselungsschlüssels.

Wenn das System keine verfügbare Entropie hat, ist die Wahl zwischen dem Blockieren, bis Entropie verfügbar ist, oder dem Zurückgreifen auf Entropie geringerer Qualität eine Frage des Kompromisses zwischen Sicherheit und Praktikabilität. Die Wahl hängt vom Anwendungsfall ab.

Unter Linux ist /dev/urandom sicher und sollte anstelle von /dev/random verwendet werden. Siehe Mythen über /dev/urandom von Thomas Hühn: „Fakt: /dev/urandom ist die bevorzugte Quelle für kryptografische Zufälligkeit auf UNIX-ähnlichen Systemen“.

getrandom(size, 0) kann unter Linux ewig blockieren

Der Ursprung des Python-Problems #26839 ist der Debian-Bug-Report #822431: Tatsächlich blockiert getrandom(size, 0) auf der virtuellen Maschine für immer. Das System bootete erfolgreich, weil systemd den blockierten Prozess nach 90 Sekunden beendete.

Lösungen wie Entropie von der Festplatte beim Booten laden reduzieren das Risiko dieses Fehlers.

Begründung

Unter Linux kann das Lesen von /dev/urandom „schwache“ Entropie zurückgeben, bevor urandom vollständig initialisiert ist, bevor der Kernel 128 Bit Entropie gesammelt hat. Linux 3.17 fügt eine neue getrandom()-Syscall hinzu, die blockieren kann, bis urandom initialisiert ist.

In Python 3.5.2 verwendet os.urandom() die getrandom(size, GRND_NONBLOCK), fällt aber auf das Lesen von /dev/urandom zurück, wenn getrandom(size, GRND_NONBLOCK) mit EAGAIN fehlschlägt.

Sicherheitsexperten empfehlen os.urandom() zur Generierung kryptografischer Schlüssel, da es mit einem kryptografisch sicheren Pseudozufallszahlengenerator (CSPRNG) implementiert ist. Übrigens wird os.urandom() aus verschiedenen Gründen gegenüber ssl.RAND_bytes() bevorzugt.

Dieses PEP schlägt vor, os.urandom() so zu ändern, dass es getrandom() im Blockierungsmodus verwendet, um keine schwache Entropie zurückzugeben, aber auch sicherzustellen, dass Python beim Start nicht blockiert.

Änderungen

Make os.urandom() blocking on Linux

Alle in diesem Abschnitt beschriebenen Änderungen sind spezifisch für die Linux-Plattform.

Änderungen

  • Ändern Sie os.urandom(), um zu blockieren, bis System-urandom initialisiert ist: os.urandom() (C-Funktion _PyOS_URandom()) wird geändert, um unter Linux und Solaris immer getrandom(size, 0) (Blockierungsmodus) aufzurufen.
  • Fügen Sie eine neue private Funktion _PyOS_URandom_Nonblocking() hinzu: Versuchen Sie, getrandom(size, GRND_NONBLOCK) unter Linux und Solaris aufzurufen, aber greifen Sie auf das Lesen von /dev/urandom zurück, wenn es mit EAGAIN fehlschlägt.
  • Initialisieren Sie das Hash-Geheimnis aus nicht blockierendem System-urandom: _PyRandom_Init() wird geändert, um _PyOS_URandom_Nonblocking() aufzurufen.
  • random.Random-Konstruktor verwendet jetzt nicht blockierendes System-urandom: Er wird geändert, um intern die neue Funktion _PyOS_URandom_Nonblocking() zu verwenden, um den RNG zu seeden.

Eine neue Funktion os.getrandom() hinzufügen

Eine neue Funktion os.getrandom(size, flags=0) wird hinzugefügt: Verwendung der getrandom()-Syscall unter Linux und der C-Funktion getrandom() unter Solaris.

Die Funktion kommt mit 2 neuen Flags

  • os.GRND_RANDOM: Bytes von /dev/random anstelle von /dev/urandom lesen
  • os.GRND_NONBLOCK: Eine BlockingIOError auslösen, wenn os.getrandom() blockieren würde

Die Funktion os.getrandom() ist ein dünner Wrapper um die getrandom()-Syscall/C-Funktion und erbt somit deren Verhalten. Zum Beispiel kann sie unter Linux weniger Bytes als angefordert zurückgeben, wenn die Syscall durch ein Signal unterbrochen wird.

Beispiele mit os.getrandom()

Best-Effort RNG

Beispiel für eine portierbare, nicht blockierende RNG-Funktion: Versuchen Sie, zufällige Bytes vom OS-urandom zu erhalten, oder greifen Sie auf das random-Modul zurück.

def best_effort_rng(size):
    # getrandom() is only available on Linux and Solaris
    if not hasattr(os, 'getrandom'):
        return os.urandom(size)

    result = bytearray()
    try:
        # need a loop because getrandom() can return less bytes than
        # requested for different reasons
        while size:
            data = os.getrandom(size, os.GRND_NONBLOCK)
            result += data
            size -= len(data)
    except BlockingIOError:
        # OS urandom is not initialized yet:
        # fallback on the Python random module
        data = bytes(random.randrange(256) for byte in range(size))
        result += data
    return bytes(result)

Diese Funktion *kann* theoretisch auf einer Plattform blockieren, auf der os.getrandom() nicht verfügbar ist, aber os.urandom() blockieren kann.

wait_for_system_rng()

Beispiel für eine Funktion, die *Timeout*-Sekunden wartet, bis das OS-urandom unter Linux oder Solaris initialisiert ist

def wait_for_system_rng(timeout, interval=1.0):
    if not hasattr(os, 'getrandom'):
        return

    deadline = time.monotonic() + timeout
    while True:
        try:
            os.getrandom(1, os.GRND_NONBLOCK)
        except BlockingIOError:
            pass
        else:
            return

        if time.monotonic() > deadline:
            raise Exception('OS urandom not initialized after %s seconds'
                            % timeout)

        time.sleep(interval)

Diese Funktion ist *nicht* portierbar. Zum Beispiel kann os.urandom() theoretisch unter FreeBSD blockieren, in der frühen Phase der Systeminitialisierung.

Einen Best-Effort RNG erstellen

Einfacheres Beispiel zur Erstellung eines nicht blockierenden RNG unter Linux: Wählen Sie zwischen Random.SystemRandom und Random.Random, je nachdem, ob getrandom(size) blockieren würde.

def create_nonblocking_random():
    if not hasattr(os, 'getrandom'):
        return random.Random()

    try:
        os.getrandom(1, os.GRND_NONBLOCK)
    except BlockingIOError:
        return random.Random()
    else:
        return random.SystemRandom()

Diese Funktion ist *nicht* portierbar. Zum Beispiel kann random.SystemRandom theoretisch unter FreeBSD blockieren, in der frühen Phase der Systeminitialisierung.

Alternative

os.urandom() unverändert lassen, os.getrandom() hinzufügen

os.urandom() bleibt unverändert: niemals blockieren, aber es kann schwache Entropie zurückgeben, wenn System-urandom noch nicht initialisiert ist.

Nur die neue Funktion os.getrandom() hinzufügen (Wrapper für die getrandom()-Syscall/C-Funktion).

Die Funktion secrets.token_bytes() sollte zum Schreiben portierbaren Codes verwendet werden.

Das Problem bei dieser Änderung ist, dass sie davon ausgeht, dass Entwickler Sicherheit gut verstehen und jede Plattform gut kennen. Python hat die Tradition, „Implementierungsdetails“ zu verbergen. Zum Beispiel ist os.urandom() kein dünner Wrapper für das Gerät /dev/urandom: Es verwendet CryptGenRandom() unter Windows, es verwendet getentropy() unter OpenBSD, es versucht getrandom() unter Linux und Solaris oder greift auf das Lesen von /dev/urandom zurück. Python verwendet bereits den besten verfügbaren System-RNG, abhängig von der Plattform.

Dieses PEP ändert die API nicht

  • os.urandom(), random.SystemRandom und secrets für Sicherheit
  • random-Modul (außer random.SystemRandom) für alle anderen Verwendungszwecke

BlockingIOError in os.urandom() auslösen

Vorschlag

PEP 522: BlockingIOError in sicherheitskritischen APIs unter Linux zulassen.

Python sollte nicht für den Entwickler entscheiden, wie der Fehler behandelt wird: Durch die sofortige Auslösung einer BlockingIOError, wenn os.urandom() blockieren würde, können Entwickler wählen, wie sie diesen Fall behandeln

  • die Ausnahme abfangen und auf eine unsichere Entropiequelle zurückgreifen: /dev/urandom unter Linux lesen, das Python random-Modul verwenden (das überhaupt nicht sicher ist), Zeit, Prozess-ID usw. verwenden
  • den Fehler nicht abfangen, das gesamte Programm schlägt mit dieser fatalen Ausnahme fehl

Allgemeiner hilft die Ausnahme, eine Benachrichtigung auszugeben, wenn etwas schiefgeht. Die Anwendung kann eine Warnung ausgeben, wenn sie beginnt, auf os.urandom() zu warten.

Kritik

Für den Anwendungsfall 2 (Webserver) ist das Zurückgreifen auf nicht sichere Entropie nicht akzeptabel. Die Anwendung muss BlockingIOError behandeln: os.urandom() abfragen, bis es abgeschlossen ist. Beispiel

def secret(n=16):
    try:
        return os.urandom(n)
    except BlockingIOError:
        pass

    print("Wait for system urandom initialization: move your "
          "mouse, use your keyboard, use your disk, ...")
    while 1:
        # Avoid busy-loop: sleep 1 ms
        time.sleep(0.001)
        try:
            return os.urandom(n)
        except BlockingIOError:
            pass

Zur Korrektheit müssen alle Anwendungen, die ein sicheres Geheimnis generieren müssen, modifiziert werden, um BlockingIOError zu behandeln, auch wenn Der Fehler unwahrscheinlich ist.

Der Fall von Anwendungen, die os.urandom() verwenden, aber keine wirkliche Sicherheit benötigen, ist nicht gut definiert. Vielleicht sollten diese Anwendungen os.urandom() gar nicht erst verwenden, sondern immer das nicht blockierende random-Modul. Wenn os.urandom() für Sicherheit verwendet wird, sind wir wieder beim Anwendungsfall 2 wie oben beschrieben: Anwendungsfall 2: Webserver. Wenn ein Entwickler os.urandom() nicht fallen lassen möchte, sollte der Code modifiziert werden. Beispiel

def almost_secret(n=16):
    try:
        return os.urandom(n)
    except BlockingIOError:
        return bytes(random.randrange(256) for byte in range(n))

Die Frage ist, ob Der Fehler häufig genug vorkommt, um zu verlangen, dass so viele Anwendungen modifiziert werden müssen.

Eine einfachere Wahl ist, die Ausführung zu verweigern, bevor das System-urandom initialisiert ist

def secret(n=16):
    try:
        return os.urandom(n)
    except BlockingIOError:
        print("Fatal error: the system urandom is not initialized")
        print("Wait a bit, and rerun the program later.")
        sys.exit(1)

Im Vergleich zu Python 2.7, Python 3.4 und Python 3.5.2, wo os.urandom() unter Linux nie blockiert oder eine Ausnahme auslöst, kann ein solches Verhaltensänderung als größere Regression angesehen werden.

Einen optionalen Block-Parameter zu os.urandom() hinzufügen

Siehe Issue #27250: Add os.urandom_block().

Fügen Sie einen optionalen Block-Parameter zu os.urandom() hinzu. Der Standardwert kann True (standardmäßig blockieren) oder False (nicht blockierend) sein.

Das erste technische Problem ist die Implementierung von os.urandom(block=False) auf allen Plattformen. Nur Linux 3.17 (und neuer) und Solaris 11.3 (und neuer) verfügen über eine gut definierte, nicht blockierende API (getrandom(size, GRND_NONBLOCK)).

Wie in BlockingIOError in os.urandom() auslösen, scheint es nicht lohnenswert, die API für einen theoretischen (oder zumindest sehr seltenen) Anwendungsfall komplexer zu machen.

Wie in os.urandom() unverändert lassen, os.getrandom() hinzufügen ist das Problem, dass dies die API komplexer und somit fehleranfälliger macht.

Akzeptanz

Das PEP wurde am 2016-08-08 von Guido van Rossum angenommen.

Anhänge

Betriebssystem-Zufallsfunktionen

os.urandom() verwendet die folgenden Funktionen

Unter Linux Befehle, um den Status von /dev/random abzurufen (Ergebnisse sind Bytes)

$ cat /proc/sys/kernel/random/entropy_avail
2850
$ cat /proc/sys/kernel/random/poolsize
4096

Warum os.urandom() verwenden?

Da os.urandom() im Kernel implementiert ist, hat es keine Probleme mit User-Space-RNGs. Zum Beispiel ist es viel schwieriger, seinen Zustand zu erhalten. Es basiert normalerweise auf einem CSPRNG, sodass selbst wenn sein Zustand „gestohlen“ wird, es schwierig ist, zuvor generierte Zahlen zu berechnen. Der Kernel hat ein gutes Wissen über Entropiequellen und speist den Entropiepool regelmäßig.

Deshalb wird os.urandom() gegenüber ssl.RAND_bytes() bevorzugt.


Quelle: https://github.com/python/peps/blob/main/peps/pep-0524.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT