In order for python-libtls
to be usable, it will need to have access to libtls
shared libraries. If these are installed into special “system” locations, then
there is nothing more that needs doing. Otherwise, you will need to specify the
location of the libtls
binaries via an environment variable or other mechanism
before you run any executable which uses python-libtls
. Refer to the section on
Specifying the location of libtls for more information.
If you are interested in TLS functionality for a server, see the Servers section. Otherwise, continue reading for information on using TLS as a client.
In general, you need to access a server which is not part of your organisation, so that its identity needs to be verified. This is generally true for any server which is accessed across the Internet.
Create a Context
instance with no parameters:
>>> from tls import Context; context = Context()
Then connect it to the external server by specifying a host and port. We’ll
specify that we want to connect to github.com
on port 443, the default port
for TLS traffic.
>>> tls_socket = context.connect('github.com', 443)
At this point, a TCP connection will have been made to the server, but no verification of its identity has yet been made. To do so, we can invoke a TLS handshake explicitly.
>>> tls_socket.do_handshake()
At this point, either a secure connection to Github will have been made, or an exception will be raised. You can get some information about the connection, as well as what’s on the other end of the connection (the peer, in network parlance). The returned socket can now be used for secure communications.
>>> context.conn_version
'TLSv1.2'
This is the most secure version of TLS which is generally available.
>>> context.peer_cert_issuer
'/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert SHA2 Extended Validation Server CA'
That’s the certificate authority (CA) which certifies that the server we’re connected to actually is a GitHub server.
You can also check certificate validity dates:
>>> context.peer_cert_notbefore
datetime.datetime(2016, 3, 10, 0, 0)
>>> context.peer_cert_notafter
datetime.datetime(2018, 5, 17, 12, 0)
You can send and receive data securely over the socket returned by the
connect()
call:
>>> tls_socket.sendall(b'GET / HTTP/1.1\r\nhost: github.com\r\n\r\n')
>>> data = tls_socket.recv(4096)
>>> len(data)
1370
>>> data[:68]
b'HTTP/1.1 200 OK\r\nServer: GitHub.com\r\nDate: Fri, 10 Feb 2017 16:53:39'
Under certain circumstances, a certificate signed by a certifying authority may have been revoked after it was issued. In such cases, you can’t rely on the validity of the certificate itself, but need to cross-check against a database of revocations which is maintained by certificate authorities. OCSP (the Online Certificate Status Protocol) is used to obtain the revocation status of a certificate.
You can ask the server to which you are connecting to provide the revocation status of its certificate (which is obtained from an OCSP Responder and is tamper-proof). If it hasn’t been revoked, then you can rely on the certificate; if it has, then you can’t. The OCSP resoonse is said to be stapled to the responses sent during TLS negotiation with the server.
You can request the server to provide an OCSP response by specifying a
require_ocsp_stapling=
keyword argument when you create the context. If a
response is provided, you can check OCSP information by looking at context
attributes such as the peer_ocsp_result
attribute.
If no OCSP response is provided, verification fails:
>>> from tls import Context; context = Context(require_ocsp_stapling=True)
>>> sock = context.connect('github.com', 443)
>>> sock.do_handshake()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "tls/__init__.py", line 193, in do_handshake
raise ValueError('handshake failed: %s' % context_error(context))
ValueError: handshake failed: no stapled OCSP response provided
OCSP stapling is not yet widely available, but where it is available, you can make use of it.
You may wish to disable verification if the server you are connecting to is used for development or test purposes, or is an internal server for which no certification is required. This is controlled by three keyword arguments which are specified when creating the context:
verify_cert
- verifies the certificate signature against a certificate
authority.verify_name
- verifies the server name against what it’s meant to be.verify_time
- verify that the certificate is valid at the time of
checking.These normally default to True
, but you can specify False
if you want
to omit the relevant checks.
If you are familiar with the details of TLS, you can specify specific constraints which control how the TLS negotiation will work:
ciphers
- a string of ciphers you want to use.dhe_params
- a string of parameters to use for the Diffie-Hellman
key exchange.ecdhe_curve
- a string describing the parameters for the ECDHE (Elliptic
Curve Diffie-Hellman Exchange) curve.alpn
- a string describing the parameters for the Application-Level
Protocol Negotiation (ALPN). This is a comma separated list of protocols, in
order of preference.Secure servers generally need to present proof of their identity to clients. Thus, for such servers, the minimum configuration will be something like
>>> context = Context(server=True, cafile='/path/to/ca_bundle.pem',
keypair_paths=('/path/to/server_cert.pem',
'/path/to/server_key.pem'))
where the server_key.pem
is the private key for the server which was
created as part of the certification process, server_cert.pem
is the
signed certificate for the server, and ca_bundle.pem
is the certificate
bundle which certifies the server certificate.
The configuration is the same as above, except that instead of a
ca_bundle.pem
which comes from a CA, you will provide a cacert.pem
which you have created yourself.
You don’t need to do anything special to deal with SNI - libtls
deals with
the details internally. When you create the server-side Context
instance, pass all the certificate and key files you need to support in the
extra_certificate_key_paths=
keyword parameter, as a sequence of 2-tuples
(certpath, keypath) or 3-tuples (certpath, keypath, ocsppath). See the
documentation for server-side keyword parameters in the Context
class for more
information.
If you require the client to present a certificate as proof of its identity,
you can specify verify_client=True
along with the other parameters of the
configuration.
You can specify an OCSP response to be sent to the client by providing a 3-tuple
for the keypair_paths=
keyword argument when initialising the
Context
, which consists of the paths to the certificate file
(PEM format), key file (PEM format) and OCSP response (DER format).
If you need to support TLS sessions, you can use the session_id=
and
session_lifetime=
parameters when setting up your context. See the
documentation for server-side keyword parameters in the Context
class for more
information.
While the above examples have shown sockets operating in synchronous mode,
python-libtls
can be used in asynchronous mode, too (i.e. with non-blocking
sockets). One of the unit tests illustrates asynchronous operation using the
asyncore
Python standard library module. Preliminary tests have also
shown successful operation with the Curio asynchronous library.
When using asynchronous mode, you typically create a socket and set it to be
non-blocking, and then pass that socket into the connect()
call on a
Context
instance. For example:
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> sock.setblocking(False)
Connecting is potentially a blocking operation. When connecting, or at any later point when you initiate a potentially blocking operation on this socket, such as a handshake or application data I/O, special exceptions may be raised to indicate that the call cannot proceed without blocking.
>>> tls_socket = context.connect(host, port, sock)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/vinay/projects/python-libtls/tls/__init__.py", line 542, in connect
return self.wrap_socket(sock, server_hostname=hostname)
File "/home/vinay/projects/python-libtls/tls/__init__.py", line 551, in wrap_socket
self._ensure_connected(sock, hostname)
File "/home/vinay/projects/python-libtls/tls/__init__.py", line 516, in _ensure_connected
sock.connect((host, port))
BlockingIOError: [Errno 115] Operation now in progress
Under Python 2.x, the traceback is slightly different:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "tls/__init__.py", line 542, in connect
return self.wrap_socket(sock, server_hostname=hostname)
File "tls/__init__.py", line 551, in wrap_socket
self._ensure_connected(sock, hostname)
File "tls/__init__.py", line 516, in _ensure_connected
sock.connect((host, port))
File "/usr/lib/python2.7/socket.py", line 228, in meth
return getattr(self._sock,name)(*args)
socket.error: [Errno 115] Operation now in progress
Normally, these exceptions will be handled by the asynchronous framework you
are using. During application I/O, python-libtls
generates the standard Python
exceptions SSLWantReadError
and SSLWantWriteError
(defined in
the built-in ssl
module) to indicate that either data or buffer space is
needed in order for the operation to complete. These exceptions are generally
handled by the asynchronous framework you are using, in the same way as if you
were using Python’s built-in ssl
module.
This is generally done using the LD_LIBRARY_PATH
environment variable, which
must be the directory where the libraries are to be found.
You need to add the location of the libtls
libraries to the Windows PATH
environment variable, or have them in the current directory where the program
that uses them resides.
First, download the official source tarball for the relevant release from the
official releases page.
Don’t use any archives from GitHub - they are not suitable for building
from. Unpack the archive into a folder, which will give you the sources in a
folder named something like libressl-X.Y.Z
. Navigate to this folder in a
terminal window.
It is assumed that you will have a C compiler on the system.
Invoke the following commands to configure, make and install libressl
, which
includes libtls
:
$ ./configure --prefix=$HOME/opt
$ make
$ make install
The above assumes you don’t want to install it into a system location or don’t have root privileges.
After these commands have run, the libtls
dynamic libraries will be in
$HOME/opt/lib
. You will need to specify this directory in the
LD_LIBRARY_PATH
environment variable before running a Python script which
uses python-libtls
.
It is assumed that you will have Visual Studio 15 or later on the system (earlier versions may not work).
You will also need a recent version of the CMake build tool. Issue commands in the root directory of your unpacked archive, using the following as a guide:
C> mkdir build
C> cd build
C> cmake -G"Visual Studio 14 2015 Win64" -DCMAKE_INSTALL_PREFIX=c:\Users\YourName\opt ..
The above example assumes that you want to build 64-bit versions of the libtls
libraries, and that you want to install the software in
c:\Users\YourName\opt.
If you don’t specify the CMAKE_INSTALL_PREFIX
,
it defaults to C:\Program Files\LibreSSL
.
Those commands create some build files in the current (build
) directory. You
can then build and install the libraries by invoking (for example):
C> cmake --build . --target INSTALL --config Release
which should build the libtls
software and install it to
c:\Users\YourName\opt
, with the shared libraries being in
c:\Users\YourName\opt\lib
. You need to add this latter directory to your
PATH
, and python-libtls
should then be usable.
libtls
ships with a set of CA certificates, and they are to be found in a
file called cert.pem
which is located in directory etc/ssl
relative to
the installation directory (in the above examples, $HOME/opt
on POSIX and
c:\Users\YourName\opt
on Windows). If you don’t specify a capath=
or a
cafile=
argument when setting up your Context
, libtls
will look
for a CA bundle in this default location. If not found, you will not be able
to perform verification – python-libtls
will throw exceptions if you haven’t
disabled verification.
If you always specify a capath=
or a cafile=
argument referencing your
own bundle, then this location isn’t searched.