summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJannis Voelker <jannis.voelker@basyskom.com>2019-03-18 12:39:20 +0100
committerJannis Voelker <jannis.voelker@basyskom.com>2019-08-17 16:28:02 +0200
commit45e3f8129a2dd6d871a627d024023eb5e6a72911 (patch)
treef94f9319ba35d38ab068da629643168348b567a3
parentc3f068568f7b8ddb3e19b0c24fc1b55caf3ce083 (diff)
Add secure connect to the open62541 plugin (v1.0)
This patch adds basic support for secure connections to the open62541 plugin. Supported security policies: - Basic128Rsa15 - Basic256 - Basic256Sha256 Unsupported: - Private keys with password - Manual override in case of certificate verification errors - X509 identity tokens Change-Id: Iae5cdde61e79d091e340c3128a30dca6b119df9e Reviewed-by: Frank Meerkoetter <frank.meerkoetter@basyskom.com>
-rw-r--r--src/opcua/client/qopcuabackend.cpp10
-rw-r--r--src/plugins/opcua/open62541/qopen62541backend.cpp184
-rw-r--r--src/plugins/opcua/open62541/qopen62541backend.h4
-rw-r--r--src/plugins/opcua/open62541/qopen62541client.cpp7
-rw-r--r--tests/auto/declarative/SecurityTest.qml5
5 files changed, 199 insertions, 11 deletions
diff --git a/src/opcua/client/qopcuabackend.cpp b/src/opcua/client/qopcuabackend.cpp
index aedfb55..9780901 100644
--- a/src/opcua/client/qopcuabackend.cpp
+++ b/src/opcua/client/qopcuabackend.cpp
@@ -101,6 +101,16 @@ bool QOpcUaBackend::verifyEndpointDescription(const QOpcUaEndpointDescription &e
*message = QLatin1String("Endpoint description is invalid because endpoint URL or security policy URL is empty");
return false;
}
+
+ if (endpoint.securityMode() != QOpcUaEndpointDescription::MessageSecurityMode::None &&
+ endpoint.securityMode() != QOpcUaEndpointDescription::MessageSecurityMode::Sign &&
+ endpoint.securityMode() != QOpcUaEndpointDescription::MessageSecurityMode::SignAndEncrypt)
+ {
+ if (message)
+ *message = QLatin1String("Endpoint description contains an invalid message security mode");
+ return false;
+ }
+
return true;
}
diff --git a/src/plugins/opcua/open62541/qopen62541backend.cpp b/src/plugins/opcua/open62541/qopen62541backend.cpp
index 72a569f..ddf10cf 100644
--- a/src/plugins/opcua/open62541/qopen62541backend.cpp
+++ b/src/plugins/opcua/open62541/qopen62541backend.cpp
@@ -43,6 +43,8 @@
#include "qopcuaauthenticationinformation.h"
#include <qopcuaerrorstate.h>
+#include <QtCore/QDir>
+#include <QtCore/QFile>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qstringlist.h>
#include <QtCore/qurl.h>
@@ -780,11 +782,12 @@ void Open62541AsyncBackend::connectToEndpoint(const QOpcUaEndpointDescription &e
return;
}
- const QString nonePolicyUri = QLatin1String("http://opcfoundation.org/UA/SecurityPolicy#None");
-
- if (endpoint.securityPolicy() != nonePolicyUri) {
- qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "open62541 does not yet support secure connections";
- emit stateAndOrErrorChanged(QOpcUaClient::Disconnected, QOpcUaClient::ClientError::NoError);
+ if (!m_clientImpl->supportedSecurityPolicies().contains(endpoint.securityPolicy())) {
+#ifndef UA_ENABLE_ENCRYPTION
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "The open62541 plugin has been built without encryption support";
+#endif
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Unsupported security policy:" << endpoint.securityPolicy();
+ emit stateAndOrErrorChanged(QOpcUaClient::Disconnected, QOpcUaClient::ClientError::InvalidUrl);
return;
}
@@ -794,12 +797,88 @@ void Open62541AsyncBackend::connectToEndpoint(const QOpcUaEndpointDescription &e
m_uaclient = UA_Client_new();
auto conf = UA_Client_getConfig(m_uaclient);
- UA_ClientConfig_setDefault(conf);
+
+ const auto identity = m_clientImpl->m_client->applicationIdentity();
+ const auto authInfo = m_clientImpl->m_client->authenticationInformation();
+#ifdef UA_ENABLE_ENCRYPTION
+ const auto pkiConfig = m_clientImpl->m_client->pkiConfiguration();
+#endif
+
+#ifdef UA_ENABLE_ENCRYPTION
+ if (pkiConfig.isPkiValid()) {
+ UA_ByteString localCertificate;
+ UA_ByteString privateKey;
+ UA_ByteString *trustList = nullptr;
+ int trustListSize = 0;
+ UA_ByteString *revocationList = nullptr;
+ int revocationListSize = 0;
+
+ bool success = loadFileToByteString(pkiConfig.clientCertificateFile(), &localCertificate);
+
+ if (!success) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to load client certificate";
+ emit stateAndOrErrorChanged(QOpcUaClient::Disconnected, QOpcUaClient::AccessDenied);
+ return;
+ }
+
+ UaDeleter<UA_ByteString> clientCertDeleter(&localCertificate, &UA_ByteString_deleteMembers);
+
+ success = loadFileToByteString(pkiConfig.privateKeyFile(), &privateKey);
+
+ if (!success) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to load private key";
+ emit stateAndOrErrorChanged(QOpcUaClient::Disconnected, QOpcUaClient::AccessDenied);
+ return;
+ }
+
+ UaDeleter<UA_ByteString> privateKeyDeleter(&privateKey, &UA_ByteString_deleteMembers);
+
+ success = loadAllFilesInDirectory(pkiConfig.trustListDirectory(), &trustList, &trustListSize);
+
+ if (!success) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to load trust list";
+ emit stateAndOrErrorChanged(QOpcUaClient::Disconnected, QOpcUaClient::AccessDenied);
+ return;
+ }
+
+ UaArrayDeleter<UA_TYPES_BYTESTRING> trustListDeleter(trustList, trustListSize);
+
+ success = loadAllFilesInDirectory(pkiConfig.revocationListDirectory(), &revocationList, &revocationListSize);
+
+ if (!success) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to load revocation list";
+ emit stateAndOrErrorChanged(QOpcUaClient::Disconnected, QOpcUaClient::AccessDenied);
+ return;
+ }
+
+ UaArrayDeleter<UA_TYPES_BYTESTRING> revocationListDeleter(revocationList, revocationListSize);
+
+ UA_StatusCode result = UA_ClientConfig_setDefaultEncryption(conf, localCertificate, privateKey, trustList,
+ trustListSize, revocationList, revocationListSize);
+
+ if (result != UA_STATUSCODE_GOOD) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to initialize PKI:" << static_cast<QOpcUa::UaStatusCode>(result);
+ emit stateAndOrErrorChanged(QOpcUaClient::Disconnected, QOpcUaClient::AccessDenied);
+ return;
+ }
+ } else {
+#else
+ {
+#endif
+ UA_ClientConfig_setDefault(conf);
+ }
+
conf->clientContext = this;
conf->stateCallback = &clientStateCallback;
+ conf->clientDescription.applicationName = UA_LOCALIZEDTEXT_ALLOC("", identity.applicationName().toUtf8().constData());
+ conf->clientDescription.applicationUri = UA_STRING_ALLOC(identity.applicationUri().toUtf8().constData());
+ conf->clientDescription.productUri = UA_STRING_ALLOC(identity.productUri().toUtf8().constData());
+ conf->clientDescription.applicationType = UA_APPLICATIONTYPE_CLIENT;
+
+ conf->securityPolicyUri = UA_STRING_ALLOC(endpoint.securityPolicy().toUtf8().constData());
+ conf->securityMode = static_cast<UA_MessageSecurityMode>(endpoint.securityMode());
UA_StatusCode ret;
- const auto authInfo = m_clientImpl->m_client->authenticationInformation();
if (authInfo.authenticationType() == QOpcUaUserTokenPolicy::TokenType::Anonymous) {
ret = UA_Client_connect(m_uaclient, endpoint.endpointUrl().toUtf8().constData());
@@ -807,14 +886,15 @@ void Open62541AsyncBackend::connectToEndpoint(const QOpcUaEndpointDescription &e
bool suitableTokenFound = false;
for (const auto token : endpoint.userIdentityTokens()) {
- if (token.tokenType() == QOpcUaUserTokenPolicy::Username && token.securityPolicy() == nonePolicyUri) {
+ if (token.tokenType() == QOpcUaUserTokenPolicy::Username &&
+ m_clientImpl->supportedSecurityPolicies().contains(token.securityPolicy())) {
suitableTokenFound = true;
break;
}
}
if (!suitableTokenFound) {
- qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "open62541 does not yet support encrypted passwords";
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "No suitable user token policy found";
emit stateAndOrErrorChanged(QOpcUaClient::Disconnected, QOpcUaClient::ClientError::NoError);
return;
}
@@ -1017,6 +1097,92 @@ void Open62541AsyncBackend::cleanupSubscriptions()
m_minPublishingInterval = 0;
}
+bool Open62541AsyncBackend::loadFileToByteString(const QString &location, UA_ByteString *target) const
+{
+ if (location.isEmpty()) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Unable to read from empty file path";
+ return false;
+ }
+
+ if (!target) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "No target ByteString given";
+ return false;
+ }
+
+ UA_ByteString_init(target);
+
+ QFile file(location);
+
+ if (!file.open(QFile::ReadOnly)) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to open file" << location << file.errorString();
+ return false;
+ }
+
+ QByteArray data = file.readAll();
+
+ UA_ByteString temp;
+ temp.length = data.length();
+ if (data.isEmpty())
+ temp.data = nullptr;
+ else {
+ if (data.startsWith('-')) { // PEM file
+ // Remove trailing newline, mbedTLS doesn't tolerate this when loading a certificate
+ data = QString::fromLatin1(data).trimmed().toLatin1();
+ }
+ temp.data = reinterpret_cast<unsigned char *>(data.data());
+ }
+
+ return UA_ByteString_copy(&temp, target) == UA_STATUSCODE_GOOD;
+}
+
+bool Open62541AsyncBackend::loadAllFilesInDirectory(const QString &location, UA_ByteString **target, int *size) const
+{
+ if (location.isEmpty()) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Unable to read from empty file path";
+ return false;
+ }
+
+ if (!target) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "No target ByteString given";
+ return false;
+ }
+
+ *target = nullptr;
+ *size = 0;
+
+ QDir dir(location);
+
+ auto entries = dir.entryList(QDir::Files);
+
+ if (entries.isEmpty()) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Directory is empty";
+ return true;
+ }
+
+ const int tempSize = entries.size();
+ UA_ByteString *list = static_cast<UA_ByteString *>(UA_Array_new(tempSize, &UA_TYPES[UA_TYPES_BYTESTRING]));
+
+ if (!list) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to allocate memory for loading files in" << location;
+ return false;
+ }
+
+ for (int i = 0; i < entries.size(); ++i) {
+ if (!loadFileToByteString(dir.filePath(entries.at(i)), &list[i])) {
+ qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to open file" << entries.at(i);
+ UA_Array_delete(list, tempSize, &UA_TYPES[UA_TYPES_BYTESTRING]);
+ size = 0;
+ *target = nullptr;
+ return false;
+ }
+ }
+
+ *target = list;
+ *size = tempSize;
+
+ return true;
+}
+
UA_ExtensionObject Open62541AsyncBackend::assembleNodeAttributes(const QOpcUaNodeCreationAttributes &nodeAttributes,
QOpcUa::NodeClass nodeClass)
{
diff --git a/src/plugins/opcua/open62541/qopen62541backend.h b/src/plugins/opcua/open62541/qopen62541backend.h
index 6073b94..5661bf6 100644
--- a/src/plugins/opcua/open62541/qopen62541backend.h
+++ b/src/plugins/opcua/open62541/qopen62541backend.h
@@ -98,6 +98,10 @@ private:
UA_ExtensionObject assembleNodeAttributes(const QOpcUaNodeCreationAttributes &nodeAttributes, QOpcUa::NodeClass nodeClass);
UA_UInt32 *copyArrayDimensions(const QVector<quint32> &arrayDimensions, size_t *outputSize);
+ // Helper
+ bool loadFileToByteString(const QString &location, UA_ByteString *target) const;
+ bool loadAllFilesInDirectory(const QString &location, UA_ByteString **target, int *size) const;
+
QTimer m_subscriptionTimer;
QHash<quint32, QOpen62541Subscription *> m_subscriptions;
diff --git a/src/plugins/opcua/open62541/qopen62541client.cpp b/src/plugins/opcua/open62541/qopen62541client.cpp
index d65f737..5a7de24 100644
--- a/src/plugins/opcua/open62541/qopen62541client.cpp
+++ b/src/plugins/opcua/open62541/qopen62541client.cpp
@@ -153,7 +153,12 @@ bool QOpen62541Client::deleteReference(const QOpcUaDeleteReferenceItem &referenc
QStringList QOpen62541Client::supportedSecurityPolicies() const
{
return QStringList {
- "http://opcfoundation.org/UA/SecurityPolicy#None",
+ "http://opcfoundation.org/UA/SecurityPolicy#None"
+#ifdef UA_ENABLE_ENCRYPTION
+ , "http://opcfoundation.org/UA/SecurityPolicy#Basic128Rsa15"
+ , "http://opcfoundation.org/UA/SecurityPolicy#Basic256"
+ , "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256"
+#endif
};
}
diff --git a/tests/auto/declarative/SecurityTest.qml b/tests/auto/declarative/SecurityTest.qml
index ce2412c..1b44c2b 100644
--- a/tests/auto/declarative/SecurityTest.qml
+++ b/tests/auto/declarative/SecurityTest.qml
@@ -65,7 +65,10 @@ Item {
compare(connection2.supportedSecurityPolicies.length, 6);
compare(connection2.supportedUserTokenTypes.length, 3);
} else if (backendName === "open62541") {
- compare(connection2.supportedSecurityPolicies.length, 1);
+ if (SERVER_SUPPORTS_SECURITY)
+ compare(connection2.supportedSecurityPolicies.length, 4);
+ else
+ compare(connection2.supportedSecurityPolicies.length, 1);
compare(connection2.supportedUserTokenTypes.length, 2);
} else {
fail(backendName, "is not support by this test case");