summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRainer Keller <Rainer.Keller@qt.io>2019-01-08 09:28:32 +0100
committerRainer Keller <Rainer.Keller@qt.io>2019-03-20 07:47:13 +0000
commitca4e5567f9e8af280c3d7f0463b959adc373c1ba (patch)
tree71680079a32444455a2526931953336bc1d1dae6
parentf960aabada45dae582890ba34813be5dd81eb0d1 (diff)
security: Support encrypted private keys
Change-Id: Ib605952a161eb01c50026dad68d7b19ac767ec86 Reviewed-by: Jannis Völker <jannis.voelker@basyskom.com> Reviewed-by: Frank Meerkoetter <frank.meerkoetter@basyskom.com>
-rw-r--r--src/opcua/client/qopcuabackend_p.h2
-rw-r--r--src/opcua/client/qopcuaclient.cpp19
-rw-r--r--src/opcua/client/qopcuaclient.h1
-rw-r--r--src/opcua/client/qopcuaclientimpl.cpp1
-rw-r--r--src/opcua/client/qopcuaclientimpl_p.h1
-rw-r--r--src/opcua/client/qopcuaclientprivate.cpp5
-rw-r--r--src/plugins/opcua/uacpp/quacppbackend.cpp20
-rw-r--r--tests/auto/security/certs.qrc1
-rw-r--r--tests/auto/security/pki/own/private/privateKeyWithPassword:secret.pem30
-rw-r--r--tests/auto/security/tst_security.cpp85
10 files changed, 163 insertions, 2 deletions
diff --git a/src/opcua/client/qopcuabackend_p.h b/src/opcua/client/qopcuabackend_p.h
index c873f38..4ad748f 100644
--- a/src/opcua/client/qopcuabackend_p.h
+++ b/src/opcua/client/qopcuabackend_p.h
@@ -106,7 +106,7 @@ Q_SIGNALS:
void deleteReferenceFinished(QString sourceNodeId, QString referenceTypeId, QOpcUaExpandedNodeId targetNodeId, bool isForwardReference,
QOpcUa::UaStatusCode statusCode);
void connectError(QOpcUaErrorState *errorState);
-
+ void passwordForPrivateKeyRequired(QString keyFilePath, QString *password, bool previousTryWasInvalid);
private:
Q_DISABLE_COPY(QOpcUaBackend)
diff --git a/src/opcua/client/qopcuaclient.cpp b/src/opcua/client/qopcuaclient.cpp
index 958bfbb..bc47ec7 100644
--- a/src/opcua/client/qopcuaclient.cpp
+++ b/src/opcua/client/qopcuaclient.cpp
@@ -181,6 +181,25 @@ Q_DECLARE_LOGGING_CATEGORY(QT_OPCUA)
*/
/*!
+ \fn QOpcUaClient::passwordForPrivateKeyRequired(QString keyFilePath, QString *password, bool previousTryWasInvalid)
+ \since QtOpcUa 5.13
+
+ This function is currently available as a Technology Preview, and therefore the API
+ and functionality provided may be subject to change at any time without
+ prior notice.
+
+ This signal is emitted when a password for an encrypted private key is required.
+ The parameter \a keyFilePath contains the file path to key which is used.
+ The parameter \a wasInvalidOnPreviousTry is true if a previous try to decrypt the key failed (aka invalid pasword).
+ The parameter \a password points to a QString that has to be filled with the actual password for the key.
+ In case the previous try failed it contains the previously used password.
+
+ During execution of a slot connected to this signal the backend is stopped and
+ waits for all slots to return. This allows to pop up a user dialog to ask the
+ enduser for the password.
+ */
+
+/*!
\fn void QOpcUaClient::namespaceArrayUpdated(QStringList namespaces)
This signal is emitted after an updateNamespaceArray operation has finished.
diff --git a/src/opcua/client/qopcuaclient.h b/src/opcua/client/qopcuaclient.h
index ce4451c..3c1cef1 100644
--- a/src/opcua/client/qopcuaclient.h
+++ b/src/opcua/client/qopcuaclient.h
@@ -159,6 +159,7 @@ Q_SIGNALS:
QOpcUa::UaStatusCode statusCode);
void deleteReferenceFinished(QString sourceNodeId, QString referenceTypeId, QOpcUaExpandedNodeId targetNodeId, bool isForwardReference,
QOpcUa::UaStatusCode statusCode);
+ void passwordForPrivateKeyRequired(QString keyFilePath, QString *password, bool previousTryWasInvalid);
private:
Q_DISABLE_COPY(QOpcUaClient)
diff --git a/src/opcua/client/qopcuaclientimpl.cpp b/src/opcua/client/qopcuaclientimpl.cpp
index 7c23409..fa18795 100644
--- a/src/opcua/client/qopcuaclientimpl.cpp
+++ b/src/opcua/client/qopcuaclientimpl.cpp
@@ -94,6 +94,7 @@ void QOpcUaClientImpl::connectBackendWithClient(QOpcUaBackend *backend)
connect(backend, &QOpcUaBackend::deleteReferenceFinished, this, &QOpcUaClientImpl::deleteReferenceFinished);
// This needs to be blocking queued because it is called from another thread, which needs to wait for a result.
connect(backend, &QOpcUaBackend::connectError, this, &QOpcUaClientImpl::connectError, Qt::BlockingQueuedConnection);
+ connect(backend, &QOpcUaBackend::passwordForPrivateKeyRequired, this, &QOpcUaClientImpl::passwordForPrivateKeyRequired, Qt::BlockingQueuedConnection);
}
void QOpcUaClientImpl::handleAttributesRead(quint64 handle, QVector<QOpcUaReadResult> attr, QOpcUa::UaStatusCode serviceResult)
diff --git a/src/opcua/client/qopcuaclientimpl_p.h b/src/opcua/client/qopcuaclientimpl_p.h
index 75e3a2c..21d3119 100644
--- a/src/opcua/client/qopcuaclientimpl_p.h
+++ b/src/opcua/client/qopcuaclientimpl_p.h
@@ -128,6 +128,7 @@ signals:
void deleteReferenceFinished(QString sourceNodeId, QString referenceTypeId, QOpcUaExpandedNodeId targetNodeId, bool isForwardReference,
QOpcUa::UaStatusCode statusCode);
void connectError(QOpcUaErrorState *errorState);
+ void passwordForPrivateKeyRequired(const QString keyFilePath, QString *password, bool previousTryWasInvalid);
private:
Q_DISABLE_COPY(QOpcUaClientImpl)
diff --git a/src/opcua/client/qopcuaclientprivate.cpp b/src/opcua/client/qopcuaclientprivate.cpp
index cfeed36..467dd41 100644
--- a/src/opcua/client/qopcuaclientprivate.cpp
+++ b/src/opcua/client/qopcuaclientprivate.cpp
@@ -112,6 +112,11 @@ QOpcUaClientPrivate::QOpcUaClientPrivate(QOpcUaClientImpl *impl)
Q_Q(QOpcUaClient);
emit q->connectError(errorState);
});
+
+ QObject::connect(m_impl.data(), &QOpcUaClientImpl::passwordForPrivateKeyRequired, [this](QString privateKeyFilePath, QString *password, bool previousTryWasInvalid) {
+ Q_Q(QOpcUaClient);
+ emit q->passwordForPrivateKeyRequired(privateKeyFilePath, password, previousTryWasInvalid);
+ });
}
QOpcUaClientPrivate::~QOpcUaClientPrivate()
diff --git a/src/plugins/opcua/uacpp/quacppbackend.cpp b/src/plugins/opcua/uacpp/quacppbackend.cpp
index 466395c..813d0cb 100644
--- a/src/plugins/opcua/uacpp/quacppbackend.cpp
+++ b/src/plugins/opcua/uacpp/quacppbackend.cpp
@@ -294,8 +294,26 @@ void UACppAsyncBackend::connectToEndpoint(const QOpcUaEndpointDescription &endpo
if (result.isGood()) {
result = sessionSecurityInfo.loadClientCertificateOpenSSL(certificateFilePath, privateKeyFilePath);
- if (!result.isGood())
+ if (!result.isGood()) {
qCWarning(QT_OPCUA_PLUGINS_UACPP) << "sessionSecurityInfo.loadClientCertificateOpenSSL failed";
+ QString password;
+
+ do {
+ // This signal is connected using Qt::BlockingQueuedConnection. It will place a metacall to a different thread and waits
+ // until this metacall is fully handled before returning.
+ emit QOpcUaBackend::passwordForPrivateKeyRequired(pkiConfig.privateKeyLocation(), &password, !password.isEmpty());
+
+ if (password.isEmpty())
+ break;
+
+ result = sessionSecurityInfo.loadClientCertificateOpenSSL(certificateFilePath, privateKeyFilePath, UaString(password.toUtf8()));
+
+ if (result.isGood())
+ break;
+
+ qCWarning(QT_OPCUA_PLUGINS_UACPP) << "sessionSecurityInfo.loadClientCertificateOpenSSL failed";
+ } while (true);
+ }
}
if (result.isNotGood()) {
diff --git a/tests/auto/security/certs.qrc b/tests/auto/security/certs.qrc
index 03d1756..2805c9a 100644
--- a/tests/auto/security/certs.qrc
+++ b/tests/auto/security/certs.qrc
@@ -2,6 +2,7 @@
<qresource prefix="/">
<file>pki/own/certs/tst_security.der</file>
<file>pki/own/private/privateKeyWithoutPassword.pem</file>
+ <file>pki/own/private/privateKeyWithPassword:secret.pem</file>
<file>pki/trusted/certs/ca.der</file>
<file>pki/trusted/certs/open62541-testserver.der</file>
<file>pki/trusted/crl/ca.crl.pem</file>
diff --git a/tests/auto/security/pki/own/private/privateKeyWithPassword:secret.pem b/tests/auto/security/pki/own/private/privateKeyWithPassword:secret.pem
new file mode 100644
index 0000000..ceaedfb
--- /dev/null
+++ b/tests/auto/security/pki/own/private/privateKeyWithPassword:secret.pem
@@ -0,0 +1,30 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-256-CBC,F0415EA92E761E40CACE6F74AC631227
+
+n7rW1/fjLfEVwWgeXoVfvS6L8Ij1KMBd8RLoETdicwS7m7MoYmjZ6r4zR/2Cz3Rh
+oHCw2tV9tXvpuG2e13CbAqv2HXRssdBHkWCwbvWFsi6Nb78mJMvD6dYPPP/yRM6Y
+BskE15tFr3Gjih4a10fzGs8yuDkg29bmcPhwDsXfYE0rSGZXsEJ+jArQxWM7c5BM
+mCq/FhYJTnezlLo1QEU+GfddZvanG3rZbNHhaSDqtIWXBECrEjpS6Y7EnkrSGInP
+q30+ND6sO8KbnOr4b8GU+fkzSZTitJcJwMRuBxn3N98cgMpWzBgJrvHPow9eW6EW
+133I0NXwMQeC1hd8jiXDMej+RWuPiXzc9cQa1YpNEmNiSsyO9QmIhWXdrxP1bTIE
+3NogCHmLlj13czEObui/BQtIbzQC8uBvzjIJsDiLII7iuaibCOOnk3e21xA/RZ/2
+VCoXjiDwURtri+82ZRS2fm5tHpvKb4HgOOEpuNNzb+bSlg280TrKV0VZ4263dl38
+WreEiPxSghfuRYsNSqL/634tdianwW3jVShCkYoAcX1dqRtPyevA7jBIj+4JwYZw
+kEsMlIB5IiNYgwn2IORK+CpxUtBovptgrzvpKOiJcP0tPCPf7IJ0hI8+bpbjx/Wv
+xnhApQ+qK86wcW71deW5bw5z0JSLa8OW06RyALcv365Na4h1z8JxNEl89w6luh9F
+8lEqWVV9uk0uN7amOeVcx/ykKYJBVwuBgtEp+99JWIT4Ds/RVtL3PO0xv5ewJDw2
+4RlmSDFoI2c+COIPJJJuIJ6u6roQuujSkSdWq/doQAI/gbPKBEPSgQbJ2PSxMOI8
+j+qG75hDtxd5ZhKKIi488ij2SFrZCUUzv2R54jL15VVh73KHHpmkvA7ju4c42W4b
+vrBfD6ZUez8WKz/tT0aGc0Uqznk82SZBA/qvkdGR/UbXVcv7mQDEsXLy24WjEZ+j
+CCbb3is191hRMpLWeBIMSfgBChEt7nHr6DQ7SbPHPXgr96xTl/wNyxjf3D0z1p/2
+9SwhlRiO/fSQMl1If4v5rdr0/KIt3GdgefAdolc/yEdXvB+2hJUNAce1xR68MjFP
+pqWkEiiALIGz6wl82xvukNh/JVOKoiBpgrPS5bHr1AJOV6Dn1kjX389p/GU+OF6R
+cUPVeY15m3eUpsvhmVqMYCOYrIl4YQCIOtnKpXIICyQpG9jaLw0jll6g7nPzyUvx
+7og0VtLnL65aQNKt/4ox1nxWcaf0NmuNKMwO3s4flvJxpvWAeCJWwRtHVl5stM82
+2dfthSQG7A59poujjZM5z8lZRU6Q+xG9QZf2rI4820IMiukwfoLvXTMUAzXWlKbh
+9GWb54uP1O8osnQe4SH1PzeJEz9P/bJ46rsHpnHpS9aRCdAQM0jFuU584PWsfHri
+3MvF1KHgJ8H4LbK5/iJfzcNV+ORNM2dUVKsBJ/ikNNyPO/xgiXoBJqxee6zzie+T
+8xSaSJGHfWjuto3Tn9BC1HBjbaJG6xqxpQWn7kA8YVExYBQEtDJAYXANObDZmmJV
+zJQIAvDPa8afp3llzz6udrNfkRezjYLjucM05zP9MtEFfpHcRNMAPDnkQAi8Ko9A
+-----END RSA PRIVATE KEY-----
diff --git a/tests/auto/security/tst_security.cpp b/tests/auto/security/tst_security.cpp
index 01ad718..9bf3c4b 100644
--- a/tests/auto/security/tst_security.cpp
+++ b/tests/auto/security/tst_security.cpp
@@ -146,6 +146,9 @@ private slots:
defineDataMethod(connectAndDisconnectUsingCertificate_data)
void connectAndDisconnectUsingCertificate();
+ defineDataMethod(connectAndDisconnectUsingEncryptedPassword_data)
+ void connectAndDisconnectUsingEncryptedPassword();
+
private:
QString envOrDefault(const char *env, QString def)
{
@@ -279,6 +282,13 @@ void Tst_QOpcUaSecurity::connectAndDisconnectUsingCertificate()
qDebug() << "Testing security policy" << endpoint.securityPolicyUri();
QSignalSpy connectSpy(client.data(), &QOpcUaClient::stateChanged);
+ int passwordRequestSpy = 0;
+ connect(client.data(), &QOpcUaClient::passwordForPrivateKeyRequired, [&passwordRequestSpy](const QString &privateKeyFilePath, QString *password, bool previousTryFailed) {
+ Q_UNUSED(privateKeyFilePath);
+ Q_UNUSED(previousTryFailed);
+ Q_UNUSED(password);
+ ++passwordRequestSpy;
+ });
client->connectToEndpoint(endpoint);
connectSpy.wait();
@@ -290,6 +300,81 @@ void Tst_QOpcUaSecurity::connectAndDisconnectUsingCertificate()
connectSpy.wait();
QCOMPARE(connectSpy.at(1).at(0), QOpcUaClient::Connected);
+ QCOMPARE(passwordRequestSpy, 0);
+
+ QCOMPARE(client->endpoint(), endpoint);
+ QCOMPARE(client->error(), QOpcUaClient::NoError);
+ qDebug() << "connected";
+
+ connectSpy.clear();
+ client->disconnectFromEndpoint();
+ connectSpy.wait();
+ QCOMPARE(connectSpy.count(), 2);
+ QCOMPARE(connectSpy.at(0).at(0), QOpcUaClient::Closing);
+ QCOMPARE(connectSpy.at(1).at(0), QOpcUaClient::Disconnected);
+}
+
+void Tst_QOpcUaSecurity::connectAndDisconnectUsingEncryptedPassword()
+{
+ QFETCH(QString, backend);
+ QFETCH(QOpcUaEndpointDescription, endpoint);
+
+ QScopedPointer<QOpcUaClient> client(m_opcUa.createClient(backend));
+ QVERIFY2(client, QString("Loading backend failed: %1").arg(backend).toLatin1().data());
+
+ if (!client->supportedUserTokenTypes().contains(QOpcUaUserTokenPolicy::TokenType::Certificate))
+ QSKIP(QString("This test is skipped because backend %1 does not support certificate authentication").arg(client->backend()).toLatin1().constData());
+
+ const QString pkidir = m_pkiData->path();
+ QOpcUaPkiConfiguration pkiConfig;
+ pkiConfig.setClientCertificateLocation(pkidir + "/own/certs/tst_security.der");
+ pkiConfig.setPrivateKeyLocation(pkidir + "/own/private/privateKeyWithPassword:secret.pem");
+ pkiConfig.setTrustListLocation(pkidir + "/trusted/certs");
+ pkiConfig.setRevocationListLocation(pkidir + "/trusted/crl");
+ pkiConfig.setIssuerListLocation(pkidir + "/issuers/certs");
+ pkiConfig.setIssuerRevocationListLocation(pkidir + "/issuers/crl");
+
+ const auto identity = pkiConfig.applicationIdentity();
+ QOpcUaAuthenticationInformation authInfo;
+ authInfo.setCertificateAuthentication();
+
+ client->setAuthenticationInformation(authInfo);
+ client->setIdentity(identity);
+ client->setPkiConfiguration(pkiConfig);
+
+ qDebug() << "Testing security policy" << endpoint.securityPolicyUri();
+ QSignalSpy connectSpy(client.data(), &QOpcUaClient::stateChanged);
+ int passwordRequestSpy = 0;
+ connect(client.data(), &QOpcUaClient::passwordForPrivateKeyRequired, [&passwordRequestSpy, &pkiConfig](const QString &privateKeyFilePath, QString *password, bool previousTryFailed) {
+ qDebug() << "Password requested";
+ if (passwordRequestSpy == 0) {
+ QVERIFY(password->isEmpty());
+ QVERIFY(previousTryFailed == false);
+ } else {
+ QVERIFY(*password == QLatin1String("wrong password"));
+ QVERIFY(previousTryFailed == true);
+ }
+
+ QCOMPARE(privateKeyFilePath, pkiConfig.privateKeyLocation());
+
+ if (passwordRequestSpy < 1)
+ *password = "wrong password";
+ else
+ *password = "secret";
+ ++passwordRequestSpy;
+ });
+
+ client->connectToEndpoint(endpoint);
+ connectSpy.wait();
+ if (client->state() == QOpcUaClient::Connecting)
+ connectSpy.wait();
+
+ QCOMPARE(connectSpy.count(), 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";