1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2018 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the test suite of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | #include <QtTest/QtTest> |
30 | |
31 | #include <QtNetwork/qhostaddress.h> |
32 | #include <QtNetwork/qsslsocket.h> |
33 | #include <QtNetwork/qudpsocket.h> |
34 | #include <QtNetwork/qdtls.h> |
35 | |
36 | #include <QtCore/qcryptographichash.h> |
37 | #include <QtCore/qsharedpointer.h> |
38 | #include <QtCore/qbytearray.h> |
39 | #include <QtCore/qstring.h> |
40 | #include <QtCore/qobject.h> |
41 | #include <QtCore/qtimer.h> |
42 | #include <QtCore/qdebug.h> |
43 | |
44 | #include <utility> |
45 | #include <vector> |
46 | |
47 | QT_BEGIN_NAMESPACE |
48 | |
49 | #define STOP_ON_FAILURE \ |
50 | if (QTest::currentTestFailed()) \ |
51 | return; |
52 | |
53 | class tst_QDtlsCookie : public QObject |
54 | { |
55 | Q_OBJECT |
56 | |
57 | public slots: |
58 | void initTestCase(); |
59 | void init(); |
60 | |
61 | private slots: |
62 | // Tests: |
63 | void construction(); |
64 | void validateParameters_data(); |
65 | void validateParameters(); |
66 | void verifyClient(); |
67 | void cookieGeneratorParameters(); |
68 | void verifyMultipleClients(); |
69 | |
70 | protected slots: |
71 | // Aux. functions: |
72 | void stopLoopOnMessage(); |
73 | void serverReadyRead(); |
74 | void clientReadyRead(); |
75 | void handleClientTimeout(); |
76 | void makeNoise(); |
77 | void spawnClients(); |
78 | |
79 | private: |
80 | void sendClientHello(QUdpSocket *socket, QDtls *handshake, |
81 | const QByteArray &serverMessage = {}); |
82 | void receiveMessage(QUdpSocket *socket, QByteArray *message, |
83 | QHostAddress *address = nullptr, |
84 | quint16 *port = nullptr); |
85 | |
86 | static QHostAddress toNonAny(const QHostAddress &addr); |
87 | |
88 | enum AddressType |
89 | { |
90 | ValidAddress, |
91 | NullAddress, |
92 | BroadcastAddress, |
93 | MulticastAddress |
94 | }; |
95 | |
96 | QUdpSocket serverSocket; |
97 | QHostAddress serverAddress; |
98 | quint16 serverPort = 0; |
99 | |
100 | QTestEventLoop testLoop; |
101 | int handshakeTimeoutMS = 500; |
102 | |
103 | QDtlsClientVerifier listener; |
104 | using HandshakePtr = QSharedPointer<QDtls>; |
105 | HandshakePtr dtls; |
106 | |
107 | const QCryptographicHash::Algorithm defaultHash = |
108 | #ifdef QT_CRYPTOGRAPHICHASH_ONLY_SHA1 |
109 | QCryptographicHash::Sha1; |
110 | #else |
111 | QCryptographicHash::Sha256; |
112 | #endif |
113 | |
114 | using CookieParams = QDtlsClientVerifier::GeneratorParameters; |
115 | |
116 | QUdpSocket noiseMaker; |
117 | QHostAddress spammerAddress; |
118 | QTimer noiseTimer; |
119 | quint16 spammerPort = 0; |
120 | const int noiseTimeoutMS = 5; |
121 | |
122 | using SocketPtr = QSharedPointer<QUdpSocket>; |
123 | using ValidClient = QPair<SocketPtr, HandshakePtr>; |
124 | unsigned clientsToWait = 0; |
125 | unsigned clientsToAdd = 0; |
126 | std::vector<ValidClient> dtlsClients; |
127 | QTimer spawnTimer; |
128 | }; |
129 | |
130 | QHostAddress tst_QDtlsCookie::toNonAny(const QHostAddress &addr) |
131 | { |
132 | if (addr == QHostAddress::Any || addr == QHostAddress::AnyIPv4) |
133 | return QHostAddress::LocalHost; |
134 | if (addr == QHostAddress::AnyIPv6) |
135 | return QHostAddress::LocalHostIPv6; |
136 | return addr; |
137 | } |
138 | |
139 | void tst_QDtlsCookie::initTestCase() |
140 | { |
141 | QVERIFY(noiseMaker.bind()); |
142 | spammerAddress = toNonAny(addr: noiseMaker.localAddress()); |
143 | spammerPort = noiseMaker.localPort(); |
144 | } |
145 | |
146 | void tst_QDtlsCookie::init() |
147 | { |
148 | if (serverSocket.state() != QAbstractSocket::UnconnectedState) { |
149 | serverSocket.close(); |
150 | // Disconnect stopLoopOnMessage or serverReadyRead slots: |
151 | serverSocket.disconnect(); |
152 | } |
153 | |
154 | QCOMPARE(serverSocket.state(), QAbstractSocket::UnconnectedState); |
155 | QVERIFY(serverSocket.bind()); |
156 | |
157 | serverAddress = toNonAny(addr: serverSocket.localAddress()); |
158 | serverPort = serverSocket.localPort(); |
159 | |
160 | dtls.reset(t: new QDtls(QSslSocket::SslClientMode)); |
161 | dtls->setPeer(address: serverAddress, port: serverPort); |
162 | } |
163 | |
164 | void tst_QDtlsCookie::construction() |
165 | { |
166 | QDtlsClientVerifier verifier; |
167 | |
168 | QCOMPARE(verifier.dtlsError(), QDtlsError::NoError); |
169 | QCOMPARE(verifier.dtlsErrorString(), QString()); |
170 | QCOMPARE(verifier.verifiedHello(), QByteArray()); |
171 | |
172 | const auto params = verifier.cookieGeneratorParameters(); |
173 | QCOMPARE(params.hash, defaultHash); |
174 | QVERIFY(params.secret.size() > 0); |
175 | } |
176 | |
177 | void tst_QDtlsCookie::validateParameters_data() |
178 | { |
179 | QTest::addColumn<bool>(name: "invalidSocket" ); |
180 | QTest::addColumn<bool>(name: "emptyDatagram" ); |
181 | QTest::addColumn<int>(name: "addressType" ); |
182 | |
183 | QTest::addRow(format: "socket" ) << true << false << int(ValidAddress); |
184 | QTest::addRow(format: "dgram" ) << false << true << int(ValidAddress); |
185 | QTest::addRow(format: "addr(invalid)" ) << false << false << int(NullAddress); |
186 | QTest::addRow(format: "addr(broadcast)" ) << false << false << int(BroadcastAddress); |
187 | QTest::addRow(format: "addr(multicast)" ) << false << false << int(MulticastAddress); |
188 | |
189 | QTest::addRow(format: "socket-dgram" ) << true << true << int(ValidAddress); |
190 | QTest::addRow(format: "socket-dgram-addr(invalid)" ) << true << true << int(NullAddress); |
191 | QTest::addRow(format: "socket-dgram-addr(broadcast)" ) << true << true << int(BroadcastAddress); |
192 | QTest::addRow(format: "socket-dgram-addr(multicast)" ) << true << true << int(MulticastAddress); |
193 | |
194 | QTest::addRow(format: "dgram-addr(invalid)" ) << false << true << int(NullAddress); |
195 | QTest::addRow(format: "dgram-addr(broadcast)" ) << false << true << int(BroadcastAddress); |
196 | QTest::addRow(format: "dgram-addr(multicast)" ) << false << true << int(MulticastAddress); |
197 | |
198 | QTest::addRow(format: "socket-addr(invalid)" ) << true << false << int(NullAddress); |
199 | QTest::addRow(format: "socket-addr(broadcast)" ) << true << false << int(BroadcastAddress); |
200 | QTest::addRow(format: "socket-addr(multicast)" ) << true << false << int(MulticastAddress); |
201 | } |
202 | |
203 | void tst_QDtlsCookie::validateParameters() |
204 | { |
205 | connect(sender: &serverSocket, signal: &QUdpSocket::readyRead, receiver: this, |
206 | slot: &tst_QDtlsCookie::stopLoopOnMessage); |
207 | |
208 | QFETCH(const bool, invalidSocket); |
209 | QFETCH(const bool, emptyDatagram); |
210 | QFETCH(const int, addressType); |
211 | |
212 | QUdpSocket clientSocket; |
213 | QByteArray hello; |
214 | QHostAddress clientAddress; |
215 | quint16 clientPort = 0; |
216 | |
217 | sendClientHello(socket: &clientSocket, handshake: dtls.data()); |
218 | STOP_ON_FAILURE |
219 | receiveMessage(socket: &serverSocket, message: &hello, address: &clientAddress, port: &clientPort); |
220 | STOP_ON_FAILURE |
221 | |
222 | switch (addressType) { |
223 | case MulticastAddress: |
224 | clientAddress.setAddress(QStringLiteral("224.0.0.0" )); |
225 | break; |
226 | case BroadcastAddress: |
227 | clientAddress = QHostAddress::Broadcast; |
228 | break; |
229 | case NullAddress: |
230 | clientAddress = {}; |
231 | break; |
232 | } |
233 | |
234 | if (emptyDatagram) |
235 | hello.clear(); |
236 | |
237 | QUdpSocket *socket = invalidSocket ? nullptr : &serverSocket; |
238 | QCOMPARE(listener.verifyClient(socket, hello, clientAddress, clientPort), false); |
239 | QCOMPARE(listener.verifiedHello(), QByteArray()); |
240 | QCOMPARE(listener.dtlsError(), QDtlsError::InvalidInputParameters); |
241 | } |
242 | |
243 | void tst_QDtlsCookie::verifyClient() |
244 | { |
245 | connect(sender: &serverSocket, signal: &QUdpSocket::readyRead, receiver: this, |
246 | slot: &tst_QDtlsCookie::stopLoopOnMessage); |
247 | |
248 | QUdpSocket clientSocket; |
249 | connect(sender: &clientSocket, signal: &QUdpSocket::readyRead, receiver: this, |
250 | slot: &tst_QDtlsCookie::stopLoopOnMessage); |
251 | |
252 | // Client: send an initial ClientHello message without any cookie: |
253 | sendClientHello(socket: &clientSocket, handshake: dtls.data()); |
254 | STOP_ON_FAILURE |
255 | // Server: read the first ClientHello message: |
256 | QByteArray dgram; |
257 | QHostAddress clientAddress; |
258 | quint16 clientPort = 0; |
259 | receiveMessage(socket: &serverSocket, message: &dgram, address: &clientAddress, port: &clientPort); |
260 | STOP_ON_FAILURE |
261 | // Server: reply with a verify hello request (the client is not verified yet): |
262 | QCOMPARE(listener.verifyClient(&serverSocket, dgram, clientAddress, clientPort), false); |
263 | QCOMPARE(listener.verifiedHello(), QByteArray()); |
264 | QCOMPARE(listener.dtlsError(), QDtlsError::NoError); |
265 | // Client: read hello verify request: |
266 | receiveMessage(socket: &clientSocket, message: &dgram); |
267 | STOP_ON_FAILURE |
268 | // Client: send a new hello message, this time with a cookie attached: |
269 | sendClientHello(socket: &clientSocket, handshake: dtls.data(), serverMessage: dgram); |
270 | STOP_ON_FAILURE |
271 | // Server: read a client-verified message: |
272 | receiveMessage(socket: &serverSocket, message: &dgram, address: &clientAddress, port: &clientPort); |
273 | STOP_ON_FAILURE |
274 | // Client's readyRead is not interesting anymore: |
275 | clientSocket.close(); |
276 | |
277 | // Verify with the address and port we extracted, do it twice (DTLS "listen" |
278 | // must be stateless and work as many times as needed): |
279 | for (int i = 0; i < 2; ++i) { |
280 | QCOMPARE(listener.verifyClient(&serverSocket, dgram, clientAddress, clientPort), true); |
281 | QCOMPARE(listener.verifiedHello(), dgram); |
282 | QCOMPARE(listener.dtlsError(), QDtlsError::NoError); |
283 | } |
284 | |
285 | // Test that another freshly created (stateless) verifier can verify: |
286 | QDtlsClientVerifier anotherListener; |
287 | QCOMPARE(anotherListener.verifyClient(&serverSocket, dgram, clientAddress, |
288 | clientPort), true); |
289 | QCOMPARE(anotherListener.verifiedHello(), dgram); |
290 | QCOMPARE(anotherListener.dtlsError(), QDtlsError::NoError); |
291 | |
292 | // Now, let's test if a DTLS server is able to create a new TLS session |
293 | // re-using the client's 'Hello' with a cookie inside: |
294 | QDtls session(QSslSocket::SslServerMode); |
295 | auto dtlsConf = QSslConfiguration::defaultDtlsConfiguration(); |
296 | dtlsConf.setDtlsCookieVerificationEnabled(true); |
297 | session.setDtlsConfiguration(dtlsConf); |
298 | session.setPeer(address: clientAddress, port: clientPort); |
299 | // Borrow a secret and hash algorithm: |
300 | session.setCookieGeneratorParameters(listener.cookieGeneratorParameters()); |
301 | // Trigger TLS state machine change to think it accepted a cookie and started |
302 | // a handshake: |
303 | QVERIFY(session.doHandshake(&serverSocket, dgram)); |
304 | |
305 | // Now let's use a wrong port: |
306 | QCOMPARE(listener.verifyClient(&serverSocket, dgram, clientAddress, serverPort), false); |
307 | // Invalid cookie, no verified hello message: |
308 | QCOMPARE(listener.verifiedHello(), QByteArray()); |
309 | // But it's UDP so we ignore this "fishy datagram", no error expected: |
310 | QCOMPARE(listener.dtlsError(), QDtlsError::NoError); |
311 | } |
312 | |
313 | void tst_QDtlsCookie::cookieGeneratorParameters() |
314 | { |
315 | CookieParams params;// By defualt, 'secret' is empty. |
316 | QCOMPARE(listener.setCookieGeneratorParameters(params), false); |
317 | QCOMPARE(listener.dtlsError(), QDtlsError::InvalidInputParameters); |
318 | params.secret = "abcdefghijklmnopqrstuvwxyz" ; |
319 | QCOMPARE(listener.setCookieGeneratorParameters(params), true); |
320 | QCOMPARE(listener.dtlsError(), QDtlsError::NoError); |
321 | } |
322 | |
323 | void tst_QDtlsCookie::verifyMultipleClients() |
324 | { |
325 | // 'verifyClient' above was quite simple - it's like working with blocking |
326 | // sockets, step by step - we write, then make sure we read a datagram back |
327 | // etc. This test is more asynchronous - we are running an event loop and don't |
328 | // stop on the first datagram received, instead, we spawn many clients |
329 | // with which to exchange handshake messages and verify requests, while at |
330 | // the same time dealing with a 'noise maker' - a fake DTLS client, who keeps |
331 | // spamming our server with non-DTLS datagrams and initial ClientHello |
332 | // messages, but never replies to client verify requests. |
333 | connect(sender: &serverSocket, signal: &QUdpSocket::readyRead, receiver: this, slot: &tst_QDtlsCookie::serverReadyRead); |
334 | |
335 | noiseTimer.setInterval(noiseTimeoutMS); |
336 | connect(sender: &noiseTimer, signal: &QTimer::timeout, receiver: this, slot: &tst_QDtlsCookie::makeNoise); |
337 | |
338 | spawnTimer.setInterval(noiseTimeoutMS * 10); |
339 | connect(sender: &spawnTimer, signal: &QTimer::timeout, receiver: this, slot: &tst_QDtlsCookie::spawnClients); |
340 | |
341 | noiseTimer.start(); |
342 | spawnTimer.start(); |
343 | |
344 | clientsToAdd = clientsToWait = 100; |
345 | |
346 | testLoop.enterLoopMSecs(ms: handshakeTimeoutMS * clientsToWait); |
347 | QVERIFY(!testLoop.timeout()); |
348 | QVERIFY(clientsToWait == 0); |
349 | } |
350 | |
351 | void tst_QDtlsCookie::sendClientHello(QUdpSocket *socket, QDtls *dtls, |
352 | const QByteArray &serverMessage) |
353 | { |
354 | Q_ASSERT(socket && dtls); |
355 | dtls->doHandshake(socket, dgram: serverMessage); |
356 | // We don't really care about QDtls in this auto-test, but must be |
357 | // sure that we, indeed, sent our hello and if not - stop early without |
358 | // running event loop: |
359 | QCOMPARE(dtls->dtlsError(), QDtlsError::NoError); |
360 | // We never complete a handshake, so it must be 'HandshakeInProgress': |
361 | QCOMPARE(dtls->handshakeState(), QDtls::HandshakeInProgress); |
362 | } |
363 | |
364 | void tst_QDtlsCookie::receiveMessage(QUdpSocket *socket, QByteArray *message, |
365 | QHostAddress *address, quint16 *port) |
366 | { |
367 | Q_ASSERT(socket && message); |
368 | |
369 | if (socket->pendingDatagramSize() <= 0) |
370 | testLoop.enterLoopMSecs(ms: handshakeTimeoutMS); |
371 | |
372 | QVERIFY(!testLoop.timeout()); |
373 | QVERIFY(socket->pendingDatagramSize()); |
374 | |
375 | message->resize(size: socket->pendingDatagramSize()); |
376 | const qint64 read = socket->readDatagram(data: message->data(), maxlen: message->size(), |
377 | host: address, port); |
378 | QVERIFY(read > 0); |
379 | |
380 | message->resize(size: read); |
381 | if (address) |
382 | QVERIFY(!address->isNull()); |
383 | } |
384 | |
385 | void tst_QDtlsCookie::stopLoopOnMessage() |
386 | { |
387 | testLoop.exitLoop(); |
388 | } |
389 | |
390 | void tst_QDtlsCookie::serverReadyRead() |
391 | { |
392 | Q_ASSERT(clientsToWait); |
393 | |
394 | if (serverSocket.pendingDatagramSize() <= 0) |
395 | return; |
396 | |
397 | QByteArray hello; |
398 | QHostAddress clientAddress; |
399 | quint16 clientPort = 0; |
400 | |
401 | receiveMessage(socket: &serverSocket, message: &hello, address: &clientAddress, port: &clientPort); |
402 | if (QTest::currentTestFailed()) |
403 | return testLoop.exitLoop(); |
404 | |
405 | const bool ok = listener.verifyClient(socket: &serverSocket, dgram: hello, address: clientAddress, port: clientPort); |
406 | if (listener.dtlsError() != QDtlsError::NoError) { |
407 | // exit early, let the test fail. |
408 | return testLoop.exitLoop(); |
409 | } |
410 | |
411 | if (!ok) // not verified yet. |
412 | return; |
413 | |
414 | if (clientAddress == spammerAddress && clientPort == spammerPort) // should never happen |
415 | return testLoop.exitLoop(); |
416 | |
417 | --clientsToWait; |
418 | if (!clientsToWait) // done, success. |
419 | testLoop.exitLoop(); |
420 | } |
421 | |
422 | void tst_QDtlsCookie::clientReadyRead() |
423 | { |
424 | QUdpSocket *clientSocket = qobject_cast<QUdpSocket *>(object: sender()); |
425 | Q_ASSERT(clientSocket); |
426 | |
427 | if (clientSocket->pendingDatagramSize() <= 0) |
428 | return; |
429 | |
430 | QDtls *handshake = nullptr; |
431 | for (ValidClient &client : dtlsClients) { |
432 | if (client.first.data() == clientSocket) { |
433 | handshake = client.second.data(); |
434 | break; |
435 | } |
436 | } |
437 | |
438 | Q_ASSERT(handshake); |
439 | |
440 | QByteArray response; |
441 | receiveMessage(socket: clientSocket, message: &response); |
442 | if (QTest::currentTestFailed() || !handshake->doHandshake(socket: clientSocket, dgram: response)) |
443 | testLoop.exitLoop(); |
444 | } |
445 | |
446 | void tst_QDtlsCookie::makeNoise() |
447 | { |
448 | noiseMaker.writeDatagram(datagram: {"Hello, my little DTLS server, take this useless dgram!" }, |
449 | host: serverAddress, port: serverPort); |
450 | QDtls fakeHandshake(QSslSocket::SslClientMode); |
451 | fakeHandshake.setPeer(address: serverAddress, port: serverPort); |
452 | fakeHandshake.doHandshake(socket: &noiseMaker, dgram: {}); |
453 | } |
454 | |
455 | void tst_QDtlsCookie::spawnClients() |
456 | { |
457 | for (int i = 0; i < 10 && clientsToAdd; ++i, --clientsToAdd) { |
458 | ValidClient newClient; |
459 | newClient.first.reset(t: new QUdpSocket); |
460 | connect(sender: newClient.first.data(), signal: &QUdpSocket::readyRead, |
461 | receiver: this, slot: &tst_QDtlsCookie::clientReadyRead); |
462 | newClient.second.reset(t: new QDtls(QSslSocket::SslClientMode)); |
463 | newClient.second->setPeer(address: serverAddress, port: serverPort); |
464 | connect(sender: newClient.second.data(), signal: &QDtls::handshakeTimeout, |
465 | receiver: this, slot: &tst_QDtlsCookie::handleClientTimeout); |
466 | newClient.second->doHandshake(socket: newClient.first.data(), dgram: {}); |
467 | dtlsClients.push_back(x: std::move(newClient)); |
468 | } |
469 | } |
470 | |
471 | void tst_QDtlsCookie::handleClientTimeout() |
472 | { |
473 | QDtls *handshake = qobject_cast<QDtls *>(object: sender()); |
474 | Q_ASSERT(handshake); |
475 | |
476 | QUdpSocket *clientSocket = nullptr; |
477 | for (ValidClient &client : dtlsClients) { |
478 | if (client.second.data() == handshake) { |
479 | clientSocket = client.first.data(); |
480 | break; |
481 | } |
482 | } |
483 | |
484 | Q_ASSERT(clientSocket); |
485 | handshake->handleTimeout(socket: clientSocket); |
486 | } |
487 | |
488 | QT_END_NAMESPACE |
489 | |
490 | QTEST_MAIN(tst_QDtlsCookie) |
491 | |
492 | #include "tst_qdtlscookie.moc" |
493 | |