summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJannis Voelker <jannis.voelker@basyskom.com>2023-11-20 08:36:24 +0100
committerJannis Völker <jannis.voelker@basyskom.com>2024-11-12 12:50:52 +0100
commitf886945957e38a72edaad0b9edc705efff6ff592 (patch)
tree12a4dd3a4df36babb1d425b4c6aa9a05364cea28
parentdc1a2eb8d6480ee713d31531f434954d665112cd (diff)
Implement certificate authentication
[ChangeLog][open62541 plugin] The open62541 plugin now supports certificate authentication. Change-Id: I6e0850d9c9f94ca7b32fa000c7ce9c8b6775a638 Reviewed-by: Frank Meerkoetter <frank.meerkoetter@basyskom.com> Reviewed-by: Kai Uwe Broulik <kai.uwe.broulik@basyskom.com>
-rw-r--r--src/opcua/client/qopcuaauthenticationinformation.cpp13
-rw-r--r--src/opcua/client/qopcuaauthenticationinformation.h1
-rw-r--r--src/plugins/opcua/open62541/qopen62541backend.cpp77
-rw-r--r--tests/auto/security/tst_security.cpp118
4 files changed, 209 insertions, 0 deletions
diff --git a/src/opcua/client/qopcuaauthenticationinformation.cpp b/src/opcua/client/qopcuaauthenticationinformation.cpp
index ae32aaf..240f7c2 100644
--- a/src/opcua/client/qopcuaauthenticationinformation.cpp
+++ b/src/opcua/client/qopcuaauthenticationinformation.cpp
@@ -152,6 +152,19 @@ void QOpcUaAuthenticationInformation::setCertificateAuthentication()
}
/*!
+ \since 6.9
+
+ Sets the authentication method to certificate authentication with a
+ certificate different from the client certificate.
+*/
+void QOpcUaAuthenticationInformation::setCertificateAuthentication(const QString &certificatePath,
+ const QString &privateKeyPath)
+{
+ data->authenticationType = QOpcUaUserTokenPolicy::TokenType::Certificate;
+ data->authenticationData = QVariant::fromValue(QPair<QString, QString>(certificatePath, privateKeyPath));
+}
+
+/*!
The content of the \l QVariant returned by this method depends on the currently selected authentication method.
*/
const QVariant &QOpcUaAuthenticationInformation::authenticationData() const
diff --git a/src/opcua/client/qopcuaauthenticationinformation.h b/src/opcua/client/qopcuaauthenticationinformation.h
index f580fd2..601b15f 100644
--- a/src/opcua/client/qopcuaauthenticationinformation.h
+++ b/src/opcua/client/qopcuaauthenticationinformation.h
@@ -27,6 +27,7 @@ public:
Q_INVOKABLE void setAnonymousAuthentication();
Q_INVOKABLE void setUsernameAuthentication(const QString &username, const QString &password);
Q_INVOKABLE void setCertificateAuthentication();
+ Q_INVOKABLE void setCertificateAuthentication(const QString &certificatePath, const QString &privateKeyPath);
const QVariant &authenticationData() const;
Q_INVOKABLE QOpcUaUserTokenPolicy::TokenType authenticationType() const;
diff --git a/src/plugins/opcua/open62541/qopen62541backend.cpp b/src/plugins/opcua/open62541/qopen62541backend.cpp
index 1e9177f..b8f8537 100644
--- a/src/plugins/opcua/open62541/qopen62541backend.cpp
+++ b/src/plugins/opcua/open62541/qopen62541backend.cpp
@@ -1057,6 +1057,25 @@ void Open62541AsyncBackend::connectToEndpoint(const QOpcUaEndpointDescription &e
const auto pkiConfig = m_clientImpl->m_client->pkiConfiguration();
#endif
+#ifndef UA_ENABLE_ENCRYPTION
+ if (authInfo.authenticationType() == QOpcUaUserTokenPolicy::TokenType::Certificate) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "The open62541 plugin has been built without encryption support,"
+ "certificate auth is not supported";
+ emit stateAndOrErrorChanged(QOpcUaClient::Disconnected,
+ QOpcUaClient::ClientError::UnsupportedAuthenticationInformation);
+ return;
+ }
+#else
+ if (authInfo.authenticationType() == QOpcUaUserTokenPolicy::TokenType::Certificate) {
+ if (!authInfo.authenticationData().isValid() && !pkiConfig.isKeyAndCertificateFileSet()) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Unable to do certificate auth when no certificate is set";
+ emit stateAndOrErrorChanged(QOpcUaClient::Disconnected,
+ QOpcUaClient::ClientError::UnsupportedAuthenticationInformation);
+ return;
+ }
+ }
+#endif
+
#ifdef UA_ENABLE_ENCRYPTION
if (pkiConfig.isPkiValid()) {
UA_ByteString localCertificate;
@@ -1193,6 +1212,64 @@ void Open62541AsyncBackend::connectToEndpoint(const QOpcUaEndpointDescription &e
const auto credentials = authInfo.authenticationData().value<QPair<QString, QString>>();
ret = UA_Client_connectUsername(m_uaclient, endpoint.endpointUrl().toUtf8().constData(),
credentials.first.toUtf8().constData(), credentials.second.toUtf8().constData());
+ } else if (authInfo.authenticationType() == QOpcUaUserTokenPolicy::TokenType::Certificate) {
+#ifdef UA_ENABLE_ENCRYPTION
+ QString certPath;
+ QString keyPath;
+
+ if (authInfo.authenticationData().canConvert<QPair<QString, QString>>()) {
+ const auto authPaths = authInfo.authenticationData().value<QPair<QString, QString>>();
+
+ if (authPaths.first.isEmpty() || authPaths.second.isEmpty()) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Certificate and private key path must be set for certificate auth";
+ emit stateAndOrErrorChanged(QOpcUaClient::Disconnected, QOpcUaClient::ClientError::UnsupportedAuthenticationInformation);
+ UA_Client_delete(m_uaclient);
+ m_uaclient = nullptr;
+ return;
+ }
+
+ certPath = authPaths.first;
+ keyPath = authPaths.second;
+ } else {
+ certPath = pkiConfig.clientCertificateFile();
+ keyPath = pkiConfig.privateKeyFile();
+ }
+
+ UA_ByteString cert = UA_BYTESTRING_NULL;
+ UA_ByteString key = UA_BYTESTRING_NULL;
+
+ if (!loadFileToByteString(certPath, &cert)) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to load certificate for certificate auth";
+ emit stateAndOrErrorChanged(QOpcUaClient::Disconnected, QOpcUaClient::ClientError::UnsupportedAuthenticationInformation);
+ UA_Client_delete(m_uaclient);
+ m_uaclient = nullptr;
+ return;
+ }
+
+ UaDeleter<UA_ByteString> certDeleter(&cert, &UA_ByteString_clear);
+
+ if (!loadFileToByteString(keyPath, &key)) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to load private key for certificate auth";
+ emit stateAndOrErrorChanged(QOpcUaClient::Disconnected, QOpcUaClient::ClientError::UnsupportedAuthenticationInformation);
+ UA_Client_delete(m_uaclient);
+ m_uaclient = nullptr;
+ return;
+ }
+
+ UaDeleter<UA_ByteString> keyDeleter(&key, &UA_ByteString_clear);
+
+ const auto result = UA_ClientConfig_setAuthenticationCert(conf, cert, key);
+
+ if (result != UA_STATUSCODE_GOOD) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to initialize certificate auth:" << UA_StatusCode_name(result);
+ emit stateAndOrErrorChanged(QOpcUaClient::Disconnected, QOpcUaClient::ClientError::UnsupportedAuthenticationInformation);
+ UA_Client_delete(m_uaclient);
+ m_uaclient = nullptr;
+ return;
+ }
+
+ ret = UA_Client_connect(m_uaclient, endpoint.endpointUrl().toUtf8().constData());
+#endif
} else {
emit stateAndOrErrorChanged(QOpcUaClient::Disconnected, QOpcUaClient::UnsupportedAuthenticationInformation);
qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to connect: Selected authentication type"
diff --git a/tests/auto/security/tst_security.cpp b/tests/auto/security/tst_security.cpp
index 7968d2d..f03dfea 100644
--- a/tests/auto/security/tst_security.cpp
+++ b/tests/auto/security/tst_security.cpp
@@ -124,6 +124,12 @@ private slots:
defineDataMethod(connectAndDisconnectSecureIgnoreUntrusted_data)
void connectAndDisconnectSecureIgnoreUntrusted();
+ defineDataMethod(connectAndDisconnectSecureWithCertAuth_data)
+ void connectAndDisconnectSecureWithCertAuth();
+
+ defineDataMethod(connectAndDisconnectSecureWithCertAuthOtherCert_data)
+ void connectAndDisconnectSecureWithCertAuthOtherCert();
+
private:
QString envOrDefault(const char *env, QString def)
{
@@ -480,6 +486,118 @@ void Tst_QOpcUaSecurity::connectAndDisconnectSecureIgnoreUntrusted()
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, QStringLiteral("Loading backend failed: %1").arg(backend).toLatin1().data());
+
+ if (!client->supportedSecurityPolicies().contains(endpoint.securityPolicy())) {
+ QSKIP(QStringLiteral("This test is skipped because backend %1 "
+ "does not support security policy %2")
+ .arg(client->backend(), endpoint.securityPolicy()).toLatin1().constData());
+ }
+
+ const QString pkidir = m_pkiData->path();
+ QOpcUaPkiConfiguration pkiConfig;
+ pkiConfig.setClientCertificateFile(pkidir + "/own/certs/tst_security.der");
+ pkiConfig.setPrivateKeyFile(pkidir + "/own/private/privateKeyWithoutPassword.pem");
+ pkiConfig.setTrustListDirectory(pkidir + "/trusted/certs");
+ pkiConfig.setRevocationListDirectory(pkidir + "/trusted/crl");
+ pkiConfig.setIssuerListDirectory(pkidir + "/issuers/certs");
+ pkiConfig.setIssuerRevocationListDirectory(pkidir + "/issuers/crl");
+
+ 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);
+
+ if (!endpoint.securityPolicy().contains("Basic256Sha256"))
+ return;
+
+ QScopedPointer<QOpcUaClient> client(m_opcUa.createClient(backend));
+ QVERIFY2(client, QStringLiteral("Loading backend failed: %1").arg(backend).toLatin1().data());
+
+ if (!client->supportedSecurityPolicies().contains(endpoint.securityPolicy())) {
+ QSKIP(QStringLiteral("This test is skipped because backend %1 "
+ "does not support security policy %2")
+ .arg(client->backend(), endpoint.securityPolicy()).toLatin1().constData());
+ }
+
+ const QString pkidir = m_pkiData->path();
+ QOpcUaPkiConfiguration pkiConfig;
+ pkiConfig.setClientCertificateFile(pkidir + "/own/certs/tst_security.der");
+ pkiConfig.setPrivateKeyFile(pkidir + "/own/private/privateKeyWithoutPassword.pem");
+ pkiConfig.setTrustListDirectory(pkidir + "/trusted/certs");
+ pkiConfig.setRevocationListDirectory(pkidir + "/trusted/crl");
+ pkiConfig.setIssuerListDirectory(pkidir + "/issuers/certs");
+ pkiConfig.setIssuerRevocationListDirectory(pkidir + "/issuers/crl");
+
+ const auto identity = pkiConfig.applicationIdentity();
+ QOpcUaAuthenticationInformation authInfo;
+ authInfo.setCertificateAuthentication(pkidir + "/own/certs/tst_security.der",
+ pkidir + "/own/private/privateKeyWithoutPassword.pem");
+
+ 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::cleanupTestCase()
{
if (m_serverProcess.state() == QProcess::Running) {