Revision control

Copy as Markdown

// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtOpcUa/QOpcUaAuthenticationInformation>
#include <QtOpcUa/QOpcUaClient>
#include <QtOpcUa/QOpcUaEndpointDescription>
#include <QtOpcUa/QOpcUaProvider>
#include <QtOpcUa/QOpcUaErrorState>
#include <QtCore/QCoreApplication>
#include <QtCore/QOperatingSystemVersion>
#include <QtCore/QProcess>
#include <QtCore/QScopedPointer>
#include <QtTest/QSignalSpy>
#include <QTest>
#include <QTcpSocket>
#include <QTcpServer>
using namespace Qt::Literals::StringLiterals;
const int signalSpyTimeout = 10000;
static QString messageSecurityModeToString(QOpcUaEndpointDescription::MessageSecurityMode msm)
{
if (msm == QOpcUaEndpointDescription::None)
return u"None"_s;
else if (msm == QOpcUaEndpointDescription::Sign)
return u"Sign"_s;
else if (msm == QOpcUaEndpointDescription::SignAndEncrypt)
return u"SignAndEncrypt"_s;
return u"Invalid"_s;
}
#define defineDataMethod(name) void name()\
{\
QTest::addColumn<QString>("backend");\
QTest::addColumn<QOpcUaEndpointDescription>("endpoint");\
for (const auto &backend : std::as_const(m_backends))\
for (const auto &endpoint : std::as_const(m_endpoints)) { \
const QString rowName = u"%1 using %2 %3"_s \
.arg(backend, endpoint.securityPolicy(), \
messageSecurityModeToString(endpoint.securityMode())); \
QTest::newRow(rowName.toLatin1().constData()) << backend << endpoint; \
} \
}
#define defineDataMethodNonePolicy(name) void name() \
{\
QTest::addColumn<QString>("backend"); \
QTest::addColumn<QOpcUaEndpointDescription>("endpoint"); \
for (const auto &backend : std::as_const(m_backends)) { \
const QString rowName = u"%1 using %2 %3"_s \
.arg(backend, m_noneEndpoint.securityPolicy(), \
messageSecurityModeToString(m_noneEndpoint.securityMode())); \
QTest::newRow(rowName.toLatin1().constData()) << backend << m_noneEndpoint; \
} \
}
class Tst_QOpcUaSecurity: public QObject
{
Q_OBJECT
public:
Tst_QOpcUaSecurity();
private slots:
void initTestCase();
void cleanupTestCase();
defineDataMethodNonePolicy(connectAndDisconnectNonePolicyAnonymousUnencryptedKey_data)
void connectAndDisconnectNonePolicyAnonymousUnencryptedKey();
defineDataMethodNonePolicy(connectAndDisconnectNonePolicyUsernameUnencryptedKey_data)
void connectAndDisconnectNonePolicyUsernameUnencryptedKey();
defineDataMethod(connectAndDisconnectSecureUnencryptedKey_data)
void connectAndDisconnectSecureUnencryptedKey();
defineDataMethod(connectAndDisconnectSecureEncryptedKey_data)
void connectAndDisconnectSecureEncryptedKey();
defineDataMethod(connectAndDisconnectSecureIgnoreUntrusted_data)
void connectAndDisconnectSecureIgnoreUntrusted();
defineDataMethod(connectAndDisconnectSecureWithCertAuth_data)
void connectAndDisconnectSecureWithCertAuth();
defineDataMethod(connectAndDisconnectSecureWithCertAuthOtherCert_data)
void connectAndDisconnectSecureWithCertAuthOtherCert();
// Error cases
defineDataMethod(attemptConnectToUntrustedServer_data)
void attemptConnectToUntrustedServer();
defineDataMethod(initializeWithBrokenCert_data)
void initializeWithBrokenCert();
defineDataMethod(initializeWithInvalidKeyPath_data)
void initializeWithInvalidKeyPath();
defineDataMethod(connectWithUnsupportedTokenPolicy_data)
void connectWithUnsupportedTokenPolicy();
defineDataMethod(connectCertificateAuthWithInvalidPath_data)
void connectCertificateAuthWithInvalidPath();
private:
QString envOrDefault(const char *env, QString def)
{
return qEnvironmentVariableIsSet(env) ? QString::fromUtf8(qgetenv(env).constData()) : def;
}
QString m_testServerPath;
QStringList m_backends;
QProcess m_serverProcess;
QList<QOpcUaEndpointDescription> m_endpoints;
QOpcUaEndpointDescription m_noneEndpoint;
QUrl m_discoveryEndpoint;
QOpcUaProvider m_opcUa;
QSharedPointer<QTemporaryDir> m_pkiData;
};
Tst_QOpcUaSecurity::Tst_QOpcUaSecurity()
{
m_backends = QOpcUaProvider::availableBackends();
}
void Tst_QOpcUaSecurity::initTestCase()
{
#if defined(Q_OS_MACOS)
const bool runsOnCI = qgetenv("QTEST_ENVIRONMENT").split(' ').contains("ci");
const auto osVer = QOperatingSystemVersion::current();
if (runsOnCI && osVer >= QOperatingSystemVersion::MacOSTahoe)
QSKIP("The tests fails on macOS 26 in CI: QTBUG-139354");
#endif
const quint16 defaultPort = 43344;
const QHostAddress defaultHost(QHostAddress::LocalHost);
m_pkiData = QTest::qExtractTestData(u"pki"_s);
qDebug() << "PKI data available at" << m_pkiData->path();
if (qEnvironmentVariableIsEmpty("OPCUA_HOST") && qEnvironmentVariableIsEmpty("OPCUA_PORT")) {
m_testServerPath = qApp->applicationDirPath()
#if defined(Q_OS_MACOS)
+ "/../../open62541-testserver/open62541-testserver.app/Contents/MacOS/open62541-testserver"_L1
#else
#if defined(Q_OS_WIN) && !defined(TESTS_CMAKE_SPECIFIC_PATH)
+ "/.."_L1
#endif
+ "/../../open62541-testserver/open62541-testserver"_L1
#ifdef Q_OS_WIN
+ ".exe"_L1
#endif
#endif
;
if (!QFile::exists(m_testServerPath)) {
qDebug() << "Server Path:" << m_testServerPath;
QSKIP("all auto tests rely on an open62541-based test-server");
}
// In this case the test is supposed to open its own server.
// Unfortunately there is no way to check if the server has started up successfully
// because of missing error handling.
// This checks will detect other servers blocking the port.
// Check for running server
QTcpSocket socket;
socket.connectToHost(defaultHost, defaultPort);
QVERIFY2(socket.waitForConnected(1500) == false, "Server is already running");
// Check for running server which does not respond
QTcpServer server;
QVERIFY2(server.listen(defaultHost, defaultPort) == true, "Port is occupied by another process. Check for defunct server.");
server.close();
m_serverProcess.start(m_testServerPath, { u"--noNonePolicyPassword"_s });
QVERIFY2(m_serverProcess.waitForStarted(), qPrintable(m_serverProcess.errorString()));
// Let the server come up
QTest::qSleep(100);
socket.connectToHost(defaultHost, defaultPort);
if (!socket.waitForConnected(5000))
{
bool success = false;
for (int i = 0; i < 50; ++i) {
QTest::qSleep(100);
socket.connectToHost(defaultHost, defaultPort);
if (socket.waitForConnected(5000)) {
success = true;
break;
}
}
if (!success)
QFAIL("Server does not run");
}
socket.disconnectFromHost();
}
QString host = envOrDefault("OPCUA_HOST", defaultHost.toString());
QString port = envOrDefault("OPCUA_PORT", QString::number(defaultPort));
m_discoveryEndpoint.setUrl("opc.tcp://%1:%2"_L1.arg(host, port));
qDebug() << "Using endpoint:" << m_discoveryEndpoint;
QScopedPointer<QOpcUaClient> client(m_opcUa.createClient(m_backends.first()));
QVERIFY2(client, "Loading backend failed");
if (client) {
QSignalSpy endpointSpy(client.data(), &QOpcUaClient::endpointsRequestFinished);
client->requestEndpoints(m_discoveryEndpoint);
endpointSpy.wait(signalSpyTimeout);
QCOMPARE(endpointSpy.size(), 1);
QCOMPARE(endpointSpy.at(0).at(2).value<QUrl>(), m_discoveryEndpoint);
const auto desc = endpointSpy.at(0).at(0).value<QList<QOpcUaEndpointDescription>>();
QVERIFY(desc.size() > 0);
m_endpoints.clear();
// Select first non-None security policy
for (const auto &endpoint : std::as_const(desc)) {
if (QOpcUa::isSecurePolicy(endpoint.securityPolicy())) {
m_endpoints.append(endpoint);
qDebug() << endpoint.securityPolicy();
} else if (endpoint.securityPolicy().contains(u"None"_s)) {
m_noneEndpoint = endpoint;
}
}
}
}
void Tst_QOpcUaSecurity::connectAndDisconnectNonePolicyAnonymousUnencryptedKey()
{
QFETCH(QString, backend);
QFETCH(QOpcUaEndpointDescription, endpoint);
QScopedPointer<QOpcUaClient> client(m_opcUa.createClient(backend));
QVERIFY2(client, u"Loading backend failed: %1"_s.arg(backend).toLatin1().data());
const QString pkidir = m_pkiData->path();
QOpcUaPkiConfiguration pkiConfig;
pkiConfig.setClientCertificateFile(pkidir + u"/own/certs/tst_security.der"_s);
pkiConfig.setPrivateKeyFile(pkidir + u"/own/private/privateKeyWithoutPassword.pem"_s);
pkiConfig.setTrustListDirectory(pkidir + u"/trusted/certs"_s);
pkiConfig.setRevocationListDirectory(pkidir + u"/trusted/crl"_s);
pkiConfig.setIssuerListDirectory(pkidir + u"/issuers/certs"_s);
pkiConfig.setIssuerRevocationListDirectory(pkidir + u"/issuers/crl"_s);
const auto identity = pkiConfig.applicationIdentity();
QOpcUaAuthenticationInformation authInfo;
// authInfo.setUsernameAuthentication("user1", "password");
authInfo.setAnonymousAuthentication();
client->setAuthenticationInformation(authInfo);
client->setApplicationIdentity(identity);
client->setPkiConfiguration(pkiConfig);
qDebug() << "Testing security policy" << endpoint.securityPolicy();
QSignalSpy connectSpy(client.data(), &QOpcUaClient::stateChanged);
client->connectToEndpoint(endpoint);
connectSpy.wait(signalSpyTimeout);
if (connectSpy.size() != 2)
connectSpy.wait(signalSpyTimeout);
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connected);
QCOMPARE(client->endpoint(), endpoint);
QCOMPARE(client->error(), QOpcUaClient::NoError);
connectSpy.clear();
client->disconnectFromEndpoint();
connectSpy.wait(signalSpyTimeout);
if (connectSpy.size() != 2)
connectSpy.wait(signalSpyTimeout);
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Closing);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
}
void Tst_QOpcUaSecurity::connectAndDisconnectNonePolicyUsernameUnencryptedKey()
{
QFETCH(QString, backend);
QFETCH(QOpcUaEndpointDescription, endpoint);
QScopedPointer<QOpcUaClient> client(m_opcUa.createClient(backend));
QVERIFY2(client, u"Loading backend failed: %1"_s.arg(backend).toLatin1().data());
const QString pkidir = m_pkiData->path();
QOpcUaPkiConfiguration pkiConfig;
pkiConfig.setClientCertificateFile(pkidir + u"/own/certs/tst_security.der"_s);
pkiConfig.setPrivateKeyFile(pkidir + u"/own/private/privateKeyWithoutPassword.pem"_s);
pkiConfig.setTrustListDirectory(pkidir + u"/trusted/certs"_s);
pkiConfig.setRevocationListDirectory(pkidir + u"/trusted/crl"_s);
pkiConfig.setIssuerListDirectory(pkidir + u"/issuers/certs"_s);
pkiConfig.setIssuerRevocationListDirectory(pkidir + u"/issuers/crl"_s);
const auto identity = pkiConfig.applicationIdentity();
QOpcUaAuthenticationInformation authInfo;
authInfo.setUsernameAuthentication(u"user1"_s, u"password"_s);
client->setAuthenticationInformation(authInfo);
client->setApplicationIdentity(identity);
client->setPkiConfiguration(pkiConfig);
QSignalSpy connectSpy(client.data(), &QOpcUaClient::stateChanged);
client->connectToEndpoint(endpoint);
connectSpy.wait(signalSpyTimeout);
if (connectSpy.size() != 2)
connectSpy.wait(signalSpyTimeout);
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connected);
QCOMPARE(client->endpoint(), endpoint);
QCOMPARE(client->error(), QOpcUaClient::NoError);
connectSpy.clear();
client->disconnectFromEndpoint();
connectSpy.wait(signalSpyTimeout);
if (connectSpy.size() != 2)
connectSpy.wait(signalSpyTimeout);
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Closing);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
}
void Tst_QOpcUaSecurity::connectAndDisconnectSecureUnencryptedKey()
{
if (m_endpoints.size() == 0)
QSKIP("No secure endpoints available");
QFETCH(QString, backend);
QFETCH(QOpcUaEndpointDescription, endpoint);
QScopedPointer<QOpcUaClient> client(m_opcUa.createClient(backend));
QVERIFY2(client, u"Loading backend failed: %1"_s.arg(backend).toLatin1().data());
if (!client->supportedSecurityPolicies().contains(endpoint.securityPolicy())) {
QSKIP(u"This test is skipped because backend %1 "
"does not support security policy %2"_s
.arg(client->backend(), endpoint.securityPolicy()).toLatin1().constData());
}
const QString pkidir = m_pkiData->path();
QOpcUaPkiConfiguration pkiConfig;
pkiConfig.setClientCertificateFile(pkidir + u"/own/certs/tst_security.der"_s);
pkiConfig.setPrivateKeyFile(pkidir + u"/own/private/privateKeyWithoutPassword.pem"_s);
pkiConfig.setTrustListDirectory(pkidir + u"/trusted/certs"_s);
pkiConfig.setRevocationListDirectory(pkidir + u"/trusted/crl"_s);
pkiConfig.setIssuerListDirectory(pkidir + u"/issuers/certs"_s);
pkiConfig.setIssuerRevocationListDirectory(pkidir + u"/issuers/crl"_s);
const auto identity = pkiConfig.applicationIdentity();
QOpcUaAuthenticationInformation authInfo;
authInfo.setUsernameAuthentication(u"user1"_s, u"password"_s);
client->setAuthenticationInformation(authInfo);
client->setApplicationIdentity(identity);
client->setPkiConfiguration(pkiConfig);
qDebug() << "Testing security policy" << endpoint.securityPolicy();
QSignalSpy connectSpy(client.data(), &QOpcUaClient::stateChanged);
int passwordRequestSpy = 0;
connect(client.data(), &QOpcUaClient::passwordForPrivateKeyRequired,
this, [&passwordRequestSpy](const QString &privateKeyFilePath, QString *password,
bool previousTryFailed) {
Q_UNUSED(privateKeyFilePath);
Q_UNUSED(previousTryFailed);
Q_UNUSED(password);
++passwordRequestSpy;
});
client->connectToEndpoint(endpoint);
connectSpy.wait(signalSpyTimeout);
if (connectSpy.size() != 2)
connectSpy.wait(signalSpyTimeout);
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connected);
QCOMPARE(passwordRequestSpy, 0);
QCOMPARE(client->endpoint(), endpoint);
QCOMPARE(client->error(), QOpcUaClient::NoError);
qDebug() << "connected";
connectSpy.clear();
client->disconnectFromEndpoint();
connectSpy.wait(signalSpyTimeout);
if (connectSpy.size() != 2)
connectSpy.wait(signalSpyTimeout);
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Closing);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
}
void Tst_QOpcUaSecurity::connectAndDisconnectSecureEncryptedKey()
{
if (m_endpoints.size() == 0)
QSKIP("No secure endpoints available");
QFETCH(QString, backend);
QFETCH(QOpcUaEndpointDescription, endpoint);
QScopedPointer<QOpcUaClient> client(m_opcUa.createClient(backend));
QVERIFY2(client, u"Loading backend failed: %1"_s.arg(backend).toLatin1().data());
if (!client->supportedSecurityPolicies().contains(endpoint.securityPolicy())) {
QSKIP(u"This test is skipped because backend %1 "
"does not support security policy %2"_s
.arg(client->backend(), endpoint.securityPolicy()).toLatin1().constData());
}
const QString pkidir = m_pkiData->path();
QOpcUaPkiConfiguration pkiConfig;
pkiConfig.setClientCertificateFile(pkidir + u"/own/certs/tst_security.der"_s);
pkiConfig.setPrivateKeyFile(pkidir + u"/own/private/privateKeyWithPassword_secret.pem"_s);
pkiConfig.setTrustListDirectory(pkidir + u"/trusted/certs"_s);
pkiConfig.setRevocationListDirectory(pkidir + u"/trusted/crl"_s);
pkiConfig.setIssuerListDirectory(pkidir + u"/issuers/certs"_s);
pkiConfig.setIssuerRevocationListDirectory(pkidir + u"/issuers/crl"_s);
const auto identity = pkiConfig.applicationIdentity();
QOpcUaAuthenticationInformation authInfo;
authInfo.setUsernameAuthentication(u"user1"_s, u"password"_s);
client->setAuthenticationInformation(authInfo);
client->setApplicationIdentity(identity);
client->setPkiConfiguration(pkiConfig);
qDebug() << "Testing security policy" << endpoint.securityPolicy();
QSignalSpy connectSpy(client.data(), &QOpcUaClient::stateChanged);
int passwordRequestSpy = 0;
connect(client.data(), &QOpcUaClient::passwordForPrivateKeyRequired,
this, [&passwordRequestSpy, &pkiConfig](const QString &privateKeyFilePath,
QString *password, bool previousTryFailed) {
qDebug() << "Password requested";
if (passwordRequestSpy == 0) {
QVERIFY(password->isEmpty());
QVERIFY(previousTryFailed == false);
} else {
QVERIFY(*password == "wrong password"_L1);
QVERIFY(previousTryFailed == true);
}
QCOMPARE(privateKeyFilePath, pkiConfig.privateKeyFile());
if (passwordRequestSpy < 1)
*password = u"wrong password"_s;
else
*password = u"secret"_s;
++passwordRequestSpy;
});
client->connectToEndpoint(endpoint);
connectSpy.wait(signalSpyTimeout);
if (connectSpy.size() != 2)
connectSpy.wait(signalSpyTimeout);
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0), QOpcUaClient::Connecting);
QCOMPARE(connectSpy.at(1).at(0), QOpcUaClient::Connected);
QCOMPARE(passwordRequestSpy, 2);
QCOMPARE(client->endpoint(), endpoint);
QCOMPARE(client->error(), QOpcUaClient::NoError);
qDebug() << "connected";
connectSpy.clear();
client->disconnectFromEndpoint();
connectSpy.wait(signalSpyTimeout);
// Sometimes, the first wait() only gets the first signal.
// Make the wait conditional because an unconditional second
// wait() would block for the full time if there is no signal.
if (connectSpy.size() != 2)
connectSpy.wait(signalSpyTimeout);
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0), QOpcUaClient::Closing);
QCOMPARE(connectSpy.at(1).at(0), QOpcUaClient::Disconnected);
}
void Tst_QOpcUaSecurity::connectAndDisconnectSecureIgnoreUntrusted()
{
if (m_endpoints.size() == 0)
QSKIP("No secure endpoints available");
QFETCH(QString, backend);
QFETCH(QOpcUaEndpointDescription, endpoint);
QScopedPointer<QOpcUaClient> client(m_opcUa.createClient(backend));
QVERIFY2(client, u"Loading backend failed: %1"_s.arg(backend).toLatin1().data());
if (!client->supportedSecurityPolicies().contains(endpoint.securityPolicy())) {
QSKIP(u"This test is skipped because backend %1 "
"does not support security policy %2"_s
.arg(client->backend(), endpoint.securityPolicy()).toLatin1().constData());
}
// Initialize a PKI with only certificate and private key paths
// This will make the connection fail with BadCertificateUntrusted
const QString pkidir = m_pkiData->path();
QOpcUaPkiConfiguration pkiConfig;
pkiConfig.setClientCertificateFile(pkidir + u"/own/certs/tst_security.der"_s);
pkiConfig.setPrivateKeyFile(pkidir + u"/own/private/privateKeyWithoutPassword.pem"_s);
const auto identity = pkiConfig.applicationIdentity();
QOpcUaAuthenticationInformation authInfo;
authInfo.setUsernameAuthentication(u"user1"_s, u"password"_s);
client->setAuthenticationInformation(authInfo);
client->setApplicationIdentity(identity);
client->setPkiConfiguration(pkiConfig);
qDebug() << "Testing security policy" << endpoint.securityPolicy();
QSignalSpy connectSpy(client.data(), &QOpcUaClient::stateChanged);
QSignalSpy errorSpy(client.data(), &QOpcUaClient::errorChanged);
bool errorStateReported = false;
connect(client.data(), &QOpcUaClient::connectError,
this, [&](QOpcUaErrorState *errorState) {
if (errorState->connectionStep() == QOpcUaErrorState::ConnectionStep::CertificateValidation) {
errorState->setIgnoreError(true);
errorStateReported = true;
}
});
client->connectToEndpoint(endpoint);
connectSpy.wait(signalSpyTimeout);
if (client->state() == QOpcUaClient::Connecting)
connectSpy.wait(signalSpyTimeout);
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connected);
QVERIFY(errorStateReported);
errorSpy.wait(std::chrono::milliseconds(100));
QVERIFY(errorSpy.empty());
connectSpy.clear();
client->disconnectFromEndpoint();
connectSpy.wait(signalSpyTimeout);
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Closing);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
}
void Tst_QOpcUaSecurity::connectAndDisconnectSecureWithCertAuth()
{
if (m_endpoints.size() == 0)
QSKIP("No secure endpoints available");
QFETCH(QString, backend);
QFETCH(QOpcUaEndpointDescription, endpoint);
QScopedPointer<QOpcUaClient> client(m_opcUa.createClient(backend));
QVERIFY2(client, u"Loading backend failed: %1"_s.arg(backend).toLatin1().data());
if (!client->supportedSecurityPolicies().contains(endpoint.securityPolicy())) {
QSKIP(u"This test is skipped because backend %1 "
"does not support security policy %2"_s
.arg(client->backend(), endpoint.securityPolicy()).toLatin1().constData());
}
const QString pkidir = m_pkiData->path();
QOpcUaPkiConfiguration pkiConfig;
pkiConfig.setClientCertificateFile(pkidir + u"/own/certs/tst_security.der"_s);
pkiConfig.setPrivateKeyFile(pkidir + u"/own/private/privateKeyWithoutPassword.pem"_s);
pkiConfig.setTrustListDirectory(pkidir + u"/trusted/certs"_s);
pkiConfig.setRevocationListDirectory(pkidir + u"/trusted/crl"_s);
pkiConfig.setIssuerListDirectory(pkidir + u"/issuers/certs"_s);
pkiConfig.setIssuerRevocationListDirectory(pkidir + u"/issuers/crl"_s);
const auto identity = pkiConfig.applicationIdentity();
QOpcUaAuthenticationInformation authInfo;
authInfo.setCertificateAuthentication();
client->setAuthenticationInformation(authInfo);
client->setApplicationIdentity(identity);
client->setPkiConfiguration(pkiConfig);
qDebug() << "Testing security policy" << endpoint.securityPolicy();
QSignalSpy connectSpy(client.data(), &QOpcUaClient::stateChanged);
client->connectToEndpoint(endpoint);
connectSpy.wait(signalSpyTimeout);
if (client->state() == QOpcUaClient::Connecting)
connectSpy.wait(signalSpyTimeout);
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connected);
connectSpy.clear();
client->disconnectFromEndpoint();
connectSpy.wait(signalSpyTimeout);
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Closing);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
}
void Tst_QOpcUaSecurity::connectAndDisconnectSecureWithCertAuthOtherCert()
{
if (m_endpoints.size() == 0)
QSKIP("No secure endpoints available");
QFETCH(QString, backend);
QFETCH(QOpcUaEndpointDescription, endpoint);
QScopedPointer<QOpcUaClient> client(m_opcUa.createClient(backend));
QVERIFY2(client, u"Loading backend failed: %1"_s.arg(backend).toLatin1().data());
if (!client->supportedSecurityPolicies().contains(endpoint.securityPolicy())) {
QSKIP(u"This test is skipped because backend %1 "
"does not support security policy %2"_s
.arg(client->backend(), endpoint.securityPolicy()).toLatin1().constData());
}
const QString pkidir = m_pkiData->path();
QOpcUaPkiConfiguration pkiConfig;
pkiConfig.setClientCertificateFile(pkidir + u"/own/certs/tst_security.der"_s);
pkiConfig.setPrivateKeyFile(pkidir + u"/own/private/privateKeyWithoutPassword.pem"_s);
pkiConfig.setTrustListDirectory(pkidir + u"/trusted/certs"_s);
pkiConfig.setRevocationListDirectory(pkidir + u"/trusted/crl"_s);
pkiConfig.setIssuerListDirectory(pkidir + u"/issuers/certs"_s);
pkiConfig.setIssuerRevocationListDirectory(pkidir + u"/issuers/crl"_s);
const auto identity = pkiConfig.applicationIdentity();
QOpcUaAuthenticationInformation authInfo;
authInfo.setCertificateAuthentication(pkidir + u"/own/certs/tst_security.der"_s,
pkidir + u"/own/private/privateKeyWithoutPassword.pem"_s);
client->setAuthenticationInformation(authInfo);
client->setApplicationIdentity(identity);
client->setPkiConfiguration(pkiConfig);
qDebug() << "Testing security policy" << endpoint.securityPolicy();
QSignalSpy connectSpy(client.data(), &QOpcUaClient::stateChanged);
client->connectToEndpoint(endpoint);
connectSpy.wait(signalSpyTimeout);
if (client->state() == QOpcUaClient::Connecting)
connectSpy.wait(signalSpyTimeout);
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connected);
connectSpy.clear();
client->disconnectFromEndpoint();
connectSpy.wait(signalSpyTimeout);
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Closing);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
}
void Tst_QOpcUaSecurity::attemptConnectToUntrustedServer()
{
if (m_endpoints.size() == 0)
QSKIP("No secure endpoints available");
QFETCH(QString, backend);
QFETCH(QOpcUaEndpointDescription, endpoint);
QScopedPointer<QOpcUaClient> client(m_opcUa.createClient(backend));
QVERIFY2(client, "Loading backend failed: %1"_L1.arg(backend).toLatin1().data());
if (!client->supportedSecurityPolicies().contains(endpoint.securityPolicy())) {
QSKIP("This test is skipped because backend %1 "
"does not support security policy %2"_L1
.arg(client->backend(), endpoint.securityPolicy()).toLatin1().constData());
}
// Initialize a PKI with only certificate and private key paths
// This will make the connection fail with BadCertificateUntrusted
const QString pkidir = m_pkiData->path();
QOpcUaPkiConfiguration pkiConfig;
pkiConfig.setClientCertificateFile(pkidir + "/own/certs/tst_security.der"_L1);
pkiConfig.setPrivateKeyFile(pkidir + "/own/private/privateKeyWithoutPassword.pem"_L1);
const auto identity = pkiConfig.applicationIdentity();
QOpcUaAuthenticationInformation authInfo;
authInfo.setUsernameAuthentication("user1"_L1, "password"_L1);
client->setAuthenticationInformation(authInfo);
client->setApplicationIdentity(identity);
client->setPkiConfiguration(pkiConfig);
qDebug() << "Testing security policy" << endpoint.securityPolicy();
QSignalSpy connectSpy(client.data(), &QOpcUaClient::stateChanged);
QSignalSpy errorSpy(client.data(), &QOpcUaClient::errorChanged);
bool errorStateReported = false;
// Register the state callback, but don't ignore the error
connect(client.data(), &QOpcUaClient::connectError,
this, [&](QOpcUaErrorState *errorState) {
if (errorState->connectionStep() == QOpcUaErrorState::ConnectionStep::CertificateValidation) {
errorState->setIgnoreError(false);
errorStateReported = true;
}
});
client->connectToEndpoint(endpoint);
connectSpy.wait(signalSpyTimeout);
if (connectSpy.size() != 2)
QVERIFY(connectSpy.wait());
if (errorSpy.isEmpty())
QVERIFY(errorSpy.wait());
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
QVERIFY(errorStateReported);
QCOMPARE(errorSpy.at(0).at(0), QOpcUaClient::ClientError::CertificateUntrusted);
QCOMPARE(client->error(), QOpcUaClient::ClientError::CertificateUntrusted);
}
void Tst_QOpcUaSecurity::initializeWithBrokenCert()
{
if (m_endpoints.size() == 0)
QSKIP("No secure endpoints available");
QFETCH(QString, backend);
QFETCH(QOpcUaEndpointDescription, endpoint);
QScopedPointer<QOpcUaClient> client(m_opcUa.createClient(backend));
QVERIFY2(client, u"Loading backend failed: %1"_s.arg(backend).toUtf8().data());
if (!client->supportedSecurityPolicies().contains(endpoint.securityPolicy())) {
QSKIP(u"This test is skipped because backend %1 "
"does not support security policy %2"_s
.arg(client->backend(), endpoint.securityPolicy()).toUtf8().constData());
}
const QString pkidir = m_pkiData->path();
QOpcUaPkiConfiguration pkiConfig;
pkiConfig.setClientCertificateFile(pkidir + "/own/certs/invalid_cert.der"_L1);
pkiConfig.setPrivateKeyFile(pkidir + "/own/private/privateKeyWithoutPassword.pem"_L1);
pkiConfig.setTrustListDirectory(pkidir + "/trusted/certs"_L1);
pkiConfig.setRevocationListDirectory(pkidir + "/trusted/crl"_L1);
pkiConfig.setIssuerListDirectory(pkidir + "/issuers/certs"_L1);
pkiConfig.setIssuerRevocationListDirectory(pkidir + "/issuers/crl"_L1);
client->setPkiConfiguration(pkiConfig);
qDebug() << "Testing invalid certificate load with security policy" << endpoint.securityPolicy();
QSignalSpy connectSpy(client.data(), &QOpcUaClient::stateChanged);
QSignalSpy errorSpy(client.data(), &QOpcUaClient::errorChanged);
client->connectToEndpoint(endpoint);
connectSpy.wait(signalSpyTimeout);
if (client->state() == QOpcUaClient::Connecting)
connectSpy.wait(signalSpyTimeout);
if (errorSpy.isEmpty())
QVERIFY(errorSpy.wait());
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
QCOMPARE(errorSpy.at(0).at(0), QOpcUaClient::ClientError::InvalidPki);
QCOMPARE(client->error(), QOpcUaClient::ClientError::InvalidPki);
}
void Tst_QOpcUaSecurity::initializeWithInvalidKeyPath()
{
if (m_endpoints.size() == 0)
QSKIP("No secure endpoints available");
QFETCH(QString, backend);
QFETCH(QOpcUaEndpointDescription, endpoint);
QScopedPointer<QOpcUaClient> client(m_opcUa.createClient(backend));
QVERIFY2(client, u"Loading backend failed: %1"_s.arg(backend).toLatin1().data());
if (!client->supportedSecurityPolicies().contains(endpoint.securityPolicy())) {
QSKIP(u"This test is skipped because backend %1 "
"does not support security policy %2"_s
.arg(client->backend(), endpoint.securityPolicy()).toLatin1().constData());
}
const QString pkidir = m_pkiData->path();
QOpcUaPkiConfiguration pkiConfig;
pkiConfig.setClientCertificateFile(pkidir + "/own/certs/tst_security.der"_L1);
pkiConfig.setPrivateKeyFile(pkidir + "/own/private/idonotexist.pem"_L1);
const auto identity = pkiConfig.applicationIdentity();
client->setApplicationIdentity(identity);
client->setPkiConfiguration(pkiConfig);
qDebug() << "Testing invalid private key path with security policy" << endpoint.securityPolicy();
QSignalSpy connectSpy(client.data(), &QOpcUaClient::stateChanged);
QSignalSpy errorSpy(client.data(), &QOpcUaClient::errorChanged);
client->connectToEndpoint(endpoint);
connectSpy.wait(signalSpyTimeout);
if (connectSpy.size() != 2)
QVERIFY(connectSpy.wait(signalSpyTimeout));
if (errorSpy.isEmpty())
QVERIFY(errorSpy.wait());
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
QCOMPARE(errorSpy.at(0).at(0), QOpcUaClient::ClientError::InvalidPki);
QCOMPARE(client->error(), QOpcUaClient::ClientError::InvalidPki);
}
void Tst_QOpcUaSecurity::connectWithUnsupportedTokenPolicy()
{
if (m_endpoints.size() == 0)
QSKIP("No secure endpoints available");
QFETCH(QString, backend);
QFETCH(QOpcUaEndpointDescription, endpoint);
QScopedPointer<QOpcUaClient> client(m_opcUa.createClient(backend));
QVERIFY2(client, u"Loading backend failed: %1"_s.arg(backend).toLatin1().data());
if (!client->supportedSecurityPolicies().contains(endpoint.securityPolicy())) {
QSKIP(u"This test is skipped because backend %1 "
"does not support security policy %2"_s
.arg(client->backend(), endpoint.securityPolicy()).toLatin1().constData());
}
QOpcUaUserTokenPolicy pol;
pol.setTokenType(QOpcUaUserTokenPolicy::Username);
pol.setSecurityPolicy("http://idonotexist"_L1);
endpoint.setUserIdentityTokens({ pol });
const QString pkidir = m_pkiData->path();
QOpcUaPkiConfiguration pkiConfig;
pkiConfig.setClientCertificateFile(pkidir + "/own/certs/tst_security.der"_L1);
pkiConfig.setPrivateKeyFile(pkidir + "/own/private/privateKeyWithoutPassword.pem"_L1);
QOpcUaAuthenticationInformation authInfo;
authInfo.setUsernameAuthentication("user1"_L1, "password"_L1);
client->setAuthenticationInformation(authInfo);
const auto identity = pkiConfig.applicationIdentity();
client->setApplicationIdentity(identity);
client->setPkiConfiguration(pkiConfig);
qDebug() << "Testing invalid user token security policy with secure channel security policy" << endpoint.securityPolicy();
QSignalSpy connectSpy(client.data(), &QOpcUaClient::stateChanged);
QSignalSpy errorSpy(client.data(), &QOpcUaClient::errorChanged);
client->connectToEndpoint(endpoint);
connectSpy.wait(signalSpyTimeout);
if (connectSpy.size() != 2)
QVERIFY(connectSpy.wait(signalSpyTimeout));
if (errorSpy.isEmpty())
QVERIFY(errorSpy.wait());
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
QCOMPARE(errorSpy.at(0).at(0), QOpcUaClient::ClientError::NoMatchingUserIdentityTokenFound);
QCOMPARE(client->error(), QOpcUaClient::ClientError::NoMatchingUserIdentityTokenFound);
}
void Tst_QOpcUaSecurity::connectCertificateAuthWithInvalidPath()
{
if (m_endpoints.size() == 0)
QSKIP("No secure endpoints available");
QFETCH(QString, backend);
QFETCH(QOpcUaEndpointDescription, endpoint);
QScopedPointer<QOpcUaClient> client(m_opcUa.createClient(backend));
QVERIFY2(client, u"Loading backend failed: %1"_s.arg(backend).toLatin1().data());
if (!client->supportedSecurityPolicies().contains(endpoint.securityPolicy())) {
QSKIP(u"This test is skipped because backend %1 "
"does not support security policy %2"_s
.arg(client->backend(), endpoint.securityPolicy()).toLatin1().constData());
}
const QString pkidir = m_pkiData->path();
QOpcUaPkiConfiguration pkiConfig;
pkiConfig.setClientCertificateFile(pkidir + "/own/certs/tst_security.der"_L1);
pkiConfig.setPrivateKeyFile(pkidir + "/own/private/privateKeyWithoutPassword.pem"_L1);
pkiConfig.setTrustListDirectory(pkidir + "/trusted/certs"_L1);
pkiConfig.setRevocationListDirectory(pkidir + "/trusted/crl"_L1);
pkiConfig.setIssuerListDirectory(pkidir + "/issuers/certs"_L1);
pkiConfig.setIssuerRevocationListDirectory(pkidir + "/issuers/crl"_L1);
const auto identity = pkiConfig.applicationIdentity();
QOpcUaAuthenticationInformation authInfo;
authInfo.setCertificateAuthentication(pkidir + "/own/certs/idontexist.der"_L1,
pkidir + "/own/private/idonotexist.pem"_L1);
client->setAuthenticationInformation(authInfo);
client->setApplicationIdentity(identity);
client->setPkiConfiguration(pkiConfig);
qDebug() << "Testing security policy" << endpoint.securityPolicy();
QSignalSpy connectSpy(client.data(), &QOpcUaClient::stateChanged);
QSignalSpy errorSpy(client.data(), &QOpcUaClient::errorChanged);
client->connectToEndpoint(endpoint);
connectSpy.wait(signalSpyTimeout);
if (connectSpy.size() != 2)
QVERIFY(connectSpy.wait(signalSpyTimeout));
if (errorSpy.empty())
QVERIFY(errorSpy.wait());
QCOMPARE(connectSpy.size(), 2);
QCOMPARE(connectSpy.at(0).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Connecting);
QCOMPARE(connectSpy.at(1).at(0).value<QOpcUaClient::ClientState>(), QOpcUaClient::Disconnected);
QCOMPARE(errorSpy.at(0).at(0), QOpcUaClient::ClientError::InvalidAuthenticationInformation);
QCOMPARE(client->error(), QOpcUaClient::ClientError::InvalidAuthenticationInformation);
}
void Tst_QOpcUaSecurity::cleanupTestCase()
{
if (m_serverProcess.state() == QProcess::Running) {
m_serverProcess.kill();
m_serverProcess.waitForFinished(2000);
}
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QTEST_SET_MAIN_SOURCE_PATH
// run tests for all available backends
QStringList availableBackends = QOpcUaProvider::availableBackends();
if (availableBackends.empty()) {
qDebug("No OPCUA backends found, skipping tests.");
return EXIT_SUCCESS;
}
Tst_QOpcUaSecurity tc;
return QTest::qExec(&tc, argc, argv);
}
#include "tst_security.moc"