PEP 748 – Eine einheitliche TLS-API für Python
- Autor:
- Joop van de Pol <joop.vandepol at trailofbits.com>, William Woodruff <william at yossarian.net>
- Sponsor:
- Alyssa Coghlan <ncoghlan at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Entwurf
- Typ:
- Standards Track
- Erstellt:
- 27-Jun-2024
- Python-Version:
- 3.14
- Post-History:
- 17-Apr-2024
- Ersetzt:
- 543
Zusammenfassung
Diese PEP definiert eine standardisierte TLS-Schnittstelle in Form einer Sammlung von Protokollklassen. Diese Schnittstelle ermöglicht es Python-Implementierungen und Drittanbieterbibliotheken, Bindungen zu anderen TLS-Bibliotheken als OpenSSL bereitzustellen.
Diese Bindungen können von Werkzeugen verwendet werden, die die von der Python-Standardbibliothek bereitgestellte Schnittstelle erwarten, mit dem Ziel, die Abhängigkeit des Python-Ökosystems von OpenSSL zu verringern.
Begründung
Es ist immer deutlicher geworden, dass eine robuste und benutzerfreundliche TLS-Unterstützung ein äußerst wichtiger Teil des Ökosystems jeder beliebigen Programmiersprache ist. Über die meiste Zeit seines Bestehens wurde diese Rolle im Python-Ökosystem hauptsächlich von dem ssl-Modul erfüllt, das eine Python-API zur OpenSSL-Bibliothek bereitstellt.
Da das ssl-Modul mit der Python-Standardbibliothek ausgeliefert wird, ist es zur mit Abstand beliebtesten Methode für die TLS-Verarbeitung in Python geworden. Eine Mehrheit der Python-Bibliotheken, sowohl in der Standardbibliothek als auch im Python Package Index, verlässt sich für ihre TLS-Konnektivität auf das ssl-Modul.
Leider hat die Vormachtstellung des ssl-Moduls dazu geführt, dass das gesamte Python-Ökosystem fest an OpenSSL gebunden ist. Dies zwingt Python-Benutzer, OpenSSL auch in Situationen zu verwenden, in denen es eine schlechtere Benutzererfahrung als alternative TLS-Implementierungen bieten mag, was eine kognitive Belastung darstellt und es schwierig macht, „plattformnative“ Erlebnisse zu bieten.
Probleme
Die Tatsache, dass das ssl-Modul in die Standardbibliothek integriert ist, hat dazu geführt, dass alle Python-Netzwerkbibliotheken der Standardbibliothek vollständig von dem OpenSSL abhängig sind, gegen das die Python-Implementierung gelinkt wurde. Dies führt zu folgenden Problemen:
- Es ist schwierig, neue, sicherere TLS-Versionen zu nutzen, ohne Python neu kompilieren zu müssen, um ein neues OpenSSL zu erhalten. Obwohl es Drittanbieter-Bindungen zu OpenSSL gibt (z.B. pyOpenSSL), müssen diese in ein Format „geschlüpft“ werden, das die Standardbibliothek versteht, was Projekte, die sie verwenden möchten, zwingt, erhebliche Kompatibilitätsschichten zu pflegen.
- Windows-Distributionen von Python müssen mit einer Kopie von OpenSSL ausgeliefert werden. Dies bringt das CPython-Entwicklungsteam in die Lage, OpenSSL-Weiterverteiler zu sein und möglicherweise Sicherheitsupdates für die Windows-Python-Distributionen ausliefern zu müssen, wenn OpenSSL-Schwachstellen veröffentlicht werden.
- macOS-Distributionen von Python müssen entweder mit einer Kopie von OpenSSL ausgeliefert oder gegen die System-OpenSSL-Bibliothek gelinkt werden. Apple hat das Linken gegen die System-OpenSSL-Bibliothek formell als veraltet erklärt, und selbst wenn sie es nicht getan hätten, war diese Bibliotheksversion zum Zeitpunkt des Schreibens seit fast einem Jahr von Upstream nicht mehr unterstützt. Das CPython-Entwicklungsteam hat begonnen, neuere OpenSSLs mit Python auszuliefern, das von python.org erhältlich ist, aber dies hat dasselbe Problem wie bei Windows.
- Benutzer möchten aus anderen Gründen mit anderen TLS-Bibliotheken als OpenSSL integrieren, z. B. wegen des Wartungsaufwands im Vergleich zu einer systemseitig bereitgestellten Implementierung, oder weil OpenSSL für ihre Plattform (z. B. für Embedded Python) einfach zu groß und unhandlich ist. Diese Benutzer sind gezwungen, Drittanbieter-Netzwerkbibliotheken zu verwenden, die mit ihrer bevorzugten TLS-Bibliothek interagieren können, oder ihre bevorzugte Bibliothek in die OpenSSL-spezifische
ssl-Modul-API zu integrieren.
Darüber hinaus schränkt das ssl-Modul, wie es heute implementiert ist, die Fähigkeit von CPython selbst ein, Unterstützung für alternative TLS-Implementierungen hinzuzufügen oder die OpenSSL-Unterstützung vollständig zu entfernen, falls eine dieser Maßnahmen notwendig oder nützlich wird. Das ssl-Modul gibt zu viele OpenSSL-spezifische Funktionsaufrufe und Features preis, um sie leicht auf eine alternative TLS-Implementierung abzubilden.
Vorschlag
Diese PEP schlägt vor, in Python 3.14 einige neue Protokollklassen einzuführen, um TLS-Funktionalität bereitzustellen, die nicht so stark an OpenSSL gebunden ist. Sie schlägt auch vor, die Standardbibliotheksmodule zu aktualisieren, um, wo immer möglich, nur die von diesen Protokollklassen exponierte Schnittstelle zu verwenden. Es gibt hier drei Ziele:
- Bereitstellung einer gemeinsamen API-Oberfläche, auf die sich sowohl Kern- als auch Drittanbieterentwickler für ihre TLS-Implementierungen ausrichten können. Dies ermöglicht TLS-Entwicklern, Schnittstellen bereitzustellen, die von den meisten Python-Codes verwendet werden können, und ermöglicht Netzwerkentwicklern eine Schnittstelle, auf die sie abzielen können und die mit einer Vielzahl von TLS-Implementierungen funktioniert.
- Bereitstellung einer API, bei der wenige oder keine OpenSSL-spezifischen Konzepte durchscheinen. Das
ssl-Modul weist heute eine Reihe von Problemen auf, die durch das Durchsickern von OpenSSL-Konzepten in die API verursacht werden: Die neuen Protokollklassen würden diese spezifischen Konzepte entfernen. - Bereitstellung eines Weges für das Kernentwicklungsteam, OpenSSL zu einer von vielen möglichen TLS-Implementierungen zu machen, anstatt zu verlangen, dass es auf einem System vorhanden ist, damit Python TLS-Unterstützung hat.
Die vorgeschlagene Schnittstelle ist unten dargestellt.
Schnittstellen
Es gibt mehrere Schnittstellen, die standardisiert werden müssen. Diese Schnittstellen sind:
- Konfigurieren von TLS, derzeit implementiert durch die
SSLContext-Klasse imssl-Modul. - Bereitstellung eines In-Memory-Puffers für die In-Memory-Verschlüsselung oder -Entschlüsselung ohne tatsächliche I/O (notwendig für asynchrone I/O-Modelle), derzeit implementiert durch die
SSLObject-Klasse imssl-Modul. - Umschließen eines Socket-Objekts, derzeit implementiert durch die
SSLSocket-Klasse imssl-Modul. - Anwenden der TLS-Konfiguration auf die umschließenden Objekte in (2) und (3). Derzeit wird dies ebenfalls von der SSLContext-Klasse im
ssl-Modul implementiert. - Spezifizieren von TLS-Cipher-Suiten. Derzeit gibt es keine Code dafür in der Standardbibliothek: stattdessen verwendet die Standardbibliothek OpenSSL-Cipher-String-Formate.
- Spezifizieren von Anwendungsprotokollen, die während des TLS-Handshakes ausgehandelt werden können.
- Spezifizieren von TLS-Versionen.
- Melden von Fehlern an den Aufrufer, derzeit implementiert durch die
SSLError-Klasse imssl-Modul. - Spezifizieren von zu ladenden Zertifikaten, entweder als Client- oder Server-Zertifikate.
- Spezifizieren, welche Vertrauensdatenbank zum Validieren von Zertifikaten verwendet werden soll, die von einem entfernten Peer präsentiert werden.
- Finden eines Weges, diese Schnittstellen zur Laufzeit zu erhalten.
Der Einfachheit halber schlägt diese PEP vor, die Schnittstellen (3) und (4) zu entfernen und sie durch eine einfachere Schnittstelle zu ersetzen, die einen Socket zurückgibt, der sicherstellt, dass die gesamte Kommunikation über den Socket durch TLS geschützt ist. Mit anderen Worten, diese Schnittstelle behandelt Konzepte wie Socket-Initialisierung, den TLS-Handshake, Server Name Indication (SNI) usw. als atomaren Teil der Erstellung einer Client- oder Serververbindung. In-Memory-Puffer werden jedoch weiterhin unterstützt, da sie für die asynchrone Kommunikation nützlich sind.
Offensichtlich erfordert (5) keine Protokollklasse: stattdessen erfordert es eine erweiterte API zur Konfiguration unterstützter Cipher Suiten, die leicht mit unterstützten Cipher Suiten für verschiedene Implementierungen aktualisiert werden kann.
(9) ist ein kniffliges Problem, denn in einer idealen Welt würden die privaten Schlüssel, die diesen Zertifikaten zugeordnet sind, niemals im Speicher des Python-Prozesses landen (d.h. die TLS-Bibliothek würde mit einem Hardware Security Module (HSM) zusammenarbeiten, um den privaten Schlüssel so bereitzustellen, dass er nicht aus dem Prozessspeicher extrahiert werden kann). Daher müssen wir ein erweiterbares Modell für die Bereitstellung von Zertifikaten bereitstellen, das konkreten Implementierungen die Bereitstellung dieser höheren Sicherheitsebene ermöglicht, während es gleichzeitig eine niedrigere Hürde für diejenigen Implementierungen bietet, die dies nicht können. Diese niedrigere Hürde wäre dieselbe wie der Status quo: d. h., das Zertifikat kann aus einem In-Memory-Puffer, aus einer Datei auf der Festplatte geladen oder zusätzlich durch eine beliebige ID referenziert werden, die einem Systemzertifikatsspeicher entspricht.
(10) stellt ebenfalls ein Problem dar, da verschiedene TLS-Implementierungen stark variieren, wie sie Benutzern die Auswahl von Vertrauensspeichern ermöglichen. Einige Implementierungen haben spezifische Vertrauensspeicherformate, die nur sie verwenden können (wie das OpenSSL CA-Verzeichnisformat, das mit c_rehash erstellt wird), und andere erlauben es Ihnen möglicherweise nicht, einen Vertrauensspeicher anzugeben, der nicht ihren Standard-Vertrauensspeicher enthält. Andererseits unterstützen die meisten Implementierungen eine Form des Ladens benutzerdefinierter DER- oder PEM-kodierter Zertifikate.
Aus diesem Grund müssen wir ein Modell bereitstellen, das sehr wenig über die Form von Vertrauensspeichern annimmt, während es gleichzeitig Typ-Kompatibilität mit anderen Implementierungen beibehält. Die Abschnitte „Zertifikat“, „Private Schlüssel“ und „Vertrauensspeicher“ unten gehen detaillierter auf die Erreichung dieses Ziels ein.
Schließlich wird diese API die Verantwortlichkeiten aufteilen, die derzeit vom SSLContext-Objekt übernommen werden: insbesondere die Verantwortung für die Speicherung und Verwaltung der Konfiguration und die Verantwortung für die Verwendung dieser Konfiguration zum Aufbau von Puffern oder Sockets.
Dies ist hauptsächlich notwendig, um Funktionalität wie Server Name Indication (SNI) zu unterstützen. In OpenSSL (und somit im ssl-Modul) hat der Server die Möglichkeit, die TLS-Konfiguration zu ändern, als Reaktion darauf, dass der Client dem Server mitteilt, über welchen Hostnamen er versucht, eine Verbindung herzustellen. Dies wird hauptsächlich verwendet, um die Zertifikatkette zu ändern, um die richtige TLS-Zertifikatkette für den gegebenen Hostnamen zu präsentieren. Der spezifische Mechanismus, mit dem dies geschieht, besteht darin, ein neues SSLContext-Objekt mit der entsprechenden Konfiguration als Teil einer vom Benutzer bereitgestellten SNI-Callback-Funktion zurückzugeben.
Dies ist kein Modell, das gut auf andere TLS-Implementierungen abgebildet werden kann, und es belastet die Benutzer mit dem Schreiben von Callback-Funktionen. Stattdessen schlagen wir vor, dass die konkreten Implementierungen SNI transparent für jeden Benutzer handhaben, nachdem sie die relevanten Zertifikate erhalten haben.
Aus diesem Grund teilen wir die Verantwortung von SSLContext in zwei separate Objekte auf, die jeweils in Server- und Client-Versionen unterteilt sind. Die Objekte TLSServerConfiguration und TLSClientConfiguration fungieren als Container für eine TLS-Konfiguration: Die ClientContext- und ServerContext-Objekte werden mit einem TLSClientConfiguration- bzw. TLSServerConfiguration-Objekt instanziiert und werden zum Erstellen von Puffern oder Sockets verwendet. Alle vier Objekte wären unveränderlich.
Hinweis
Die folgenden API-Deklarationen verwenden einheitlich Typ-Hinweise zur leichteren Lesbarkeit.
Konfiguration
Die konkreten Klassen TLSServerConfiguration und TLSClientConfiguration definieren Objekte, die TLS-Konfigurationen speichern und verwalten können. Die Ziele dieser Klassen sind wie folgt:
- Bereitstellung einer Methode zur Spezifizierung der TLS-Konfiguration, die das Risiko von Tippfehlern vermeidet (dies schließt die Verwendung eines einfachen Wörterbuchs aus).
- Bereitstellung eines Objekts, das sicher mit anderen Konfigurationsobjekten verglichen werden kann, um Änderungen der TLS-Konfiguration zu erkennen, zur Verwendung mit der SNI-Callback-Funktion.
Diese Klassen sind keine Protokollklassen, hauptsächlich weil nicht erwartet wird, dass sie implementierungsspezifisches Verhalten aufweisen. Die Verantwortung für die Umwandlung eines TLSServerConfiguration- oder TLSClientConfiguration-Objekts in einen nützlichen Satz von Konfigurationen für eine gegebene TLS-Implementierung liegt bei den unten besprochenen Kontextobjekten.
Diese Klassen haben eine weitere bemerkenswerte Eigenschaft: sie sind unveränderlich. Dies ist aus mehreren Gründen wünschenswert. Am wichtigsten ist, dass Unveränderlichkeit per Voreinstellung eine gute Ingenieurpraxis ist. Als Nebeneffekt können diese Objekte als Schlüssel in Wörterbüchern verwendet werden, was für bestimmte TLS-Implementierungen und ihre SNI-Konfiguration nützlich sein kann. Darüber hinaus müssen sich Implementierungen keine Sorgen machen, dass ihre Konfigurationsobjekte unter ihren Füßen geändert werden, was es ihnen ermöglicht, sich nicht um sorgfältige Synchronisierung von Änderungen zwischen ihren konkreten Datenstrukturen und dem Konfigurationsobjekt kümmern zu müssen.
Diese Objekte sind erweiterbar: das heißt, zukünftige Python-Versionen können Konfigurationsfelder zu diesen Objekten hinzufügen, wenn sie nützlich werden. Aus Gründen der Abwärtskompatibilität werden neue Felder nur an diese Objekte angehängt. Bestehende Felder werden niemals entfernt, umbenannt oder neu geordnet. Sie sind zwischen Client und Server aufgeteilt, um die API-Verwirrung zu minimieren.
Die Klasse TLSClientConfiguration würde durch den folgenden Code definiert:
class TLSClientConfiguration:
__slots__ = (
"_certificate_chain",
"_ciphers",
"_inner_protocols",
"_lowest_supported_version",
"_highest_supported_version",
"_trust_store",
)
def __init__(
self,
certificate_chain: SigningChain | None = None,
ciphers: Sequence[CipherSuite] | None = None,
inner_protocols: Sequence[NextProtocol | bytes] | None = None,
lowest_supported_version: TLSVersion | None = None,
highest_supported_version: TLSVersion | None = None,
trust_store: TrustStore | None = None,
) -> None:
if inner_protocols is None:
inner_protocols = []
self._certificate_chain = certificate_chain
self._ciphers = ciphers
self._inner_protocols = inner_protocols
self._lowest_supported_version = lowest_supported_version
self._highest_supported_version = highest_supported_version
self._trust_store = trust_store
@property
def certificate_chain(self) -> SigningChain | None:
return self._certificate_chain
@property
def ciphers(self) -> Sequence[CipherSuite | int] | None:
return self._ciphers
@property
def inner_protocols(self) -> Sequence[NextProtocol | bytes]:
return self._inner_protocols
@property
def lowest_supported_version(self) -> TLSVersion | None:
return self._lowest_supported_version
@property
def highest_supported_version(self) -> TLSVersion | None:
return self._highest_supported_version
@property
def trust_store(self) -> TrustStore | None:
return self._trust_store
Das Objekt TLSServerConfiguration ist dem Client-Objekt ähnlich, außer dass es eine Sequence[SigningChain] als Parameter certificate_chain entgegennimmt.
Kontext
Wir definieren zwei Kontext-Protokollklassen. Diese Protokollklassen definieren Objekte, die die Konfiguration von TLS auf spezifische Verbindungen anwenden. Sie können als Fabriken für TLSSocket- und TLSBuffer-Objekte betrachtet werden.
Im Gegensatz zum aktuellen ssl-Modul bieten wir zwei Kontextklassen anstelle einer. Speziell bieten wir die Klassen ClientContext und ServerContext an. Dies vereinfacht die APIs (zum Beispiel hat es für den Server keinen Sinn, den Parameter server_hostname an wrap_socket() zu übergeben, aber da es nur eine Kontextklasse gibt, ist dieser Parameter immer noch verfügbar) und stellt sicher, dass Implementierungen so früh wie möglich wissen, auf welcher Seite einer TLS-Verbindung sie dienen werden. Zusätzlich ermöglicht es Implementierungen, sich von einer oder beiden Seiten der Verbindung abzumelden.
Soweit möglich sollten Implementierer darauf abzielen, diese Klassen unveränderlich zu machen: das heißt, sie sollten es vorziehen, Benutzern nicht zu erlauben, ihren internen Zustand direkt zu verändern, sondern stattdessen neue Kontexte aus neuen TLSConfiguration-Objekten zu erstellen. Offensichtlich können die Protokollklassen diese Einschränkung nicht erzwingen und versuchen dies daher auch nicht.
Die Protokollklasse ClientContext hat die folgende Klassendefinition:
class ClientContext(Protocol):
@abstractmethod
def __init__(self, configuration: TLSClientConfiguration) -> None:
"""Create a new client context object from a given TLS client configuration."""
...
@property
@abstractmethod
def configuration(self) -> TLSClientConfiguration:
"""Returns the TLS client configuration that was used to create the client context."""
...
@abstractmethod
def connect(self, address: tuple[str | None, int]) -> TLSSocket:
"""Creates a TLSSocket that behaves like a socket.socket, and
contains information about the TLS exchange
(cipher, negotiated_protocol, negotiated_tls_version, etc.).
"""
...
@abstractmethod
def create_buffer(self, server_hostname: str) -> TLSBuffer:
"""Creates a TLSBuffer that acts as an in-memory channel,
and contains information about the TLS exchange
(cipher, negotiated_protocol, negotiated_tls_version, etc.)."""
...
Die ServerContext ist ähnlich, nimmt jedoch stattdessen eine TLSServerConfiguration entgegen.
Socket
Der Kontext kann zum Erstellen von Sockets verwendet werden, die der Spezifikation der Protokollklasse TLSSocket folgen müssen. Insbesondere müssen Implementierungen Folgendes implementieren:
recvundsendlistenundacceptclosegetsocknamegetpeername
Sie müssen auch einige Schnittstellen implementieren, die Informationen über die TLS-Verbindung liefern, wie z. B.:
- Das zugrundeliegende Kontextobjekt, das zum Erstellen dieses Sockets verwendet wurde
- Der ausgehandelte Cipher
- Das ausgehandelte „next“-Protokoll
- Die ausgehandelte TLS-Version
Der folgende Code beschreibt diese Funktionen im Detail:
class TLSSocket(Protocol):
"""This class implements a socket.socket-like object that creates an OS
socket, wraps it in an SSL context, and provides read and write methods
over that channel."""
@abstractmethod
def __init__(self, *args: tuple, **kwargs: tuple) -> None:
"""TLSSockets should not be constructed by the user.
The implementation should implement a method to construct a TLSSocket
object and call it in ClientContext.connect() and
ServerContext.connect()."""
...
@abstractmethod
def recv(self, bufsize: int) -> bytes:
"""Receive data from the socket. The return value is a bytes object
representing the data received. Should not work before the handshake
is completed."""
...
@abstractmethod
def send(self, bytes: bytes) -> int:
"""Send data to the socket. The socket must be connected to a remote socket."""
...
@abstractmethod
def close(self, force: bool = False) -> None:
"""Shuts down the connection and mark the socket closed.
If force is True, this method should send the close_notify alert and shut down
the socket without waiting for the other side.
If force is False, this method should send the close_notify alert and raise
the WantReadError exception until a corresponding close_notify alert has been
received from the other side.
In either case, this method should return WantWriteError if sending the
close_notify alert currently fails."""
...
@abstractmethod
def listen(self, backlog: int) -> None:
"""Enable a server to accept connections. If backlog is specified, it
specifies the number of unaccepted connections that the system will allow
before refusing new connections."""
...
@abstractmethod
def accept(self) -> tuple[TLSSocket, tuple[str | None, int]]:
"""Accept a connection. The socket must be bound to an address and listening
for connections. The return value is a pair (conn, address) where conn is a
new TLSSocket object usable to send and receive data on the connection, and
address is the address bound to the socket on the other end of the connection."""
...
@abstractmethod
def getsockname(self) -> tuple[str | None, int]:
"""Return the local address to which the socket is connected."""
...
@abstractmethod
def getpeercert(self) -> bytes | None:
"""
Return the raw DER bytes of the certificate provided by the peer
during the handshake, if applicable.
"""
...
@abstractmethod
def getpeername(self) -> tuple[str | None, int]:
"""Return the remote address to which the socket is connected."""
...
@property
@abstractmethod
def context(self) -> ClientContext | ServerContext:
"""The ``Context`` object this socket is tied to."""
...
@abstractmethod
def cipher(self) -> CipherSuite | int | None:
"""
Returns the CipherSuite entry for the cipher that has been negotiated on the connection.
If no connection has been negotiated, returns ``None``. If the cipher negotiated is not
defined in CipherSuite, returns the 16-bit integer representing that cipher directly.
"""
...
@abstractmethod
def negotiated_protocol(self) -> NextProtocol | bytes | None:
"""
Returns the protocol that was selected during the TLS handshake.
This selection may have been made using ALPN or some future
negotiation mechanism.
If the negotiated protocol is one of the protocols defined in the
``NextProtocol`` enum, the value from that enum will be returned.
Otherwise, the raw bytestring of the negotiated protocol will be
returned.
If ``Context.set_inner_protocols()`` was not called, if the other
party does not support protocol negotiation, if this socket does
not support any of the peer's proposed protocols, or if the
handshake has not happened yet, ``None`` is returned.
"""
...
@property
@abstractmethod
def negotiated_tls_version(self) -> TLSVersion | None:
"""The version of TLS that has been negotiated on this connection."""
...
Puffer
Der Kontext kann auch zum Erstellen von Puffern verwendet werden, die der Spezifikation der Protokollklasse TLSBuffer folgen müssen. Insbesondere müssen Implementierungen Folgendes implementieren:
readundwritedo_handshakeshutdownprocess_incomingundprocess_outgoingincoming_bytes_bufferedundoutgoing_bytes_bufferedgetpeercert
Ähnlich wie im Socket-Fall müssen sie auch einige Schnittstellen implementieren, die Informationen über die TLS-Verbindung liefern, wie z. B.:
- Das zugrundeliegende Kontextobjekt, das zum Erstellen dieses Puffers verwendet wurde
- Der ausgehandelte Cipher
- Das ausgehandelte „next“-Protokoll
- Die ausgehandelte TLS-Version
Der folgende Code beschreibt diese Funktionen im Detail:
class TLSBuffer(Protocol):
"""This class implements an in memory-channel that creates two buffers,
wraps them in an SSL context, and provides read and write methods over
that channel."""
@abstractmethod
def read(self, amt: int, buffer: Buffer | None) -> bytes | int:
"""
Read up to ``amt`` bytes of data from the input buffer and return
the result as a ``bytes`` instance. If an optional buffer is
provided, the result is written into the buffer and the number of
bytes is returned instead.
Once EOF is reached, all further calls to this method return the
empty byte string ``b''``.
May read "short": that is, fewer bytes may be returned than were
requested.
Raise ``WantReadError`` or ``WantWriteError`` if there is
insufficient data in either the input or output buffer and the
operation would have caused data to be written or read.
May raise ``RaggedEOF`` if the connection has been closed without a
graceful TLS shutdown. Whether this is an exception that should be
ignored or not is up to the specific application.
As at any time a re-negotiation is possible, a call to ``read()``
can also cause write operations.
"""
...
@abstractmethod
def write(self, buf: Buffer) -> int:
"""
Write ``buf`` in encrypted form to the output buffer and return the
number of bytes written. The ``buf`` argument must be an object
supporting the buffer interface.
Raise ``WantReadError`` or ``WantWriteError`` if there is
insufficient data in either the input or output buffer and the
operation would have caused data to be written or read. In either
case, users should endeavour to resolve that situation and then
re-call this method. When re-calling this method users *should*
re-use the exact same ``buf`` object, as some implementations require that
the exact same buffer be used.
This operation may write "short": that is, fewer bytes may be
written than were in the buffer.
As at any time a re-negotiation is possible, a call to ``write()``
can also cause read operations.
"""
...
@abstractmethod
def do_handshake(self) -> None:
"""
Performs the TLS handshake. Also performs certificate validation
and hostname verification.
"""
...
@abstractmethod
def cipher(self) -> CipherSuite | int | None:
"""
Returns the CipherSuite entry for the cipher that has been
negotiated on the connection. If no connection has been negotiated,
returns ``None``. If the cipher negotiated is not defined in
CipherSuite, returns the 16-bit integer representing that cipher
directly.
"""
...
@abstractmethod
def negotiated_protocol(self) -> NextProtocol | bytes | None:
"""
Returns the protocol that was selected during the TLS handshake.
This selection may have been made using ALPN, NPN, or some future
negotiation mechanism.
If the negotiated protocol is one of the protocols defined in the
``NextProtocol`` enum, the value from that enum will be returned.
Otherwise, the raw bytestring of the negotiated protocol will be
returned.
If ``Context.set_inner_protocols()`` was not called, if the other
party does not support protocol negotiation, if this socket does
not support any of the peer's proposed protocols, or if the
handshake has not happened yet, ``None`` is returned.
"""
...
@property
@abstractmethod
def context(self) -> ClientContext | ServerContext:
"""
The ``Context`` object this buffer is tied to.
"""
...
@property
@abstractmethod
def negotiated_tls_version(self) -> TLSVersion | None:
"""
The version of TLS that has been negotiated on this connection.
"""
...
@abstractmethod
def shutdown(self) -> None:
"""
Performs a clean TLS shut down. This should generally be used
whenever possible to signal to the remote peer that the content is
finished.
"""
...
@abstractmethod
def process_incoming(self, data_from_network: bytes) -> None:
"""
Receives some TLS data from the network and stores it in an
internal buffer.
If the internal buffer is overfull, this method will raise
``WantReadError`` and store no data. At this point, the user must
call ``read`` to remove some data from the internal buffer
before repeating this call.
"""
...
@abstractmethod
def incoming_bytes_buffered(self) -> int:
"""
Returns how many bytes are in the incoming buffer waiting to be processed.
"""
...
@abstractmethod
def process_outgoing(self, amount_bytes_for_network: int) -> bytes:
"""
Returns the next ``amt`` bytes of data that should be written to
the network from the outgoing data buffer, removing it from the
internal buffer.
"""
...
@abstractmethod
def outgoing_bytes_buffered(self) -> int:
"""
Returns how many bytes are in the outgoing buffer waiting to be sent.
"""
...
@abstractmethod
def getpeercert(self) -> bytes | None:
"""
Return the raw DER bytes of the certificate provided by the peer
during the handshake, if applicable.
"""
...
Cipher Suiten
Die Unterstützung von Cipher Suiten auf eine wirklich bibliotheksunabhängige Weise ist ein bemerkenswert schwieriges Unterfangen. Verschiedene TLS-Implementierungen haben oft radikal unterschiedliche APIs zur Spezifizierung von Cipher Suiten, aber problematischer ist, dass diese APIs häufig in ihrer Fähigkeit sowie in ihrem Stil variieren.
Unten sind Beispiele für verschiedene Cipher-Suiten-Auswahl-APIs aufgeführt. Diese Beispiele sind nicht dazu gedacht, eine Implementierung gegen jede API zu verpflichten, sondern nur, um die von jeder auferlegten Einschränkungen zu beleuchten.
OpenSSL
OpenSSL verwendet ein bekanntes Cipher-String-Format. Dieses Format wurde als Konfigurationssprache von den meisten Produkten übernommen, die OpenSSL verwenden, einschließlich Python. Dieses Format ist relativ einfach zu lesen, hat aber eine Reihe von Nachteilen: Es ist ein String, was die Eingabe falscher Eingaben erleichtert; es fehlt eine detaillierte Validierung, was bedeutet, dass es möglich ist, OpenSSL so zu konfigurieren, dass es überhaupt keine Cipher aushandeln kann; und es erlaubt die Angabe von Cipher Suiten auf verschiedene Weise, was die Analyse erschwert. Das größte Problem dieses Formats ist, dass es keine formelle Spezifikation dafür gibt, was bedeutet, dass die einzige Möglichkeit, einen gegebenen String so zu analysieren, wie es OpenSSL tun würde, darin besteht, OpenSSL dazu zu bringen, ihn zu analysieren.
OpenSSL-Cipher-Strings können wie folgt aussehen:
"ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:DH+CHACHA20:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5"
Dieser String demonstriert einige der Komplexität des OpenSSL-Formats. Zum Beispiel ist es möglich, dass ein Eintrag mehrere Cipher Suiten angibt: der Eintrag ECDH+AESGCM bedeutet „alle Cipher Suiten, die sowohl Elliptic-Curve-Diffie-Hellman-Schlüsselaustausch als auch AES im Galois Counter Mode enthalten“. Genauer gesagt, dies wird zu vier Cipher Suiten erweitert:
"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"
Das macht die Analyse eines vollständigen OpenSSL-Cipher-Strings extrem schwierig. Hinzu kommt, dass es weitere Metazeichen gibt, wie z. B. „!“ (schließt alle Cipher Suiten aus, die diesem Kriterium entsprechen, auch wenn sie sonst enthalten wären: „!MD5“ bedeutet, dass keine Cipher Suiten, die den MD5-Hash-Algorithmus verwenden, enthalten sein dürfen), „-“ (schließt übereinstimmende Cipher aus, wenn sie bereits enthalten waren, erlaubt aber, sie später wieder hinzuzufügen, wenn sie wieder aufgenommen werden), und „+“ (schließt übereinstimmende Cipher ein, platziert sie aber am Ende der Liste), und man erhält ein extrem komplexes Format zum Analysieren. Zusätzlich zu dieser Komplexität ist zu beachten, dass das tatsächliche Ergebnis von der OpenSSL-Version abhängt, da ein OpenSSL-Cipher-String gültig ist, solange er mindestens einen Cipher enthält, den OpenSSL erkennt.
OpenSSL verwendet auch andere Namen für seine Cipher als die Namen, die in den relevanten Spezifikationen verwendet werden. Weitere Einzelheiten finden Sie auf der Handbuchseite für ciphers(1).
Die tatsächliche API innerhalb von OpenSSL für den Cipher-String ist einfach:
char *cipher_list = <some cipher list>;
int rc = SSL_CTX_set_cipher_list(context, cipher_list);
Das bedeutet, dass jedes Format, das von diesem Modul verwendet wird, in einen OpenSSL-Cipher-String für die Verwendung mit OpenSSL konvertiert werden können muss.
Network Framework
Network Framework ist die TLS-Systembibliothek von macOS (10.15+). Diese Bibliothek ist in vielerlei Hinsicht erheblich eingeschränkter als OpenSSL, da sie eine viel eingeschränktere Benutzerklasse hat. Eine dieser wesentlichen Einschränkungen ist die Kontrolle über unterstützte Cipher Suiten.
Cipher in Network Framework werden durch eine Objective-C uint16_t-Enumeration dargestellt. Diese Enumeration hat einen Eintrag pro Cipher-Suite, ohne aggregierte Einträge, was bedeutet, dass es nicht möglich ist, die Bedeutung eines OpenSSL-Cipher-Strings wie „ECDH+AESGCM“ zu reproduzieren, ohne manuell zu kodieren, zu welchen Kategorien jedes Enumerationsmitglied gehört.
Die Namen der meisten Enumerationsmitglieder stimmen jedoch mit den formellen Namen der Cipher-Suiten überein: das heißt, die Cipher-Suite, die OpenSSL als „ECDHE-ECDSA-AES256-GCM-SHA384“ bezeichnet, heißt „tls_ciphersuite_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384“ in Network Framework.
Die API zur Konfiguration von Cipher Suiten innerhalb von Network Framework ist einfach:
void sec_protocol_options_append_tls_ciphersuite(sec_protocol_options_t options, tls_ciphersuite_t ciphersuite);
SChannel
SChannel ist die Windows-TLS-Systembibliothek.
SChannel hat eine extrem eingeschränkte Unterstützung für die Kontrolle der verfügbaren TLS-Cipher-Suiten und verwendet zusätzlich eine dritte Methode zur Angabe, welche TLS-Cipher-Suiten unterstützt werden.
Insbesondere definiert SChannel eine Reihe von ALG_ID-Konstanten (C unsigned ints). Jede dieser Konstanten bezieht sich nicht auf eine gesamte Cipher-Suite, sondern auf einen einzelnen Algorithmus. Einige Beispiele sind CALG_3DES und CALG_AES_256, die sich auf den Bulk-Verschlüsselungsalgorithmus beziehen, der in einer Cipher-Suite verwendet wird, CALG_ECDH_EPHEM und CALG_RSA_KEYX, die sich auf Teile des Schlüsselaustauschalgorithmus beziehen, der in einer Cipher-Suite verwendet wird, CALG_SHA_256 und CALG_SHA_384, die sich auf den in einer Cipher-Suite verwendeten Message Authentication Code beziehen, und CALG_ECDSA und CALG_RSA_SIGN, die sich auf die Signaturteile des Schlüsselaustauschs beziehen.
In früheren Versionen der SChannel-API wurden diese Konstanten verwendet, um die erlaubten Algorithmen zu definieren. Die neueste Version verwendet jedoch diese Konstanten, um zu verbieten, welche Algorithmen verwendet werden dürfen.
Dies kann als die Hälfte der Funktionalität von OpenSSL betrachtet werden, die Network Framework nicht hat: Network Framework erlaubt nur die Angabe exakter Cipher Suiten (und einer begrenzten Anzahl vordefinierter Cipher Suite-Gruppen), während SChannel nur die Angabe von Teilen der Cipher Suite erlaubt, während OpenSSL beides erlaubt.
Die Bestimmung, welche Cipher Suiten bei einer bestimmten Verbindung zulässig sind, erfolgt durch Angabe eines Zeigers auf ein Array dieser ALG_ID-Konstanten. Das bedeutet, dass jede geeignete API es dem Python-Code ermöglichen muss, zu bestimmen, welche ALG_ID-Konstanten angegeben werden müssen.
Network Security Services (NSS)
NSS ist die Krypto- und TLS-Bibliothek von Mozilla. Sie wird in Firefox, Thunderbird und als Alternative zu OpenSSL in mehreren Bibliotheken (z.B. curl) verwendet.
Standardmäßig verfügt NSS über eine sichere Konfiguration erlaubter Cipher. Auf einigen Plattformen wie Fedora ist die Liste der aktivierten Cipher global in einer Systemrichtlinie konfiguriert. Im Allgemeinen sollten Anwendungen Cipher Suiten nicht ändern, es sei denn, sie haben spezifische Gründe dafür.
NSS verfügt sowohl über prozessweite als auch über verbindungsbezogene Einstellungen für Cipher Suiten. Es gibt kein Konzept von SSLContext wie bei OpenSSL. Ein SSLContext-ähnliches Verhalten kann leicht emuliert werden. Speziell können Cipher global mit SSL_CipherPrefSetDefault(PRInt32 cipher, PRBool enabled) und SSL_CipherPrefSet(PRFileDesc *fd, PRInt32 cipher, PRBool enabled) für eine Verbindung aktiviert oder deaktiviert werden. Die PRInt32-Nummer des Ciphers ist eine vorzeichenbehaftete 32-Bit-Ganzzahl, die direkt einer registrierten IANA-ID entspricht, z.B. 0x1301 ist TLS_AES_128_GCM_SHA256. Im Gegensatz zu OpenSSL ist die Präferenzreihenfolge der Cipher fest und kann zur Laufzeit nicht geändert werden.
Ähnlich wie Network Framework hat NSS keine API für aggregierte Einträge. Einige NSS-Konsumenten haben benutzerdefinierte Zuordnungen von OpenSSL-Cipher-Namen und -Regeln zu NSS-Ciphers implementiert, z.B. mod_nss.
Vorgeschlagene Schnittstelle
Die vorgeschlagene Schnittstelle für das neue Modul wird von der kombinierten Menge der Einschränkungen der oben genannten Implementierungen beeinflusst. Insbesondere da jede Implementierung außer OpenSSL verlangt, dass jeder einzelne Cipher bereitgestellt wird, gibt es keine andere Wahl, als diesen kleinsten gemeinsamen Nenner-Ansatz zu verfolgen.
Der einfachste Ansatz ist die Bereitstellung eines aufzählbaren Typs, der eine große Teilmenge der für TLS definierten Cipher Suiten enthält. Die Werte der Enum-Mitglieder sind ihre zweibaitige Cipher-Kennung, wie sie im TLS-Handshake verwendet wird, gespeichert als 16-Bit-Ganzzahl. Die Namen der Enum-Mitglieder sind ihre IANA-registrierten Cipher-Suite-Namen.
Die IANA Cipher Suite Registry enthält derzeit über 320 Cipher Suiten. Ein großer Teil der Cipher Suiten ist für TLS-Verbindungen zu Netzwerkdiensten irrelevant. Andere Suiten spezifizieren veraltete und unsichere Algorithmen, die von neueren Versionen von Implementierungen nicht mehr bereitgestellt werden. Die Enum enthält die fünf festen Cipher Suiten, die für TLS v1.3 definiert sind. Für TLS v1.2 enthält sie nur die Cipher Suiten, die den TLS v1.3 Cipher Suiten entsprechen, mit ECDHE-Schlüsselaustausch (für Perfect Forward Secrecy) und ECDSA- oder RSA-Signaturen, was weitere zehn Cipher Suiten sind.
Zusätzlich zu dieser Enum definiert die Schnittstelle eine Standard-Cipher-Suite-Liste für TLS v1.2, die nur die definierten Cipher Suiten auf Basis von AES-GCM oder ChaCha20-Poly1305 enthält. Die Standard-Cipher-Suite-Liste für TLS v1.3 umfasst die fünf in der Spezifikation definierten Cipher Suiten.
Die aktuelle Enum ist recht eingeschränkt und enthält nur Cipher Suiten, die Perfect Forward Secrecy bieten. Da die Enum nicht jeden definierten Cipher enthält, und auch um zukunftsorientierte Anwendungen zu ermöglichen, akzeptieren alle Teile dieser API, die CipherSuite-Objekte entgegennehmen, auch direkt rohe 16-Bit-Ganzzahlen.
class CipherSuite(IntEnum):
"""
Known cipher suites.
See: <https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml>
"""
TLS_AES_128_GCM_SHA256 = 0x1301
TLS_AES_256_GCM_SHA384 = 0x1302
TLS_CHACHA20_POLY1305_SHA256 = 0x1303
TLS_AES_128_CCM_SHA256 = 0x1304
TLS_AES_128_CCM_8_SHA256 = 0x1305
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 0xC02B
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 0xC02C
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 0xC02F
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 = 0xC030
TLS_ECDHE_ECDSA_WITH_AES_128_CCM = 0xC0AC
TLS_ECDHE_ECDSA_WITH_AES_256_CCM = 0xC0AD
TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 = 0xC0AE
TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8 = 0xC0AF
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 0xCCA8
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 = 0xCCA9
Für das Network Framework verweisen diese Enum-Mitglieder direkt auf die Werte der Cipher-Suite-Konstanten. Zum Beispiel definiert das Network Framework das Enum-Mitglied der Cipher-Suite tls_ciphersuite_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 mit dem Wert 0xC02C. Nicht zufällig ist dies identisch mit seinem Wert in der obigen Enum. Dies erleichtert die Zuordnung zwischen Network Framework und der obigen Enum erheblich.
Für SChannel gibt es keine einfache direkte Zuordnung, da SChannel Chiffren anstelle von Cipher-Suiten konfiguriert. Dies stellt ein anhaltendes Problem für SChannel dar, nämlich dass es im Vergleich zu anderen TLS-Implementierungen sehr schwierig zu konfigurieren ist.
Für die Zwecke dieses PEP muss jede SChannel-Implementierung bestimmen, welche Chiffren basierend auf den Enum-Mitgliedern ausgewählt werden sollen. Dies kann offener sein, als die tatsächliche Liste der Cipher-Suiten zulässt, oder es kann restriktiver sein, abhängig von den Entscheidungen der Implementierung. Dieses PEP empfiehlt, dass es restriktiver ist, aber natürlich kann dies nicht erzwungen werden.
Schließlich gehen wir davon aus, dass für die meisten Benutzer sichere Standardeinstellungen ausreichen werden. Wenn keine Liste von Chiffren angegeben wird, sollten die Implementierungen sichere Standardeinstellungen verwenden (möglicherweise abgeleitet von systemempfohlenen Einstellungen).
Protokollverhandlung
ALPN ermöglicht die Protokollverhandlung als Teil des HTTP/2-Handshakes. Während ALPN grundlegend auf Byte-Strings aufgebaut ist, sind zeichenkettenbasierte APIs häufig problematisch, da sie Tippfehler zulassen, die schwer zu erkennen sind.
Aus diesem Grund wird dieses Modul einen Typ definieren, den Implementierungen der Protokollverhandlung übergeben und von ihnen erhalten können. Dieser Typ würde einen Byte-String kapseln, um Aliase für bekannte Protokolle zu ermöglichen. Dies ermöglicht es uns, die Probleme zu vermeiden, die Tippfehlern bei bekannten Protokollen inhärent sind, während die volle Erweiterbarkeit der Protokollverhandlungsschicht erhalten bleibt, wenn dies von Benutzern benötigt wird, indem Byte-Strings direkt übergeben werden.
class NextProtocol(Enum):
"""The underlying negotiated ("next") protocol."""
H2 = b"h2"
H2C = b"h2c"
HTTP1 = b"http/1.1"
WEBRTC = b"webrtc"
C_WEBRTC = b"c-webrtc"
FTP = b"ftp"
STUN = b"stun.nat-discovery"
TURN = b"stun.turn"
TLS-Versionen
Es ist oft nützlich, die TLS-Versionen einschränken zu können, die man zu unterstützen bereit ist. Es gibt viele Sicherheitsvorteile, wenn man sich weigert, alte TLS-Versionen zu verwenden, und einige schlecht funktionierende Server können TLS-Clients, die die Unterstützung für neuere Versionen bewerben, nicht korrekt behandeln.
Der folgende Aufzählungstyp kann verwendet werden, um TLS-Versionen zu steuern. Zukunftsgerichtete Anwendungen sollten niemals eine maximale TLS-Version festlegen, es sei denn, sie müssen dies unbedingt tun, da eine TLS-Implementierung, die neuer ist als das Python, das sie verwendet, möglicherweise TLS-Versionen unterstützt, die nicht in diesem Aufzählungstyp enthalten sind.
Zusätzlich definiert dieser Aufzählungstyp zwei zusätzliche Flags, die immer verwendet werden können, um entweder die niedrigste oder die höchste von einer Implementierung unterstützte TLS-Version anzufordern. Wie bei Cipher-Suiten gehen wir davon aus, dass für die meisten Benutzer sichere Standardeinstellungen ausreichen. Wenn keine Liste von TLS-Versionen angegeben wird, sollten die Implementierungen sichere Standardeinstellungen verwenden (möglicherweise abgeleitet von systemempfohlenen Einstellungen).
class TLSVersion(Enum):
"""
TLS versions.
The `MINIMUM_SUPPORTED` and `MAXIMUM_SUPPORTED` variants are "open ended",
and refer to the "lowest mutually supported" and "highest mutually supported"
TLS versions, respectively.
"""
MINIMUM_SUPPORTED = "MINIMUM_SUPPORTED"
TLSv1_2 = "TLSv1.2"
TLSv1_3 = "TLSv1.3"
MAXIMUM_SUPPORTED = "MAXIMUM_SUPPORTED"
Fehler
Dieses Modul würde vier Basisklassen für die Fehlerbehandlung definieren. Im Gegensatz zu vielen der hier definierten anderen Klassen sind diese Klassen nicht abstrakt, da sie kein Verhalten aufweisen. Sie existieren einfach, um bestimmte allgemeine Verhaltensweisen zu signalisieren. TLS-Implementierungen sollten diese Ausnahmen in ihren eigenen Paketen unterklassen, müssen aber kein Verhalten für sie definieren.
Im Allgemeinen sollten konkrete Implementierungen diese Ausnahmen unterklassen, anstatt sie direkt auszulösen. Dies erleichtert es mäßig, während des Debuggens unerwarteter Fehler zu ermitteln, welche konkrete TLS-Implementierung in Gebrauch ist. Dies ist jedoch nicht zwingend erforderlich.
Die Definitionen der Fehler sind unten aufgeführt.
class TLSError(Exception):
"""
The base exception for all TLS related errors from any implementation.
Catching this error should be sufficient to catch *all* TLS errors,
regardless of what implementation is used.
"""
class WantWriteError(TLSError):
"""
A special signaling exception used only when non-blocking or buffer-only I/O is used.
This error signals that the requested
operation cannot complete until more data is written to the network,
or until the output buffer is drained.
This error is should only be raised when it is completely impossible
to write any data. If a partial write is achievable then this should
not be raised.
"""
class WantReadError(TLSError):
"""
A special signaling exception used only when non-blocking or buffer-only I/O is used.
This error signals that the requested
operation cannot complete until more data is read from the network, or
until more data is available in the input buffer.
This error should only be raised when it is completely impossible to
write any data. If a partial write is achievable then this should not
be raised.
"""
class RaggedEOF(TLSError):
"""A special signaling exception used when a TLS connection has been
closed gracelessly: that is, when a TLS CloseNotify was not received
from the peer before the underlying TCP socket reached EOF. This is a
so-called "ragged EOF".
This exception is not guaranteed to be raised in the face of a ragged
EOF: some implementations may not be able to detect or report the
ragged EOF.
This exception is not always a problem. Ragged EOFs are a concern only
when protocols are vulnerable to length truncation attacks. Any
protocol that can detect length truncation attacks at the application
layer (e.g. HTTP/1.1 and HTTP/2) is not vulnerable to this kind of
attack and so can ignore this exception.
"""
class ConfigurationError(TLSError):
"""An special exception that implementations can use when the provided
configuration uses features not supported by that implementation."""
Zertifikate
Dieses Modul würde eine konkrete Zertifikatklasse definieren. Diese Klasse hätte fast kein Verhalten, da das Ziel dieses Moduls nicht ist, alle möglichen relevanten kryptografischen Funktionen bereitzustellen, die von X.509-Zertifikaten bereitgestellt werden könnten. Stattdessen benötigen wir lediglich die Möglichkeit, die Quelle eines Zertifikats an eine konkrete Implementierung zu signalisieren.
Aus diesem Grund definiert diese Zertifikatklasse drei Attribute, die den drei vorgesehenen Konstruktoren entsprechen: Zertifikate aus Dateien, Zertifikate aus dem Speicher oder Zertifikate aus beliebigen Bezeichnern. Es ist möglich, dass Implementierungen nicht alle diese Konstruktoren unterstützen, und sie können dies den Benutzern wie im Abschnitt "Laufzeit" unten beschrieben mitteilen. Zertifikate aus beliebigen Bezeichnern werden insbesondere voraussichtlich hauptsächlich für Benutzer nützlich sein, die Integrationen auf Basis von HSMs, TPMs, SSMs und ähnlichem aufbauen möchten.
Insbesondere analysiert diese Klasse keine bereitgestellten Eingaben, um zu überprüfen, ob es sich um ein korrektes Zertifikat handelt, und bietet auch keine Form der Introspektion eines bestimmten Zertifikats. TLS-Implementierungen sind ebenfalls nicht verpflichtet, eine solche Introspektion bereitzustellen. Peer-Zertifikate, die während des Handshakes empfangen werden, werden als rohe DER-Bytes bereitgestellt.
class Certificate:
"""Object representing a certificate used in TLS."""
__slots__ = (
"_buffer",
"_path",
"_id",
)
def __init__(
self, buffer: bytes | None = None, path: os.PathLike[str] | None = None, id: bytes | None = None
):
"""
Creates a Certificate object from a path, buffer, or ID.
If none of these is given, an exception is raised.
"""
if buffer is None and path is None and id is None:
raise ValueError("Certificate cannot be empty.")
self._buffer = buffer
self._path = path
self._id = id
@classmethod
def from_buffer(cls, buffer: bytes) -> Certificate:
"""
Creates a Certificate object from a byte buffer. This byte buffer
may be either PEM-encoded or DER-encoded. If the buffer is PEM
encoded it *must* begin with the standard PEM preamble (a series of
dashes followed by the ASCII bytes "BEGIN CERTIFICATE" and another
series of dashes). In the absence of that preamble, the
implementation may assume that the certificate is DER-encoded
instead.
"""
return cls(buffer=buffer)
@classmethod
def from_file(cls, path: os.PathLike[str]) -> Certificate:
"""
Creates a Certificate object from a file on disk. The file on disk
should contain a series of bytes corresponding to a certificate that
may be either PEM-encoded or DER-encoded. If the bytes are PEM encoded
it *must* begin with the standard PEM preamble (a series of dashes
followed by the ASCII bytes "BEGIN CERTIFICATE" and another series of
dashes). In the absence of that preamble, the implementation may
assume that the certificate is DER-encoded instead.
"""
return cls(path=path)
@classmethod
def from_id(cls, id: bytes) -> Certificate:
"""
Creates a Certificate object from an arbitrary identifier. This may
be useful for implementations that rely on system certificate stores.
"""
return cls(id=id)
Private Schlüssel
Dieses Modul würde eine konkrete Schlüsselklasse definieren. Ähnlich wie die Certificate-Klasse hat diese Klasse drei Attribute, die den drei Konstruktoren entsprechen, und darüber hinaus alle Vorbehalte der Certificate-Klasse.
class PrivateKey:
"""Object representing a private key corresponding to a public key
for a certificate used in TLS."""
__slots__ = (
"_buffer",
"_path",
"_id",
)
def __init__(
self, buffer: bytes | None = None, path: os.PathLike | None = None, id: bytes | None = None
):
"""
Creates a PrivateKey object from a path, buffer, or ID.
If none of these is given, an exception is raised.
"""
if buffer is None and path is None and id is None:
raise ValueError("PrivateKey cannot be empty.")
self._buffer = buffer
self._path = path
self._id = id
@classmethod
def from_buffer(cls, buffer: bytes) -> PrivateKey:
"""
Creates a PrivateKey object from a byte buffer. This byte buffer
may be either PEM-encoded or DER-encoded. If the buffer is PEM
encoded it *must* begin with the standard PEM preamble (a series of
dashes followed by the ASCII bytes "BEGIN", the key type, and
another series of dashes). In the absence of that preamble, the
implementation may assume that the private key is DER-encoded
instead.
"""
return cls(buffer=buffer)
@classmethod
def from_file(cls, path: os.PathLike) -> PrivateKey:
"""
Creates a PrivateKey object from a file on disk. The file on disk
should contain a series of bytes corresponding to a certificate that
may be either PEM-encoded or DER-encoded. If the bytes are PEM encoded
it *must* begin with the standard PEM preamble (a series of dashes
followed by the ASCII bytes "BEGIN", the key type, and another series
of dashes). In the absence of that preamble, the implementation may
assume that the certificate is DER-encoded instead.
"""
return cls(path=path)
@classmethod
def from_id(cls, id: bytes) -> PrivateKey:
"""
Creates a PrivateKey object from an arbitrary identifier. This may
be useful for implementations that rely on system private key stores.
"""
return cls(id=id)
Signaturkette
Um sich zu authentifizieren, müssen TLS-Teilnehmer ein Blattzertifikat mit einer Kette bereitstellen, die zu einem Stammzertifikat führt, das von der Gegenseite als vertrauenswürdig eingestuft wird. Server müssen sich immer bei Clients authentifizieren, aber Clients können sich während der Client-Authentifizierung auch bei Servern authentifizieren. Zusätzlich muss das Blattzertifikat von einem privaten Schlüssel begleitet werden, der entweder in einem separaten Objekt oder zusammen mit dem Blattzertifikat selbst gespeichert werden kann. Dieses Modul definiert die Sammlung dieser Objekte als SigningChain, wie unten detailliert beschrieben.
class SigningChain:
"""Object representing a certificate chain used in TLS."""
leaf: tuple[Certificate, PrivateKey | None]
chain: list[Certificate]
def __init__(
self,
leaf: tuple[Certificate, PrivateKey | None],
chain: Sequence[Certificate] | None = None,
):
"""Initializes a SigningChain object."""
self.leaf = leaf
if chain is None:
chain = []
self.chain = list(chain)
Wie in den obigen Konfigurationsklassen gezeigt, kann ein Client im Falle einer Client-Authentifizierung eine Signaturkette haben oder andernfalls keine. Ein Server kann eine Sequenz von Signaturketten haben, was nützlich ist, wenn er für mehrere Domänen verantwortlich ist.
Vertrauensspeicher
Wie oben erläutert, stellt das Laden eines Trust Stores ein Problem dar, da verschiedene TLS-Implementierungen stark variieren, wie Benutzer Trust Stores auswählen können. Aus diesem Grund müssen wir ein Modell bereitstellen, das nur sehr wenige Annahmen über die Form von Trust Stores trifft.
Dieses Problem ist dasselbe wie das, das die Typen Certificate und PrivateKey lösen müssen. Aus diesem Grund verwenden wir genau dasselbe Modell, indem wir eine konkrete Klasse erstellen, die die verschiedenen Möglichkeiten erfasst, wie Benutzer einen Trust Store definieren könnten.
Eine gegebene TLS-Implementierung ist nicht verpflichtet, alle möglichen Trust Stores zu verarbeiten. Es wird jedoch dringend empfohlen, dass eine gegebene TLS-Implementierung den system-Konstruktor nach Möglichkeit handhabt, da dies der am häufigsten verwendete Validierungs-Trust Store ist. TLS-Implementierungen können nicht unterstützte Optionen wie im Abschnitt "Laufzeit" unten beschrieben mitteilen.
class TrustStore:
"""
The trust store that is used to verify certificate validity.
"""
__slots__ = (
"_buffer",
"_path",
"_id",
)
def __init__(
self, buffer: bytes | None = None, path: os.PathLike | None = None, id: bytes | None = None
):
"""
Creates a TrustStore object from a path, buffer, or ID.
If none of these is given, the default system trust store is used.
"""
self._buffer = buffer
self._path = path
self._id = id
@classmethod
def system(cls) -> TrustStore:
"""
Returns a TrustStore object that represents the system trust
database.
"""
return cls()
@classmethod
def from_buffer(cls, buffer: bytes) -> TrustStore:
"""
Initializes a trust store from a buffer of PEM-encoded certificates.
"""
return cls(buffer=buffer)
@classmethod
def from_file(cls, path: os.PathLike) -> TrustStore:
"""
Initializes a trust store from a single file containing PEMs.
"""
return cls(path=path)
@classmethod
def from_id(cls, id: bytes) -> TrustStore:
"""
Initializes a trust store from an arbitrary identifier.
"""
return cls(id=id)
Laufzeitzugriff
Ein nicht unüblicher Anwendungsfall ist, dass Bibliotheksbenutzer die zu verwendende TLS-Implementierung angeben möchten, während die Bibliothek die Details der tatsächlichen TLS-Verbindung konfigurieren kann. Benutzer von requests möchten beispielsweise zwischen OpenSSL und einer plattformnativen Lösung unter Windows und macOS oder zwischen OpenSSL und NSS auf einigen Linux-Plattformen wählen können. Diese Benutzer kümmern sich jedoch möglicherweise nicht darum, wie genau ihre TLS-Konfiguration durchgeführt wird.
Dies wirft zwei Probleme auf: Wie kann eine Bibliothek angesichts einer beliebigen konkreten Implementierung
- Herausfinden, ob die Implementierung bestimmte Konstruktoren für Zertifikate oder Trust Stores unterstützt (z. B. aus beliebigen Bezeichnern)?
- Die richtigen Typen für die beiden Kontextklassen erhalten?
Die Erstellung von Zertifikat- und Trust-Store-Objekten sollte außerhalb der Implementierung möglich sein. Daher müssen die Implementierungen einen Weg bereitstellen, damit Benutzer überprüfen können, ob die Implementierung mit vom Benutzer erstellten Zertifikaten und Trust Stores kompatibel ist. Daher sollte jede Implementierung eine Methode validate_config implementieren, die ein TLSClientConfiguration- oder TLSServerConfiguration-Objekt übernimmt und eine Ausnahme auslöst, wenn nicht unterstützte Konstruktoren verwendet wurden.
Für die Typen gibt es zwei Optionen: Entweder können alle konkreten Implementierungen gezwungen werden, in ein bestimmtes Namensschema zu passen, oder wir können eine API bereitstellen, die es ermöglicht, diese Objekte abzurufen.
Dieses PEP schlägt vor, dass wir den zweiten Ansatz verfolgen. Dies gibt konkreten Implementierungen die größte Freiheit, ihren Code nach eigenem Ermessen zu strukturieren, und erfordert lediglich, dass sie ein einziges Objekt bereitstellen, das die entsprechenden Eigenschaften aufweist. Benutzer können dieses Implementierungsobjekt dann an Bibliotheken übergeben, die es unterstützen, und diese Bibliotheken kümmern sich um die Konfiguration und Verwendung der konkreten Implementierung.
Alle konkreten Implementierungen müssen eine Methode zur Beschaffung eines TLSImplementation-Objekts bereitstellen. Das TLSImplementation-Objekt kann ein globales Singleton sein oder kann von einem Aufrufbaren erstellt werden, wenn dies von Vorteil ist.
Das TLSImplementation-Objekt hat die folgende Definition.
class TLSImplementation(Generic[_ClientContext, _ServerContext]):
__slots__ = (
"_client_context",
"_server_context",
"_validate_config",
)
def __init__(
self,
client_context: type[_ClientContext],
server_context: type[_ServerContext],
validate_config: Callable[[TLSClientConfiguration | TLSServerConfiguration], None],
) -> None:
self._client_context = client_context
self._server_context = server_context
self._validate_config = validate_config
Die ersten beiden Eigenschaften müssen die konkrete Implementierung der relevanten Protokollklasse bereitstellen. Zum Beispiel für den Client-Kontext
@property
def client_context(self) -> type[_ClientContext]:
"""The concrete implementation of the PEP 543 Client Context object,
if this TLS implementation supports being the client on a TLS connection.
"""
return self._client_context
Dies stellt sicher, dass Code wie dieser für jede Implementierung funktioniert.
client_config = TLSClientConfiguration()
client_context = implementation.client_context(client_config)
Die dritte Eigenschaft muss eine Funktion bereitstellen, die überprüft, ob eine gegebene TLS-Konfiguration implementierungskompatible Zertifikate, private Schlüssel und einen Trust Store enthält.
@property
def validate_config(self) -> Callable[[TLSClientConfiguration | TLSServerConfiguration], None]:
"""A function that reveals whether this TLS implementation supports a
particular TLS configuration.
"""
return self._validate_config
Beachten Sie, dass diese Funktion nur überprüfen muss, ob unterstützte Konstruktoren für die Zertifikate, privaten Schlüssel und den Trust Store verwendet wurden. Sie muss die Objekte nicht analysieren oder abrufen, um sie weiter zu validieren.
Unsichere Verwendung
All dies geht davon aus, dass Benutzer das Modul auf sichere Weise verwenden möchten. Manchmal möchten Benutzer unvorsichtige Dinge tun, wie z. B. die Zertifikatsvalidierung zu Testzwecken zu deaktivieren. Zu diesem Zweck schlagen wir ein separates insecure-Modul vor, das Benutzern dies ermöglicht. Dieses Modul enthält unsichere Varianten der Konfigurations-, Kontext- und Implementierungsobjekte, die die Deaktivierung der Zertifikatsvalidierung sowie der Server-Hostname-Prüfung ermöglichen.
Diese Funktionalität ist in einem separaten Modul untergebracht, um es legitimen Benutzern so schwer wie möglich zu machen, die unsichere Funktionalität versehentlich zu verwenden. Darüber hinaus definiert es eine neue Warnung namens SecurityWarning und warnt lautstark bei jedem Schritt, wenn versucht wird, eine unsichere Verbindung herzustellen.
Dieses Modul ist nur für Testzwecke vorgesehen. In realen Situationen, in denen ein Benutzer eine Verbindung zu einem IoT-Gerät herstellen möchte, das nur ein selbstsigniertes Zertifikat hat, wird dringend empfohlen, dieses Zertifikat in einen benutzerdefinierten Trust Store aufzunehmen, anstatt das unsichere Modul zum Deaktivieren der Zertifikatsvalidierung zu verwenden.
Änderungen an der Standardbibliothek
Die Teile der Standardbibliothek, die mit TLS interagieren, sollten überarbeitet werden, um diese Protokollklassen zu verwenden. Dies wird ihnen ermöglichen, mit anderen TLS-Implementierungen zu funktionieren. Dies umfasst die folgenden Module:
Migration des ssl-Moduls
Natürlich müssen wir das ssl-Modul selbst erweitern, um diesen Protokollklassen zu entsprechen. Diese Erweiterung wird in Form neuer Klassen erfolgen, möglicherweise in einem völlig neuen Modul. Dies ermöglicht es Anwendungen, die das aktuelle ssl-Modul nutzen, dies weiterhin zu tun, während die neuen APIs für Anwendungen und Bibliotheken aktiviert werden, die sie verwenden möchten.
Im Allgemeinen wird die Migration vom ssl-Modul zu den neuen Protokollklassen nicht eins zu eins erwartet. Dies ist normalerweise akzeptabel: Die meisten Tools, die das ssl-Modul verwenden, verbergen es vor dem Benutzer, und daher sollte die Umstrukturierung zur Verwendung des neuen Moduls unsichtbar sein.
Ein spezifisches Problem ergibt sich jedoch aus Bibliotheken oder Anwendungen, die Ausnahmen aus dem ssl-Modul lecken, entweder als Teil ihrer definierten API oder versehentlich (was leicht passiert). Benutzer dieser Tools haben möglicherweise Code geschrieben, der Ausnahmen aus dem ssl-Modul toleriert und behandelt, die ausgelöst werden: Die Migration zu den hier vorgestellten Protokollklassen würde potenziell dazu führen, dass die oben definierten Ausnahmen stattdessen ausgelöst werden, und vorhandene except-Blöcke würden sie nicht abfangen.
Aus diesem Grund würde ein Teil der Migration des ssl-Moduls erfordern, dass die Ausnahmen im ssl-Modul Aliase für die oben definierten sind. Das heißt, sie würden erfordern, dass die folgenden Anweisungen alle erfolgreich sind:
assert ssl.SSLError is tls.TLSError
assert ssl.SSLWantReadError is tls.WantReadError
assert ssl.SSLWantWriteError is tls.WantWriteError
Die genauen Mechanismen, wie dies geschehen wird, liegen außerhalb des Rahmens dieses PEP, da sie aufgrund der Tatsache, dass die aktuellen SSL-Ausnahmen in C-Code definiert sind, komplexer sind. Weitere Details finden Sie in einer E-Mail, die Christian Heimes an die Security-SIG gesendet hat.
Zukunft
Große zukünftige TLS-Funktionen erfordern möglicherweise Überarbeitungen dieser Protokollklassen. Diese Überarbeitungen sollten mit Vorsicht erfolgen: Viele Implementierungen können möglicherweise nicht schnell vorankommen und werden durch Änderungen an diesen Protokollklassen ungültig. Dies ist akzeptabel, aber wo immer möglich sollten Funktionen, die für einzelne Implementierungen spezifisch sind, nicht zu den Protokollklassen hinzugefügt werden. Die Protokollklassen sollten sich auf High-Level-Beschreibungen von IETF-spezifizierten Funktionen beschränken.
Gut begründete Erweiterungen dieser API sollten jedoch unbedingt vorgenommen werden. Der Fokus dieser API liegt darin, eine vereinheitlichende Option für die Konfiguration des kleinsten gemeinsamen Nenners für die Python-Community bereitzustellen. TLS ist kein statisches Ziel, und mit der Weiterentwicklung von TLS muss sich auch diese API weiterentwickeln.
Danksagungen
Dieses PEP wurde erheblich aus PEP 543 übernommen, das 2020 zurückgezogen wurde. PEP 543 wurde von Cory Benfield und Christian Heimes verfasst und erhielt umfangreiche Rückmeldungen von einer Reihe von Personen aus der Community, die maßgeblich zu seiner Gestaltung beigetragen haben. Detaillierte Überprüfung für sowohl PEP 543 als auch dieses PEP wurde bereitgestellt von
- Alex Chan
- Alex Gaynor
- Antoine Pitrou
- Ashwini Oruganti
- Donald Stufft
- Ethan Furman
- Glyph
- Hynek Schlawack
- Jim J Jewett
- Nathaniel J. Smith
- Alyssa Coghlan
- Paul Kehrer
- Steve Dower
- Steven Fackler
- Wes Turner
- Will Bond
- Cory Benfield
- Marc-André Lemburg
- Seth M. Larson
- Victor Stinner
- Ronald Oussoren
Weitere Überprüfungen von PEP 543 wurden von den Mailinglisten Security-SIG und python-ideas bereitgestellt.
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0748.rst
Zuletzt geändert: 2025-04-01 14:40:02 GMT