DTLS client

This example demonstrates how to implement client-side DTLS connections.

../_images/secureudpclient-example.png

Note

The DTLS client example is intended to be run alongside the DTLS server example.

The example DTLS client can establish several DTLS connections to one or many DTLS servers. A client-side DTLS connection is implemented by the DtlsAssociation class. This class uses QUdpSocket to read and write datagrams and QDtls for encryption:

class DtlsAssociation(QObject):

    Q_OBJECT
# public
    DtlsAssociation(QHostAddress address, quint16 port,
                    connectionName) = QString()
    ~DtlsAssociation()
    def startHandshake():
signals:
    def errorMessage(message):
    def warningMessage(message):
    def infoMessage(message):
    def serverResponse(clientInfo, datagraam,):
                        plainText) = QByteArray()
slots: = private()
    def udpSocketConnected():
    def readyRead():
    def handshakeTimeout():
    def pskRequired(auth):
    def pingTimeout():
# private
    name = QString()
    socket = QUdpSocket()
    crypto = QDtls()
    pingTimer = QTimer()
    ping = 0
    Q_DISABLE_COPY(DtlsAssociation)

The constructor sets the minimal TLS configuration for the new DTLS connection, and sets the address and the port of the server:

    ...
configuration = QSslConfiguration.defaultDtlsConfiguration()
configuration.setPeerVerifyMode(QSslSocket.VerifyNone)
crypto.setPeer(address, port)
crypto.setDtlsConfiguration(configuration)            ...

The handshakeTimeout() signal is connected to the handleTimeout() slot to deal with packet loss and retransmission during the handshake phase:

    ...
connect(crypto, QDtls.handshakeTimeout, self, DtlsAssociation.handshakeTimeout)            ...

To ensure we receive only the datagrams from the server, we connect our UDP socket to the server:

    ...
socket.connectToHost(address.toString(), port)            ...

The readyRead() signal is connected to the readyRead() slot:

    ...
connect(socket, QUdpSocket.readyRead, self, DtlsAssociation.readyRead)            ...

When a secure connection to a server is established, a DtlsAssociation object will be sending short ping messages to the server, using a timer:

pingTimer.setInterval(5000)
connect(pingTimer, QTimer.timeout, self, DtlsAssociation.pingTimeout)

startHandshake() starts a handshake with the server:

def startHandshake(self):

    if (socket.state() != QAbstractSocket.ConnectedState) {
        infoMessage.emit(tr("%1: connecting UDP socket first ...").arg(name))
        connect(socket, QAbstractSocket.connected, self, DtlsAssociation.udpSocketConnected)
        return

    if (not crypto.doHandshake(socket))
        errorMessage.emit(tr("%1: failed to start a handshake - %2").arg(name, crypto.dtlsErrorString()))
else:
        infoMessage.emit(tr("%1: starting a handshake").arg(name))

The readyRead() slot reads a datagram sent by the server:

dgram = QByteArray(socket.pendingDatagramSize(), Qt.Uninitialized)
bytesRead = socket.readDatagram(dgram.data(), dgram.size())
if (bytesRead <= 0) {
    warningMessage.emit(tr("%1: spurious read notification?").arg(name))
    return

dgram.resize(bytesRead)

If the handshake was already completed, this datagram is decrypted:

if (crypto.isConnectionEncrypted()) {
    plainText = crypto.decryptDatagram(socket, dgram)
    if (plainText.size()) {
        serverResponse.emit(name, dgram, plainText)
        return

    if (crypto.dtlsError() == QDtlsError.RemoteClosedConnectionError) {
        errorMessage.emit(tr("%1: shutdown alert received").arg(name))
        socket.close()
        pingTimer.stop()
        return

    warningMessage.emit(tr("%1: zero-length datagram received?").arg(name))
else:

otherwise, we try to continue the handshake:

if (not crypto.doHandshake(socket, dgram)) {
    errorMessage.emit(tr("%1: handshake error - %2").arg(name, crypto.dtlsErrorString()))
    return

When the handshake has completed, we send our first ping message:

if (crypto.isConnectionEncrypted()) {
    infoMessage.emit(tr("%1: encrypted connection established!").arg(name))
    pingTimer.start()
    pingTimeout()
else:

The pskRequired() slot provides the Pre-Shared Key (PSK) needed during the handshake phase:

def pskRequired(self, auth):

    Q_ASSERT(auth)
    infoMessage.emit(tr("%1: providing pre-shared key ...").arg(name))
    auth.setIdentity(name.toLatin1())
    auth.setPreSharedKey(QByteArrayLiteral("\x1a\x2b\x3c\x4d\x5e\x6f"))

Note

For the sake of brevity, the definition of pskRequired() is oversimplified. The documentation for the QSslPreSharedKeyAuthenticator class explains in detail how this slot can be properly implemented.

pingTimeout() sends an encrypted message to the server:

def pingTimeout(self):

    message = QStringLiteral("I am %1, please, accept our ping %2")
    written = crypto.writeDatagramEncrypted(socket, message.arg(name).arg(ping).toLatin1())
    if (written <= 0) {
        errorMessage.emit(tr("%1: failed to send a ping - %2").arg(name, crypto.dtlsErrorString()))
        pingTimer.stop()
        return

    ping = ping + 1

During the handshake phase the client must handle possible timeouts, which can happen due to packet loss. The handshakeTimeout() slot retransmits the handshake messages:

def handshakeTimeout(self):

    warningMessage.emit(tr("%1: handshake timeout, trying to re-transmit").arg(name))
    if (not crypto.handleTimeout(socket))
        errorMessage.emit(tr("%1: failed to re-transmit - %2").arg(name, crypto.dtlsErrorString()))

Before a client connection is destroyed, its DTLS connection must be shut down:

DtlsAssociation::~DtlsAssociation()

    if (crypto.isConnectionEncrypted())
        crypto.shutdown(socket)

Error messages, informational messages, and decrypted responses from servers are displayed by the UI:

colorizer = QString(QStringLiteral("<font color=\"%1\">%2"))
def addErrorMessage(self, message):

    ui.clientMessages.insertHtml(colorizer.arg(QStringLiteral("Crimson"), message))

def addWarningMessage(self, message):

    ui.clientMessages.insertHtml(colorizer.arg(QStringLiteral("DarkOrange"), message))

def addInfoMessage(self, message):

    ui.clientMessages.insertHtml(colorizer.arg(QStringLiteral("DarkBlue"), message))

def addServerResponse(self, clientInfo, datagram,):
                                   plainText) = QByteArray()

    messageColor = QStringLiteral("DarkMagenta")
    formatter = QStringLiteral("<br>---------------"()
                                                    "<br>%1 received a DTLS datagram:<br> %2"
                                                    "<br>As plain text:<br> %3")
    html = formatter.arg(clientInfo, QString.fromUtf8(datagram.toHex(' ')),
                                       QString.fromUtf8(plainText))
    ui.serverMessages.insertHtml(colorizer.arg(messageColor, html))

Example project @ code.qt.io