diff --git a/client/LdapSearch.cpp b/client/LdapSearch.cpp index 98779c374..4079b8e02 100644 --- a/client/LdapSearch.cpp +++ b/client/LdapSearch.cpp @@ -26,9 +26,12 @@ #include #ifdef Q_OS_WIN -#include +#include +#include #include #include +#include + using STR_T = PWSTR; #define STR(X) const_cast(L##X) #define TO_STR(X) STR_T((X).utf16()) @@ -36,6 +39,7 @@ using STR_T = PWSTR; #define LDAP_DEPRECATED 1 #include #include + using ULONG = int; using LDAP_TIMEVAL = timeval; using STR_T = char *; @@ -57,21 +61,130 @@ static constexpr auto TO_QSTR(const T *str) return QStringView(str); } -class LdapSearch::Private +struct LdapSearch::Private { -public: +#ifdef Q_OS_WIN + static thread_local const Private *activeInstance; + static BOOLEAN LDAPAPI verifyServerCertificate(PLDAP, PCCERT_CONTEXT *serverCert); + ~Private() { if(trustStore) CertCloseStore(trustStore, 0); } + HCERTSTORE trustStore = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, CERT_STORE_CREATE_NEW_FLAG, nullptr); +#else + QByteArray caCertPath; +#endif LDAP *ldap {}; QUrl url; - QByteArray caCertPath; QTimer *timer {}; }; -LdapSearch::LdapSearch(const QString &url, const QString &caCertPath, QObject *parent) +#ifdef Q_OS_WIN +thread_local const LdapSearch::Private *LdapSearch::Private::activeInstance {}; + +BOOLEAN LDAPAPI LdapSearch::Private::verifyServerCertificate(PLDAP, PCCERT_CONTEXT *serverCert) try +{ + PCCERT_CONTEXT certificate = serverCert ? *serverCert : nullptr; + const auto freeCertificate = qScopeGuard([certificate] { + if(certificate) + CertFreeCertificateContext(certificate); + }); + + const Private *d = activeInstance; + if(!certificate || !d || !d->trustStore) + return FALSE; + + CERT_CHAIN_ENGINE_CONFIG engineConfig { + .cbSize = sizeof(engineConfig), + .hExclusiveRoot = d->trustStore, + }; + HCERTCHAINENGINE engine {}; + if(!CertCreateCertificateChainEngine(&engineConfig, &engine)) + return FALSE; + const auto freeEngine = qScopeGuard([engine] { + CertFreeCertificateChainEngine(engine); + }); + + LPSTR serverAuthOID = const_cast(szOID_PKIX_KP_SERVER_AUTH); + CERT_CHAIN_PARA chainPara { + .cbSize = sizeof(chainPara), + .RequestedUsage { + .dwType = USAGE_MATCH_TYPE_OR, + .Usage { + .cUsageIdentifier = 1, + .rgpszUsageIdentifier = &serverAuthOID, + }, + }, + }; + + PCCERT_CHAIN_CONTEXT chainContext {}; + BOOL built = CertGetCertificateChain(engine, certificate, nullptr, certificate->hCertStore, + &chainPara, CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT, nullptr, &chainContext); + const auto freeChain = qScopeGuard([&chainContext] { + if(chainContext) + CertFreeCertificateChain(chainContext); + }); + + BOOLEAN accepted = FALSE; + if(built && chainContext) + { + constexpr DWORD softFailMask = CERT_TRUST_IS_OFFLINE_REVOCATION | CERT_TRUST_REVOCATION_STATUS_UNKNOWN; + if((chainContext->TrustStatus.dwErrorStatus & ~softFailMask) == 0) + { + const QString host = d->url.host(); + HTTPSPolicyCallbackData sslPolicy { + .cbStruct = sizeof(sslPolicy), + .dwAuthType = AUTHTYPE_SERVER, + .fdwChecks = 0, + .pwszServerName = TO_STR(host), + }; + + CERT_CHAIN_POLICY_PARA policyPara { + .cbSize = sizeof(policyPara), + .pvExtraPolicyPara = &sslPolicy, + }; + + CERT_CHAIN_POLICY_STATUS policyStatus { + .cbSize = sizeof(policyStatus), + }; + + if(CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, chainContext, &policyPara, &policyStatus) && + (policyStatus.dwError == 0 || + policyStatus.dwError == DWORD(CRYPT_E_NO_REVOCATION_CHECK) || + policyStatus.dwError == DWORD(CRYPT_E_REVOCATION_OFFLINE))) + accepted = TRUE; + } + } + return accepted; +} +catch(...) +{ + return FALSE; +} +#endif + +LdapSearch::LdapSearch(const QString &url, +#ifdef Q_OS_WIN + const QList &caCerts, +#else + const QString &caCertPath, +#endif + QObject *parent) : QObject( parent ) , d(new Private) { d->url = QUrl(url); +#ifdef Q_OS_WIN + if(d->trustStore) + { + for(const QSslCertificate &cert: caCerts) + { + QByteArray der = cert.toDer(); + CertAddEncodedCertificateToStore(d->trustStore, X509_ASN_ENCODING, + reinterpret_cast(der.constData()), DWORD(der.size()), + CERT_STORE_ADD_ALWAYS, nullptr); + } + } +#else d->caCertPath = QFile::encodeName(caCertPath); +#endif d->timer = new QTimer(this); d->timer->setSingleShot(true); connect(d->timer, &QTimer::timeout, this, [this]{ @@ -105,6 +218,10 @@ bool LdapSearch::init() setLastError(tr("Failed to init ldap"), int(LdapGetLastError())); return false; } + + if(ssl && d->trustStore) + ldap_set_option(d->ldap, LDAP_OPT_SERVER_CERTIFICATE, &Private::verifyServerCertificate); + ULONG err = 0; #else if(!d->caCertPath.isEmpty()) @@ -138,9 +255,16 @@ bool LdapSearch::init() return false; } - if(auto err = ldap_simple_bind_s(d->ldap, nullptr, nullptr)) +#ifdef Q_OS_WIN + Private::activeInstance = d; +#endif + auto bindErr = ldap_simple_bind_s(d->ldap, nullptr, nullptr); +#ifdef Q_OS_WIN + Private::activeInstance = nullptr; +#endif + if(bindErr) { - setLastError(tr("Failed to init ldap"), err); + setLastError(tr("Failed to init ldap"), bindErr); return false; } @@ -228,4 +352,3 @@ void LdapSearch::setLastError( const QString &msg, int err ) } Q_EMIT error( res, details ); } - diff --git a/client/LdapSearch.h b/client/LdapSearch.h index fca009945..de0c35abd 100644 --- a/client/LdapSearch.h +++ b/client/LdapSearch.h @@ -27,7 +27,11 @@ class LdapSearch final: public QObject Q_OBJECT public: +#ifdef Q_OS_WIN + LdapSearch(const QString &url, const QList &caCerts = {}, QObject *parent = nullptr); +#else LdapSearch(const QString &url, const QString &caCertPath = {}, QObject *parent = nullptr); +#endif ~LdapSearch() final; void search(const QString &search, const QVariantMap &userData); @@ -40,6 +44,6 @@ class LdapSearch final: public QObject bool init(); void setLastError( const QString &msg, int err ); - class Private; + struct Private; Private *d; }; diff --git a/client/dialogs/AddRecipients.cpp b/client/dialogs/AddRecipients.cpp index a81ecbfd5..9c1ffca21 100644 --- a/client/dialogs/AddRecipients.cpp +++ b/client/dialogs/AddRecipients.cpp @@ -44,21 +44,31 @@ AddRecipients::AddRecipients(ItemList* itemList, QWidget *parent) : QDialog(parent) , ui(new Ui::AddRecipients) { -#ifndef Q_OS_WIN - if(const auto list = Application::confValue(QLatin1String("LDAP-CERTS")).toArray(); - !list.isEmpty() && ldapCACerts.open()) +#ifdef Q_OS_WIN + QList ldapTrust; + for(const auto &entry: Application::confValue(QLatin1String("LDAP-CERTS")).toArray()) + { + if(QSslCertificate cert(QByteArray::fromBase64(entry.toString().toLatin1()), QSsl::Der); !cert.isNull()) + ldapTrust.append(cert); + } +#else + if(ldapCACerts.open()) { - for(const auto &entry : list) - ldapCACerts.write(QSslCertificate(QByteArray::fromBase64(entry.toString().toLatin1()), QSsl::Der).toPem()); + for(const auto &entry: Application::confValue(QLatin1String("LDAP-CERTS")).toArray()) + { + if(QSslCertificate cert(QByteArray::fromBase64(entry.toString().toLatin1()), QSsl::Der); !cert.isNull()) + ldapCACerts.write(cert.toPem()); + } ldapCACerts.close(); } + const QString ldapTrust = ldapCACerts.fileName(); #endif - ldap_corp = new LdapSearch(Application::confValue(QLatin1String("LDAP-CORP-URL")).toString(QStringLiteral("ldaps://k3.ldap.sk.ee")), ldapCACerts.fileName(), this); + ldap_corp = new LdapSearch(Application::confValue(QLatin1String("LDAP-CORP-URL")).toString(QStringLiteral("ldaps://k3.ldap.sk.ee")), ldapTrust, this); for(const auto list = Application::confValue(QLatin1String("LDAP-PERSON-URLS")).toArray(); auto url: list) - ldap_person.append(new LdapSearch(url.toString(), ldapCACerts.fileName(), this)); + ldap_person.append(new LdapSearch(url.toString(), ldapTrust, this)); if(ldap_person.isEmpty()) { - ldap_person.append(new LdapSearch(QStringLiteral("ldaps://esteid.ldap.sk.ee"), ldapCACerts.fileName(), this)); - ldap_person.append(new LdapSearch(QStringLiteral("ldaps://ldap.eidpki.ee/dc=eidpki,dc=ee"), ldapCACerts.fileName(), this)); + ldap_person.append(new LdapSearch(QStringLiteral("ldaps://esteid.ldap.sk.ee"), ldapTrust, this)); + ldap_person.append(new LdapSearch(QStringLiteral("ldaps://ldap.eidpki.ee/dc=eidpki,dc=ee"), ldapTrust, this)); } ui->setupUi(this); diff --git a/client/dialogs/AddRecipients.h b/client/dialogs/AddRecipients.h index 3c0b6b339..d10a845c7 100644 --- a/client/dialogs/AddRecipients.h +++ b/client/dialogs/AddRecipients.h @@ -22,7 +22,9 @@ #include "CertificateHistory.h" #include +#ifndef Q_OS_WIN #include +#endif namespace Ui { class AddRecipients; @@ -62,6 +64,8 @@ class AddRecipients final : public QDialog LdapSearch *ldap_corp; int multiSearch = 0; +#ifndef Q_OS_WIN QTemporaryFile ldapCACerts; +#endif HistoryList historyCertData; };