From d77d4fc548075120213b8876259f65818cb9561e Mon Sep 17 00:00:00 2001 From: Timur Pocheptsov Date: Mon, 16 Apr 2018 13:03:38 +0200 Subject: [PATCH] QDtlsClientVerifier - add auto-test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This part of DTLS is relatively easy to test: we never do a complete handshake. Certificates, verification, ciphers, etc. - do not matter at this stage (to be tested in tst_QDtls). Errors are mostly insignificant and can be ignored or handled trivially. The test is OpenSSL-only: SecureTransport failed to correctly implement/ support server-side DTLS, the problem reported quite some time ago and no fixes from Apple so far. Task-number: QTBUG-67597 Change-Id: I21ad4907de444ef95d5d83b50083ffe211a184f8 Reviewed-by: MÃ¥rten Nordheim Reviewed-by: Edward Welbourne --- .../network/ssl/qdtlscookie/qdtlscookie.pro | 16 + .../ssl/qdtlscookie/tst_qdtlscookie.cpp | 480 ++++++++++++++++++ tests/auto/network/ssl/ssl.pro | 8 +- 3 files changed, 502 insertions(+), 2 deletions(-) create mode 100644 tests/auto/network/ssl/qdtlscookie/qdtlscookie.pro create mode 100644 tests/auto/network/ssl/qdtlscookie/tst_qdtlscookie.cpp diff --git a/tests/auto/network/ssl/qdtlscookie/qdtlscookie.pro b/tests/auto/network/ssl/qdtlscookie/qdtlscookie.pro new file mode 100644 index 0000000000..ec7ee2cdf5 --- /dev/null +++ b/tests/auto/network/ssl/qdtlscookie/qdtlscookie.pro @@ -0,0 +1,16 @@ +CONFIG += testcase + +SOURCES += tst_qdtlscookie.cpp +win32:LIBS += -lws2_32 +QT = core network-private testlib + +TARGET = tst_qdtlscookie + +win32 { + CONFIG(debug, debug|release) { + DESTDIR = debug + } else { + DESTDIR = release + } +} + diff --git a/tests/auto/network/ssl/qdtlscookie/tst_qdtlscookie.cpp b/tests/auto/network/ssl/qdtlscookie/tst_qdtlscookie.cpp new file mode 100644 index 0000000000..05c46dbd9f --- /dev/null +++ b/tests/auto/network/ssl/qdtlscookie/tst_qdtlscookie.cpp @@ -0,0 +1,480 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +QT_BEGIN_NAMESPACE + +#define STOP_ON_FAILURE \ + if (QTest::currentTestFailed()) \ + return; + +class tst_QDtlsCookie : public QObject +{ + Q_OBJECT + +public slots: + void initTestCase(); + void init(); + +private slots: + // Tests: + void construction(); + void validateParameters_data(); + void validateParameters(); + void verifyClient(); + void cookieGeneratorParameters(); + void verifyMultipleClients(); + +protected slots: + // Aux. functions: + void stopLoopOnMessage(); + void serverReadyRead(); + void clientReadyRead(); + void handleClientTimeout(); + void makeNoise(); + void spawnClients(); + +private: + void sendClientHello(QUdpSocket *socket, QDtls *handshake, + const QByteArray &serverMessage = {}); + void receiveMessage(QUdpSocket *socket, QByteArray *message, + QHostAddress *address = nullptr, + quint16 *port = nullptr); + + static QHostAddress toNonAny(const QHostAddress &addr); + + enum AddressType + { + ValidAddress, + NullAddress, + BroadcastAddress, + MulticastAddress + }; + + QUdpSocket serverSocket; + QHostAddress serverAddress; + quint16 serverPort = 0; + + QTestEventLoop testLoop; + int handshakeTimeoutMS = 500; + + QDtlsClientVerifier listener; + using HandshakePtr = QSharedPointer; + HandshakePtr dtls; + + const QCryptographicHash::Algorithm defaultHash = +#ifdef QT_CRYPTOGRAPHICHASH_ONLY_SHA1 + QCryptographicHash::Sha1; +#else + QCryptographicHash::Sha256; +#endif + + using CookieParams = QDtlsClientVerifier::GeneratorParameters; + + QUdpSocket noiseMaker; + QHostAddress spammerAddress; + QTimer noiseTimer; + quint16 spammerPort = 0; + const int noiseTimeoutMS = 5; + + using SocketPtr = QSharedPointer; + using ValidClient = QPair; + unsigned clientsToWait = 0; + unsigned clientsToAdd = 0; + std::vector dtlsClients; + QTimer spawnTimer; +}; + +QHostAddress tst_QDtlsCookie::toNonAny(const QHostAddress &addr) +{ + if (addr == QHostAddress::Any || addr == QHostAddress::AnyIPv4) + return QHostAddress::LocalHost; + if (addr == QHostAddress::AnyIPv6) + return QHostAddress::LocalHostIPv6; + return addr; +} + +void tst_QDtlsCookie::initTestCase() +{ + QVERIFY(noiseMaker.bind()); + spammerAddress = toNonAny(noiseMaker.localAddress()); + spammerPort = noiseMaker.localPort(); +} + +void tst_QDtlsCookie::init() +{ + if (serverSocket.state() != QAbstractSocket::UnconnectedState) { + serverSocket.close(); + // Disconnect stopLoopOnMessage or serverReadyRead slots: + serverSocket.disconnect(); + } + + QCOMPARE(serverSocket.state(), QAbstractSocket::UnconnectedState); + QVERIFY(serverSocket.bind()); + + serverAddress = toNonAny(serverSocket.localAddress()); + serverPort = serverSocket.localPort(); + + dtls.reset(new QDtls(QSslSocket::SslClientMode)); + dtls->setRemote(serverAddress, serverPort); +} + +void tst_QDtlsCookie::construction() +{ + QDtlsClientVerifier verifier; + + QCOMPARE(verifier.dtlsError(), QDtlsError::NoError); + QCOMPARE(verifier.dtlsErrorString(), QString()); + QCOMPARE(verifier.verifiedHello(), QByteArray()); + + const auto params = verifier.cookieGeneratorParameters(); + QCOMPARE(params.hash, defaultHash); + QVERIFY(params.secret.size() > 0); +} + +void tst_QDtlsCookie::validateParameters_data() +{ + QTest::addColumn("invalidSocket"); + QTest::addColumn("emptyDatagram"); + QTest::addColumn("addressType"); + + QTest::addRow("socket") << true << false << int(ValidAddress); + QTest::addRow("dgram") << false << true << int(ValidAddress); + QTest::addRow("addr(invalid)") << false << false << int(NullAddress); + QTest::addRow("addr(broadcast)") << false << false << int(BroadcastAddress); + QTest::addRow("addr(multicast)") << false << false << int(MulticastAddress); + + QTest::addRow("socket-dgram") << true << true << int(ValidAddress); + QTest::addRow("socket-dgram-addr(invalid)") << true << true << int(NullAddress); + QTest::addRow("socket-dgram-addr(broadcast)") << true << true << int(BroadcastAddress); + QTest::addRow("socket-dgram-addr(multicast)") << true << true << int(MulticastAddress); + + QTest::addRow("dgram-addr(invalid)") << false << true << int(NullAddress); + QTest::addRow("dgram-addr(broadcast)") << false << true << int(BroadcastAddress); + QTest::addRow("dgram-addr(multicast)") << false << true << int(MulticastAddress); + + QTest::addRow("socket-addr(invalid)") << true << false << int(NullAddress); + QTest::addRow("socket-addr(broadcast)") << true << false << int(BroadcastAddress); + QTest::addRow("socket-addr(multicast)") << true << false << int(MulticastAddress); +} + +void tst_QDtlsCookie::validateParameters() +{ + connect(&serverSocket, &QUdpSocket::readyRead, this, + &tst_QDtlsCookie::stopLoopOnMessage); + + QFETCH(const bool, invalidSocket); + QFETCH(const bool, emptyDatagram); + QFETCH(const int, addressType); + + QUdpSocket clientSocket; + QByteArray hello; + QHostAddress clientAddress; + quint16 clientPort = 0; + + sendClientHello(&clientSocket, dtls.data()); + STOP_ON_FAILURE + receiveMessage(&serverSocket, &hello, &clientAddress, &clientPort); + STOP_ON_FAILURE + + switch (addressType) { + case MulticastAddress: + clientAddress.setAddress(QStringLiteral("224.0.0.0")); + break; + case BroadcastAddress: + clientAddress = QHostAddress::Broadcast; + break; + case NullAddress: + clientAddress = {}; + break; + } + + if (emptyDatagram) + hello.clear(); + + QUdpSocket *socket = invalidSocket ? nullptr : &serverSocket; + QCOMPARE(listener.verifyClient(socket, hello, clientAddress, clientPort), false); + QCOMPARE(listener.verifiedHello(), QByteArray()); + QCOMPARE(listener.dtlsError(), QDtlsError::InvalidInputParameters); +} + +void tst_QDtlsCookie::verifyClient() +{ + connect(&serverSocket, &QUdpSocket::readyRead, this, + &tst_QDtlsCookie::stopLoopOnMessage); + + QUdpSocket clientSocket; + connect(&clientSocket, &QUdpSocket::readyRead, this, + &tst_QDtlsCookie::stopLoopOnMessage); + + // Client: send an initial ClientHello message without any cookie: + sendClientHello(&clientSocket, dtls.data()); + STOP_ON_FAILURE + // Server: read the first ClientHello message: + QByteArray dgram; + QHostAddress clientAddress; + quint16 clientPort = 0; + receiveMessage(&serverSocket, &dgram, &clientAddress, &clientPort); + STOP_ON_FAILURE + // Server: reply with a verify hello request (the client is not verified yet): + QCOMPARE(listener.verifyClient(&serverSocket, dgram, clientAddress, clientPort), false); + QCOMPARE(listener.verifiedHello(), QByteArray()); + QCOMPARE(listener.dtlsError(), QDtlsError::NoError); + // Client: read hello verify request: + receiveMessage(&clientSocket, &dgram); + STOP_ON_FAILURE + // Client: send a new hello message, this time with a cookie attached: + sendClientHello(&clientSocket, dtls.data(), dgram); + STOP_ON_FAILURE + // Server: read a client-verified message: + receiveMessage(&serverSocket, &dgram, &clientAddress, &clientPort); + STOP_ON_FAILURE + // Client's readyRead is not interesting anymore: + clientSocket.close(); + + // Verify with the address and port we extracted, do it twice (DTLS "listen" + // must be stateless and work as many times as needed): + for (int i = 0; i < 2; ++i) { + QCOMPARE(listener.verifyClient(&serverSocket, dgram, clientAddress, clientPort), true); + QCOMPARE(listener.verifiedHello(), dgram); + QCOMPARE(listener.dtlsError(), QDtlsError::NoError); + } + + // Test that another freshly created (stateless) verifier can verify: + QDtlsClientVerifier anotherListener; + QCOMPARE(anotherListener.verifyClient(&serverSocket, dgram, clientAddress, + clientPort), true); + QCOMPARE(anotherListener.verifiedHello(), dgram); + QCOMPARE(anotherListener.dtlsError(), QDtlsError::NoError); + // Now let's use a wrong port: + QCOMPARE(listener.verifyClient(&serverSocket, dgram, clientAddress, serverPort), false); + // Invalid cookie, no verified hello message: + QCOMPARE(listener.verifiedHello(), QByteArray()); + // But it's UDP so we ignore this "fishy datagram", no error expected: + QCOMPARE(listener.dtlsError(), QDtlsError::NoError); +} + +void tst_QDtlsCookie::cookieGeneratorParameters() +{ + CookieParams params;// By defualt, 'secret' is empty. + QCOMPARE(listener.setCookieGeneratorParameters(params), false); + QCOMPARE(listener.dtlsError(), QDtlsError::InvalidInputParameters); + params.secret = "abcdefghijklmnopqrstuvwxyz"; + QCOMPARE(listener.setCookieGeneratorParameters(params), true); + QCOMPARE(listener.dtlsError(), QDtlsError::NoError); +} + +void tst_QDtlsCookie::verifyMultipleClients() +{ + // 'verifyClient' above was quite simple - it's like working with blocking + // sockets, step by step - we write, then make sure we read a datagram back + // etc. This test is more asynchronous - we are running an event loop and don't + // stop on the first datagram received, instead, we spawn many clients + // with which to exchange handshake messages and verify requests, while at + // the same time dealing with a 'noise maker' - a fake DTLS client, who keeps + // spamming our server with non-DTLS datagrams and initial ClientHello + // messages, but never replies to client verify requests. + connect(&serverSocket, &QUdpSocket::readyRead, this, &tst_QDtlsCookie::serverReadyRead); + + noiseTimer.setInterval(noiseTimeoutMS); + connect(&noiseTimer, &QTimer::timeout, this, &tst_QDtlsCookie::makeNoise); + + spawnTimer.setInterval(noiseTimeoutMS * 10); + connect(&spawnTimer, &QTimer::timeout, this, &tst_QDtlsCookie::spawnClients); + + noiseTimer.start(); + spawnTimer.start(); + + clientsToAdd = clientsToWait = 100; + + testLoop.enterLoop(handshakeTimeoutMS * clientsToWait); + QVERIFY(!testLoop.timeout()); + QVERIFY(clientsToWait == 0); +} + +void tst_QDtlsCookie::sendClientHello(QUdpSocket *socket, QDtls *dtls, + const QByteArray &serverMessage) +{ + Q_ASSERT(socket && dtls); + dtls->doHandshake(socket, serverMessage); + // We don't really care about QDtls in this auto-test, but must be + // sure that we, indeed, sent our hello and if not - stop early without + // running event loop: + QCOMPARE(socket->error(), QAbstractSocket::UnknownSocketError); + QCOMPARE(dtls->dtlsError(), QDtlsError::NoError); + // We never complete a handshake, so it must be 'HandshakeInProgress': + QCOMPARE(dtls->handshakeState(), QDtls::HandshakeInProgress); +} + +void tst_QDtlsCookie::receiveMessage(QUdpSocket *socket, QByteArray *message, + QHostAddress *address, quint16 *port) +{ + Q_ASSERT(socket && message); + + if (!socket->pendingDatagramSize()) + testLoop.enterLoopMSecs(handshakeTimeoutMS); + + QVERIFY(!testLoop.timeout()); + QVERIFY(socket->pendingDatagramSize()); + + message->resize(socket->pendingDatagramSize()); + const qint64 read = socket->readDatagram(message->data(), message->size(), + address, port); + QCOMPARE(socket->error(), QAbstractSocket::UnknownSocketError); + QVERIFY(read > 0); + + message->resize(read); + if (address) + QVERIFY(!address->isNull()); +} + +void tst_QDtlsCookie::stopLoopOnMessage() +{ + testLoop.exitLoop(); +} + +void tst_QDtlsCookie::serverReadyRead() +{ + Q_ASSERT(clientsToWait); + + if (!serverSocket.pendingDatagramSize()) + return; + + QByteArray hello; + QHostAddress clientAddress; + quint16 clientPort = 0; + + receiveMessage(&serverSocket, &hello, &clientAddress, &clientPort); + if (QTest::currentTestFailed()) + return testLoop.exitLoop(); + + const bool ok = listener.verifyClient(&serverSocket, hello, clientAddress, clientPort); + if (listener.dtlsError() != QDtlsError::NoError) { + // exit early, let the test fail. + return testLoop.exitLoop(); + } + + if (!ok) // not verified yet. + return; + + if (clientAddress == spammerAddress && clientPort == spammerPort) // should never happen + return testLoop.exitLoop(); + + --clientsToWait; + if (!clientsToWait) // done, success. + testLoop.exitLoop(); +} + +void tst_QDtlsCookie::clientReadyRead() +{ + QUdpSocket *clientSocket = qobject_cast(sender()); + Q_ASSERT(clientSocket); + + if (!clientSocket->pendingDatagramSize()) + return; + + QDtls *handshake = nullptr; + for (ValidClient &client : dtlsClients) { + if (client.first.data() == clientSocket) { + handshake = client.second.data(); + break; + } + } + + Q_ASSERT(handshake); + + QByteArray response; + receiveMessage(clientSocket, &response); + if (QTest::currentTestFailed() || !handshake->doHandshake(clientSocket, response)) + testLoop.exitLoop(); +} + +void tst_QDtlsCookie::makeNoise() +{ + noiseMaker.writeDatagram({"Hello, my little DTLS server, take this useless dgram!"}, + serverAddress, serverPort); + QDtls fakeHandshake(QSslSocket::SslClientMode); + fakeHandshake.setRemote(serverAddress, serverPort); + fakeHandshake.doHandshake(&noiseMaker, {}); +} + +void tst_QDtlsCookie::spawnClients() +{ + for (int i = 0; i < 10 && clientsToAdd; ++i, --clientsToAdd) { + ValidClient newClient; + newClient.first.reset(new QUdpSocket); + connect(newClient.first.data(), &QUdpSocket::readyRead, + this, &tst_QDtlsCookie::clientReadyRead); + newClient.second.reset(new QDtls(QSslSocket::SslClientMode)); + newClient.second->setRemote(serverAddress, serverPort); + connect(newClient.second.data(), &QDtls::handshakeTimeout, + this, &tst_QDtlsCookie::handleClientTimeout); + newClient.second->doHandshake(newClient.first.data(), {}); + dtlsClients.push_back(std::move(newClient)); + } +} + +void tst_QDtlsCookie::handleClientTimeout() +{ + QDtls *handshake = qobject_cast(sender()); + Q_ASSERT(handshake); + + QUdpSocket *clientSocket = nullptr; + for (ValidClient &client : dtlsClients) { + if (client.second.data() == handshake) { + clientSocket = client.first.data(); + break; + } + } + + Q_ASSERT(clientSocket); + handshake->handleTimeout(clientSocket); +} + +QT_END_NAMESPACE + +QTEST_MAIN(tst_QDtlsCookie) + +#include "tst_qdtlscookie.moc" diff --git a/tests/auto/network/ssl/ssl.pro b/tests/auto/network/ssl/ssl.pro index 5f2173044e..1729f046aa 100644 --- a/tests/auto/network/ssl/ssl.pro +++ b/tests/auto/network/ssl/ssl.pro @@ -1,5 +1,5 @@ TEMPLATE=subdirs -QT_FOR_CONFIG += network +QT_FOR_CONFIG += network-private SUBDIRS=\ qpassworddigestor \ @@ -14,7 +14,11 @@ qtConfig(ssl) { SUBDIRS += \ qsslsocket \ qsslsocket_onDemandCertificates_member \ - qsslsocket_onDemandCertificates_static \ + qsslsocket_onDemandCertificates_static + qtConfig(openssl) { + SUBDIRS += \ + qdtlscookie + } } }