diff options
author | Rainer Keller <Rainer.Keller@qt.io> | 2019-01-08 09:28:32 +0100 |
---|---|---|
committer | Rainer Keller <Rainer.Keller@qt.io> | 2019-03-20 07:47:13 +0000 |
commit | ca4e5567f9e8af280c3d7f0463b959adc373c1ba (patch) | |
tree | 71680079a32444455a2526931953336bc1d1dae6 | |
parent | f960aabada45dae582890ba34813be5dd81eb0d1 (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.h | 2 | ||||
-rw-r--r-- | src/opcua/client/qopcuaclient.cpp | 19 | ||||
-rw-r--r-- | src/opcua/client/qopcuaclient.h | 1 | ||||
-rw-r--r-- | src/opcua/client/qopcuaclientimpl.cpp | 1 | ||||
-rw-r--r-- | src/opcua/client/qopcuaclientimpl_p.h | 1 | ||||
-rw-r--r-- | src/opcua/client/qopcuaclientprivate.cpp | 5 | ||||
-rw-r--r-- | src/plugins/opcua/uacpp/quacppbackend.cpp | 20 | ||||
-rw-r--r-- | tests/auto/security/certs.qrc | 1 | ||||
-rw-r--r-- | tests/auto/security/pki/own/private/privateKeyWithPassword:secret.pem | 30 | ||||
-rw-r--r-- | tests/auto/security/tst_security.cpp | 85 |
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"; |