From 152efa2ae2532d29b03d449f245b2bd6895d3fcb Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Sun, 16 May 2010 18:19:27 +0000 Subject: [PATCH] Issue #8550: Add first class `SSLContext` objects to the ssl module. --- Doc/library/ssl.rst | 281 +++++++++++++++---- Lib/ssl.py | 71 +++-- Lib/test/capath/6e88d7b8.0 | 14 + Lib/test/capath/99d0fa06.0 | 41 +++ Lib/test/test_ssl.py | 203 ++++++++++++-- Misc/NEWS | 2 + Modules/_ssl.c | 535 ++++++++++++++++++++++++------------- 7 files changed, 864 insertions(+), 283 deletions(-) create mode 100644 Lib/test/capath/6e88d7b8.0 create mode 100644 Lib/test/capath/99d0fa06.0 diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 95f5f7021c7..45ffcb0ad52 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -36,6 +36,11 @@ additional :meth:`read` and :meth:`write` methods, along with a method, connection, and a method, :meth:`cipher`, to retrieve the cipher being used for the secure connection. +For more sophisticated applications, the :class:`ssl.SSLContext` class +helps manage settings and certificates, which can then be inherited +by SSL sockets created through the :meth:`SSLContext.wrap_socket` method. + + Functions, Constants, and Exceptions ------------------------------------ @@ -64,19 +69,6 @@ Functions, Constants, and Exceptions connection. See the discussion of :ref:`ssl-certificates` for more information on how the certificate is stored in the ``certfile``. - Often the private key is stored in the same file as the certificate; in this - case, only the ``certfile`` parameter need be passed. If the private key is - stored in a separate file, both parameters must be used. If the private key - is stored in the ``certfile``, it should come before the first certificate in - the certificate chain:: - - -----BEGIN RSA PRIVATE KEY----- - ... (private key in base64 encoding) ... - -----END RSA PRIVATE KEY----- - -----BEGIN CERTIFICATE----- - ... (certificate in base64 PEM encoding) ... - -----END CERTIFICATE----- - The parameter ``server_side`` is a boolean which identifies whether server-side or client-side behavior is desired from this socket. @@ -208,24 +200,36 @@ Functions, Constants, and Exceptions .. data:: CERT_NONE - Value to pass to the ``cert_reqs`` parameter to :func:`sslobject` when no - certificates will be required or validated from the other side of the socket - connection. + Possible value for :attr:`SSLContext.verify_mode`, or the ``cert_reqs`` + parameter to :func:`wrap_socket`. In this mode (the default), no + certificates will be required from the other side of the socket connection. + If a certificate is received from the other end, no attempt to validate it + is made. + + See the discussion of :ref:`ssl-security` below. .. data:: CERT_OPTIONAL - Value to pass to the ``cert_reqs`` parameter to :func:`sslobject` when no - certificates will be required from the other side of the socket connection, - but if they are provided, will be validated. Note that use of this setting - requires a valid certificate validation file also be passed as a value of the - ``ca_certs`` parameter. + Possible value for :attr:`SSLContext.verify_mode`, or the ``cert_reqs`` + parameter to :func:`wrap_socket`. In this mode no certificates will be + required from the other side of the socket connection; but if they + are provided, validation will be attempted and an :class:`SSLError` + will be raised on failure. + + Use of this setting requires a valid set of CA certificates to + be passed, either to :meth:`SSLContext.load_verify_locations` or as a + value of the ``ca_certs`` parameter to :func:`wrap_socket`. .. data:: CERT_REQUIRED - Value to pass to the ``cert_reqs`` parameter to :func:`sslobject` when - certificates will be required from the other side of the socket connection. - Note that use of this setting requires a valid certificate validation file - also be passed as a value of the ``ca_certs`` parameter. + Possible value for :attr:`SSLContext.verify_mode`, or the ``cert_reqs`` + parameter to :func:`wrap_socket`. In this mode, certificates are + required from the other side of the socket connection; an :class:`SSLError` + will be raised if no certificate is provided, or if its validation fails. + + Use of this setting requires a valid set of CA certificates to + be passed, either to :meth:`SSLContext.load_verify_locations` or as a + value of the ``ca_certs`` parameter to :func:`wrap_socket`. .. data:: PROTOCOL_SSLv2 @@ -284,8 +288,8 @@ Functions, Constants, and Exceptions .. versionadded:: 3.2 -SSLSocket Objects ------------------ +SSL Sockets +----------- .. method:: SSLSocket.read(nbytes=1024, buffer=None) @@ -371,6 +375,83 @@ SSLSocket Objects returned socket should always be used for further communication with the other side of the connection, rather than the original socket. + +SSL Contexts +------------ + +.. class:: SSLContext(protocol) + + An object holding various data longer-lived than single SSL connections, + such as SSL configuration options, certificate(s) and private key(s). + You must pass *protocol* which must be one of the ``PROTOCOL_*`` constants + defined in this module. :data:`PROTOCOL_SSLv23` is recommended for + maximum interoperability. + +:class:`SSLContext` objects have the following methods and attributes: + +.. method:: SSLContext.load_cert_chain(certfile, keyfile=None) + + Load a private key and the corresponding certificate. The *certfile* + string must be the path to a single file in PEM format containing the + certificate as well as any number of CA certificates needed to establish + the certificate's authenticity. The *keyfile* string, if present, must + point to a file containing the private key in. Otherwise the private + key will be taken from *certfile* as well. See the discussion of + :ref:`ssl-certificates` for more information on how the certificate + is stored in the *certfile*. + + An :class:`SSLError` is raised if the private key doesn't + match with the certificate. + +.. method:: SSLContext.load_verify_locations(cafile=None, capath=None) + + Load a set of "certification authority" (CA) certificates used to validate + other peers' certificates when :data:`verify_mode` is other than + :data:`CERT_NONE`. At least one of *cafile* or *capath* must be specified. + + The *cafile* string, if present, is the path to a file of concatenated + CA certificates in PEM format. See the discussion of + :ref:`ssl-certificates` for more information about how to arrange the + certificates in this file. + + The *capath* string, if present, is + the path to a directory containing several CA certificates in PEM format, + following an `OpenSSL specific layout + `_. + +.. method:: SSLContext.set_ciphers(ciphers) + + Set the available ciphers for sockets created with this context. + It should be a string in the `OpenSSL cipher list format + `_. + If no cipher can be selected (because compile-time options or other + configuration forbids use of all the specified ciphers), an + :class:`SSLError` will be raised. + + .. note:: + when connected, the :meth:`SSLSocket.cipher` method of SSL sockets will + give the currently selected cipher. + +.. method:: SSLContext.wrap_socket(sock, server_side=False, do_handshake_on_connect=True, suppress_ragged_eofs=True) + + Wrap an existing Python socket *sock* and return an :class:`SSLSocket` + object. The SSL socket is tied to the context, its settings and + certificates. The parameters *server_side*, *do_handshake_on_connect* + and *suppress_ragged_eofs* have the same meaning as in the top-level + :func:`wrap_socket` function. + +.. attribute:: SSLContext.protocol + + The protocol version chosen when constructing the context. This attribute + is read-only. + +.. attribute:: SSLContext.verify_mode + + Whether to try to verify other peers' certificates and how to behave + if verification fails. This attribute must be one of + :data:`CERT_NONE`, :data:`CERT_OPTIONAL` or :data:`CERT_REQUIRED`. + + .. index:: single: certificates .. index:: single: X509 certificate @@ -416,6 +497,9 @@ and a footer line:: ... (certificate in base64 PEM encoding) ... -----END CERTIFICATE----- +Certificate chains +^^^^^^^^^^^^^^^^^^ + The Python files which contain certificates can contain a sequence of certificates, sometimes called a *certificate chain*. This chain should start with the specific certificate for the principal who "is" the client or server, @@ -439,6 +523,9 @@ certification authority's certificate:: ... (the root certificate for the CA's issuer)... -----END CERTIFICATE----- +CA certificates +^^^^^^^^^^^^^^^ + If you are going to require validation of the other side of the connection's certificate, you need to provide a "CA certs" file, filled with the certificate chains for each issuer you are willing to trust. Again, this file just contains @@ -458,6 +545,25 @@ peer is supposed to furnish the other certificates necessary to chain from its certificate to a root certificate. See :rfc:`4158` for more discussion of the way in which certification chains can be built. +Combined key and certificate +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Often the private key is stored in the same file as the certificate; in this +case, only the ``certfile`` parameter to :meth:`SSLContext.load_cert_chain` +and :func:`wrap_socket` needs to be passed. If the private key is stored +with the certificate, it should come before the first certificate in +the certificate chain:: + + -----BEGIN RSA PRIVATE KEY----- + ... (private key in base64 encoding) ... + -----END RSA PRIVATE KEY----- + -----BEGIN CERTIFICATE----- + ... (certificate in base64 PEM encoding) ... + -----END CERTIFICATE----- + +Self-signed certificates +^^^^^^^^^^^^^^^^^^^^^^^^ + If you are going to create a server that provides SSL-encrypted connection services, you will need to acquire a certificate for that service. There are many ways of acquiring appropriate certificates, such as buying one from a @@ -530,8 +636,7 @@ certificate, sends some bytes, and reads part of the response:: print(pprint.pformat(ssl_sock.getpeercert())) # Set a simple HTTP request -- use http.client in actual code. - ssl_sock.write("""GET / HTTP/1.0\r - Host: www.verisign.com\r\n\r\n""") + ssl_sock.write(b"GET / HTTP/1.0\r\nHost: www.verisign.com\r\n\r\n") # Read a chunk of data. Will not necessarily # read all the data returned by the server. @@ -561,39 +666,91 @@ this:: which is a fairly poorly-formed ``subject`` field. +This other example first creates an SSL context, instructs it to verify +certificates sent by peers, and feeds it a set of recognized certificate +authorities (CA):: + + >>> context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + >>> context.verify_mode = ssl.CERT_OPTIONAL + >>> context.load_verify_locations("/etc/ssl/certs/ca-bundle.crt") + +(it is assumed your operating system places a bundle of all CA certificates +in ``/etc/ssl/certs/ca-bundle.crt``; if not, you'll get an error and have +to adjust the location) + +When you use the context to connect to a server, :const:`CERT_OPTIONAL` +validates the server certificate: it ensures that the server certificate +was signed with one of the CA certificates, and checks the signature for +correctness:: + + >>> conn = context.wrap_socket(socket.socket(socket.AF_INET)) + >>> conn.connect(("linuxfr.org", 443)) + +You should then fetch the certificate and check its fields for conformity. +Here, the ``commonName`` field in the ``subject`` matches the desired HTTPS +host ``linuxfr.org``:: + + >>> pprint.pprint(conn.getpeercert()) + {'notAfter': 'Jun 26 21:41:46 2011 GMT', + 'subject': ((('commonName', 'linuxfr.org'),),), + 'subjectAltName': (('DNS', 'linuxfr.org'), ('othername', ''))} + +Now that you are assured of its authenticity, you can proceed to talk with +the server:: + + >>> conn.write(b"HEAD / HTTP/1.0\r\nHost: linuxfr.org\r\n\r\n") + 38 + >>> pprint.pprint(conn.read().split(b"\r\n")) + [b'HTTP/1.1 302 Found', + b'Date: Sun, 16 May 2010 13:43:28 GMT', + b'Server: Apache/2.2', + b'Location: https://linuxfr.org/pub/', + b'Vary: Accept-Encoding', + b'Connection: close', + b'Content-Type: text/html; charset=iso-8859-1', + b'', + b''] + + +See the discussion of :ref:`ssl-security` below. + + Server-side operation ^^^^^^^^^^^^^^^^^^^^^ -For server operation, typically you'd need to have a server certificate, and -private key, each in a file. You'd open a socket, bind it to a port, call -:meth:`listen` on it, then start waiting for clients to connect:: +For server operation, typically you'll need to have a server certificate, and +private key, each in a file. You'll first create a context holding the key +and the certificate, so that clients can check your authenticity. Then +you'll open a socket, bind it to a port, call :meth:`listen` on it, and start +waiting for clients to connect:: import socket, ssl + context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + context.load_cert_chain(certfile="mycertfile", keyfile="mykeyfile") + bindsocket = socket.socket() bindsocket.bind(('myaddr.mydomain.com', 10023)) bindsocket.listen(5) -When one did, you'd call :meth:`accept` on the socket to get the new socket from -the other end, and use :func:`wrap_socket` to create a server-side SSL context -for it:: +When a client connects, you'll call :meth:`accept` on the socket to get the +new socket from the other end, and use the context's :meth:`SSLContext.wrap_socket` +method to create a server-side SSL socket for the connection:: while True: newsocket, fromaddr = bindsocket.accept() - connstream = ssl.wrap_socket(newsocket, - server_side=True, - certfile="mycertfile", - keyfile="mykeyfile", - ssl_version=ssl.PROTOCOL_TLSv1) - deal_with_client(connstream) + connstream = context.wrap_socket(newsocket, server_side=True) + try: + deal_with_client(connstream) + finally: + connstream.close() -Then you'd read data from the ``connstream`` and do something with it till you +Then you'll read data from the ``connstream`` and do something with it till you are finished with the client (or the client is finished with you):: def deal_with_client(connstream): - data = connstream.read() - # null data means the client is finished with us + # empty data means the client is finished with us while data: if not do_something(connstream, data): # we'll assume do_something returns False @@ -601,9 +758,41 @@ are finished with the client (or the client is finished with you):: break data = connstream.read() # finished with client - connstream.close() -And go back to listening for new client connections. +And go back to listening for new client connections (of course, a real server +would probably handle each client connection in a separate thread, or put +the sockets in non-blocking mode and use an event loop). + + +.. _ssl-security: + +Security considerations +----------------------- + +Verifying certificates +^^^^^^^^^^^^^^^^^^^^^^ + +:const:`CERT_NONE` is the default. Since it does not authenticate the other +peer, it can be insecure, especially in client mode where most of time you +would like to ensure the authenticity of the server you're talking to. +Therefore, when in client mode, it is highly recommended to use +:const:`CERT_REQUIRED`. However, it is in itself not sufficient; you also +have to check that the server certificate (obtained with +:meth:`SSLSocket.getpeercert`) matches the desired service. The exact way +of doing so depends on the higher-level protocol used; for example, with +HTTPS, you'll check that the host name in the URL matches either the +``commonName`` field in the ``subjectName``, or one of the ``DNS`` fields +in the ``subjectAltName``. + +In server mode, if you want to authenticate your clients using the SSL layer +(rather than using a higher-level authentication mechanism), you'll also have +to specify :const:`CERT_REQUIRED` and similarly check the client certificate. + + .. note:: + + In client mode, :const:`CERT_OPTIONAL` and :const:`CERT_REQUIRED` are + equivalent unless anonymous ciphers are enabled (they are disabled + by default). .. seealso:: diff --git a/Lib/ssl.py b/Lib/ssl.py index 19f50f427f9..24d377157cd 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -59,7 +59,7 @@ import textwrap import _ssl # if we can't import it, let the error propagate from _ssl import OPENSSL_VERSION_NUMBER, OPENSSL_VERSION_INFO, OPENSSL_VERSION -from _ssl import SSLError +from _ssl import _SSLContext, SSLError from _ssl import CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED from _ssl import (PROTOCOL_SSLv2, PROTOCOL_SSLv3, PROTOCOL_SSLv23, PROTOCOL_TLSv1) @@ -84,8 +84,29 @@ import base64 # for DER-to-PEM translation import traceback import errno -class SSLSocket(socket): +class SSLContext(_SSLContext): + """An SSLContext holds various SSL-related configuration options and + data, such as certificates and possibly a private key.""" + + __slots__ = ('protocol',) + + def __new__(cls, protocol, *args, **kwargs): + return _SSLContext.__new__(cls, protocol) + + def __init__(self, protocol): + self.protocol = protocol + + def wrap_socket(self, sock, server_side=False, + do_handshake_on_connect=True, + suppress_ragged_eofs=True): + return SSLSocket(sock=sock, server_side=server_side, + do_handshake_on_connect=do_handshake_on_connect, + suppress_ragged_eofs=suppress_ragged_eofs, + _context=self) + + +class SSLSocket(socket): """This class implements a subtype of socket.socket that wraps the underlying OS socket in an SSL context when necessary, and provides read and write methods over that channel.""" @@ -95,8 +116,31 @@ class SSLSocket(socket): ssl_version=PROTOCOL_SSLv23, ca_certs=None, do_handshake_on_connect=True, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None, - suppress_ragged_eofs=True, ciphers=None): + suppress_ragged_eofs=True, ciphers=None, + _context=None): + if _context: + self.context = _context + else: + if certfile and not keyfile: + keyfile = certfile + self.context = SSLContext(ssl_version) + self.context.verify_mode = cert_reqs + if ca_certs: + self.context.load_verify_locations(ca_certs) + if certfile: + self.context.load_cert_chain(certfile, keyfile) + if ciphers: + self.context.set_ciphers(ciphers) + self.keyfile = keyfile + self.certfile = certfile + self.cert_reqs = cert_reqs + self.ssl_version = ssl_version + self.ca_certs = ca_certs + self.ciphers = ciphers + + self.do_handshake_on_connect = do_handshake_on_connect + self.suppress_ragged_eofs = suppress_ragged_eofs connected = False if sock is not None: socket.__init__(self, @@ -119,18 +163,12 @@ class SSLSocket(socket): else: socket.__init__(self, family=family, type=type, proto=proto) - if certfile and not keyfile: - keyfile = certfile - self._closed = False self._sslobj = None if connected: # create the SSL object try: - self._sslobj = _ssl.sslwrap(self, server_side, - keyfile, certfile, - cert_reqs, ssl_version, ca_certs, - ciphers) + self._sslobj = self.context._wrap_socket(self, server_side) if do_handshake_on_connect: timeout = self.gettimeout() if timeout == 0.0: @@ -142,15 +180,6 @@ class SSLSocket(socket): self.close() raise x - self.keyfile = keyfile - self.certfile = certfile - self.cert_reqs = cert_reqs - self.ssl_version = ssl_version - self.ca_certs = ca_certs - self.ciphers = ciphers - self.do_handshake_on_connect = do_handshake_on_connect - self.suppress_ragged_eofs = suppress_ragged_eofs - def dup(self): raise NotImplemented("Can't dup() %s instances" % self.__class__.__name__) @@ -331,9 +360,7 @@ class SSLSocket(socket): if self._sslobj: raise ValueError("attempt to connect already-connected SSLSocket!") socket.connect(self, addr) - self._sslobj = _ssl.sslwrap(self, False, self.keyfile, self.certfile, - self.cert_reqs, self.ssl_version, - self.ca_certs, self.ciphers) + self._sslobj = self.context._wrap_socket(self, False) try: if self.do_handshake_on_connect: self.do_handshake() diff --git a/Lib/test/capath/6e88d7b8.0 b/Lib/test/capath/6e88d7b8.0 new file mode 100644 index 00000000000..9d7ac238d86 --- /dev/null +++ b/Lib/test/capath/6e88d7b8.0 @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICLDCCAdYCAQAwDQYJKoZIhvcNAQEEBQAwgaAxCzAJBgNVBAYTAlBUMRMwEQYD +VQQIEwpRdWVlbnNsYW5kMQ8wDQYDVQQHEwZMaXNib2ExFzAVBgNVBAoTDk5ldXJv +bmlvLCBMZGEuMRgwFgYDVQQLEw9EZXNlbnZvbHZpbWVudG8xGzAZBgNVBAMTEmJy +dXR1cy5uZXVyb25pby5wdDEbMBkGCSqGSIb3DQEJARYMc2FtcG9AaWtpLmZpMB4X +DTk2MDkwNTAzNDI0M1oXDTk2MTAwNTAzNDI0M1owgaAxCzAJBgNVBAYTAlBUMRMw +EQYDVQQIEwpRdWVlbnNsYW5kMQ8wDQYDVQQHEwZMaXNib2ExFzAVBgNVBAoTDk5l +dXJvbmlvLCBMZGEuMRgwFgYDVQQLEw9EZXNlbnZvbHZpbWVudG8xGzAZBgNVBAMT +EmJydXR1cy5uZXVyb25pby5wdDEbMBkGCSqGSIb3DQEJARYMc2FtcG9AaWtpLmZp +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAL7+aty3S1iBA/+yxjxv4q1MUTd1kjNw +L4lYKbpzzlmC5beaQXeQ2RmGMTXU+mDvuqItjVHOK3DvPK7lTcSGftUCAwEAATAN +BgkqhkiG9w0BAQQFAANBAFqPEKFjk6T6CKTHvaQeEAsX0/8YHPHqH/9AnhSjrwuX +9EBc0n6bVGhN7XaXd6sJ7dym9sbsWxb+pJdurnkxjx4= +-----END CERTIFICATE----- diff --git a/Lib/test/capath/99d0fa06.0 b/Lib/test/capath/99d0fa06.0 new file mode 100644 index 00000000000..e7dfc82947e --- /dev/null +++ b/Lib/test/capath/99d0fa06.0 @@ -0,0 +1,41 @@ +-----BEGIN CERTIFICATE----- +MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290 +IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB +IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA +Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO +BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi +MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ +ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ +8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6 +zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y +fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7 +w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc +G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k +epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q +laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ +QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU +fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826 +YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w +ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY +gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe +MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0 +IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy +dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw +czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0 +dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl +aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC +AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg +b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB +ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc +nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg +18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c +gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl +Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY +sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T +SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF +CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum +GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk +zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW +omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD +-----END CERTIFICATE----- diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 7895b4afcc2..80c586b5ede 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -10,6 +10,7 @@ import gc import os import errno import pprint +import tempfile import urllib.parse, urllib.request import traceback import asyncore @@ -25,8 +26,30 @@ except ImportError: skip_expected = True HOST = support.HOST -CERTFILE = None -SVN_PYTHON_ORG_ROOT_CERT = None +PROTOCOLS = [ + ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_SSLv3, + ssl.PROTOCOL_SSLv23, ssl.PROTOCOL_TLSv1 +] + +data_file = lambda name: os.path.join(os.path.dirname(__file__), name) +fsencode = lambda name: name.encode(sys.getfilesystemencoding(), "surrogateescape") + +CERTFILE = data_file("keycert.pem") +BYTES_CERTFILE = fsencode(CERTFILE) +ONLYCERT = data_file("ssl_cert.pem") +ONLYKEY = data_file("ssl_key.pem") +BYTES_ONLYCERT = fsencode(ONLYCERT) +BYTES_ONLYKEY = fsencode(ONLYKEY) +CAPATH = data_file("capath") +BYTES_CAPATH = fsencode(CAPATH) + +SVN_PYTHON_ORG_ROOT_CERT = data_file("https_svn_python_org_root.pem") + +EMPTYCERT = data_file("nullcert.pem") +BADCERT = data_file("badcert.pem") +WRONGCERT = data_file("XXXnonexisting.pem") +BADKEY = data_file("badkey.pem") + def handle_error(prefix): exc_format = ' '.join(traceback.format_exception(*sys.exc_info())) @@ -34,7 +57,7 @@ def handle_error(prefix): sys.stdout.write(prefix + exc_format) -class BasicTests(unittest.TestCase): +class BasicSocketTests(unittest.TestCase): def test_constants(self): ssl.PROTOCOL_SSLv2 @@ -116,11 +139,10 @@ class BasicTests(unittest.TestCase): s = ssl.wrap_socket(socket.socket(socket.AF_INET), cert_reqs=ssl.CERT_NONE, ciphers="DEFAULT") s.connect(remote) - # Error checking occurs when connecting, because the SSL context - # isn't created before. - s = ssl.wrap_socket(socket.socket(socket.AF_INET), - cert_reqs=ssl.CERT_NONE, ciphers="^$:,;?*'dorothyx") + # Error checking can happen at instantiation or when connecting with self.assertRaisesRegexp(ssl.SSLError, "No cipher can be selected"): + s = ssl.wrap_socket(socket.socket(socket.AF_INET), + cert_reqs=ssl.CERT_NONE, ciphers="^$:,;?*'dorothyx") s.connect(remote) @support.cpython_only @@ -143,26 +165,103 @@ class BasicTests(unittest.TestCase): self.assertEqual(timeout, ss.gettimeout()) +class ContextTests(unittest.TestCase): + + def test_constructor(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv2) + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv3) + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + self.assertRaises(TypeError, ssl.SSLContext) + self.assertRaises(ValueError, ssl.SSLContext, -1) + self.assertRaises(ValueError, ssl.SSLContext, 42) + + def test_protocol(self): + for proto in PROTOCOLS: + ctx = ssl.SSLContext(proto) + self.assertEqual(ctx.protocol, proto) + + def test_ciphers(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + ctx.set_ciphers("ALL") + ctx.set_ciphers("DEFAULT") + with self.assertRaisesRegexp(ssl.SSLError, "No cipher can be selected"): + ctx.set_ciphers("^$:,;?*'dorothyx") + + def test_verify(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + # Default value + self.assertEqual(ctx.verify_mode, ssl.CERT_NONE) + ctx.verify_mode = ssl.CERT_OPTIONAL + self.assertEqual(ctx.verify_mode, ssl.CERT_OPTIONAL) + ctx.verify_mode = ssl.CERT_REQUIRED + self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED) + ctx.verify_mode = ssl.CERT_NONE + self.assertEqual(ctx.verify_mode, ssl.CERT_NONE) + with self.assertRaises(TypeError): + ctx.verify_mode = None + with self.assertRaises(ValueError): + ctx.verify_mode = 42 + + def test_load_cert_chain(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + # Combined key and cert in a single file + ctx.load_cert_chain(CERTFILE) + ctx.load_cert_chain(CERTFILE, keyfile=CERTFILE) + self.assertRaises(TypeError, ctx.load_cert_chain, keyfile=CERTFILE) + with self.assertRaisesRegexp(ssl.SSLError, "system lib"): + ctx.load_cert_chain(WRONGCERT) + with self.assertRaisesRegexp(ssl.SSLError, "PEM lib"): + ctx.load_cert_chain(BADCERT) + with self.assertRaisesRegexp(ssl.SSLError, "PEM lib"): + ctx.load_cert_chain(EMPTYCERT) + # Separate key and cert + ctx.load_cert_chain(ONLYCERT, ONLYKEY) + ctx.load_cert_chain(certfile=ONLYCERT, keyfile=ONLYKEY) + ctx.load_cert_chain(certfile=BYTES_ONLYCERT, keyfile=BYTES_ONLYKEY) + with self.assertRaisesRegexp(ssl.SSLError, "PEM lib"): + ctx.load_cert_chain(ONLYCERT) + with self.assertRaisesRegexp(ssl.SSLError, "PEM lib"): + ctx.load_cert_chain(ONLYKEY) + with self.assertRaisesRegexp(ssl.SSLError, "PEM lib"): + ctx.load_cert_chain(certfile=ONLYKEY, keyfile=ONLYCERT) + # Mismatching key and cert + with self.assertRaisesRegexp(ssl.SSLError, "key values mismatch"): + ctx.load_cert_chain(CERTFILE, ONLYKEY) + + def test_load_verify_locations(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + ctx.load_verify_locations(CERTFILE) + ctx.load_verify_locations(cafile=CERTFILE, capath=None) + ctx.load_verify_locations(BYTES_CERTFILE) + ctx.load_verify_locations(cafile=BYTES_CERTFILE, capath=None) + self.assertRaises(TypeError, ctx.load_verify_locations) + self.assertRaises(TypeError, ctx.load_verify_locations, None, None) + with self.assertRaisesRegexp(ssl.SSLError, "system lib"): + ctx.load_verify_locations(WRONGCERT) + with self.assertRaisesRegexp(ssl.SSLError, "PEM lib"): + ctx.load_verify_locations(BADCERT) + ctx.load_verify_locations(CERTFILE, CAPATH) + ctx.load_verify_locations(CERTFILE, capath=BYTES_CAPATH) + + class NetworkedTests(unittest.TestCase): def test_connect(self): s = ssl.wrap_socket(socket.socket(socket.AF_INET), cert_reqs=ssl.CERT_NONE) - s.connect(("svn.python.org", 443)) - c = s.getpeercert() - if c: - self.fail("Peer cert %s shouldn't be here!") - s.close() + try: + s.connect(("svn.python.org", 443)) + self.assertEqual({}, s.getpeercert()) + finally: + s.close() # this should fail because we have no verification certs s = ssl.wrap_socket(socket.socket(socket.AF_INET), cert_reqs=ssl.CERT_REQUIRED) - try: - s.connect(("svn.python.org", 443)) - except ssl.SSLError: - pass - finally: - s.close() + self.assertRaisesRegexp(ssl.SSLError, "certificate verify failed", + s.connect, ("svn.python.org", 443)) + s.close() # this should succeed because we specify the root cert s = ssl.wrap_socket(socket.socket(socket.AF_INET), @@ -170,6 +269,56 @@ class NetworkedTests(unittest.TestCase): ca_certs=SVN_PYTHON_ORG_ROOT_CERT) try: s.connect(("svn.python.org", 443)) + self.assertTrue(s.getpeercert()) + finally: + s.close() + + def test_connect_with_context(self): + # Same as test_connect, but with a separately created context + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + s = ctx.wrap_socket(socket.socket(socket.AF_INET)) + s.connect(("svn.python.org", 443)) + try: + self.assertEqual({}, s.getpeercert()) + finally: + s.close() + # This should fail because we have no verification certs + ctx.verify_mode = ssl.CERT_REQUIRED + s = ctx.wrap_socket(socket.socket(socket.AF_INET)) + self.assertRaisesRegexp(ssl.SSLError, "certificate verify failed", + s.connect, ("svn.python.org", 443)) + s.close() + # This should succeed because we specify the root cert + ctx.load_verify_locations(SVN_PYTHON_ORG_ROOT_CERT) + s = ctx.wrap_socket(socket.socket(socket.AF_INET)) + s.connect(("svn.python.org", 443)) + try: + cert = s.getpeercert() + self.assertTrue(cert) + finally: + s.close() + + def test_connect_capath(self): + # Verify server certificates using the `capath` argument + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ctx.verify_mode = ssl.CERT_REQUIRED + ctx.load_verify_locations(capath=CAPATH) + s = ctx.wrap_socket(socket.socket(socket.AF_INET)) + s.connect(("svn.python.org", 443)) + try: + cert = s.getpeercert() + self.assertTrue(cert) + finally: + s.close() + # Same with a bytes `capath` argument + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ctx.verify_mode = ssl.CERT_REQUIRED + ctx.load_verify_locations(capath=BYTES_CAPATH) + s = ctx.wrap_socket(socket.socket(socket.AF_INET)) + s.connect(("svn.python.org", 443)) + try: + cert = s.getpeercert() + self.assertTrue(cert) finally: s.close() @@ -1227,18 +1376,14 @@ def test_main(verbose=False): if skip_expected: raise unittest.SkipTest("No SSL support") - global CERTFILE, SVN_PYTHON_ORG_ROOT_CERT - CERTFILE = os.path.join(os.path.dirname(__file__) or os.curdir, - "keycert.pem") - SVN_PYTHON_ORG_ROOT_CERT = os.path.join( - os.path.dirname(__file__) or os.curdir, - "https_svn_python_org_root.pem") + for filename in [ + CERTFILE, SVN_PYTHON_ORG_ROOT_CERT, BYTES_CERTFILE, + ONLYCERT, ONLYKEY, BYTES_ONLYCERT, BYTES_ONLYKEY, + BADCERT, BADKEY, EMPTYCERT]: + if not os.path.exists(filename): + raise support.TestFailed("Can't read certificate file %r" % filename) - if (not os.path.exists(CERTFILE) or - not os.path.exists(SVN_PYTHON_ORG_ROOT_CERT)): - raise support.TestFailed("Can't read certificate files!") - - tests = [BasicTests] + tests = [ContextTests, BasicSocketTests] if support.is_resource_enabled('network'): tests.append(NetworkedTests) diff --git a/Misc/NEWS b/Misc/NEWS index 386e9e0572e..31adc03d137 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -363,6 +363,8 @@ C-API Library ------- +- Issue #8550: Add first class ``SSLContext`` objects to the ssl module. + - Issue #8681: Make the zlib module's error messages more informative when the zlib itself doesn't give any detailed explanation. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 9ab184b0304..96d79b31333 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -115,23 +115,29 @@ static unsigned int _ssl_locks_count = 0; typedef struct { PyObject_HEAD - PyObject *Socket; /* weakref to socket on which we're layered */ - SSL_CTX* ctx; - SSL* ssl; - X509* peer_cert; - int shutdown_seen_zero; + SSL_CTX *ctx; +} PySSLContext; -} PySSLObject; +typedef struct { + PyObject_HEAD + PyObject *Socket; /* weakref to socket on which we're layered */ + SSL *ssl; + X509 *peer_cert; + int shutdown_seen_zero; +} PySSLSocket; -static PyTypeObject PySSL_Type; -static PyObject *PySSL_SSLwrite(PySSLObject *self, PyObject *args); -static PyObject *PySSL_SSLread(PySSLObject *self, PyObject *args); +static PyTypeObject PySSLContext_Type; +static PyTypeObject PySSLSocket_Type; + +static PyObject *PySSL_SSLwrite(PySSLSocket *self, PyObject *args); +static PyObject *PySSL_SSLread(PySSLSocket *self, PyObject *args); static int check_socket_and_wait_for_timeout(PySocketSockObject *s, int writing); -static PyObject *PySSL_peercert(PySSLObject *self, PyObject *args); -static PyObject *PySSL_cipher(PySSLObject *self); +static PyObject *PySSL_peercert(PySSLSocket *self, PyObject *args); +static PyObject *PySSL_cipher(PySSLSocket *self); -#define PySSLObject_Check(v) (Py_TYPE(v) == &PySSL_Type) +#define PySSLContext_Check(v) (Py_TYPE(v) == &PySSLContext_Type) +#define PySSLSocket_Check(v) (Py_TYPE(v) == &PySSLSocket_Type) typedef enum { SOCKET_IS_NONBLOCKING, @@ -154,7 +160,7 @@ typedef enum { */ static PyObject * -PySSL_SetError(PySSLObject *obj, int ret, char *filename, int lineno) +PySSL_SetError(PySSLSocket *obj, int ret, char *filename, int lineno) { PyObject *v; char buf[2048]; @@ -258,126 +264,28 @@ _setSSLError (char *errstr, int errcode, char *filename, int lineno) { return NULL; } -static PySSLObject * -newPySSLObject(PySocketSockObject *Sock, char *key_file, char *cert_file, - enum py_ssl_server_or_client socket_type, - enum py_ssl_cert_requirements certreq, - enum py_ssl_version proto_version, - char *cacerts_file, char *ciphers) +static PySSLSocket * +newPySSLSocket(SSL_CTX *ctx, PySocketSockObject *sock, + enum py_ssl_server_or_client socket_type) { - PySSLObject *self; - char *errstr = NULL; - int ret; - int verification_mode; + PySSLSocket *self; - self = PyObject_New(PySSLObject, &PySSL_Type); /* Create new object */ + self = PyObject_New(PySSLSocket, &PySSLSocket_Type); if (self == NULL) return NULL; + self->peer_cert = NULL; self->ssl = NULL; - self->ctx = NULL; self->Socket = NULL; /* Make sure the SSL error state is initialized */ (void) ERR_get_state(); ERR_clear_error(); - if ((key_file && !cert_file) || (!key_file && cert_file)) { - errstr = ERRSTR("Both the key & certificate files " - "must be specified"); - goto fail; - } - - if ((socket_type == PY_SSL_SERVER) && - ((key_file == NULL) || (cert_file == NULL))) { - errstr = ERRSTR("Both the key & certificate files " - "must be specified for server-side operation"); - goto fail; - } - PySSL_BEGIN_ALLOW_THREADS - if (proto_version == PY_SSL_VERSION_TLS1) - self->ctx = SSL_CTX_new(TLSv1_method()); /* Set up context */ - else if (proto_version == PY_SSL_VERSION_SSL3) - self->ctx = SSL_CTX_new(SSLv3_method()); /* Set up context */ - else if (proto_version == PY_SSL_VERSION_SSL2) - self->ctx = SSL_CTX_new(SSLv2_method()); /* Set up context */ - else if (proto_version == PY_SSL_VERSION_SSL23) - self->ctx = SSL_CTX_new(SSLv23_method()); /* Set up context */ + self->ssl = SSL_new(ctx); PySSL_END_ALLOW_THREADS - - if (self->ctx == NULL) { - errstr = ERRSTR("Invalid SSL protocol variant specified."); - goto fail; - } - - if (ciphers != NULL) { - ret = SSL_CTX_set_cipher_list(self->ctx, ciphers); - if (ret == 0) { - errstr = ERRSTR("No cipher can be selected."); - goto fail; - } - } - - if (certreq != PY_SSL_CERT_NONE) { - if (cacerts_file == NULL) { - errstr = ERRSTR("No root certificates specified for " - "verification of other-side certificates."); - goto fail; - } else { - PySSL_BEGIN_ALLOW_THREADS - ret = SSL_CTX_load_verify_locations(self->ctx, - cacerts_file, - NULL); - PySSL_END_ALLOW_THREADS - if (ret != 1) { - _setSSLError(NULL, 0, __FILE__, __LINE__); - goto fail; - } - } - } - if (key_file) { - PySSL_BEGIN_ALLOW_THREADS - ret = SSL_CTX_use_PrivateKey_file(self->ctx, key_file, - SSL_FILETYPE_PEM); - PySSL_END_ALLOW_THREADS - if (ret != 1) { - _setSSLError(NULL, ret, __FILE__, __LINE__); - goto fail; - } - - PySSL_BEGIN_ALLOW_THREADS - ret = SSL_CTX_use_certificate_chain_file(self->ctx, - cert_file); - PySSL_END_ALLOW_THREADS - if (ret != 1) { - /* - fprintf(stderr, "ret is %d, errcode is %lu, %lu, with file \"%s\"\n", - ret, ERR_peek_error(), ERR_peek_last_error(), cert_file); - */ - if (ERR_peek_last_error() != 0) { - _setSSLError(NULL, ret, __FILE__, __LINE__); - goto fail; - } - } - } - - /* ssl compatibility */ - SSL_CTX_set_options(self->ctx, SSL_OP_ALL); - - verification_mode = SSL_VERIFY_NONE; - if (certreq == PY_SSL_CERT_OPTIONAL) - verification_mode = SSL_VERIFY_PEER; - else if (certreq == PY_SSL_CERT_REQUIRED) - verification_mode = (SSL_VERIFY_PEER | - SSL_VERIFY_FAIL_IF_NO_PEER_CERT); - SSL_CTX_set_verify(self->ctx, verification_mode, - NULL); /* set verify lvl */ - - PySSL_BEGIN_ALLOW_THREADS - self->ssl = SSL_new(self->ctx); /* New ssl struct */ - PySSL_END_ALLOW_THREADS - SSL_set_fd(self->ssl, Sock->sock_fd); /* Set the socket for SSL */ + SSL_set_fd(self->ssl, sock->sock_fd); #ifdef SSL_MODE_AUTO_RETRY SSL_set_mode(self->ssl, SSL_MODE_AUTO_RETRY); #endif @@ -385,8 +293,7 @@ newPySSLObject(PySocketSockObject *Sock, char *key_file, char *cert_file, /* If the socket is in non-blocking mode or timeout mode, set the BIO * to non-blocking mode (blocking is the default) */ - if (Sock->sock_timeout >= 0.0) { - /* Set both the read and write BIO's to non-blocking mode */ + if (sock->sock_timeout >= 0.0) { BIO_set_nbio(SSL_get_rbio(self->ssl), 1); BIO_set_nbio(SSL_get_wbio(self->ssl), 1); } @@ -398,57 +305,13 @@ newPySSLObject(PySocketSockObject *Sock, char *key_file, char *cert_file, SSL_set_accept_state(self->ssl); PySSL_END_ALLOW_THREADS - self->Socket = PyWeakref_NewRef((PyObject *) Sock, Py_None); + self->Socket = PyWeakref_NewRef((PyObject *) sock, NULL); return self; - fail: - if (errstr) - PyErr_SetString(PySSLErrorObject, errstr); - Py_DECREF(self); - return NULL; } -static PyObject * -PySSL_sslwrap(PyObject *self, PyObject *args) -{ - PySocketSockObject *Sock; - int server_side = 0; - int verification_mode = PY_SSL_CERT_NONE; - int protocol = PY_SSL_VERSION_SSL23; - char *key_file = NULL; - char *cert_file = NULL; - char *cacerts_file = NULL; - char *ciphers = NULL; - - if (!PyArg_ParseTuple(args, "O!i|zziizz:sslwrap", - PySocketModule.Sock_Type, - &Sock, - &server_side, - &key_file, &cert_file, - &verification_mode, &protocol, - &cacerts_file, &ciphers)) - return NULL; - - /* - fprintf(stderr, - "server_side is %d, keyfile %p, certfile %p, verify_mode %d, " - "protocol %d, certs %p\n", - server_side, key_file, cert_file, verification_mode, - protocol, cacerts_file); - */ - - return (PyObject *) newPySSLObject(Sock, key_file, cert_file, - server_side, verification_mode, - protocol, cacerts_file, - ciphers); -} - -PyDoc_STRVAR(ssl_doc, -"sslwrap(socket, server_side, [keyfile, certfile, certs_mode, protocol,\n" -" cacertsfile, ciphers]) -> sslobject"); - /* SSL object methods */ -static PyObject *PySSL_SSLdo_handshake(PySSLObject *self) +static PyObject *PySSL_SSLdo_handshake(PySSLSocket *self) { int ret; int err; @@ -986,7 +849,7 @@ PySSL_test_decode_certificate (PyObject *mod, PyObject *args) { static PyObject * -PySSL_peercert(PySSLObject *self, PyObject *args) +PySSL_peercert(PySSLSocket *self, PyObject *args) { PyObject *retval = NULL; int len; @@ -1017,8 +880,7 @@ PySSL_peercert(PySSLObject *self, PyObject *args) return retval; } else { - - verification = SSL_CTX_get_verify_mode(self->ctx); + verification = SSL_CTX_get_verify_mode(SSL_get_SSL_CTX(self->ssl)); if ((verification & SSL_VERIFY_PEER) == 0) return PyDict_New(); else @@ -1038,7 +900,7 @@ If the optional argument is True, returns a DER-encoded copy of the\n\ peer certificate, or None if no certificate was provided. This will\n\ return the certificate even if it wasn't validated."); -static PyObject *PySSL_cipher (PySSLObject *self) { +static PyObject *PySSL_cipher (PySSLSocket *self) { PyObject *retval, *v; SSL_CIPHER *current; @@ -1084,14 +946,12 @@ static PyObject *PySSL_cipher (PySSLObject *self) { return NULL; } -static void PySSL_dealloc(PySSLObject *self) +static void PySSL_dealloc(PySSLSocket *self) { if (self->peer_cert) /* Possible not to have one? */ X509_free (self->peer_cert); if (self->ssl) SSL_free(self->ssl); - if (self->ctx) - SSL_CTX_free(self->ctx); Py_XDECREF(self->Socket); PyObject_Del(self); } @@ -1166,7 +1026,7 @@ normal_return: return rc == 0 ? SOCKET_HAS_TIMED_OUT : SOCKET_OPERATION_OK; } -static PyObject *PySSL_SSLwrite(PySSLObject *self, PyObject *args) +static PyObject *PySSL_SSLwrite(PySSLSocket *self, PyObject *args) { Py_buffer buf; int len; @@ -1250,7 +1110,7 @@ PyDoc_STRVAR(PySSL_SSLwrite_doc, Writes the string s into the SSL object. Returns the number\n\ of bytes written."); -static PyObject *PySSL_SSLpending(PySSLObject *self) +static PyObject *PySSL_SSLpending(PySSLSocket *self) { int count = 0; @@ -1269,7 +1129,7 @@ PyDoc_STRVAR(PySSL_SSLpending_doc, Returns the number of already decrypted bytes available for read,\n\ pending on the connection.\n"); -static PyObject *PySSL_SSLread(PySSLObject *self, PyObject *args) +static PyObject *PySSL_SSLread(PySSLSocket *self, PyObject *args) { PyObject *dest = NULL; Py_buffer buf; @@ -1392,7 +1252,7 @@ PyDoc_STRVAR(PySSL_SSLread_doc, \n\ Read up to len bytes from the SSL socket."); -static PyObject *PySSL_SSLshutdown(PySSLObject *self) +static PyObject *PySSL_SSLshutdown(PySSLSocket *self) { int err, ssl_err, sockstate, nonblocking; int zeros = 0; @@ -1497,10 +1357,10 @@ static PyMethodDef PySSLMethods[] = { {NULL, NULL} }; -static PyTypeObject PySSL_Type = { +static PyTypeObject PySSLSocket_Type = { PyVarObject_HEAD_INIT(NULL, 0) - "ssl.SSLContext", /*tp_name*/ - sizeof(PySSLObject), /*tp_basicsize*/ + "_ssl._SSLSocket", /*tp_name*/ + sizeof(PySSLSocket), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)PySSL_dealloc, /*tp_dealloc*/ @@ -1529,6 +1389,306 @@ static PyTypeObject PySSL_Type = { PySSLMethods, /*tp_methods*/ }; + +/* + * _SSLContext objects + */ + +static PyObject * +context_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + char *kwlist[] = {"protocol", NULL}; + PySSLContext *self; + int proto_version = PY_SSL_VERSION_SSL23; + SSL_CTX *ctx = NULL; + + if (!PyArg_ParseTupleAndKeywords( + args, kwds, "i:_SSLContext", kwlist, + &proto_version)) + return NULL; + + PySSL_BEGIN_ALLOW_THREADS + if (proto_version == PY_SSL_VERSION_TLS1) + ctx = SSL_CTX_new(TLSv1_method()); + else if (proto_version == PY_SSL_VERSION_SSL3) + ctx = SSL_CTX_new(SSLv3_method()); + else if (proto_version == PY_SSL_VERSION_SSL2) + ctx = SSL_CTX_new(SSLv2_method()); + else if (proto_version == PY_SSL_VERSION_SSL23) + ctx = SSL_CTX_new(SSLv23_method()); + else + proto_version = -1; + PySSL_END_ALLOW_THREADS + + if (proto_version == -1) { + PyErr_SetString(PyExc_ValueError, + "invalid protocol version"); + return NULL; + } + if (ctx == NULL) { + PyErr_SetString(PySSLErrorObject, + "failed to allocate SSL context"); + return NULL; + } + + assert(type != NULL && type->tp_alloc != NULL); + self = (PySSLContext *) type->tp_alloc(type, 0); + if (self == NULL) { + SSL_CTX_free(ctx); + return NULL; + } + self->ctx = ctx; + /* Defaults */ + SSL_CTX_set_verify(self->ctx, SSL_VERIFY_NONE, NULL); + SSL_CTX_set_options(self->ctx, SSL_OP_ALL); + + return (PyObject *)self; +} + +static void +context_dealloc(PySSLContext *self) +{ + SSL_CTX_free(self->ctx); + Py_TYPE(self)->tp_free(self); +} + +static PyObject * +set_ciphers(PySSLContext *self, PyObject *args) +{ + int ret; + const char *cipherlist; + + if (!PyArg_ParseTuple(args, "s:set_ciphers", &cipherlist)) + return NULL; + ret = SSL_CTX_set_cipher_list(self->ctx, cipherlist); + if (ret == 0) { + PyErr_SetString(PySSLErrorObject, + "No cipher can be selected."); + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +get_verify_mode(PySSLContext *self, void *c) +{ + switch (SSL_CTX_get_verify_mode(self->ctx)) { + case SSL_VERIFY_NONE: + return PyLong_FromLong(PY_SSL_CERT_NONE); + case SSL_VERIFY_PEER: + return PyLong_FromLong(PY_SSL_CERT_OPTIONAL); + case SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT: + return PyLong_FromLong(PY_SSL_CERT_REQUIRED); + } + PyErr_SetString(PySSLErrorObject, + "invalid return value from SSL_CTX_get_verify_mode"); + return NULL; +} + +static int +set_verify_mode(PySSLContext *self, PyObject *arg, void *c) +{ + int n, mode; + if (!PyArg_Parse(arg, "i", &n)) + return -1; + if (n == PY_SSL_CERT_NONE) + mode = SSL_VERIFY_NONE; + else if (n == PY_SSL_CERT_OPTIONAL) + mode = SSL_VERIFY_PEER; + else if (n == PY_SSL_CERT_REQUIRED) + mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + else { + PyErr_SetString(PyExc_ValueError, + "invalid value for verify_mode"); + return -1; + } + SSL_CTX_set_verify(self->ctx, mode, NULL); + return 0; +} + +static PyObject * +load_cert_chain(PySSLContext *self, PyObject *args, PyObject *kwds) +{ + char *kwlist[] = {"certfile", "keyfile", NULL}; + PyObject *certfile, *keyfile = NULL; + PyObject *certfile_bytes = NULL, *keyfile_bytes = NULL; + int r; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "O|O:load_cert_chain", kwlist, + &certfile, &keyfile)) + return NULL; + if (keyfile == Py_None) + keyfile = NULL; + if (!PyUnicode_FSConverter(certfile, &certfile_bytes)) { + PyErr_SetString(PyExc_TypeError, + "certfile should be a valid filesystem path"); + return NULL; + } + if (keyfile && !PyUnicode_FSConverter(keyfile, &keyfile_bytes)) { + PyErr_SetString(PyExc_TypeError, + "keyfile should be a valid filesystem path"); + goto error; + } + PySSL_BEGIN_ALLOW_THREADS + r = SSL_CTX_use_certificate_chain_file(self->ctx, + PyBytes_AS_STRING(certfile_bytes)); + PySSL_END_ALLOW_THREADS + if (r != 1) { + _setSSLError(NULL, 0, __FILE__, __LINE__); + goto error; + } + PySSL_BEGIN_ALLOW_THREADS + r = SSL_CTX_use_RSAPrivateKey_file(self->ctx, + PyBytes_AS_STRING(keyfile ? keyfile_bytes : certfile_bytes), + SSL_FILETYPE_PEM); + PySSL_END_ALLOW_THREADS + Py_XDECREF(keyfile_bytes); + Py_XDECREF(certfile_bytes); + if (r != 1) { + _setSSLError(NULL, 0, __FILE__, __LINE__); + return NULL; + } + PySSL_BEGIN_ALLOW_THREADS + r = SSL_CTX_check_private_key(self->ctx); + PySSL_END_ALLOW_THREADS + if (r != 1) { + _setSSLError(NULL, 0, __FILE__, __LINE__); + return NULL; + } + Py_RETURN_NONE; + +error: + Py_XDECREF(keyfile_bytes); + Py_XDECREF(certfile_bytes); + return NULL; +} + +static PyObject * +load_verify_locations(PySSLContext *self, PyObject *args, PyObject *kwds) +{ + char *kwlist[] = {"cafile", "capath", NULL}; + PyObject *cafile = NULL, *capath = NULL; + PyObject *cafile_bytes = NULL, *capath_bytes = NULL; + const char *cafile_buf = NULL, *capath_buf = NULL; + int r; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "|OO:load_verify_locations", kwlist, + &cafile, &capath)) + return NULL; + if (cafile == Py_None) + cafile = NULL; + if (capath == Py_None) + capath = NULL; + if (cafile == NULL && capath == NULL) { + PyErr_SetString(PyExc_TypeError, + "cafile and capath cannot be both omitted"); + return NULL; + } + if (cafile && !PyUnicode_FSConverter(cafile, &cafile_bytes)) { + PyErr_SetString(PyExc_TypeError, + "cafile should be a valid filesystem path"); + return NULL; + } + if (capath && !PyUnicode_FSConverter(capath, &capath_bytes)) { + Py_DECREF(cafile_bytes); + PyErr_SetString(PyExc_TypeError, + "capath should be a valid filesystem path"); + return NULL; + } + if (cafile) + cafile_buf = PyBytes_AS_STRING(cafile_bytes); + if (capath) + capath_buf = PyBytes_AS_STRING(capath_bytes); + PySSL_BEGIN_ALLOW_THREADS + r = SSL_CTX_load_verify_locations(self->ctx, cafile_buf, capath_buf); + PySSL_END_ALLOW_THREADS + Py_XDECREF(cafile_bytes); + Py_XDECREF(capath_bytes); + if (r != 1) { + _setSSLError(NULL, 0, __FILE__, __LINE__); + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +context_wrap_socket(PySSLContext *self, PyObject *args, PyObject *kwds) +{ + char *kwlist[] = {"sock", "server_side", NULL}; + PySocketSockObject *sock; + int server_side = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!i:_wrap_socket", kwlist, + PySocketModule.Sock_Type, + &sock, &server_side)) + return NULL; + + return (PyObject *) newPySSLSocket(self->ctx, sock, server_side); +} + +static PyGetSetDef context_getsetlist[] = { + {"verify_mode", (getter) get_verify_mode, + (setter) set_verify_mode, NULL}, + {NULL}, /* sentinel */ +}; + +static struct PyMethodDef context_methods[] = { + {"_wrap_socket", (PyCFunction) context_wrap_socket, + METH_VARARGS | METH_KEYWORDS, NULL}, + {"set_ciphers", (PyCFunction) set_ciphers, + METH_VARARGS, NULL}, + {"load_cert_chain", (PyCFunction) load_cert_chain, + METH_VARARGS | METH_KEYWORDS, NULL}, + {"load_verify_locations", (PyCFunction) load_verify_locations, + METH_VARARGS | METH_KEYWORDS, NULL}, + {NULL, NULL} /* sentinel */ +}; + +static PyTypeObject PySSLContext_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "_ssl._SSLContext", /*tp_name*/ + sizeof(PySSLContext), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + (destructor)context_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_reserved*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + context_methods, /*tp_methods*/ + 0, /*tp_members*/ + context_getsetlist, /*tp_getset*/ + 0, /*tp_base*/ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + 0, /*tp_init*/ + 0, /*tp_alloc*/ + context_new, /*tp_new*/ +}; + + + #ifdef HAVE_OPENSSL_RAND /* helper routines for seeding the SSL PRNG */ @@ -1598,8 +1758,6 @@ fails or if it does provide enough data to seed PRNG."); /* List of functions exported by this module. */ static PyMethodDef PySSL_methods[] = { - {"sslwrap", PySSL_sslwrap, - METH_VARARGS, ssl_doc}, {"_test_decode_cert", PySSL_test_decode_certificate, METH_VARARGS}, #ifdef HAVE_OPENSSL_RAND @@ -1708,7 +1866,9 @@ PyInit__ssl(void) unsigned int major, minor, fix, patch, status; PySocketModule_APIObject *socket_api; - if (PyType_Ready(&PySSL_Type) < 0) + if (PyType_Ready(&PySSLContext_Type) < 0) + return NULL; + if (PyType_Ready(&PySSLSocket_Type) < 0) return NULL; m = PyModule_Create(&_sslmodule); @@ -1741,8 +1901,11 @@ PyInit__ssl(void) return NULL; if (PyDict_SetItemString(d, "SSLError", PySSLErrorObject) != 0) return NULL; - if (PyDict_SetItemString(d, "SSLType", - (PyObject *)&PySSL_Type) != 0) + if (PyDict_SetItemString(d, "_SSLContext", + (PyObject *)&PySSLContext_Type) != 0) + return NULL; + if (PyDict_SetItemString(d, "_SSLSocket", + (PyObject *)&PySSLSocket_Type) != 0) return NULL; PyModule_AddIntConstant(m, "SSL_ERROR_ZERO_RETURN", PY_SSL_ERROR_ZERO_RETURN);