ab6affac10
Change-Id: I256a4b0bac4d2f5eb170967fedc4eccaec8c7f2a
1320 lines
50 KiB
C++
1320 lines
50 KiB
C++
/****************************************************************************
|
|
**
|
|
** 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/qsslpresharedkeyauthenticator.h>
|
|
#include <QtNetwork/qsslconfiguration.h>
|
|
#include <QtNetwork/qhostaddress.h>
|
|
#include <QtNetwork/qsslsocket.h>
|
|
#include <QtNetwork/qsslcipher.h>
|
|
#include <QtNetwork/qudpsocket.h>
|
|
#include <QtNetwork/qsslerror.h>
|
|
#include <QtNetwork/qsslkey.h>
|
|
#include <QtNetwork/qdtls.h>
|
|
#include <QtNetwork/qssl.h>
|
|
|
|
#include <QtCore/qcryptographichash.h>
|
|
#include <QtCore/qbytearray.h>
|
|
#include <QtCore/qvector.h>
|
|
#include <QtCore/qstring.h>
|
|
#include <QtCore/qobject.h>
|
|
|
|
#include <algorithm>
|
|
|
|
QT_BEGIN_NAMESPACE
|
|
|
|
namespace
|
|
{
|
|
|
|
bool dtlsErrorIsCleared(const QDtls &dtls)
|
|
{
|
|
return dtls.dtlsError() == QDtlsError::NoError && dtls.dtlsErrorString().isEmpty();
|
|
}
|
|
|
|
using DtlsPtr = QScopedPointer<QDtls>;
|
|
|
|
bool dtlsErrorIsCleared(DtlsPtr &dtls)
|
|
{
|
|
return dtlsErrorIsCleared(*dtls);
|
|
}
|
|
|
|
} // unnamed namespace
|
|
|
|
#define QDTLS_VERIFY_NO_ERROR(obj) QVERIFY(dtlsErrorIsCleared(obj))
|
|
|
|
#define QDTLS_VERIFY_HANDSHAKE_SUCCESS(obj) \
|
|
QVERIFY(obj->isConnectionEncrypted()); \
|
|
QCOMPARE(obj->handshakeState(), QDtls::HandshakeComplete); \
|
|
QDTLS_VERIFY_NO_ERROR(obj); \
|
|
QCOMPARE(obj->peerVerificationErrors().size(), 0)
|
|
|
|
class tst_QDtls : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
public slots:
|
|
void initTestCase();
|
|
void init();
|
|
|
|
private slots:
|
|
// Tests:
|
|
void construction_data();
|
|
void construction();
|
|
void configuration_data();
|
|
void configuration();
|
|
void invalidConfiguration();
|
|
void setPeer_data();
|
|
void setPeer();
|
|
void handshake_data();
|
|
void handshake();
|
|
void handshakeWithRetransmission();
|
|
void sessionCipher();
|
|
void cipherPreferences_data();
|
|
void cipherPreferences();
|
|
void protocolVersionMatching_data();
|
|
void protocolVersionMatching();
|
|
void verificationErrors_data();
|
|
void verificationErrors();
|
|
void presetExpectedErrors_data();
|
|
void presetExpectedErrors();
|
|
void verifyServerCertificate_data();
|
|
void verifyServerCertificate();
|
|
void verifyClientCertificate_data();
|
|
void verifyClientCertificate();
|
|
void blacklistedCerificate();
|
|
void readWriteEncrypted_data();
|
|
void readWriteEncrypted();
|
|
void datagramFragmentation();
|
|
|
|
protected slots:
|
|
void handshakeReadyRead();
|
|
void encryptedReadyRead();
|
|
void pskRequested(QSslPreSharedKeyAuthenticator *auth);
|
|
void handleHandshakeTimeout();
|
|
|
|
private:
|
|
void clientServerData();
|
|
void connectHandshakeReadingSlots();
|
|
void connectEncryptedReadingSlots();
|
|
bool verificationErrorDetected(QSslError::SslError code) const;
|
|
|
|
static QHostAddress toNonAny(const QHostAddress &addr);
|
|
|
|
QUdpSocket serverSocket;
|
|
QHostAddress serverAddress;
|
|
quint16 serverPort = 0;
|
|
QSslConfiguration defaultServerConfig;
|
|
QSslCertificate selfSignedCert;
|
|
QString hostName;
|
|
QSslKey serverKeySS;
|
|
bool serverDropDgram = false;
|
|
const QByteArray serverExpectedPlainText = "Hello W ... hmm, I mean DTLS server!";
|
|
QByteArray serverReceivedPlainText;
|
|
|
|
QUdpSocket clientSocket;
|
|
QHostAddress clientAddress;
|
|
quint16 clientPort = 0;
|
|
bool clientDropDgram = false;
|
|
const QByteArray clientExpectedPlainText = "Hello DTLS client.";
|
|
QByteArray clientReceivedPlainText;
|
|
|
|
DtlsPtr serverCrypto;
|
|
DtlsPtr clientCrypto;
|
|
|
|
QTestEventLoop testLoop;
|
|
const int handshakeTimeoutMS = 5000;
|
|
const int dataExchangeTimeoutMS = 1000;
|
|
|
|
const QByteArray presharedKey = "DEADBEEFDEADBEEF";
|
|
QString certDirPath;
|
|
};
|
|
|
|
QT_END_NAMESPACE
|
|
|
|
Q_DECLARE_METATYPE(QSsl::SslProtocol)
|
|
Q_DECLARE_METATYPE(QSslSocket::SslMode)
|
|
Q_DECLARE_METATYPE(QSslSocket::PeerVerifyMode)
|
|
Q_DECLARE_METATYPE(QList<QSslCertificate>)
|
|
Q_DECLARE_METATYPE(QSslKey)
|
|
|
|
QT_BEGIN_NAMESPACE
|
|
|
|
void tst_QDtls::initTestCase()
|
|
{
|
|
certDirPath = QFileInfo(QFINDTESTDATA("certs")).absolutePath();
|
|
QVERIFY(certDirPath.size() > 0);
|
|
certDirPath += QDir::separator() + QStringLiteral("certs") + QDir::separator();
|
|
|
|
QVERIFY(QSslSocket::supportsSsl());
|
|
|
|
QFile keyFile(certDirPath + QStringLiteral("ss-srv-key.pem"));
|
|
QVERIFY(keyFile.open(QIODevice::ReadOnly));
|
|
serverKeySS = QSslKey(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, "foobar");
|
|
QVERIFY(!serverKeySS.isNull());
|
|
|
|
QList<QSslCertificate> certificates = QSslCertificate::fromPath(certDirPath + QStringLiteral("ss-srv-cert.pem"));
|
|
QVERIFY(!certificates.isEmpty());
|
|
QVERIFY(!certificates.first().isNull());
|
|
selfSignedCert = certificates.first();
|
|
|
|
defaultServerConfig = QSslConfiguration::defaultDtlsConfiguration();
|
|
defaultServerConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
|
|
defaultServerConfig.setDtlsCookieVerificationEnabled(false);
|
|
|
|
hostName = QStringLiteral("bob.org");
|
|
}
|
|
|
|
void tst_QDtls::init()
|
|
{
|
|
if (serverSocket.state() != QAbstractSocket::UnconnectedState) {
|
|
serverSocket.close();
|
|
// disconnect signals/slots:
|
|
serverSocket.disconnect();
|
|
}
|
|
|
|
QVERIFY(serverSocket.bind());
|
|
serverAddress = toNonAny(serverSocket.localAddress());
|
|
serverPort = serverSocket.localPort();
|
|
|
|
if (clientSocket.localPort()) {
|
|
clientSocket.close();
|
|
// disconnect signals/slots:
|
|
clientSocket.disconnect();
|
|
}
|
|
|
|
clientAddress = {};
|
|
clientPort = 0;
|
|
|
|
serverCrypto.reset(new QDtls(QSslSocket::SslServerMode));
|
|
serverDropDgram = false;
|
|
serverReceivedPlainText.clear();
|
|
|
|
clientCrypto.reset(new QDtls(QSslSocket::SslClientMode));
|
|
clientDropDgram = false;
|
|
clientReceivedPlainText.clear();
|
|
|
|
connect(clientCrypto.data(), &QDtls::handshakeTimeout,
|
|
this, &tst_QDtls::handleHandshakeTimeout);
|
|
connect(serverCrypto.data(), &QDtls::handshakeTimeout,
|
|
this, &tst_QDtls::handleHandshakeTimeout);
|
|
}
|
|
|
|
void tst_QDtls::construction_data()
|
|
{
|
|
clientServerData();
|
|
}
|
|
|
|
void tst_QDtls::construction()
|
|
{
|
|
QFETCH(const QSslSocket::SslMode, mode);
|
|
|
|
QDtls dtls(mode);
|
|
QCOMPARE(dtls.peerAddress(), QHostAddress());
|
|
QCOMPARE(dtls.peerPort(), quint16());
|
|
QCOMPARE(dtls.peerVerificationName(), QString());
|
|
QCOMPARE(dtls.sslMode(), mode);
|
|
|
|
QCOMPARE(dtls.mtuHint(), quint16());
|
|
|
|
const auto params = dtls.cookieGeneratorParameters();
|
|
QVERIFY(params.secret.size() > 0);
|
|
#ifdef QT_CRYPTOGRAPHICHASH_ONLY_SHA1
|
|
QCOMPARE(params.hash, QCryptographicHash::Sha1);
|
|
#else
|
|
QCOMPARE(params.hash, QCryptographicHash::Sha256);
|
|
#endif
|
|
|
|
QCOMPARE(dtls.dtlsConfiguration(), QSslConfiguration::defaultDtlsConfiguration());
|
|
|
|
QCOMPARE(dtls.handshakeState(), QDtls::HandshakeNotStarted);
|
|
QCOMPARE(dtls.isConnectionEncrypted(), false);
|
|
QCOMPARE(dtls.sessionCipher(), QSslCipher());
|
|
QCOMPARE(dtls.sessionProtocol(), QSsl::UnknownProtocol);
|
|
|
|
QCOMPARE(dtls.dtlsError(), QDtlsError::NoError);
|
|
QCOMPARE(dtls.dtlsErrorString(), QString());
|
|
QCOMPARE(dtls.peerVerificationErrors().size(), 0);
|
|
}
|
|
|
|
void tst_QDtls::configuration_data()
|
|
{
|
|
clientServerData();
|
|
}
|
|
|
|
void tst_QDtls::configuration()
|
|
{
|
|
// There is a proper auto-test for QSslConfiguration in our TLS test suite,
|
|
// here we only test several DTLS-related details.
|
|
auto config = QSslConfiguration::defaultDtlsConfiguration();
|
|
QCOMPARE(config.protocol(), QSsl::DtlsV1_2OrLater);
|
|
|
|
const QList<QSslCipher> ciphers = config.ciphers();
|
|
QVERIFY(ciphers.size() > 0);
|
|
for (const auto &cipher : ciphers)
|
|
QVERIFY(cipher.usedBits() >= 128);
|
|
|
|
QCOMPARE(config.dtlsCookieVerificationEnabled(), true);
|
|
|
|
QFETCH(const QSslSocket::SslMode, mode);
|
|
QDtls dtls(mode);
|
|
QCOMPARE(dtls.dtlsConfiguration(), config);
|
|
config.setProtocol(QSsl::DtlsV1_0OrLater);
|
|
config.setDtlsCookieVerificationEnabled(false);
|
|
QCOMPARE(config.dtlsCookieVerificationEnabled(), false);
|
|
|
|
QVERIFY(dtls.setDtlsConfiguration(config));
|
|
QDTLS_VERIFY_NO_ERROR(dtls);
|
|
QCOMPARE(dtls.dtlsConfiguration(), config);
|
|
|
|
if (mode == QSslSocket::SslClientMode) {
|
|
// Testing a DTLS server would be more complicated, we'd need a DTLS
|
|
// client sending ClientHello(s), running an event loop etc. - way too
|
|
// much dancing for a simple setter/getter test.
|
|
QVERIFY(dtls.setPeer(serverAddress, serverPort));
|
|
QDTLS_VERIFY_NO_ERROR(dtls);
|
|
|
|
QUdpSocket clientSocket;
|
|
QVERIFY(dtls.doHandshake(&clientSocket));
|
|
QDTLS_VERIFY_NO_ERROR(dtls);
|
|
QCOMPARE(dtls.handshakeState(), QDtls::HandshakeInProgress);
|
|
// As soon as handshake started, it's not allowed to change configuration:
|
|
QVERIFY(!dtls.setDtlsConfiguration(QSslConfiguration::defaultDtlsConfiguration()));
|
|
QCOMPARE(dtls.dtlsError(), QDtlsError::InvalidOperation);
|
|
QCOMPARE(dtls.dtlsConfiguration(), config);
|
|
}
|
|
}
|
|
|
|
void tst_QDtls::invalidConfiguration()
|
|
{
|
|
QUdpSocket socket;
|
|
QDtls crypto(QSslSocket::SslClientMode);
|
|
QVERIFY(crypto.setPeer(serverAddress, serverPort));
|
|
// Note: not defaultDtlsConfiguration(), so the protocol is TLS (without D):
|
|
QVERIFY(crypto.setDtlsConfiguration(QSslConfiguration::defaultConfiguration()));
|
|
QDTLS_VERIFY_NO_ERROR(crypto);
|
|
QCOMPARE(crypto.dtlsConfiguration(), QSslConfiguration::defaultConfiguration());
|
|
// Try to start the handshake:
|
|
QCOMPARE(crypto.doHandshake(&socket), false);
|
|
QCOMPARE(crypto.dtlsError(), QDtlsError::TlsInitializationError);
|
|
}
|
|
|
|
void tst_QDtls::setPeer_data()
|
|
{
|
|
clientServerData();
|
|
}
|
|
|
|
void tst_QDtls::setPeer()
|
|
{
|
|
static const QHostAddress invalid[] = {QHostAddress(),
|
|
QHostAddress(QHostAddress::Broadcast),
|
|
QHostAddress(QStringLiteral("224.0.0.0"))};
|
|
static const QString peerName = QStringLiteral("does not matter actually");
|
|
|
|
QFETCH(const QSslSocket::SslMode, mode);
|
|
QDtls dtls(mode);
|
|
|
|
for (const auto &addr : invalid) {
|
|
QCOMPARE(dtls.setPeer(addr, 100, peerName), false);
|
|
QCOMPARE(dtls.dtlsError(), QDtlsError::InvalidInputParameters);
|
|
QCOMPARE(dtls.peerAddress(), QHostAddress());
|
|
QCOMPARE(dtls.peerPort(), quint16());
|
|
QCOMPARE(dtls.peerVerificationName(), QString());
|
|
}
|
|
|
|
QVERIFY(dtls.setPeer(serverAddress, serverPort, peerName));
|
|
QDTLS_VERIFY_NO_ERROR(dtls);
|
|
QCOMPARE(dtls.peerAddress(), serverAddress);
|
|
QCOMPARE(dtls.peerPort(), serverPort);
|
|
QCOMPARE(dtls.peerVerificationName(), peerName);
|
|
|
|
if (mode == QSslSocket::SslClientMode) {
|
|
// We test for client mode only, for server mode we'd have to run event
|
|
// loop etc. too much work for a simple setter/getter test.
|
|
QUdpSocket clientSocket;
|
|
QVERIFY(dtls.doHandshake(&clientSocket));
|
|
QDTLS_VERIFY_NO_ERROR(dtls);
|
|
QCOMPARE(dtls.handshakeState(), QDtls::HandshakeInProgress);
|
|
QCOMPARE(dtls.setPeer(serverAddress, serverPort), false);
|
|
QCOMPARE(dtls.dtlsError(), QDtlsError::InvalidOperation);
|
|
}
|
|
}
|
|
|
|
void tst_QDtls::handshake_data()
|
|
{
|
|
QTest::addColumn<bool>("withCertificate");
|
|
|
|
QTest::addRow("no-cert") << false;
|
|
QTest::addRow("with-cert") << true;
|
|
}
|
|
|
|
void tst_QDtls::handshake()
|
|
{
|
|
connectHandshakeReadingSlots();
|
|
|
|
QFETCH(const bool, withCertificate);
|
|
|
|
auto serverConfig = defaultServerConfig;
|
|
auto clientConfig = QSslConfiguration::defaultDtlsConfiguration();
|
|
|
|
if (!withCertificate) {
|
|
connect(serverCrypto.data(), &QDtls::pskRequired, this, &tst_QDtls::pskRequested);
|
|
connect(clientCrypto.data(), &QDtls::pskRequired, this, &tst_QDtls::pskRequested);
|
|
clientConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
|
|
QVERIFY(clientConfig.peerCertificate().isNull());
|
|
} else {
|
|
serverConfig.setPrivateKey(serverKeySS);
|
|
serverConfig.setLocalCertificate(selfSignedCert);
|
|
clientConfig.setCaCertificates({selfSignedCert});
|
|
}
|
|
|
|
QVERIFY(serverCrypto->setDtlsConfiguration(serverConfig));
|
|
QVERIFY(clientCrypto->setDtlsConfiguration(clientConfig));
|
|
|
|
// Some early checks before we run event loop.
|
|
// Remote was not set yet:
|
|
QVERIFY(!clientCrypto->doHandshake(&clientSocket));
|
|
QCOMPARE(clientCrypto->dtlsError(), QDtlsError::InvalidOperation);
|
|
QVERIFY(!serverCrypto->doHandshake(&serverSocket, QByteArray("ClientHello")));
|
|
QCOMPARE(serverCrypto->dtlsError(), QDtlsError::InvalidOperation);
|
|
|
|
QVERIFY(clientCrypto->setPeer(serverAddress, serverPort, hostName));
|
|
|
|
// Invalid socket:
|
|
QVERIFY(!clientCrypto->doHandshake(nullptr));
|
|
QCOMPARE(clientCrypto->dtlsError(), QDtlsError::InvalidInputParameters);
|
|
|
|
// Now we are ready for handshake:
|
|
QVERIFY(clientCrypto->doHandshake(&clientSocket));
|
|
QDTLS_VERIFY_NO_ERROR(clientCrypto);
|
|
QCOMPARE(clientCrypto->handshakeState(), QDtls::HandshakeInProgress);
|
|
|
|
testLoop.enterLoopMSecs(handshakeTimeoutMS);
|
|
|
|
QVERIFY(!testLoop.timeout());
|
|
|
|
QVERIFY(serverCrypto->isConnectionEncrypted());
|
|
QDTLS_VERIFY_NO_ERROR(serverCrypto);
|
|
QCOMPARE(serverCrypto->handshakeState(), QDtls::HandshakeComplete);
|
|
QCOMPARE(serverCrypto->peerVerificationErrors().size(), 0);
|
|
|
|
QVERIFY(clientCrypto->isConnectionEncrypted());
|
|
QDTLS_VERIFY_NO_ERROR(clientCrypto);
|
|
QCOMPARE(clientCrypto->handshakeState(), QDtls::HandshakeComplete);
|
|
QCOMPARE(clientCrypto->peerVerificationErrors().size(), 0);
|
|
|
|
if (withCertificate) {
|
|
const auto serverCert = clientCrypto->dtlsConfiguration().peerCertificate();
|
|
QVERIFY(!serverCert.isNull());
|
|
QCOMPARE(serverCert, selfSignedCert);
|
|
}
|
|
|
|
// Already in 'HandshakeComplete' state/encrypted.
|
|
QVERIFY(!clientCrypto->doHandshake(&clientSocket));
|
|
QCOMPARE(clientCrypto->dtlsError(), QDtlsError::InvalidOperation);
|
|
QVERIFY(!serverCrypto->doHandshake(&serverSocket, {"ServerHello"}));
|
|
QCOMPARE(serverCrypto->dtlsError(), QDtlsError::InvalidOperation);
|
|
// Cannot change a remote without calling shutdown first.
|
|
QVERIFY(!clientCrypto->setPeer(serverAddress, serverPort));
|
|
QCOMPARE(clientCrypto->dtlsError(), QDtlsError::InvalidOperation);
|
|
QVERIFY(!serverCrypto->setPeer(clientAddress, clientPort));
|
|
QCOMPARE(serverCrypto->dtlsError(), QDtlsError::InvalidOperation);
|
|
}
|
|
|
|
void tst_QDtls::handshakeWithRetransmission()
|
|
{
|
|
connectHandshakeReadingSlots();
|
|
|
|
auto serverConfig = defaultServerConfig;
|
|
serverConfig.setPrivateKey(serverKeySS);
|
|
serverConfig.setLocalCertificate(selfSignedCert);
|
|
QVERIFY(serverCrypto->setDtlsConfiguration(serverConfig));
|
|
|
|
auto clientConfig = QSslConfiguration::defaultDtlsConfiguration();
|
|
clientConfig.setCaCertificates({selfSignedCert});
|
|
QVERIFY(clientCrypto->setDtlsConfiguration(clientConfig));
|
|
QVERIFY(clientCrypto->setPeer(serverAddress, serverPort, hostName));
|
|
|
|
// Now we are ready for handshake:
|
|
QVERIFY(clientCrypto->doHandshake(&clientSocket));
|
|
QDTLS_VERIFY_NO_ERROR(clientCrypto);
|
|
QCOMPARE(clientCrypto->handshakeState(), QDtls::HandshakeInProgress);
|
|
|
|
serverDropDgram = true;
|
|
clientDropDgram = true;
|
|
// Every failed re-transmission doubles the next timeout. We don't want to
|
|
// slow down the test just to check the re-transmission ability, so we'll
|
|
// drop only the first 'ClientHello' and 'ServerHello' datagrams. The
|
|
// arithmetic is approximately this: the first ClientHello to be dropped -
|
|
// client will re-transmit in 1s., the first part of 'ServerHello' to be
|
|
// dropped, the client then will re-transmit after another 2 s. Thus it's ~3.
|
|
// We err on safe side and double our (already quite generous) 5s.
|
|
testLoop.enterLoopMSecs(handshakeTimeoutMS * 2);
|
|
|
|
QVERIFY(!testLoop.timeout());
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(serverCrypto);
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(clientCrypto);
|
|
}
|
|
|
|
void tst_QDtls::sessionCipher()
|
|
{
|
|
connectHandshakeReadingSlots();
|
|
|
|
auto serverConfig = defaultServerConfig;
|
|
serverConfig.setPrivateKey(serverKeySS);
|
|
serverConfig.setLocalCertificate(selfSignedCert);
|
|
QVERIFY(serverCrypto->setDtlsConfiguration(serverConfig));
|
|
|
|
auto clientConfig = QSslConfiguration::defaultDtlsConfiguration();
|
|
clientConfig.setCaCertificates({selfSignedCert});
|
|
QVERIFY(clientCrypto->setDtlsConfiguration(clientConfig));
|
|
|
|
QVERIFY(clientCrypto->setPeer(serverAddress, serverPort, hostName));
|
|
QVERIFY(clientCrypto->doHandshake(&clientSocket));
|
|
|
|
testLoop.enterLoopMSecs(handshakeTimeoutMS);
|
|
|
|
QVERIFY(!testLoop.timeout());
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(clientCrypto);
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(serverCrypto);
|
|
|
|
const auto defaultDtlsConfig = QSslConfiguration::defaultDtlsConfiguration();
|
|
|
|
const auto clCipher = clientCrypto->sessionCipher();
|
|
QVERIFY(!clCipher.isNull());
|
|
QVERIFY(defaultDtlsConfig.ciphers().contains(clCipher));
|
|
|
|
const auto srvCipher = serverCrypto->sessionCipher();
|
|
QVERIFY(!srvCipher.isNull());
|
|
QVERIFY(defaultDtlsConfig.ciphers().contains(srvCipher));
|
|
|
|
QCOMPARE(clCipher, srvCipher);
|
|
}
|
|
|
|
void tst_QDtls::cipherPreferences_data()
|
|
{
|
|
QTest::addColumn<bool>("preferClient");
|
|
|
|
QTest::addRow("prefer-server") << true;
|
|
QTest::addRow("prefer-client") << false;
|
|
}
|
|
|
|
void tst_QDtls::cipherPreferences()
|
|
{
|
|
// This test is based on the similar case in tst_QSslSocket. We test it for QDtls
|
|
// because it's possible to set ciphers and corresponding ('server preferred')
|
|
// options via QSslConfiguration.
|
|
const QSslCipher aes128(QStringLiteral("AES128-SHA"));
|
|
const QSslCipher aes256(QStringLiteral("AES256-SHA"));
|
|
|
|
auto serverConfig = defaultServerConfig;
|
|
const QList<QSslCipher> ciphers = serverConfig.ciphers();
|
|
if (!ciphers.contains(aes128) || !ciphers.contains(aes256))
|
|
QSKIP("The ciphers needed by this test were not found in the default DTLS configuration");
|
|
|
|
serverConfig.setCiphers({aes128, aes256});
|
|
serverConfig.setLocalCertificate(selfSignedCert);
|
|
serverConfig.setPrivateKey(serverKeySS);
|
|
|
|
QFETCH(const bool, preferClient);
|
|
if (preferClient)
|
|
serverConfig.setSslOption(QSsl::SslOptionDisableServerCipherPreference, true);
|
|
|
|
QVERIFY(serverCrypto->setDtlsConfiguration(serverConfig));
|
|
QDTLS_VERIFY_NO_ERROR(serverCrypto);
|
|
|
|
auto clientConfig = QSslConfiguration::defaultDtlsConfiguration();
|
|
clientConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
|
|
clientConfig.setCiphers({aes256, aes128});
|
|
QVERIFY(clientCrypto->setDtlsConfiguration(clientConfig));
|
|
QVERIFY(clientCrypto->setPeer(serverAddress, serverPort));
|
|
QDTLS_VERIFY_NO_ERROR(clientCrypto);
|
|
|
|
connectHandshakeReadingSlots();
|
|
|
|
QVERIFY(clientCrypto->doHandshake(&clientSocket));
|
|
QDTLS_VERIFY_NO_ERROR(clientCrypto);
|
|
|
|
testLoop.enterLoopMSecs(handshakeTimeoutMS);
|
|
QVERIFY(!testLoop.timeout());
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(clientCrypto);
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(serverCrypto);
|
|
|
|
if (preferClient) {
|
|
QCOMPARE(clientCrypto->sessionCipher(), aes256);
|
|
QCOMPARE(serverCrypto->sessionCipher(), aes256);
|
|
} else {
|
|
QCOMPARE(clientCrypto->sessionCipher(), aes128);
|
|
QCOMPARE(serverCrypto->sessionCipher(), aes128);
|
|
}
|
|
}
|
|
|
|
void tst_QDtls::protocolVersionMatching_data()
|
|
{
|
|
QTest::addColumn<QSsl::SslProtocol>("serverProtocol");
|
|
QTest::addColumn<QSsl::SslProtocol>("clientProtocol");
|
|
QTest::addColumn<bool>("works");
|
|
|
|
QTest::addRow("DtlsV1_0 <-> DtlsV1_0") << QSsl::DtlsV1_0 << QSsl::DtlsV1_0 << true;
|
|
QTest::addRow("DtlsV1_0OrLater <-> DtlsV1_0") << QSsl::DtlsV1_0OrLater << QSsl::DtlsV1_0 << true;
|
|
QTest::addRow("DtlsV1_0 <-> DtlsV1_0OrLater") << QSsl::DtlsV1_0 << QSsl::DtlsV1_0OrLater << true;
|
|
QTest::addRow("DtlsV1_0OrLater <-> DtlsV1_0OrLater") << QSsl::DtlsV1_0OrLater << QSsl::DtlsV1_0OrLater << true;
|
|
|
|
QTest::addRow("DtlsV1_2 <-> DtlsV1_2") << QSsl::DtlsV1_2 << QSsl::DtlsV1_2 << true;
|
|
QTest::addRow("DtlsV1_2OrLater <-> DtlsV1_2") << QSsl::DtlsV1_2OrLater << QSsl::DtlsV1_2 << true;
|
|
QTest::addRow("DtlsV1_2 <-> DtlsV1_2OrLater") << QSsl::DtlsV1_2 << QSsl::DtlsV1_2OrLater << true;
|
|
QTest::addRow("DtlsV1_2OrLater <-> DtlsV1_2OrLater") << QSsl::DtlsV1_2OrLater << QSsl::DtlsV1_2OrLater << true;
|
|
|
|
QTest::addRow("DtlsV1_0 <-> DtlsV1_2") << QSsl::DtlsV1_0 << QSsl::DtlsV1_2 << false;
|
|
QTest::addRow("DtlsV1_0 <-> DtlsV1_2OrLater") << QSsl::DtlsV1_0 << QSsl::DtlsV1_2OrLater << false;
|
|
QTest::addRow("DtlsV1_2 <-> DtlsV1_0") << QSsl::DtlsV1_2 << QSsl::DtlsV1_0 << false;
|
|
QTest::addRow("DtlsV1_2OrLater <-> DtlsV1_0") << QSsl::DtlsV1_2OrLater << QSsl::DtlsV1_0 << false;
|
|
}
|
|
|
|
void tst_QDtls::protocolVersionMatching()
|
|
{
|
|
QFETCH(const QSsl::SslProtocol, serverProtocol);
|
|
QFETCH(const QSsl::SslProtocol, clientProtocol);
|
|
QFETCH(const bool, works);
|
|
|
|
connectHandshakeReadingSlots();
|
|
|
|
connect(serverCrypto.data(), &QDtls::pskRequired, this, &tst_QDtls::pskRequested);
|
|
connect(clientCrypto.data(), &QDtls::pskRequired, this, &tst_QDtls::pskRequested);
|
|
|
|
auto serverConfig = defaultServerConfig;
|
|
serverConfig.setProtocol(serverProtocol);
|
|
QVERIFY(serverCrypto->setDtlsConfiguration(serverConfig));
|
|
|
|
auto clientConfig = QSslConfiguration::defaultDtlsConfiguration();
|
|
clientConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
|
|
clientConfig.setProtocol(clientProtocol);
|
|
QVERIFY(clientCrypto->setDtlsConfiguration(clientConfig));
|
|
|
|
QVERIFY(clientCrypto->setPeer(serverAddress, serverPort));
|
|
QVERIFY(clientCrypto->doHandshake(&clientSocket));
|
|
|
|
testLoop.enterLoopMSecs(handshakeTimeoutMS);
|
|
|
|
if (works) {
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(serverCrypto);
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(clientCrypto);
|
|
} else {
|
|
QCOMPARE(serverCrypto->isConnectionEncrypted(), false);
|
|
QVERIFY(serverCrypto->handshakeState() != QDtls::HandshakeComplete);
|
|
QCOMPARE(clientCrypto->isConnectionEncrypted(), false);
|
|
QVERIFY(clientCrypto->handshakeState() != QDtls::HandshakeComplete);
|
|
}
|
|
}
|
|
|
|
void tst_QDtls::verificationErrors_data()
|
|
{
|
|
QTest::addColumn<bool>("abortHandshake");
|
|
|
|
QTest::addRow("abort-handshake") << true;
|
|
QTest::addRow("ignore-errors") << false;
|
|
}
|
|
|
|
void tst_QDtls::verificationErrors()
|
|
{
|
|
connectHandshakeReadingSlots();
|
|
|
|
auto serverConfig = defaultServerConfig;
|
|
serverConfig.setPrivateKey(serverKeySS);
|
|
serverConfig.setLocalCertificate(selfSignedCert);
|
|
QVERIFY(serverCrypto->setDtlsConfiguration(serverConfig));
|
|
// And our client already has the default DTLS configuration.
|
|
|
|
QVERIFY(clientCrypto->setPeer(serverAddress, serverPort));
|
|
// Now we are ready for handshake:
|
|
QVERIFY(clientCrypto->doHandshake(&clientSocket));
|
|
|
|
testLoop.enterLoopMSecs(handshakeTimeoutMS);
|
|
|
|
QVERIFY(!testLoop.timeout());
|
|
QDTLS_VERIFY_NO_ERROR(serverCrypto);
|
|
|
|
QCOMPARE(clientCrypto->dtlsError(), QDtlsError::PeerVerificationError);
|
|
QCOMPARE(clientCrypto->handshakeState(), QDtls::PeerVerificationFailed);
|
|
QVERIFY(!clientCrypto->isConnectionEncrypted());
|
|
|
|
QVERIFY(verificationErrorDetected(QSslError::HostNameMismatch));
|
|
QVERIFY(verificationErrorDetected(QSslError::SelfSignedCertificate));
|
|
|
|
const auto serverCert = clientCrypto->dtlsConfiguration().peerCertificate();
|
|
QVERIFY(!serverCert.isNull());
|
|
QCOMPARE(selfSignedCert, serverCert);
|
|
|
|
QFETCH(const bool, abortHandshake);
|
|
|
|
if (abortHandshake) {
|
|
QVERIFY(!clientCrypto->abortHandshake(nullptr));
|
|
QCOMPARE(clientCrypto->dtlsError(), QDtlsError::InvalidInputParameters);
|
|
QVERIFY(clientCrypto->abortHandshake(&clientSocket));
|
|
QDTLS_VERIFY_NO_ERROR(clientCrypto);
|
|
QVERIFY(!clientCrypto->isConnectionEncrypted());
|
|
QCOMPARE(clientCrypto->handshakeState(), QDtls::HandshakeNotStarted);
|
|
QCOMPARE(clientCrypto->sessionCipher(), QSslCipher());
|
|
QCOMPARE(clientCrypto->sessionProtocol(), QSsl::UnknownProtocol);
|
|
const auto config = clientCrypto->dtlsConfiguration();
|
|
QVERIFY(config.peerCertificate().isNull());
|
|
QCOMPARE(config.peerCertificateChain().size(), 0);
|
|
QCOMPARE(clientCrypto->peerVerificationErrors().size(), 0);
|
|
} else {
|
|
clientCrypto->ignoreVerificationErrors(clientCrypto->peerVerificationErrors());
|
|
QVERIFY(!clientCrypto->resumeHandshake(nullptr));
|
|
QCOMPARE(clientCrypto->dtlsError(), QDtlsError::InvalidInputParameters);
|
|
QVERIFY(clientCrypto->resumeHandshake(&clientSocket));
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(clientCrypto);
|
|
QVERIFY(clientCrypto->isConnectionEncrypted());
|
|
QCOMPARE(clientCrypto->handshakeState(), QDtls::HandshakeComplete);
|
|
QCOMPARE(clientCrypto->peerVerificationErrors().size(), 0);
|
|
}
|
|
}
|
|
|
|
void tst_QDtls::presetExpectedErrors_data()
|
|
{
|
|
QTest::addColumn<QVector<QSslError>>("expectedTlsErrors");
|
|
QTest::addColumn<bool>("works");
|
|
|
|
QVector<QSslError> expectedErrors{{QSslError::HostNameMismatch, selfSignedCert}};
|
|
QTest::addRow("unexpected-self-signed") << expectedErrors << false;
|
|
expectedErrors.push_back({QSslError::SelfSignedCertificate, selfSignedCert});
|
|
QTest::addRow("all-errors-ignored") << expectedErrors << true;
|
|
}
|
|
|
|
void tst_QDtls::presetExpectedErrors()
|
|
{
|
|
QFETCH(const QVector<QSslError>, expectedTlsErrors);
|
|
QFETCH(const bool, works);
|
|
|
|
connectHandshakeReadingSlots();
|
|
|
|
auto serverConfig = defaultServerConfig;
|
|
serverConfig.setPrivateKey(serverKeySS);
|
|
serverConfig.setLocalCertificate(selfSignedCert);
|
|
QVERIFY(serverCrypto->setDtlsConfiguration(serverConfig));
|
|
|
|
clientCrypto->ignoreVerificationErrors(expectedTlsErrors);
|
|
QVERIFY(clientCrypto->setPeer(serverAddress, serverPort));
|
|
QVERIFY(clientCrypto->doHandshake(&clientSocket));
|
|
|
|
testLoop.enterLoopMSecs(handshakeTimeoutMS);
|
|
|
|
QVERIFY(!testLoop.timeout());
|
|
|
|
if (works) {
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(serverCrypto);
|
|
QCOMPARE(clientCrypto->handshakeState(), QDtls::HandshakeComplete);
|
|
QVERIFY(clientCrypto->isConnectionEncrypted());
|
|
} else {
|
|
QCOMPARE(clientCrypto->dtlsError(), QDtlsError::PeerVerificationError);
|
|
QVERIFY(!clientCrypto->isConnectionEncrypted());
|
|
QCOMPARE(clientCrypto->handshakeState(), QDtls::PeerVerificationFailed);
|
|
}
|
|
}
|
|
|
|
void tst_QDtls::verifyServerCertificate_data()
|
|
{
|
|
QTest::addColumn<QSslSocket::PeerVerifyMode>("verifyMode");
|
|
QTest::addColumn<QList<QSslCertificate>>("serverCerts");
|
|
QTest::addColumn<QSslKey>("serverKey");
|
|
QTest::addColumn<QString>("peerName");
|
|
QTest::addColumn<bool>("encrypted");
|
|
|
|
{
|
|
// A special case - null key (but with certificate):
|
|
const auto chain = QSslCertificate::fromPath(certDirPath + QStringLiteral("bogus-server.crt"));
|
|
QCOMPARE(chain.size(), 1);
|
|
|
|
QSslKey nullKey;
|
|
// Only one row - server must fail to start handshake immediately.
|
|
QTest::newRow("valid-server-cert-no-key : VerifyPeer") << QSslSocket::VerifyPeer << chain << nullKey << QString() << false;
|
|
}
|
|
{
|
|
// Valid certificate:
|
|
auto chain = QSslCertificate::fromPath(certDirPath + QStringLiteral("bogus-server.crt"));
|
|
QCOMPARE(chain.size(), 1);
|
|
|
|
const auto caCert = QSslCertificate::fromPath(certDirPath + QStringLiteral("bogus-ca.crt"));
|
|
QCOMPARE(caCert.size(), 1);
|
|
chain += caCert;
|
|
|
|
QFile keyFile(certDirPath + QStringLiteral("bogus-server.key"));
|
|
QVERIFY(keyFile.open(QIODevice::ReadOnly));
|
|
const QSslKey key(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
|
|
QVERIFY(!key.isNull());
|
|
|
|
auto cert = chain.first();
|
|
const QString name(cert.subjectInfo(QSslCertificate::CommonName).first());
|
|
QTest::newRow("valid-server-cert : AutoVerifyPeer") << QSslSocket::AutoVerifyPeer << chain << key << name << true;
|
|
QTest::newRow("valid-server-cert : QueryPeer") << QSslSocket::QueryPeer << chain << key << name << true;
|
|
QTest::newRow("valid-server-cert : VerifyNone") << QSslSocket::VerifyNone << chain << key << name << true;
|
|
QTest::newRow("valid-server-cert : VerifyPeer (add CA)") << QSslSocket::VerifyPeer << chain << key << name << true;
|
|
QTest::newRow("valid-server-cert : VerifyPeer (no CA)") << QSslSocket::VerifyPeer << chain << key << name << false;
|
|
QTest::newRow("valid-server-cert : VerifyPeer (name mismatch)") << QSslSocket::VerifyPeer << chain << key << QString() << false;
|
|
}
|
|
}
|
|
|
|
void tst_QDtls::verifyServerCertificate()
|
|
{
|
|
QFETCH(const QSslSocket::PeerVerifyMode, verifyMode);
|
|
QFETCH(const QList<QSslCertificate>, serverCerts);
|
|
QFETCH(const QSslKey, serverKey);
|
|
QFETCH(const QString, peerName);
|
|
QFETCH(const bool, encrypted);
|
|
|
|
auto serverConfig = defaultServerConfig;
|
|
serverConfig.setLocalCertificateChain(serverCerts);
|
|
serverConfig.setPrivateKey(serverKey);
|
|
QVERIFY(serverCrypto->setDtlsConfiguration(serverConfig));
|
|
|
|
auto clientConfig = QSslConfiguration::defaultDtlsConfiguration();
|
|
|
|
if (serverCerts.size() == 2 && encrypted) {
|
|
auto caCerts = clientConfig.caCertificates();
|
|
caCerts.append(serverCerts.at(1));
|
|
clientConfig.setCaCertificates(caCerts);
|
|
}
|
|
|
|
clientConfig.setPeerVerifyMode(verifyMode);
|
|
|
|
QVERIFY(clientCrypto->setDtlsConfiguration(clientConfig));
|
|
QVERIFY(clientCrypto->setPeer(serverAddress, serverPort, peerName));
|
|
|
|
connectHandshakeReadingSlots();
|
|
|
|
QVERIFY(clientCrypto->doHandshake(&clientSocket));
|
|
|
|
testLoop.enterLoopMSecs(handshakeTimeoutMS);
|
|
QVERIFY(!testLoop.timeout());
|
|
|
|
if (serverKey.isNull() && !serverCerts.isEmpty()) {
|
|
QDTLS_VERIFY_NO_ERROR(clientCrypto);
|
|
QCOMPARE(clientCrypto->handshakeState(), QDtls::HandshakeInProgress);
|
|
QCOMPARE(serverCrypto->dtlsError(), QDtlsError::TlsInitializationError);
|
|
QCOMPARE(serverCrypto->handshakeState(), QDtls::HandshakeNotStarted);
|
|
return;
|
|
}
|
|
|
|
if (encrypted) {
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(serverCrypto);
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(clientCrypto);
|
|
} else {
|
|
QVERIFY(!clientCrypto->isConnectionEncrypted());
|
|
QCOMPARE(clientCrypto->handshakeState(), QDtls::PeerVerificationFailed);
|
|
QVERIFY(clientCrypto->peerVerificationErrors().size());
|
|
QVERIFY(clientCrypto->writeDatagramEncrypted(&clientSocket, "something") < 0);
|
|
QCOMPARE(clientCrypto->dtlsError(), QDtlsError::InvalidOperation);
|
|
}
|
|
}
|
|
|
|
void tst_QDtls::verifyClientCertificate_data()
|
|
{
|
|
QTest::addColumn<QSslSocket::PeerVerifyMode>("verifyMode");
|
|
QTest::addColumn<QList<QSslCertificate>>("clientCerts");
|
|
QTest::addColumn<QSslKey>("clientKey");
|
|
QTest::addColumn<bool>("encrypted");
|
|
{
|
|
// No certficates, no key:
|
|
QList<QSslCertificate> chain;
|
|
QSslKey key;
|
|
QTest::newRow("no-cert : AutoVerifyPeer") << QSslSocket::AutoVerifyPeer << chain << key << true;
|
|
QTest::newRow("no-cert : QueryPeer") << QSslSocket::QueryPeer << chain << key << true;
|
|
QTest::newRow("no-cert : VerifyNone") << QSslSocket::VerifyNone << chain << key << true;
|
|
QTest::newRow("no-cert : VerifyPeer") << QSslSocket::VerifyPeer << chain << key << false;
|
|
}
|
|
{
|
|
const auto chain = QSslCertificate::fromPath(certDirPath + QStringLiteral("fluke.cert"));
|
|
QCOMPARE(chain.size(), 1);
|
|
|
|
QFile keyFile(certDirPath + QStringLiteral("fluke.key"));
|
|
QVERIFY(keyFile.open(QIODevice::ReadOnly));
|
|
const QSslKey key(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
|
|
QVERIFY(!key.isNull());
|
|
|
|
QTest::newRow("self-signed-cert : AutoVerifyPeer") << QSslSocket::AutoVerifyPeer << chain << key << true;
|
|
QTest::newRow("self-signed-cert : QueryPeer") << QSslSocket::QueryPeer << chain << key << true;
|
|
QTest::newRow("self-signed-cert : VerifyNone") << QSslSocket::VerifyNone << chain << key << true;
|
|
QTest::newRow("self-signed-cert : VerifyPeer") << QSslSocket::VerifyPeer << chain << key << false;
|
|
}
|
|
{
|
|
// Valid certificate, but wrong usage (server certificate):
|
|
const auto chain = QSslCertificate::fromPath(certDirPath + QStringLiteral("bogus-server.crt"));
|
|
QCOMPARE(chain.size(), 1);
|
|
|
|
QFile keyFile(certDirPath + QStringLiteral("bogus-server.key"));
|
|
QVERIFY(keyFile.open(QIODevice::ReadOnly));
|
|
const QSslKey key(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
|
|
QVERIFY(!key.isNull());
|
|
|
|
QTest::newRow("valid-server-cert : AutoVerifyPeer") << QSslSocket::AutoVerifyPeer << chain << key << true;
|
|
QTest::newRow("valid-server-cert : QueryPeer") << QSslSocket::QueryPeer << chain << key << true;
|
|
QTest::newRow("valid-server-cert : VerifyNone") << QSslSocket::VerifyNone << chain << key << true;
|
|
QTest::newRow("valid-server-cert : VerifyPeer") << QSslSocket::VerifyPeer << chain << key << false;
|
|
}
|
|
{
|
|
// Valid certificate, correct usage (client certificate):
|
|
auto chain = QSslCertificate::fromPath(certDirPath + QStringLiteral("bogus-client.crt"));
|
|
QCOMPARE(chain.size(), 1);
|
|
|
|
QFile keyFile(certDirPath + QStringLiteral("bogus-client.key"));
|
|
QVERIFY(keyFile.open(QIODevice::ReadOnly));
|
|
const QSslKey key(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
|
|
QVERIFY(!key.isNull());
|
|
|
|
QTest::newRow("valid-client-cert : AutoVerifyPeer") << QSslSocket::AutoVerifyPeer << chain << key << true;
|
|
QTest::newRow("valid-client-cert : QueryPeer") << QSslSocket::QueryPeer << chain << key << true;
|
|
QTest::newRow("valid-client-cert : VerifyNone") << QSslSocket::VerifyNone << chain << key << true;
|
|
QTest::newRow("valid-client-cert : VerifyPeer") << QSslSocket::VerifyPeer << chain << key << true;
|
|
|
|
// Valid certificate, correct usage (client certificate), with chain:
|
|
chain += QSslCertificate::fromPath(certDirPath + QStringLiteral("bogus-ca.crt"));
|
|
QCOMPARE(chain.size(), 2);
|
|
|
|
QTest::newRow("valid-client-chain : AutoVerifyPeer") << QSslSocket::AutoVerifyPeer << chain << key << true;
|
|
QTest::newRow("valid-client-chain : QueryPeer") << QSslSocket::QueryPeer << chain << key << true;
|
|
QTest::newRow("valid-client-chain : VerifyNone") << QSslSocket::VerifyNone << chain << key << true;
|
|
QTest::newRow("valid-client-chain : VerifyPeer") << QSslSocket::VerifyPeer << chain << key << true;
|
|
}
|
|
}
|
|
|
|
void tst_QDtls::verifyClientCertificate()
|
|
{
|
|
connectHandshakeReadingSlots();
|
|
|
|
QFETCH(const QSslSocket::PeerVerifyMode, verifyMode);
|
|
QFETCH(const QList<QSslCertificate>, clientCerts);
|
|
QFETCH(const QSslKey, clientKey);
|
|
QFETCH(const bool, encrypted);
|
|
|
|
QSslConfiguration serverConfig = defaultServerConfig;
|
|
serverConfig.setLocalCertificate(selfSignedCert);
|
|
serverConfig.setPrivateKey(serverKeySS);
|
|
serverConfig.setPeerVerifyMode(verifyMode);
|
|
|
|
if (verifyMode == QSslSocket::VerifyPeer && clientCerts.size()) {
|
|
// Not always needed even if these conditions met, but does not hurt
|
|
// either.
|
|
const auto certs = QSslCertificate::fromPath(certDirPath + QStringLiteral("bogus-ca.crt"));
|
|
QCOMPARE(certs.size(), 1);
|
|
serverConfig.setCaCertificates(serverConfig.caCertificates() + certs);
|
|
}
|
|
|
|
QVERIFY(serverCrypto->setDtlsConfiguration(serverConfig));
|
|
serverConfig = serverCrypto->dtlsConfiguration();
|
|
QVERIFY(serverConfig.peerCertificate().isNull());
|
|
QCOMPARE(serverConfig.peerCertificateChain().size(), 0);
|
|
|
|
auto clientConfig = QSslConfiguration::defaultDtlsConfiguration();
|
|
clientConfig.setLocalCertificateChain(clientCerts);
|
|
clientConfig.setPrivateKey(clientKey);
|
|
clientConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
|
|
QVERIFY(clientCrypto->setDtlsConfiguration(clientConfig));
|
|
QVERIFY(clientCrypto->setPeer(serverAddress, serverPort));
|
|
|
|
QVERIFY(clientCrypto->doHandshake(&clientSocket));
|
|
QDTLS_VERIFY_NO_ERROR(clientCrypto);
|
|
|
|
testLoop.enterLoopMSecs(handshakeTimeoutMS);
|
|
|
|
serverConfig = serverCrypto->dtlsConfiguration();
|
|
|
|
if (verifyMode == QSslSocket::VerifyNone || clientCerts.isEmpty()) {
|
|
QVERIFY(serverConfig.peerCertificate().isNull());
|
|
QCOMPARE(serverConfig.peerCertificateChain().size(), 0);
|
|
} else {
|
|
QCOMPARE(serverConfig.peerCertificate(), clientCerts.first());
|
|
QCOMPARE(serverConfig.peerCertificateChain(), clientCerts);
|
|
}
|
|
|
|
if (encrypted) {
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(serverCrypto);
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(clientCrypto);
|
|
} else {
|
|
QVERIFY(!serverCrypto->isConnectionEncrypted());
|
|
QCOMPARE(serverCrypto->handshakeState(), QDtls::PeerVerificationFailed);
|
|
QVERIFY(serverCrypto->dtlsErrorString().size() > 0);
|
|
QVERIFY(serverCrypto->peerVerificationErrors().size() > 0);
|
|
|
|
QVERIFY(!clientCrypto->isConnectionEncrypted());
|
|
QDTLS_VERIFY_NO_ERROR(clientCrypto);
|
|
QCOMPARE(clientCrypto->handshakeState(), QDtls::HandshakeInProgress);
|
|
}
|
|
}
|
|
|
|
void tst_QDtls::blacklistedCerificate()
|
|
{
|
|
const auto serverChain = QSslCertificate::fromPath(certDirPath + QStringLiteral("fake-login.live.com.pem"));
|
|
QCOMPARE(serverChain.size(), 1);
|
|
|
|
QFile keyFile(certDirPath + QStringLiteral("fake-login.live.com.key"));
|
|
QVERIFY(keyFile.open(QIODevice::ReadOnly));
|
|
const QSslKey key(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
|
|
QVERIFY(!key.isNull());
|
|
|
|
auto serverConfig = defaultServerConfig;
|
|
serverConfig.setLocalCertificateChain(serverChain);
|
|
serverConfig.setPrivateKey(key);
|
|
QVERIFY(serverCrypto->setDtlsConfiguration(serverConfig));
|
|
|
|
connectHandshakeReadingSlots();
|
|
const QString name(serverChain.first().subjectInfo(QSslCertificate::CommonName).first());
|
|
QVERIFY(clientCrypto->setPeer(serverAddress, serverPort, name));
|
|
QVERIFY(clientCrypto->doHandshake(&clientSocket));
|
|
|
|
testLoop.enterLoopMSecs(handshakeTimeoutMS);
|
|
QVERIFY(!testLoop.timeout());
|
|
QCOMPARE(clientCrypto->handshakeState(), QDtls::PeerVerificationFailed);
|
|
QCOMPARE(clientCrypto->dtlsError(), QDtlsError::PeerVerificationError);
|
|
QVERIFY(!clientCrypto->isConnectionEncrypted());
|
|
QVERIFY(verificationErrorDetected(QSslError::CertificateBlacklisted));
|
|
}
|
|
|
|
void tst_QDtls::readWriteEncrypted_data()
|
|
{
|
|
QTest::addColumn<bool>("serverSideShutdown");
|
|
|
|
QTest::addRow("client-shutdown") << false;
|
|
QTest::addRow("server-shutdown") << true;
|
|
}
|
|
|
|
void tst_QDtls::readWriteEncrypted()
|
|
{
|
|
connectHandshakeReadingSlots();
|
|
|
|
auto serverConfig = defaultServerConfig;
|
|
serverConfig.setLocalCertificate(selfSignedCert);
|
|
serverConfig.setPrivateKey(serverKeySS);
|
|
QVERIFY(serverCrypto->setDtlsConfiguration(serverConfig));
|
|
|
|
auto clientConfig = QSslConfiguration::defaultDtlsConfiguration();
|
|
clientConfig.setCaCertificates({selfSignedCert});
|
|
QVERIFY(clientCrypto->setDtlsConfiguration(clientConfig));
|
|
QVERIFY(clientCrypto->setPeer(serverAddress, serverPort, hostName));
|
|
|
|
// 0. Verify we cannot write any encrypted message without handshake done
|
|
QDTLS_VERIFY_NO_ERROR(clientCrypto);
|
|
QVERIFY(clientCrypto->writeDatagramEncrypted(&clientSocket, serverExpectedPlainText) <= 0);
|
|
QCOMPARE(clientCrypto->dtlsError(), QDtlsError::InvalidOperation);
|
|
QVERIFY(!clientCrypto->shutdown(&clientSocket));
|
|
QCOMPARE(clientCrypto->dtlsError(), QDtlsError::InvalidOperation);
|
|
QDTLS_VERIFY_NO_ERROR(serverCrypto);
|
|
QVERIFY(serverCrypto->writeDatagramEncrypted(&serverSocket, clientExpectedPlainText) <= 0);
|
|
QCOMPARE(serverCrypto->dtlsError(), QDtlsError::InvalidOperation);
|
|
QVERIFY(!serverCrypto->shutdown(&serverSocket));
|
|
QCOMPARE(serverCrypto->dtlsError(), QDtlsError::InvalidOperation);
|
|
|
|
// 1. Initiate a handshake:
|
|
QVERIFY(clientCrypto->doHandshake(&clientSocket));
|
|
QDTLS_VERIFY_NO_ERROR(clientCrypto);
|
|
// 1.1 Verify we cannot read yet. What the datagram is - not really important,
|
|
// invalid state/operation - is what we verify:
|
|
const QByteArray dummy = clientCrypto->decryptDatagram(&clientSocket, "BS dgram");
|
|
QCOMPARE(dummy.size(), 0);
|
|
QCOMPARE(clientCrypto->dtlsError(), QDtlsError::InvalidOperation);
|
|
|
|
// 1.2 Finish the handshake:
|
|
testLoop.enterLoopMSecs(handshakeTimeoutMS);
|
|
QVERIFY(!testLoop.timeout());
|
|
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(clientCrypto);
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(serverCrypto);
|
|
|
|
// 2. Change reading slots:
|
|
connectEncryptedReadingSlots();
|
|
|
|
// 3. Test parameter validation:
|
|
QVERIFY(clientCrypto->writeDatagramEncrypted(nullptr, serverExpectedPlainText) <= 0);
|
|
QCOMPARE(clientCrypto->dtlsError(), QDtlsError::InvalidInputParameters);
|
|
// 4. Write the client's message:
|
|
qint64 clientBytesWritten = clientCrypto->writeDatagramEncrypted(&clientSocket, serverExpectedPlainText);
|
|
QDTLS_VERIFY_NO_ERROR(clientCrypto);
|
|
QVERIFY(clientBytesWritten > 0);
|
|
|
|
// 5. Exchange client/server messages:
|
|
testLoop.enterLoopMSecs(dataExchangeTimeoutMS);
|
|
QVERIFY(!testLoop.timeout());
|
|
|
|
QCOMPARE(serverExpectedPlainText, serverReceivedPlainText);
|
|
QCOMPARE(clientExpectedPlainText, clientReceivedPlainText);
|
|
|
|
QFETCH(const bool, serverSideShutdown);
|
|
DtlsPtr &crypto = serverSideShutdown ? serverCrypto : clientCrypto;
|
|
QUdpSocket *socket = serverSideShutdown ? &serverSocket : &clientSocket;
|
|
// 6. Parameter validation:
|
|
QVERIFY(!crypto->shutdown(nullptr));
|
|
QCOMPARE(crypto->dtlsError(), QDtlsError::InvalidInputParameters);
|
|
// 7. Send shutdown alert:
|
|
QVERIFY(crypto->shutdown(socket));
|
|
QDTLS_VERIFY_NO_ERROR(crypto);
|
|
QCOMPARE(crypto->handshakeState(), QDtls::HandshakeNotStarted);
|
|
QVERIFY(!crypto->isConnectionEncrypted());
|
|
// 8. Receive this read notification and handle it:
|
|
testLoop.enterLoopMSecs(dataExchangeTimeoutMS);
|
|
QVERIFY(!testLoop.timeout());
|
|
|
|
DtlsPtr &peerCrypto = serverSideShutdown ? clientCrypto : serverCrypto;
|
|
QVERIFY(!peerCrypto->isConnectionEncrypted());
|
|
QCOMPARE(peerCrypto->handshakeState(), QDtls::HandshakeNotStarted);
|
|
QCOMPARE(peerCrypto->dtlsError(), QDtlsError::RemoteClosedConnectionError);
|
|
}
|
|
|
|
void tst_QDtls::datagramFragmentation()
|
|
{
|
|
connectHandshakeReadingSlots();
|
|
|
|
auto serverConfig = defaultServerConfig;
|
|
serverConfig.setLocalCertificate(selfSignedCert);
|
|
serverConfig.setPrivateKey(serverKeySS);
|
|
QVERIFY(serverCrypto->setDtlsConfiguration(serverConfig));
|
|
|
|
auto clientConfig = QSslConfiguration::defaultDtlsConfiguration();
|
|
clientConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
|
|
QVERIFY(clientCrypto->setDtlsConfiguration(clientConfig));
|
|
QVERIFY(clientCrypto->setPeer(serverAddress, serverPort));
|
|
|
|
QVERIFY(clientCrypto->doHandshake(&clientSocket));
|
|
|
|
testLoop.enterLoopMSecs(handshakeTimeoutMS);
|
|
QVERIFY(!testLoop.timeout());
|
|
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(clientCrypto);
|
|
QDTLS_VERIFY_HANDSHAKE_SUCCESS(serverCrypto);
|
|
|
|
// Done with handshake, reconnect readyRead:
|
|
connectEncryptedReadingSlots();
|
|
|
|
// Verify our dgram is not fragmented and some error set (either UnderlyingSocketError
|
|
// if OpenSSL somehow had attempted a write or TlsFatalError in case OpenSSL
|
|
// noticed how big the chunk is).
|
|
QVERIFY(clientCrypto->writeDatagramEncrypted(&clientSocket, QByteArray(1024 * 17, Qt::Uninitialized)) <= 0);
|
|
QVERIFY(clientCrypto->dtlsError() != QDtlsError::NoError);
|
|
// Error to write does not mean QDtls is broken:
|
|
QVERIFY(clientCrypto->isConnectionEncrypted());
|
|
QVERIFY(clientCrypto->writeDatagramEncrypted(&clientSocket, "Hello, I'm a tiny datagram") > 0);
|
|
QDTLS_VERIFY_NO_ERROR(clientCrypto);
|
|
}
|
|
|
|
void tst_QDtls::handshakeReadyRead()
|
|
{
|
|
QUdpSocket *socket = qobject_cast<QUdpSocket *>(sender());
|
|
Q_ASSERT(socket);
|
|
|
|
if (socket->pendingDatagramSize() <= 0)
|
|
return;
|
|
|
|
const bool isServer = socket == &serverSocket;
|
|
DtlsPtr &crypto = isServer ? serverCrypto : clientCrypto;
|
|
DtlsPtr &peerCrypto = isServer ? clientCrypto : serverCrypto;
|
|
QHostAddress addr;
|
|
quint16 port = 0;
|
|
|
|
QByteArray dgram(socket->pendingDatagramSize(), Qt::Uninitialized);
|
|
const qint64 size = socket->readDatagram(dgram.data(), dgram.size(), &addr, &port);
|
|
if (size != dgram.size())
|
|
return;
|
|
|
|
if (isServer) {
|
|
if (!clientPort) {
|
|
// It's probably an initial 'ClientHello' message. Let's set remote's
|
|
// address/port. But first we make sure it is, indeed, 'ClientHello'.
|
|
if (int(dgram.constData()[0]) != 22)
|
|
return;
|
|
|
|
if (addr.isNull() || addr.isBroadcast()) // Could never be us (client), bail out
|
|
return;
|
|
|
|
if (!crypto->setPeer(addr, port))
|
|
return testLoop.exitLoop();
|
|
|
|
// Check parameter validation:
|
|
if (crypto->doHandshake(nullptr, dgram) || crypto->dtlsError() != QDtlsError::InvalidInputParameters)
|
|
return testLoop.exitLoop();
|
|
|
|
if (crypto->doHandshake(&serverSocket, {}) || crypto->dtlsError() != QDtlsError::InvalidInputParameters)
|
|
return testLoop.exitLoop();
|
|
|
|
// Make sure we cannot decrypt yet:
|
|
const QByteArray dummyDgram = crypto->decryptDatagram(&serverSocket, dgram);
|
|
if (dummyDgram.size() > 0 || crypto->dtlsError() != QDtlsError::InvalidOperation)
|
|
return testLoop.exitLoop();
|
|
|
|
clientAddress = addr;
|
|
clientPort = port;
|
|
} else if (clientPort != port || clientAddress != addr) {
|
|
return;
|
|
}
|
|
|
|
if (serverDropDgram) {
|
|
serverDropDgram = false;
|
|
return;
|
|
}
|
|
} else if (clientDropDgram) {
|
|
clientDropDgram = false;
|
|
return;
|
|
}
|
|
|
|
if (!crypto->doHandshake(socket, dgram))
|
|
return testLoop.exitLoop();
|
|
|
|
const auto state = crypto->handshakeState();
|
|
if (state != QDtls::HandshakeInProgress && state != QDtls::HandshakeComplete)
|
|
return testLoop.exitLoop();
|
|
|
|
if (state == QDtls::HandshakeComplete && peerCrypto->handshakeState() == QDtls::HandshakeComplete)
|
|
testLoop.exitLoop();
|
|
}
|
|
|
|
void tst_QDtls::encryptedReadyRead()
|
|
{
|
|
QUdpSocket *socket = qobject_cast<QUdpSocket *>(sender());
|
|
Q_ASSERT(socket);
|
|
|
|
if (socket->pendingDatagramSize() <= 0)
|
|
return;
|
|
|
|
QByteArray dtlsMessage(int(socket->pendingDatagramSize()), Qt::Uninitialized);
|
|
QHostAddress addr;
|
|
quint16 port = 0;
|
|
const qint64 bytesRead = socket->readDatagram(dtlsMessage.data(), dtlsMessage.size(), &addr, &port);
|
|
if (bytesRead <= 0)
|
|
return;
|
|
|
|
dtlsMessage.resize(int(bytesRead));
|
|
|
|
if (socket == &serverSocket) {
|
|
if (addr != clientAddress || port != clientPort)
|
|
return;
|
|
|
|
if (serverExpectedPlainText == dtlsMessage) // No way it can happen!
|
|
return testLoop.exitLoop();
|
|
|
|
serverReceivedPlainText = serverCrypto->decryptDatagram(nullptr, dtlsMessage);
|
|
if (serverReceivedPlainText.size() > 0 || serverCrypto->dtlsError() != QDtlsError::InvalidInputParameters)
|
|
return testLoop.exitLoop();
|
|
|
|
serverReceivedPlainText = serverCrypto->decryptDatagram(&serverSocket, dtlsMessage);
|
|
|
|
const int messageType = dtlsMessage.data()[0];
|
|
if (serverReceivedPlainText != serverExpectedPlainText
|
|
&& (messageType == 23 || messageType == 21)) {
|
|
// Type 23 is for application data, 21 is shutdown alert. Here we test
|
|
// write/read operations and shutdown alerts, not expecting and thus
|
|
// ignoring any other types of messages.
|
|
return testLoop.exitLoop();
|
|
}
|
|
|
|
if (serverCrypto->dtlsError() != QDtlsError::NoError)
|
|
return testLoop.exitLoop();
|
|
|
|
// Verify it cannot be done twice:
|
|
const QByteArray replayed = serverCrypto->decryptDatagram(&serverSocket, dtlsMessage);
|
|
if (replayed.size() > 0)
|
|
return testLoop.exitLoop();
|
|
|
|
if (serverCrypto->writeDatagramEncrypted(&serverSocket, clientExpectedPlainText) <= 0)
|
|
testLoop.exitLoop();
|
|
} else {
|
|
if (port != serverPort)
|
|
return;
|
|
|
|
if (clientExpectedPlainText == dtlsMessage) // What a disaster!
|
|
return testLoop.exitLoop();
|
|
|
|
clientReceivedPlainText = clientCrypto->decryptDatagram(&clientSocket, dtlsMessage);
|
|
testLoop.exitLoop();
|
|
}
|
|
}
|
|
|
|
void tst_QDtls::pskRequested(QSslPreSharedKeyAuthenticator *auth)
|
|
{
|
|
Q_ASSERT(auth);
|
|
|
|
auth->setPreSharedKey(presharedKey);
|
|
}
|
|
|
|
void tst_QDtls::handleHandshakeTimeout()
|
|
{
|
|
auto crypto = qobject_cast<QDtls *>(sender());
|
|
Q_ASSERT(crypto);
|
|
|
|
if (!crypto->handleTimeout(&clientSocket))
|
|
testLoop.exitLoop();
|
|
}
|
|
|
|
void tst_QDtls::clientServerData()
|
|
{
|
|
QTest::addColumn<QSslSocket::SslMode>("mode");
|
|
|
|
QTest::addRow("client") << QSslSocket::SslClientMode;
|
|
QTest::addRow("server") << QSslSocket::SslServerMode;
|
|
}
|
|
|
|
void tst_QDtls::connectHandshakeReadingSlots()
|
|
{
|
|
connect(&serverSocket, &QUdpSocket::readyRead, this, &tst_QDtls::handshakeReadyRead);
|
|
connect(&clientSocket, &QUdpSocket::readyRead, this, &tst_QDtls::handshakeReadyRead);
|
|
}
|
|
|
|
void tst_QDtls::connectEncryptedReadingSlots()
|
|
{
|
|
serverSocket.disconnect();
|
|
clientSocket.disconnect();
|
|
connect(&serverSocket, &QUdpSocket::readyRead, this, &tst_QDtls::encryptedReadyRead);
|
|
connect(&clientSocket, &QUdpSocket::readyRead, this, &tst_QDtls::encryptedReadyRead);
|
|
}
|
|
|
|
bool tst_QDtls::verificationErrorDetected(QSslError::SslError code) const
|
|
{
|
|
Q_ASSERT(clientCrypto.data());
|
|
|
|
const auto errors = clientCrypto->peerVerificationErrors();
|
|
for (const QSslError &error : errors) {
|
|
if (error.error() == code)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
QHostAddress tst_QDtls::toNonAny(const QHostAddress &addr)
|
|
{
|
|
if (addr == QHostAddress::Any || addr == QHostAddress::AnyIPv4)
|
|
return QHostAddress::LocalHost;
|
|
if (addr == QHostAddress::AnyIPv6)
|
|
return QHostAddress::LocalHostIPv6;
|
|
return addr;
|
|
}
|
|
|
|
QT_END_NAMESPACE
|
|
|
|
QTEST_MAIN(tst_QDtls)
|
|
|
|
#include "tst_qdtls.moc"
|