diff options
author | Jannis Voelker <jannis.voelker@basyskom.com> | 2023-11-20 08:36:24 +0100 |
---|---|---|
committer | Jannis Völker <jannis.voelker@basyskom.com> | 2024-11-12 12:50:52 +0100 |
commit | f886945957e38a72edaad0b9edc705efff6ff592 (patch) | |
tree | 12a4dd3a4df36babb1d425b4c6aa9a05364cea28 | |
parent | dc1a2eb8d6480ee713d31531f434954d665112cd (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.cpp | 13 | ||||
-rw-r--r-- | src/opcua/client/qopcuaauthenticationinformation.h | 1 | ||||
-rw-r--r-- | src/plugins/opcua/open62541/qopen62541backend.cpp | 77 | ||||
-rw-r--r-- | tests/auto/security/tst_security.cpp | 118 |
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) { |