QDtlsClientVerifier - add auto-test

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 <marten.nordheim@qt.io>
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
This commit is contained in:
Timur Pocheptsov 2018-04-16 13:03:38 +02:00
parent ac583b686d
commit d77d4fc548
3 changed files with 502 additions and 2 deletions

View File

@ -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
}
}

View File

@ -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 <QtTest/QtTest>
#include <QtNetwork/qhostaddress.h>
#include <QtNetwork/qsslsocket.h>
#include <QtNetwork/qudpsocket.h>
#include <QtNetwork/qdtls.h>
#include <QtCore/qcryptographichash.h>
#include <QtCore/qsharedpointer.h>
#include <QtCore/qbytearray.h>
#include <QtCore/qstring.h>
#include <QtCore/qobject.h>
#include <QtCore/qtimer.h>
#include <QtCore/qdebug.h>
#include <utility>
#include <vector>
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<QDtls>;
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<QUdpSocket>;
using ValidClient = QPair<SocketPtr, HandshakePtr>;
unsigned clientsToWait = 0;
unsigned clientsToAdd = 0;
std::vector<ValidClient> 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<bool>("invalidSocket");
QTest::addColumn<bool>("emptyDatagram");
QTest::addColumn<int>("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<QUdpSocket *>(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<QDtls *>(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"

View File

@ -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
}
}
}