| /**************************************************************************** |
| ** |
| ** Copyright (C) 2018 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtNetwork module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| // #define QSSLSOCKET_DEBUG |
| |
| #include "qssl_p.h" |
| #include "qsslsocket.h" |
| #include "qsslsocket_schannel_p.h" |
| #include "qsslcertificate.h" |
| #include "qsslcertificateextension.h" |
| #include "qsslcertificate_p.h" |
| #include "qsslcipher_p.h" |
| |
| #include <QtCore/qscopeguard.h> |
| #include <QtCore/qoperatingsystemversion.h> |
| #include <QtCore/qregularexpression.h> |
| #include <QtCore/qdatastream.h> |
| #include <QtCore/qmutex.h> |
| |
| #define SECURITY_WIN32 |
| #include <security.h> |
| #include <schnlsp.h> |
| |
| #if NTDDI_VERSION >= NTDDI_WINBLUE && !defined(Q_CC_MINGW) |
| // ALPN = Application Layer Protocol Negotiation |
| #define SUPPORTS_ALPN 1 |
| #endif |
| |
| // Not defined in MinGW |
| #ifndef SECBUFFER_ALERT |
| #define SECBUFFER_ALERT 17 |
| #endif |
| #ifndef SECPKG_ATTR_APPLICATION_PROTOCOL |
| #define SECPKG_ATTR_APPLICATION_PROTOCOL 35 |
| #endif |
| |
| // Another missing MinGW define |
| #ifndef SEC_E_APPLICATION_PROTOCOL_MISMATCH |
| #define SEC_E_APPLICATION_PROTOCOL_MISMATCH _HRESULT_TYPEDEF_(0x80090367L) |
| #endif |
| |
| // Also not defined in MinGW....... |
| #ifndef SP_PROT_TLS1_SERVER |
| #define SP_PROT_TLS1_SERVER 0x00000040 |
| #endif |
| #ifndef SP_PROT_TLS1_CLIENT |
| #define SP_PROT_TLS1_CLIENT 0x00000080 |
| #endif |
| #ifndef SP_PROT_TLS1_0_SERVER |
| #define SP_PROT_TLS1_0_SERVER SP_PROT_TLS1_SERVER |
| #endif |
| #ifndef SP_PROT_TLS1_0_CLIENT |
| #define SP_PROT_TLS1_0_CLIENT SP_PROT_TLS1_CLIENT |
| #endif |
| #ifndef SP_PROT_TLS1_0 |
| #define SP_PROT_TLS1_0 (SP_PROT_TLS1_0_CLIENT | SP_PROT_TLS1_0_SERVER) |
| #endif |
| #ifndef SP_PROT_TLS1_1_SERVER |
| #define SP_PROT_TLS1_1_SERVER 0x00000100 |
| #endif |
| #ifndef SP_PROT_TLS1_1_CLIENT |
| #define SP_PROT_TLS1_1_CLIENT 0x00000200 |
| #endif |
| #ifndef SP_PROT_TLS1_1 |
| #define SP_PROT_TLS1_1 (SP_PROT_TLS1_1_CLIENT | SP_PROT_TLS1_1_SERVER) |
| #endif |
| #ifndef SP_PROT_TLS1_2_SERVER |
| #define SP_PROT_TLS1_2_SERVER 0x00000400 |
| #endif |
| #ifndef SP_PROT_TLS1_2_CLIENT |
| #define SP_PROT_TLS1_2_CLIENT 0x00000800 |
| #endif |
| #ifndef SP_PROT_TLS1_2 |
| #define SP_PROT_TLS1_2 (SP_PROT_TLS1_2_CLIENT | SP_PROT_TLS1_2_SERVER) |
| #endif |
| #ifndef SP_PROT_TLS1_3_SERVER |
| #define SP_PROT_TLS1_3_SERVER 0x00001000 |
| #endif |
| #ifndef SP_PROT_TLS1_3_CLIENT |
| #define SP_PROT_TLS1_3_CLIENT 0x00002000 |
| #endif |
| #ifndef SP_PROT_TLS1_3 |
| #define SP_PROT_TLS1_3 (SP_PROT_TLS1_3_CLIENT | SP_PROT_TLS1_3_SERVER) |
| #endif |
| |
| /* |
| @future!: |
| |
| - Transmitting intermediate certificates |
| - Look for a way to avoid putting intermediate certificates in the certificate store |
| - No documentation on how to send the chain |
| - A stackoverflow question on this from 3 years ago implies schannel only sends intermediate |
| certificates if it's "in the system or user certificate store". |
| - https://stackoverflow.com/q/30156584/2493610 |
| - This can be done by users, but we shouldn't add any and all local intermediate |
| certs to the stores automatically. |
| - PSK support |
| - Was added in Windows 10 (it seems), documentation at time of writing is sparse/non-existent. |
| - Specifically about how to supply credentials when they're requested. |
| - Or how to recognize that they're requested in the first place. |
| - Skip certificate verification. |
| - Check if "PSK-only" is still required to do PSK _at all_ (all-around bad solution). |
| - Check if SEC_I_INCOMPLETE_CREDENTIALS is still returned for both "missing certificate" and |
| "missing PSK" when calling InitializeSecurityContext in "performHandshake". |
| |
| Medium priority: |
| - Setting cipher-suites (or ALG_ID) |
| - People have survived without it in WinRT |
| |
| Low priority: |
| - Possibly make RAII wrappers for SecBuffer (which I commonly create QScopeGuards for) |
| |
| */ |
| |
| QT_BEGIN_NAMESPACE |
| |
| namespace { |
| SecBuffer createSecBuffer(void *ptr, unsigned long length, unsigned long bufferType) |
| { |
| return SecBuffer{ length, bufferType, ptr }; |
| } |
| |
| SecBuffer createSecBuffer(QByteArray &buffer, unsigned long bufferType) |
| { |
| return createSecBuffer(buffer.data(), static_cast<unsigned long>(buffer.length()), bufferType); |
| } |
| |
| QString schannelErrorToString(qint32 status) |
| { |
| switch (status) { |
| case SEC_E_INSUFFICIENT_MEMORY: |
| return QSslSocket::tr("Insufficient memory"); |
| case SEC_E_INTERNAL_ERROR: |
| return QSslSocket::tr("Internal error"); |
| case SEC_E_INVALID_HANDLE: |
| return QSslSocket::tr("An internal handle was invalid"); |
| case SEC_E_INVALID_TOKEN: |
| return QSslSocket::tr("An internal token was invalid"); |
| case SEC_E_LOGON_DENIED: |
| // According to the link below we get this error when Schannel receives TLS1_ALERT_ACCESS_DENIED |
| // https://docs.microsoft.com/en-us/windows/desktop/secauthn/schannel-error-codes-for-tls-and-ssl-alerts |
| return QSslSocket::tr("Access denied"); |
| case SEC_E_NO_AUTHENTICATING_AUTHORITY: |
| return QSslSocket::tr("No authority could be contacted for authorization"); |
| case SEC_E_NO_CREDENTIALS: |
| return QSslSocket::tr("No credentials"); |
| case SEC_E_TARGET_UNKNOWN: |
| return QSslSocket::tr("The target is unknown or unreachable"); |
| case SEC_E_UNSUPPORTED_FUNCTION: |
| return QSslSocket::tr("An unsupported function was requested"); |
| case SEC_E_WRONG_PRINCIPAL: |
| // SNI error |
| return QSslSocket::tr("The hostname provided does not match the one received from the peer"); |
| case SEC_E_APPLICATION_PROTOCOL_MISMATCH: |
| return QSslSocket::tr("No common protocol exists between the client and the server"); |
| case SEC_E_ILLEGAL_MESSAGE: |
| return QSslSocket::tr("Unexpected or badly-formatted message received"); |
| case SEC_E_ENCRYPT_FAILURE: |
| return QSslSocket::tr("The data could not be encrypted"); |
| case SEC_E_ALGORITHM_MISMATCH: |
| return QSslSocket::tr("No cipher suites in common"); |
| case SEC_E_UNKNOWN_CREDENTIALS: |
| // This can mean "invalid argument" in some cases... |
| return QSslSocket::tr("The credentials were not recognized / Invalid argument"); |
| case SEC_E_MESSAGE_ALTERED: |
| // According to the Internet it also triggers for messages that are out of order. |
| // https://microsoft.public.platformsdk.security.narkive.com/4JAvlMvD/help-please-schannel-security-contexts-and-decryptmessage |
| return QSslSocket::tr("The message was tampered with, damaged or out of sequence."); |
| case SEC_E_OUT_OF_SEQUENCE: |
| return QSslSocket::tr("A message was received out of sequence."); |
| case SEC_E_CONTEXT_EXPIRED: |
| return QSslSocket::tr("The TLS/SSL connection has been closed"); |
| default: |
| return QSslSocket::tr("Unknown error occurred: %1").arg(status); |
| } |
| } |
| |
| DWORD toSchannelProtocol(QSsl::SslProtocol protocol) |
| { |
| DWORD protocols = SP_PROT_NONE; |
| switch (protocol) { |
| case QSsl::UnknownProtocol: |
| return DWORD(-1); |
| case QSsl::DtlsV1_0: |
| case QSsl::DtlsV1_2: |
| case QSsl::DtlsV1_0OrLater: |
| case QSsl::DtlsV1_2OrLater: |
| return DWORD(-1); // Not supported at the moment (@future) |
| case QSsl::AnyProtocol: |
| protocols = SP_PROT_TLS1_0 | SP_PROT_TLS1_1 | SP_PROT_TLS1_2; |
| // @future Add TLS 1.3 when supported by Windows! |
| break; |
| case QSsl::SslV2: |
| case QSsl::SslV3: |
| return DWORD(-1); // Not supported |
| case QSsl::TlsV1SslV3: |
| protocols = SP_PROT_TLS1_0; |
| break; |
| case QSsl::TlsV1_0: |
| protocols = SP_PROT_TLS1_0; |
| break; |
| case QSsl::TlsV1_1: |
| protocols = SP_PROT_TLS1_1; |
| break; |
| case QSsl::TlsV1_2: |
| protocols = SP_PROT_TLS1_2; |
| break; |
| case QSsl::TlsV1_3: |
| if ((false)) // @future[0/1] Replace with version check once it's supported in Windows |
| protocols = SP_PROT_TLS1_3; |
| else |
| protocols = DWORD(-1); |
| break; |
| case QSsl::SecureProtocols: // TLS v1.0 and later is currently considered secure |
| case QSsl::TlsV1_0OrLater: |
| // For the "OrLater" protocols we fall through from one to the next, adding all of them |
| // in ascending order |
| protocols = SP_PROT_TLS1_0; |
| Q_FALLTHROUGH(); |
| case QSsl::TlsV1_1OrLater: |
| protocols |= SP_PROT_TLS1_1; |
| Q_FALLTHROUGH(); |
| case QSsl::TlsV1_2OrLater: |
| protocols |= SP_PROT_TLS1_2; |
| Q_FALLTHROUGH(); |
| case QSsl::TlsV1_3OrLater: |
| if ((false)) // @future[1/1] Also replace this with a version check |
| protocols |= SP_PROT_TLS1_3; |
| else if (protocol == QSsl::TlsV1_3OrLater) |
| protocols = DWORD(-1); // if TlsV1_3OrLater was specifically chosen we should fail |
| break; |
| } |
| return protocols; |
| } |
| |
| /*! |
| \internal |
| Used when converting the established session's \a protocol back to |
| Qt's own SslProtocol type. |
| |
| Only one protocol should be passed in at a time. |
| */ |
| QSsl::SslProtocol toQtSslProtocol(DWORD protocol) |
| { |
| #define MAP_PROTOCOL(sp_protocol, q_protocol) \ |
| if (protocol & sp_protocol) { \ |
| Q_ASSERT(!(protocol & ~sp_protocol)); \ |
| return q_protocol; \ |
| } |
| |
| MAP_PROTOCOL(SP_PROT_TLS1_0, QSsl::TlsV1_0) |
| MAP_PROTOCOL(SP_PROT_TLS1_1, QSsl::TlsV1_1) |
| MAP_PROTOCOL(SP_PROT_TLS1_2, QSsl::TlsV1_2) |
| MAP_PROTOCOL(SP_PROT_TLS1_3, QSsl::TlsV1_3) |
| #undef MAP_PROTOCOL |
| Q_UNREACHABLE(); |
| return QSsl::UnknownProtocol; |
| } |
| |
| /*! |
| \internal |
| Used by verifyCertContext to check if a client cert is used by a server or vice versa. |
| */ |
| bool netscapeWrongCertType(const QList<QSslCertificateExtension> &extensions, bool isClient) |
| { |
| const auto netscapeIt = std::find_if( |
| extensions.cbegin(), extensions.cend(), |
| [](const QSslCertificateExtension &extension) { |
| const auto netscapeCertType = QStringLiteral("2.16.840.1.113730.1.1"); |
| return extension.oid() == netscapeCertType; |
| }); |
| if (netscapeIt != extensions.cend()) { |
| const QByteArray netscapeCertTypeByte = netscapeIt->value().toByteArray(); |
| int netscapeCertType = 0; |
| QDataStream dataStream(netscapeCertTypeByte); |
| dataStream >> netscapeCertType; |
| if (dataStream.status() != QDataStream::Status::Ok) |
| return true; |
| const int expectedPeerCertType = isClient ? NETSCAPE_SSL_SERVER_AUTH_CERT_TYPE |
| : NETSCAPE_SSL_CLIENT_AUTH_CERT_TYPE; |
| if ((netscapeCertType & expectedPeerCertType) == 0) |
| return true; |
| } |
| return false; |
| } |
| |
| /*! |
| \internal |
| Used by verifyCertContext to check the basicConstraints certificate |
| extension to see if the certificate is a certificate authority. |
| Returns false if the certificate does not have the basicConstraints |
| extension or if it is not a certificate authority. |
| */ |
| bool isCertificateAuthority(const QList<QSslCertificateExtension> &extensions) |
| { |
| auto it = std::find_if(extensions.cbegin(), extensions.cend(), |
| [](const QSslCertificateExtension &extension) { |
| return extension.name() == QLatin1String("basicConstraints"); |
| }); |
| if (it != extensions.cend()) { |
| QVariantMap basicConstraints = it->value().toMap(); |
| return basicConstraints.value(QLatin1String("ca"), false).toBool(); |
| } |
| return false; |
| } |
| |
| /*! |
| \internal |
| Returns true if the attributes we requested from the context/handshake have |
| been given. |
| */ |
| bool matchesContextRequirements(DWORD attributes, DWORD requirements, |
| QSslSocket::PeerVerifyMode verifyMode, |
| bool isClient) |
| { |
| #ifdef QSSLSOCKET_DEBUG |
| #define DEBUG_WARN(message) qCWarning(lcSsl, message) |
| #else |
| #define DEBUG_WARN(message) |
| #endif |
| |
| #define CHECK_ATTRIBUTE(attributeName) \ |
| do { \ |
| const DWORD req##attributeName = isClient ? ISC_REQ_##attributeName : ASC_REQ_##attributeName; \ |
| const DWORD ret##attributeName = isClient ? ISC_RET_##attributeName : ASC_RET_##attributeName; \ |
| if (!(requirements & req##attributeName) != !(attributes & ret##attributeName)) { \ |
| DEBUG_WARN("Missing attribute \"" #attributeName "\""); \ |
| return false; \ |
| } \ |
| } while (false) |
| |
| CHECK_ATTRIBUTE(CONFIDENTIALITY); |
| CHECK_ATTRIBUTE(REPLAY_DETECT); |
| CHECK_ATTRIBUTE(SEQUENCE_DETECT); |
| CHECK_ATTRIBUTE(STREAM); |
| if (verifyMode == QSslSocket::PeerVerifyMode::VerifyPeer) |
| CHECK_ATTRIBUTE(MUTUAL_AUTH); |
| |
| // This one is manual because there is no server / ASC_ version |
| if (isClient) { |
| const auto reqManualCredValidation = ISC_REQ_MANUAL_CRED_VALIDATION; |
| const auto retManualCredValidation = ISC_RET_MANUAL_CRED_VALIDATION; |
| if (!(requirements & reqManualCredValidation) != !(attributes & retManualCredValidation)) { |
| DEBUG_WARN("Missing attribute \"MANUAL_CRED_VALIDATION\""); |
| return false; |
| } |
| } |
| |
| return true; |
| #undef CHECK_ATTRIBUTE |
| #undef DEBUG_WARN |
| } |
| |
| template<typename Required, typename Actual> |
| Required const_reinterpret_cast(Actual *p) |
| { |
| return Required(p); |
| } |
| |
| #ifdef SUPPORTS_ALPN |
| bool supportsAlpn() |
| { |
| return QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8_1; |
| } |
| |
| QByteArray createAlpnString(const QByteArrayList &nextAllowedProtocols) |
| { |
| QByteArray alpnString; |
| if (!nextAllowedProtocols.isEmpty() && supportsAlpn()) { |
| const QByteArray names = [&nextAllowedProtocols]() { |
| QByteArray protocolString; |
| for (QByteArray proto : nextAllowedProtocols) { |
| if (proto.size() > 255) { |
| qCWarning(lcSsl) << "TLS ALPN extension" << proto |
| << "is too long and will be ignored."; |
| continue; |
| } else if (proto.isEmpty()) { |
| continue; |
| } |
| protocolString += char(proto.length()) + proto; |
| } |
| return protocolString; |
| }(); |
| if (names.isEmpty()) |
| return alpnString; |
| |
| const quint16 namesSize = names.size(); |
| const quint32 alpnId = SecApplicationProtocolNegotiationExt_ALPN; |
| const quint32 totalSize = sizeof(alpnId) + sizeof(namesSize) + namesSize; |
| alpnString = QByteArray::fromRawData(reinterpret_cast<const char *>(&totalSize), sizeof(totalSize)) |
| + QByteArray::fromRawData(reinterpret_cast<const char *>(&alpnId), sizeof(alpnId)) |
| + QByteArray::fromRawData(reinterpret_cast<const char *>(&namesSize), sizeof(namesSize)) |
| + names; |
| } |
| return alpnString; |
| } |
| #endif // SUPPORTS_ALPN |
| } // anonymous namespace |
| |
| bool QSslSocketPrivate::s_loadRootCertsOnDemand = true; |
| bool QSslSocketPrivate::s_loadedCiphersAndCerts = false; |
| Q_GLOBAL_STATIC(QRecursiveMutex, qt_schannel_mutex) |
| |
| void QSslSocketPrivate::ensureInitialized() |
| { |
| const QMutexLocker locker(qt_schannel_mutex); |
| if (s_loadedCiphersAndCerts) |
| return; |
| s_loadedCiphersAndCerts = true; |
| |
| setDefaultCaCertificates(systemCaCertificates()); |
| s_loadRootCertsOnDemand = true; // setDefaultCaCertificates sets it to false, re-enable it. |
| |
| resetDefaultCiphers(); |
| } |
| |
| void QSslSocketPrivate::resetDefaultCiphers() |
| { |
| setDefaultSupportedCiphers(QSslSocketBackendPrivate::defaultCiphers()); |
| setDefaultCiphers(QSslSocketBackendPrivate::defaultCiphers()); |
| } |
| |
| void QSslSocketPrivate::resetDefaultEllipticCurves() |
| { |
| Q_UNIMPLEMENTED(); |
| } |
| |
| bool QSslSocketPrivate::supportsSsl() |
| { |
| return true; |
| } |
| |
| QList<QSslCertificate> QSslSocketPrivate::systemCaCertificates() |
| { |
| // Copied from qsslsocket_openssl.cpp's systemCaCertificates function. |
| QList<QSslCertificate> systemCerts; |
| auto hSystemStore = QHCertStorePointer(CertOpenSystemStore(0, L"ROOT")); |
| if (hSystemStore) { |
| PCCERT_CONTEXT pc = nullptr; |
| while ((pc = CertFindCertificateInStore(hSystemStore.get(), X509_ASN_ENCODING, 0, |
| CERT_FIND_ANY, nullptr, pc))) { |
| systemCerts.append(QSslCertificatePrivate::QSslCertificate_from_CERT_CONTEXT(pc)); |
| } |
| } |
| return systemCerts; |
| } |
| |
| long QSslSocketPrivate::sslLibraryVersionNumber() |
| { |
| const auto os = QOperatingSystemVersion::current(); |
| return (os.majorVersion() << 24) | ((os.minorVersion() & 0xFF) << 16) | (os.microVersion() & 0xFFFF); |
| } |
| |
| QString QSslSocketPrivate::sslLibraryVersionString() |
| { |
| const auto os = QOperatingSystemVersion::current(); |
| return QString::fromLatin1("Secure Channel, %1 %2.%3.%4") |
| .arg(os.name(), |
| QString::number(os.majorVersion()), |
| QString::number(os.minorVersion()), |
| QString::number(os.microVersion())); |
| } |
| |
| long QSslSocketPrivate::sslLibraryBuildVersionNumber() |
| { |
| // There is no separate build version |
| return sslLibraryVersionNumber(); |
| } |
| |
| QString QSslSocketPrivate::sslLibraryBuildVersionString() |
| { |
| const auto os = QOperatingSystemVersion::current(); |
| return QString::fromLatin1("%1.%2.%3") |
| .arg(QString::number(os.majorVersion()), |
| QString::number(os.minorVersion()), |
| QString::number(os.microVersion())); |
| } |
| |
| QSslSocketBackendPrivate::QSslSocketBackendPrivate() |
| { |
| SecInvalidateHandle(&credentialHandle); |
| SecInvalidateHandle(&contextHandle); |
| ensureInitialized(); |
| } |
| |
| QSslSocketBackendPrivate::~QSslSocketBackendPrivate() |
| { |
| closeCertificateStores(); |
| deallocateContext(); |
| freeCredentialsHandle(); |
| CertFreeCertificateContext(localCertContext); |
| } |
| |
| bool QSslSocketBackendPrivate::sendToken(void *token, unsigned long tokenLength, bool emitError) |
| { |
| if (tokenLength == 0) |
| return true; |
| const qint64 written = plainSocket->write(static_cast<const char *>(token), tokenLength); |
| if (written != qint64(tokenLength)) { |
| // Failed to write/buffer everything or an error occurred |
| if (emitError) |
| setErrorAndEmit(plainSocket->error(), plainSocket->errorString()); |
| return false; |
| } |
| return true; |
| } |
| |
| QString QSslSocketBackendPrivate::targetName() const |
| { |
| // Used for SNI extension |
| return verificationPeerName.isEmpty() ? q_func()->peerName() : verificationPeerName; |
| } |
| |
| ULONG QSslSocketBackendPrivate::getContextRequirements() |
| { |
| const bool isClient = mode == QSslSocket::SslClientMode; |
| ULONG req = 0; |
| |
| req |= ISC_REQ_ALLOCATE_MEMORY; // Allocate memory for buffers automatically |
| req |= ISC_REQ_CONFIDENTIALITY; // Encrypt messages |
| req |= ISC_REQ_REPLAY_DETECT; // Detect replayed messages |
| req |= ISC_REQ_SEQUENCE_DETECT; // Detect out of sequence messages |
| req |= ISC_REQ_STREAM; // Support a stream-oriented connection |
| |
| if (isClient) { |
| req |= ISC_REQ_MANUAL_CRED_VALIDATION; // Manually validate certificate |
| } else { |
| switch (configuration.peerVerifyMode) { |
| case QSslSocket::PeerVerifyMode::VerifyNone: |
| // There doesn't seem to be a way to ask for an optional client cert :-( |
| case QSslSocket::PeerVerifyMode::AutoVerifyPeer: |
| case QSslSocket::PeerVerifyMode::QueryPeer: |
| break; |
| case QSslSocket::PeerVerifyMode::VerifyPeer: |
| req |= ISC_REQ_MUTUAL_AUTH; |
| break; |
| } |
| } |
| |
| return req; |
| } |
| |
| bool QSslSocketBackendPrivate::acquireCredentialsHandle() |
| { |
| Q_ASSERT(schannelState == SchannelState::InitializeHandshake); |
| |
| const bool isClient = mode == QSslSocket::SslClientMode; |
| const DWORD protocols = toSchannelProtocol(configuration.protocol); |
| if (protocols == DWORD(-1)) { |
| setErrorAndEmit(QAbstractSocket::SslInvalidUserDataError, |
| QSslSocket::tr("Invalid protocol chosen")); |
| return false; |
| } |
| |
| const CERT_CHAIN_CONTEXT *chainContext = nullptr; |
| auto freeCertChain = qScopeGuard([&chainContext]() { |
| if (chainContext) |
| CertFreeCertificateChain(chainContext); |
| }); |
| |
| DWORD certsCount = 0; |
| // Set up our certificate stores before trying to use one... |
| initializeCertificateStores(); |
| |
| // Check if user has specified a certificate chain but it could not be loaded. |
| // This happens if there was something wrong with the certificate chain or there was no private |
| // key. |
| if (!configuration.localCertificateChain.isEmpty() && !localCertificateStore) |
| return true; // 'true' because "tst_QSslSocket::setEmptyKey" expects us to not disconnect |
| |
| if (localCertificateStore != nullptr) { |
| CERT_CHAIN_FIND_BY_ISSUER_PARA findParam; |
| ZeroMemory(&findParam, sizeof(findParam)); |
| findParam.cbSize = sizeof(findParam); |
| findParam.pszUsageIdentifier = isClient ? szOID_PKIX_KP_CLIENT_AUTH : szOID_PKIX_KP_SERVER_AUTH; |
| |
| // There should only be one chain in our store, so.. we grab that one. |
| chainContext = CertFindChainInStore(localCertificateStore.get(), |
| X509_ASN_ENCODING, |
| 0, |
| CERT_CHAIN_FIND_BY_ISSUER, |
| &findParam, |
| nullptr); |
| if (!chainContext) { |
| const QString message = isClient |
| ? QSslSocket::tr("The certificate provided cannot be used for a client.") |
| : QSslSocket::tr("The certificate provided cannot be used for a server."); |
| setErrorAndEmit(QAbstractSocket::SocketError::SslInvalidUserDataError, message); |
| return false; |
| } |
| Q_ASSERT(chainContext->cChain == 1); |
| Q_ASSERT(chainContext->rgpChain[0]); |
| Q_ASSERT(chainContext->rgpChain[0]->cbSize >= 1); |
| Q_ASSERT(chainContext->rgpChain[0]->rgpElement[0]); |
| Q_ASSERT(!localCertContext); |
| localCertContext = CertDuplicateCertificateContext(chainContext->rgpChain[0] |
| ->rgpElement[0] |
| ->pCertContext); |
| certsCount = 1; |
| Q_ASSERT(localCertContext); |
| } |
| |
| SCHANNEL_CRED cred{ |
| SCHANNEL_CRED_VERSION, // dwVersion |
| certsCount, // cCreds |
| &localCertContext, // paCred (certificate(s) containing a private key for authentication) |
| nullptr, // hRootStore |
| |
| 0, // cMappers (reserved) |
| nullptr, // aphMappers (reserved) |
| |
| 0, // cSupportedAlgs |
| nullptr, // palgSupportedAlgs (nullptr = system default) @future: QSslCipher-related |
| |
| protocols, // grbitEnabledProtocols |
| 0, // dwMinimumCipherStrength (0 = system default) |
| 0, // dwMaximumCipherStrength (0 = system default) |
| 0, // dwSessionLifespan (0 = schannel default, 10 hours) |
| SCH_CRED_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT |
| | SCH_CRED_NO_DEFAULT_CREDS, // dwFlags |
| 0 // dwCredFormat (must be 0) |
| }; |
| |
| TimeStamp expiration{}; |
| auto status = AcquireCredentialsHandle(nullptr, // pszPrincipal (unused) |
| const_cast<wchar_t *>(UNISP_NAME), // pszPackage |
| isClient ? SECPKG_CRED_OUTBOUND : SECPKG_CRED_INBOUND, // fCredentialUse |
| nullptr, // pvLogonID (unused) |
| &cred, // pAuthData |
| nullptr, // pGetKeyFn (unused) |
| nullptr, // pvGetKeyArgument (unused) |
| &credentialHandle, // phCredential |
| &expiration // ptsExpir |
| ); |
| |
| if (status != SEC_E_OK) { |
| setErrorAndEmit(QAbstractSocket::SslInternalError, schannelErrorToString(status)); |
| return false; |
| } |
| return true; |
| } |
| |
| void QSslSocketBackendPrivate::deallocateContext() |
| { |
| if (SecIsValidHandle(&contextHandle)) { |
| DeleteSecurityContext(&contextHandle); |
| SecInvalidateHandle(&contextHandle); |
| } |
| } |
| |
| void QSslSocketBackendPrivate::freeCredentialsHandle() |
| { |
| if (SecIsValidHandle(&credentialHandle)) { |
| FreeCredentialsHandle(&credentialHandle); |
| SecInvalidateHandle(&credentialHandle); |
| } |
| } |
| |
| void QSslSocketBackendPrivate::closeCertificateStores() |
| { |
| localCertificateStore.reset(); |
| peerCertificateStore.reset(); |
| caCertificateStore.reset(); |
| } |
| |
| bool QSslSocketBackendPrivate::createContext() |
| { |
| Q_ASSERT(SecIsValidHandle(&credentialHandle)); |
| Q_ASSERT(schannelState == SchannelState::InitializeHandshake); |
| Q_ASSERT(mode == QSslSocket::SslClientMode); |
| ULONG contextReq = getContextRequirements(); |
| |
| SecBuffer outBuffers[3]; |
| outBuffers[0] = createSecBuffer(nullptr, 0, SECBUFFER_TOKEN); |
| outBuffers[1] = createSecBuffer(nullptr, 0, SECBUFFER_ALERT); |
| outBuffers[2] = createSecBuffer(nullptr, 0, SECBUFFER_EMPTY); |
| auto freeBuffers = qScopeGuard([&outBuffers]() { |
| for (auto i = 0ull; i < ARRAYSIZE(outBuffers); i++) { |
| if (outBuffers[i].pvBuffer) |
| FreeContextBuffer(outBuffers[i].pvBuffer); |
| } |
| }); |
| SecBufferDesc outputBufferDesc{ |
| SECBUFFER_VERSION, |
| ARRAYSIZE(outBuffers), |
| outBuffers |
| }; |
| |
| TimeStamp expiry; |
| |
| SecBufferDesc alpnBufferDesc; |
| bool useAlpn = false; |
| #ifdef SUPPORTS_ALPN |
| configuration.nextProtocolNegotiationStatus = QSslConfiguration::NextProtocolNegotiationNone; |
| QByteArray alpnString = createAlpnString(configuration.nextAllowedProtocols); |
| useAlpn = !alpnString.isEmpty(); |
| SecBuffer alpnBuffers[1]; |
| alpnBuffers[0] = createSecBuffer(alpnString, SECBUFFER_APPLICATION_PROTOCOLS); |
| alpnBufferDesc = { |
| SECBUFFER_VERSION, |
| ARRAYSIZE(alpnBuffers), |
| alpnBuffers |
| }; |
| #endif |
| |
| auto status = InitializeSecurityContext(&credentialHandle, // phCredential |
| nullptr, // phContext |
| const_reinterpret_cast<SEC_WCHAR *>(targetName().utf16()), // pszTargetName |
| contextReq, // fContextReq |
| 0, // Reserved1 |
| 0, // TargetDataRep (unused) |
| useAlpn ? &alpnBufferDesc : nullptr, // pInput |
| 0, // Reserved2 |
| &contextHandle, // phNewContext |
| &outputBufferDesc, // pOutput |
| &contextAttributes, // pfContextAttr |
| &expiry // ptsExpiry |
| ); |
| |
| // This is the first call to InitializeSecurityContext, so theoretically "CONTINUE_NEEDED" |
| // should be the only non-error return-code here. |
| if (status != SEC_I_CONTINUE_NEEDED) { |
| setErrorAndEmit(QAbstractSocket::SslInternalError, |
| QSslSocket::tr("Error creating SSL context (%1)").arg(schannelErrorToString(status))); |
| return false; |
| } |
| |
| if (!sendToken(outBuffers[0].pvBuffer, outBuffers[0].cbBuffer)) |
| return false; |
| schannelState = SchannelState::PerformHandshake; |
| return true; |
| } |
| |
| bool QSslSocketBackendPrivate::acceptContext() |
| { |
| Q_ASSERT(SecIsValidHandle(&credentialHandle)); |
| Q_ASSERT(schannelState == SchannelState::InitializeHandshake); |
| Q_ASSERT(mode == QSslSocket::SslServerMode); |
| ULONG contextReq = getContextRequirements(); |
| |
| intermediateBuffer += plainSocket->read(16384); |
| if (intermediateBuffer.isEmpty()) |
| return true; // definitely need more data.. |
| |
| SecBuffer inBuffers[2]; |
| inBuffers[0] = createSecBuffer(intermediateBuffer, SECBUFFER_TOKEN); |
| |
| #ifdef SUPPORTS_ALPN |
| configuration.nextProtocolNegotiationStatus = QSslConfiguration::NextProtocolNegotiationNone; |
| // The string must be alive when we call AcceptSecurityContext |
| QByteArray alpnString = createAlpnString(configuration.nextAllowedProtocols); |
| if (!alpnString.isEmpty()) { |
| inBuffers[1] = createSecBuffer(alpnString, SECBUFFER_APPLICATION_PROTOCOLS); |
| } else |
| #endif |
| { |
| inBuffers[1] = createSecBuffer(nullptr, 0, SECBUFFER_EMPTY); |
| } |
| |
| SecBufferDesc inputBufferDesc{ |
| SECBUFFER_VERSION, |
| ARRAYSIZE(inBuffers), |
| inBuffers |
| }; |
| |
| SecBuffer outBuffers[3]; |
| outBuffers[0] = createSecBuffer(nullptr, 0, SECBUFFER_TOKEN); |
| outBuffers[1] = createSecBuffer(nullptr, 0, SECBUFFER_ALERT); |
| outBuffers[2] = createSecBuffer(nullptr, 0, SECBUFFER_EMPTY); |
| auto freeBuffers = qScopeGuard([&outBuffers]() { |
| for (auto i = 0ull; i < ARRAYSIZE(outBuffers); i++) { |
| if (outBuffers[i].pvBuffer) |
| FreeContextBuffer(outBuffers[i].pvBuffer); |
| } |
| }); |
| SecBufferDesc outputBufferDesc{ |
| SECBUFFER_VERSION, |
| ARRAYSIZE(outBuffers), |
| outBuffers |
| }; |
| |
| TimeStamp expiry; |
| auto status = AcceptSecurityContext( |
| &credentialHandle, // phCredential |
| nullptr, // phContext |
| &inputBufferDesc, // pInput |
| contextReq, // fContextReq |
| 0, // TargetDataRep (unused) |
| &contextHandle, // phNewContext |
| &outputBufferDesc, // pOutput |
| &contextAttributes, // pfContextAttr |
| &expiry // ptsTimeStamp |
| ); |
| |
| if (status == SEC_E_INCOMPLETE_MESSAGE) { |
| // Need more data |
| return true; |
| } |
| |
| if (inBuffers[1].BufferType == SECBUFFER_EXTRA) { |
| // https://docs.microsoft.com/en-us/windows/desktop/secauthn/extra-buffers-returned-by-schannel |
| // inBuffers[1].cbBuffer indicates the amount of bytes _NOT_ processed, the rest need to |
| // be stored. |
| intermediateBuffer = intermediateBuffer.right(int(inBuffers[1].cbBuffer)); |
| } else { /* No 'extra' data, message not incomplete */ |
| intermediateBuffer.clear(); |
| } |
| |
| if (status != SEC_I_CONTINUE_NEEDED) { |
| setErrorAndEmit(QAbstractSocket::SslHandshakeFailedError, |
| QSslSocket::tr("Error creating SSL context (%1)").arg(schannelErrorToString(status))); |
| return false; |
| } |
| if (!sendToken(outBuffers[0].pvBuffer, outBuffers[0].cbBuffer)) |
| return false; |
| schannelState = SchannelState::PerformHandshake; |
| return true; |
| } |
| |
| bool QSslSocketBackendPrivate::performHandshake() |
| { |
| if (plainSocket->state() == QAbstractSocket::UnconnectedState) { |
| setErrorAndEmit(QAbstractSocket::RemoteHostClosedError, |
| QSslSocket::tr("The TLS/SSL connection has been closed")); |
| return false; |
| } |
| Q_ASSERT(SecIsValidHandle(&credentialHandle)); |
| Q_ASSERT(SecIsValidHandle(&contextHandle)); |
| Q_ASSERT(schannelState == SchannelState::PerformHandshake); |
| |
| #ifdef QSSLSOCKET_DEBUG |
| qCDebug(lcSsl) << "Bytes available from socket:" << plainSocket->bytesAvailable(); |
| qCDebug(lcSsl) << "intermediateBuffer size:" << intermediateBuffer.size(); |
| #endif |
| |
| intermediateBuffer += plainSocket->read(16384); |
| if (intermediateBuffer.isEmpty()) |
| return true; // no data, will fail |
| |
| SecBuffer inputBuffers[2]; |
| inputBuffers[0] = createSecBuffer(intermediateBuffer, SECBUFFER_TOKEN); |
| inputBuffers[1] = createSecBuffer(nullptr, 0, SECBUFFER_EMPTY); |
| SecBufferDesc inputBufferDesc{ |
| SECBUFFER_VERSION, |
| ARRAYSIZE(inputBuffers), |
| inputBuffers |
| }; |
| |
| SecBuffer outBuffers[3]; |
| outBuffers[0] = createSecBuffer(nullptr, 0, SECBUFFER_TOKEN); |
| outBuffers[1] = createSecBuffer(nullptr, 0, SECBUFFER_ALERT); |
| outBuffers[2] = createSecBuffer(nullptr, 0, SECBUFFER_EMPTY); |
| auto freeBuffers = qScopeGuard([&outBuffers]() { |
| for (auto i = 0ull; i < ARRAYSIZE(outBuffers); i++) { |
| if (outBuffers[i].pvBuffer) |
| FreeContextBuffer(outBuffers[i].pvBuffer); |
| } |
| }); |
| SecBufferDesc outputBufferDesc{ |
| SECBUFFER_VERSION, |
| ARRAYSIZE(outBuffers), |
| outBuffers |
| }; |
| |
| ULONG contextReq = getContextRequirements(); |
| TimeStamp expiry; |
| auto status = InitializeSecurityContext(&credentialHandle, // phCredential |
| &contextHandle, // phContext |
| const_reinterpret_cast<SEC_WCHAR *>(targetName().utf16()), // pszTargetName |
| contextReq, // fContextReq |
| 0, // Reserved1 |
| 0, // TargetDataRep (unused) |
| &inputBufferDesc, // pInput |
| 0, // Reserved2 |
| nullptr, // phNewContext (we already have one) |
| &outputBufferDesc, // pOutput |
| &contextAttributes, // pfContextAttr |
| &expiry // ptsExpiry |
| ); |
| |
| if (inputBuffers[1].BufferType == SECBUFFER_EXTRA) { |
| // https://docs.microsoft.com/en-us/windows/desktop/secauthn/extra-buffers-returned-by-schannel |
| // inputBuffers[1].cbBuffer indicates the amount of bytes _NOT_ processed, the rest need to |
| // be stored. |
| intermediateBuffer = intermediateBuffer.right(int(inputBuffers[1].cbBuffer)); |
| } else { |
| // Clear the buffer if we weren't asked for more data |
| if (status != SEC_E_INCOMPLETE_MESSAGE) |
| intermediateBuffer.clear(); |
| } |
| switch (status) { |
| case SEC_E_OK: |
| // Need to transmit a final token in the handshake if 'cbBuffer' is non-zero. |
| if (!sendToken(outBuffers[0].pvBuffer, outBuffers[0].cbBuffer)) |
| return false; |
| schannelState = SchannelState::VerifyHandshake; |
| return true; |
| case SEC_I_CONTINUE_NEEDED: |
| if (!sendToken(outBuffers[0].pvBuffer, outBuffers[0].cbBuffer)) |
| return false; |
| // Must call InitializeSecurityContext again later (done through continueHandshake) |
| return true; |
| case SEC_I_INCOMPLETE_CREDENTIALS: |
| // Schannel takes care of picking certificate to send (other than the one we can specify), |
| // so if we get here then that means we don't have a certificate the server accepts. |
| setErrorAndEmit(QAbstractSocket::SslHandshakeFailedError, |
| QSslSocket::tr("Server did not accept any certificate we could present.")); |
| return false; |
| case SEC_I_CONTEXT_EXPIRED: |
| // "The message sender has finished using the connection and has initiated a shutdown." |
| if (outBuffers[0].BufferType == SECBUFFER_TOKEN) { |
| if (!sendToken(outBuffers[0].pvBuffer, outBuffers[0].cbBuffer)) |
| return false; |
| } |
| if (!shutdown) { // we did not initiate this |
| setErrorAndEmit(QAbstractSocket::RemoteHostClosedError, |
| QSslSocket::tr("The TLS/SSL connection has been closed")); |
| } |
| return true; |
| case SEC_E_INCOMPLETE_MESSAGE: |
| // Simply incomplete, wait for more data |
| return true; |
| case SEC_E_ALGORITHM_MISMATCH: |
| setErrorAndEmit(QAbstractSocket::SslHandshakeFailedError, |
| QSslSocket::tr("Algorithm mismatch")); |
| shutdown = true; // skip sending the "Shutdown" alert |
| return false; |
| } |
| |
| // Note: We can get here if the connection is using TLS 1.2 and the server certificate uses |
| // MD5, which is not allowed in Schannel. This causes an "invalid token" error during handshake. |
| // (If you came here investigating an error: md5 is insecure, update your certificate) |
| setErrorAndEmit(QAbstractSocket::SslHandshakeFailedError, |
| QSslSocket::tr("Handshake failed: %1").arg(schannelErrorToString(status))); |
| return false; |
| } |
| |
| bool QSslSocketBackendPrivate::verifyHandshake() |
| { |
| Q_Q(QSslSocket); |
| sslErrors.clear(); |
| |
| const bool isClient = mode == QSslSocket::SslClientMode; |
| #define CHECK_STATUS(status) \ |
| if (status != SEC_E_OK) { \ |
| setErrorAndEmit(QAbstractSocket::SslInternalError, \ |
| QSslSocket::tr("Failed to query the TLS context: %1") \ |
| .arg(schannelErrorToString(status))); \ |
| return false; \ |
| } |
| |
| // Everything is set up, now make sure there's nothing wrong and query some attributes... |
| if (!matchesContextRequirements(contextAttributes, getContextRequirements(), |
| configuration.peerVerifyMode, isClient)) { |
| setErrorAndEmit(QAbstractSocket::SslHandshakeFailedError, |
| QSslSocket::tr("Did not get the required attributes for the connection.")); |
| return false; |
| } |
| |
| // Get stream sizes (to know the max size of a message and the size of the header and trailer) |
| auto status = QueryContextAttributes(&contextHandle, |
| SECPKG_ATTR_STREAM_SIZES, |
| &streamSizes); |
| CHECK_STATUS(status); |
| |
| // Get session cipher info |
| status = QueryContextAttributes(&contextHandle, |
| SECPKG_ATTR_CONNECTION_INFO, |
| &connectionInfo); |
| CHECK_STATUS(status); |
| |
| #ifdef SUPPORTS_ALPN |
| if (!configuration.nextAllowedProtocols.isEmpty() && supportsAlpn()) { |
| SecPkgContext_ApplicationProtocol alpn; |
| status = QueryContextAttributes(&contextHandle, |
| SECPKG_ATTR_APPLICATION_PROTOCOL, |
| &alpn); |
| CHECK_STATUS(status); |
| if (alpn.ProtoNegoStatus == SecApplicationProtocolNegotiationStatus_Success) { |
| QByteArray negotiatedProto = QByteArray((const char *)alpn.ProtocolId, |
| alpn.ProtocolIdSize); |
| if (!configuration.nextAllowedProtocols.contains(negotiatedProto)) { |
| setErrorAndEmit(QAbstractSocket::SslHandshakeFailedError, |
| QSslSocket::tr("Unwanted protocol was negotiated")); |
| return false; |
| } |
| configuration.nextNegotiatedProtocol = negotiatedProto; |
| configuration.nextProtocolNegotiationStatus = QSslConfiguration::NextProtocolNegotiationNegotiated; |
| } else { |
| configuration.nextNegotiatedProtocol = ""; |
| configuration.nextProtocolNegotiationStatus = QSslConfiguration::NextProtocolNegotiationUnsupported; |
| } |
| } |
| #endif // supports ALPN |
| |
| #undef CHECK_STATUS |
| |
| // Verify certificate |
| CERT_CONTEXT *certificateContext = nullptr; |
| auto freeCertificate = qScopeGuard([&certificateContext]() { |
| if (certificateContext) |
| CertFreeCertificateContext(certificateContext); |
| }); |
| status = QueryContextAttributes(&contextHandle, |
| SECPKG_ATTR_REMOTE_CERT_CONTEXT, |
| &certificateContext); |
| |
| // QueryPeer can (currently) not work in Schannel since Schannel itself doesn't have a way to |
| // ask for a certificate and then still be OK if it's not received. |
| // To work around this we don't request a certificate at all for QueryPeer. |
| // For servers AutoVerifyPeer is supposed to be treated the same as QueryPeer. |
| // This means that servers using Schannel will only request client certificate for "VerifyPeer". |
| if ((!isClient && configuration.peerVerifyMode == QSslSocket::PeerVerifyMode::VerifyPeer) |
| || (isClient && configuration.peerVerifyMode != QSslSocket::PeerVerifyMode::VerifyNone |
| && configuration.peerVerifyMode != QSslSocket::PeerVerifyMode::QueryPeer)) { |
| if (status != SEC_E_OK) { |
| #ifdef QSSLSOCKET_DEBUG |
| qCDebug(lcSsl) << "Couldn't retrieve peer certificate, status:" |
| << schannelErrorToString(status); |
| #endif |
| const QSslError error{ QSslError::NoPeerCertificate }; |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| } |
| |
| // verifyCertContext returns false if the user disconnected while it was checking errors. |
| if (certificateContext && !verifyCertContext(certificateContext)) |
| return false; |
| |
| if (!checkSslErrors() || state != QAbstractSocket::ConnectedState) { |
| #ifdef QSSLSOCKET_DEBUG |
| qCDebug(lcSsl) << __func__ << "was unsuccessful. Paused:" << paused; |
| #endif |
| // If we're paused then checkSslErrors returned false, but it's not an error |
| return paused && state == QAbstractSocket::ConnectedState; |
| } |
| |
| schannelState = SchannelState::Done; |
| return true; |
| } |
| |
| bool QSslSocketBackendPrivate::renegotiate() |
| { |
| SecBuffer outBuffers[3]; |
| outBuffers[0] = createSecBuffer(nullptr, 0, SECBUFFER_TOKEN); |
| outBuffers[1] = createSecBuffer(nullptr, 0, SECBUFFER_ALERT); |
| outBuffers[2] = createSecBuffer(nullptr, 0, SECBUFFER_EMPTY); |
| auto freeBuffers = qScopeGuard([&outBuffers]() { |
| for (auto i = 0ull; i < ARRAYSIZE(outBuffers); i++) { |
| if (outBuffers[i].pvBuffer) |
| FreeContextBuffer(outBuffers[i].pvBuffer); |
| } |
| }); |
| SecBufferDesc outputBufferDesc{ |
| SECBUFFER_VERSION, |
| ARRAYSIZE(outBuffers), |
| outBuffers |
| }; |
| |
| ULONG contextReq = getContextRequirements(); |
| TimeStamp expiry; |
| SECURITY_STATUS status; |
| if (mode == QSslSocket::SslClientMode) { |
| status = InitializeSecurityContext(&credentialHandle, // phCredential |
| &contextHandle, // phContext |
| const_reinterpret_cast<SEC_WCHAR *>(targetName().utf16()), // pszTargetName |
| contextReq, // fContextReq |
| 0, // Reserved1 |
| 0, // TargetDataRep (unused) |
| nullptr, // pInput (nullptr for renegotiate) |
| 0, // Reserved2 |
| nullptr, // phNewContext (we already have one) |
| &outputBufferDesc, // pOutput |
| &contextAttributes, // pfContextAttr |
| &expiry // ptsExpiry |
| ); |
| } else { |
| status = AcceptSecurityContext( |
| &credentialHandle, // phCredential |
| &contextHandle, // phContext |
| nullptr, // pInput |
| contextReq, // fContextReq |
| 0, // TargetDataRep (unused) |
| nullptr, // phNewContext |
| &outputBufferDesc, // pOutput |
| &contextAttributes, // pfContextAttr, |
| &expiry // ptsTimeStamp |
| ); |
| } |
| if (status == SEC_I_CONTINUE_NEEDED) { |
| schannelState = SchannelState::PerformHandshake; |
| return sendToken(outBuffers[0].pvBuffer, outBuffers[0].cbBuffer); |
| } |
| setErrorAndEmit(QAbstractSocket::SslHandshakeFailedError, |
| QSslSocket::tr("Renegotiation was unsuccessful: %1").arg(schannelErrorToString(status))); |
| return false; |
| } |
| |
| /*! |
| \internal |
| reset the state in preparation for reuse of socket |
| */ |
| void QSslSocketBackendPrivate::reset() |
| { |
| closeCertificateStores(); // certificate stores could've changed |
| deallocateContext(); |
| freeCredentialsHandle(); // in case we already had one (@future: session resumption requires re-use) |
| |
| connectionInfo = {}; |
| streamSizes = {}; |
| |
| CertFreeCertificateContext(localCertContext); |
| localCertContext = nullptr; |
| |
| contextAttributes = 0; |
| intermediateBuffer.clear(); |
| schannelState = SchannelState::InitializeHandshake; |
| |
| connectionEncrypted = false; |
| shutdown = false; |
| renegotiating = false; |
| } |
| |
| void QSslSocketBackendPrivate::startClientEncryption() |
| { |
| if (connectionEncrypted) |
| return; // let's not mess up the connection... |
| reset(); |
| continueHandshake(); |
| } |
| |
| void QSslSocketBackendPrivate::startServerEncryption() |
| { |
| if (connectionEncrypted) |
| return; // let's not mess up the connection... |
| reset(); |
| continueHandshake(); |
| } |
| |
| void QSslSocketBackendPrivate::transmit() |
| { |
| Q_Q(QSslSocket); |
| |
| // Can happen if called through QSslSocket::abort->QSslSocket::close->QSslSocket::flush->here |
| if (plainSocket->state() == QAbstractSocket::SocketState::UnconnectedState) |
| return; |
| |
| if (schannelState != SchannelState::Done) { |
| continueHandshake(); |
| return; |
| } |
| |
| if (connectionEncrypted) { // encrypt data in writeBuffer and write it to plainSocket |
| qint64 totalBytesWritten = 0; |
| qint64 writeBufferSize; |
| while ((writeBufferSize = writeBuffer.size()) > 0) { |
| const int headerSize = int(streamSizes.cbHeader); |
| const int trailerSize = int(streamSizes.cbTrailer); |
| // Try to read 'cbMaximumMessage' bytes from buffer before encrypting. |
| const int size = int(std::min(writeBufferSize, qint64(streamSizes.cbMaximumMessage))); |
| QByteArray fullMessage(headerSize + trailerSize + size, Qt::Uninitialized); |
| { |
| // Use peek() here instead of read() so we don't lose data if encryption fails. |
| qint64 copied = writeBuffer.peek(fullMessage.data() + headerSize, size); |
| Q_ASSERT(copied == size); |
| } |
| |
| SecBuffer inputBuffers[4]{ |
| createSecBuffer(fullMessage.data(), headerSize, SECBUFFER_STREAM_HEADER), |
| createSecBuffer(fullMessage.data() + headerSize, size, SECBUFFER_DATA), |
| createSecBuffer(fullMessage.data() + headerSize + size, trailerSize, SECBUFFER_STREAM_TRAILER), |
| createSecBuffer(nullptr, 0, SECBUFFER_EMPTY) |
| }; |
| SecBufferDesc message{ |
| SECBUFFER_VERSION, |
| ARRAYSIZE(inputBuffers), |
| inputBuffers |
| }; |
| auto status = EncryptMessage(&contextHandle, 0, &message, 0); |
| if (status != SEC_E_OK) { |
| setErrorAndEmit(QAbstractSocket::SslInternalError, |
| QSslSocket::tr("Schannel failed to encrypt data: %1") |
| .arg(schannelErrorToString(status))); |
| return; |
| } |
| // Data was encrypted successfully, so we free() what we peek()ed earlier |
| writeBuffer.free(size); |
| |
| // The trailer's size is not final, so resize fullMessage to not send trailing junk |
| fullMessage.resize(inputBuffers[0].cbBuffer + inputBuffers[1].cbBuffer + inputBuffers[2].cbBuffer); |
| const qint64 bytesWritten = plainSocket->write(fullMessage); |
| #ifdef QSSLSOCKET_DEBUG |
| qCDebug(lcSsl) << "Wrote" << bytesWritten << "of total" |
| << fullMessage.length() << "bytes"; |
| #endif |
| if (bytesWritten >= 0) { |
| totalBytesWritten += bytesWritten; |
| } else { |
| setErrorAndEmit(plainSocket->error(), plainSocket->errorString()); |
| return; |
| } |
| } |
| |
| if (totalBytesWritten > 0) { |
| // Don't emit bytesWritten() recursively. |
| if (!emittedBytesWritten) { |
| emittedBytesWritten = true; |
| emit q->bytesWritten(totalBytesWritten); |
| emittedBytesWritten = false; |
| } |
| emit q->channelBytesWritten(0, totalBytesWritten); |
| } |
| } |
| |
| if (connectionEncrypted) { // Decrypt data from remote |
| int totalRead = 0; |
| bool hadIncompleteData = false; |
| while (!readBufferMaxSize || buffer.size() < readBufferMaxSize) { |
| QByteArray ciphertext; |
| if (intermediateBuffer.length()) { |
| #ifdef QSSLSOCKET_DEBUG |
| qCDebug(lcSsl) << "Restoring data from intermediateBuffer:" |
| << intermediateBuffer.length() << "bytes"; |
| #endif |
| ciphertext.swap(intermediateBuffer); |
| } |
| int initialLength = ciphertext.length(); |
| ciphertext += plainSocket->read(16384); |
| #ifdef QSSLSOCKET_DEBUG |
| qCDebug(lcSsl) << "Read" << ciphertext.length() - initialLength |
| << "encrypted bytes from the socket"; |
| #endif |
| if (ciphertext.length() == 0 || (hadIncompleteData && initialLength == ciphertext.length())) { |
| #ifdef QSSLSOCKET_DEBUG |
| qCDebug(lcSsl) << (hadIncompleteData ? "No new data received, leaving loop!" |
| : "Nothing to decrypt, leaving loop!"); |
| #endif |
| if (ciphertext.length()) // We have data, it came from intermediateBuffer, swap back |
| intermediateBuffer.swap(ciphertext); |
| break; |
| } |
| hadIncompleteData = false; |
| #ifdef QSSLSOCKET_DEBUG |
| qCDebug(lcSsl) << "Total amount of bytes to decrypt:" << ciphertext.length(); |
| #endif |
| |
| SecBuffer dataBuffer[4]{ |
| createSecBuffer(ciphertext, SECBUFFER_DATA), |
| createSecBuffer(nullptr, 0, SECBUFFER_EMPTY), |
| createSecBuffer(nullptr, 0, SECBUFFER_EMPTY), |
| createSecBuffer(nullptr, 0, SECBUFFER_EMPTY) |
| }; |
| SecBufferDesc message{ |
| SECBUFFER_VERSION, |
| ARRAYSIZE(dataBuffer), |
| dataBuffer |
| }; |
| auto status = DecryptMessage(&contextHandle, &message, 0, nullptr); |
| if (status == SEC_E_OK || status == SEC_I_RENEGOTIATE || status == SEC_I_CONTEXT_EXPIRED) { |
| // There can still be 0 output even if it succeeds, this is fine |
| if (dataBuffer[1].cbBuffer > 0) { |
| // It is always decrypted in-place. |
| // But [0] is the STREAM_HEADER, [1] is the DATA and [2] is the STREAM_TRAILER. |
| // The pointers in all of those still point into the 'ciphertext' byte array. |
| buffer.append(static_cast<char *>(dataBuffer[1].pvBuffer), |
| dataBuffer[1].cbBuffer); |
| totalRead += dataBuffer[1].cbBuffer; |
| #ifdef QSSLSOCKET_DEBUG |
| qCDebug(lcSsl) << "Decrypted" << dataBuffer[1].cbBuffer |
| << "bytes. New read buffer size:" << buffer.size(); |
| #endif |
| } |
| if (dataBuffer[3].BufferType == SECBUFFER_EXTRA) { |
| // https://docs.microsoft.com/en-us/windows/desktop/secauthn/extra-buffers-returned-by-schannel |
| // dataBuffer[3].cbBuffer indicates the amount of bytes _NOT_ processed, |
| // the rest need to be stored. |
| #ifdef QSSLSOCKET_DEBUG |
| qCDebug(lcSsl) << "We've got excess data, moving it to the intermediate buffer:" |
| << dataBuffer[3].cbBuffer << "bytes"; |
| #endif |
| intermediateBuffer = ciphertext.right(int(dataBuffer[3].cbBuffer)); |
| } |
| } |
| |
| if (status == SEC_E_INCOMPLETE_MESSAGE) { |
| // Need more data before we can decrypt.. to the buffer it goes! |
| #ifdef QSSLSOCKET_DEBUG |
| qCDebug(lcSsl, "We didn't have enough data to decrypt anything, will try again!"); |
| #endif |
| Q_ASSERT(intermediateBuffer.isEmpty()); |
| intermediateBuffer.swap(ciphertext); |
| // We try again, but if we don't get any more data then we leave |
| hadIncompleteData = true; |
| } else if (status == SEC_E_INVALID_HANDLE) { |
| // I don't think this should happen, if it does we're done... |
| qCWarning(lcSsl, "The internal SSPI handle is invalid!"); |
| Q_UNREACHABLE(); |
| } else if (status == SEC_E_INVALID_TOKEN) { |
| qCWarning(lcSsl, "Got SEC_E_INVALID_TOKEN!"); |
| Q_UNREACHABLE(); // Happened once due to a bug, but shouldn't generally happen(?) |
| } else if (status == SEC_E_MESSAGE_ALTERED) { |
| // The message has been altered, disconnect now. |
| shutdown = true; // skips sending the shutdown alert |
| disconnectFromHost(); |
| setErrorAndEmit(QAbstractSocket::SslInternalError, |
| schannelErrorToString(status)); |
| break; |
| } else if (status == SEC_E_OUT_OF_SEQUENCE) { |
| // @todo: I don't know if this one is actually "fatal".. |
| // This path might never be hit as it seems this is for connection-oriented connections, |
| // while SEC_E_MESSAGE_ALTERED is for stream-oriented ones (what we use). |
| shutdown = true; // skips sending the shutdown alert |
| disconnectFromHost(); |
| setErrorAndEmit(QAbstractSocket::SslInternalError, |
| schannelErrorToString(status)); |
| break; |
| } else if (status == SEC_I_CONTEXT_EXPIRED) { |
| // 'remote' has initiated a shutdown |
| disconnectFromHost(); |
| setErrorAndEmit(QAbstractSocket::RemoteHostClosedError, |
| schannelErrorToString(status)); |
| break; |
| } else if (status == SEC_I_RENEGOTIATE) { |
| // 'remote' wants to renegotiate |
| #ifdef QSSLSOCKET_DEBUG |
| qCDebug(lcSsl, "The peer wants to renegotiate."); |
| #endif |
| schannelState = SchannelState::Renegotiate; |
| renegotiating = true; |
| |
| // We need to call 'continueHandshake' or else there's no guarantee it ever gets called |
| continueHandshake(); |
| break; |
| } |
| } |
| |
| if (totalRead) { |
| if (readyReadEmittedPointer) |
| *readyReadEmittedPointer = true; |
| emit q->readyRead(); |
| emit q->channelReadyRead(0); |
| } |
| } |
| } |
| |
| void QSslSocketBackendPrivate::sendShutdown() |
| { |
| const bool isClient = mode == QSslSocket::SslClientMode; |
| DWORD shutdownToken = SCHANNEL_SHUTDOWN; |
| SecBuffer buffer = createSecBuffer(&shutdownToken, sizeof(SCHANNEL_SHUTDOWN), SECBUFFER_TOKEN); |
| SecBufferDesc token{ |
| SECBUFFER_VERSION, |
| 1, |
| &buffer |
| }; |
| auto status = ApplyControlToken(&contextHandle, &token); |
| |
| if (status != SEC_E_OK) { |
| #ifdef QSSLSOCKET_DEBUG |
| qCDebug(lcSsl) << "Failed to apply shutdown control token:" << schannelErrorToString(status); |
| #endif |
| return; |
| } |
| |
| SecBuffer outBuffers[3]; |
| outBuffers[0] = createSecBuffer(nullptr, 0, SECBUFFER_TOKEN); |
| outBuffers[1] = createSecBuffer(nullptr, 0, SECBUFFER_ALERT); |
| outBuffers[2] = createSecBuffer(nullptr, 0, SECBUFFER_EMPTY); |
| auto freeBuffers = qScopeGuard([&outBuffers]() { |
| for (auto i = 0ull; i < ARRAYSIZE(outBuffers); i++) { |
| if (outBuffers[i].pvBuffer) |
| FreeContextBuffer(outBuffers[i].pvBuffer); |
| } |
| }); |
| SecBufferDesc outputBufferDesc{ |
| SECBUFFER_VERSION, |
| ARRAYSIZE(outBuffers), |
| outBuffers |
| }; |
| |
| ULONG contextReq = getContextRequirements(); |
| TimeStamp expiry; |
| if (isClient) { |
| status = InitializeSecurityContext(&credentialHandle, // phCredential |
| &contextHandle, // phContext |
| const_reinterpret_cast<SEC_WCHAR *>(targetName().utf16()), // pszTargetName |
| contextReq, // fContextReq |
| 0, // Reserved1 |
| 0, // TargetDataRep (unused) |
| nullptr, // pInput |
| 0, // Reserved2 |
| nullptr, // phNewContext (we already have one) |
| &outputBufferDesc, // pOutput |
| &contextAttributes, // pfContextAttr |
| &expiry // ptsExpiry |
| ); |
| } else { |
| status = AcceptSecurityContext( |
| &credentialHandle, // phCredential |
| &contextHandle, // phContext |
| nullptr, // pInput |
| contextReq, // fContextReq |
| 0, // TargetDataRep (unused) |
| nullptr, // phNewContext |
| &outputBufferDesc, // pOutput |
| &contextAttributes, // pfContextAttr, |
| &expiry // ptsTimeStamp |
| ); |
| } |
| if (status == SEC_E_OK || status == SEC_I_CONTEXT_EXPIRED) { |
| if (!sendToken(outBuffers[0].pvBuffer, outBuffers[0].cbBuffer, false)) { |
| // We failed to send the shutdown message, but it's not that important since we're |
| // shutting down anyway. |
| return; |
| } |
| } else { |
| #ifdef QSSLSOCKET_DEBUG |
| qCDebug(lcSsl) << "Failed to initialize shutdown:" << schannelErrorToString(status); |
| #endif |
| } |
| } |
| |
| void QSslSocketBackendPrivate::disconnectFromHost() |
| { |
| if (SecIsValidHandle(&contextHandle)) { |
| if (!shutdown) { |
| shutdown = true; |
| if (plainSocket->state() != QAbstractSocket::UnconnectedState) { |
| if (connectionEncrypted) { |
| // Read as much as possible because this is likely our last chance |
| qint64 tempMax = readBufferMaxSize; |
| readBufferMaxSize = 0; |
| transmit(); |
| readBufferMaxSize = tempMax; |
| sendShutdown(); |
| } |
| } |
| } |
| } |
| if (plainSocket->state() != QAbstractSocket::UnconnectedState) |
| plainSocket->disconnectFromHost(); |
| } |
| |
| void QSslSocketBackendPrivate::disconnected() |
| { |
| shutdown = true; |
| connectionEncrypted = false; |
| deallocateContext(); |
| freeCredentialsHandle(); |
| } |
| |
| QSslCipher QSslSocketBackendPrivate::sessionCipher() const |
| { |
| if (!connectionEncrypted) |
| return QSslCipher(); |
| return QSslCipher(QStringLiteral("Schannel"), sessionProtocol()); |
| } |
| |
| QSsl::SslProtocol QSslSocketBackendPrivate::sessionProtocol() const |
| { |
| if (!connectionEncrypted) |
| return QSsl::SslProtocol::UnknownProtocol; |
| return toQtSslProtocol(connectionInfo.dwProtocol); |
| } |
| |
| void QSslSocketBackendPrivate::continueHandshake() |
| { |
| Q_Q(QSslSocket); |
| const bool isServer = mode == QSslSocket::SslServerMode; |
| switch (schannelState) { |
| case SchannelState::InitializeHandshake: |
| if (!SecIsValidHandle(&credentialHandle) && !acquireCredentialsHandle()) { |
| disconnectFromHost(); |
| return; |
| } |
| if (!SecIsValidHandle(&credentialHandle)) // Needed to support tst_QSslSocket::setEmptyKey |
| return; |
| if (!SecIsValidHandle(&contextHandle) && !(isServer ? acceptContext() : createContext())) { |
| disconnectFromHost(); |
| return; |
| } |
| if (schannelState != SchannelState::PerformHandshake) |
| break; |
| Q_FALLTHROUGH(); |
| case SchannelState::PerformHandshake: |
| if (!performHandshake()) { |
| disconnectFromHost(); |
| return; |
| } |
| if (schannelState != SchannelState::VerifyHandshake) |
| break; |
| Q_FALLTHROUGH(); |
| case SchannelState::VerifyHandshake: |
| // if we're in shutdown or renegotiating then we might not need to verify |
| // (since we already did) |
| if (!verifyHandshake()) { |
| shutdown = true; // Skip sending shutdown alert |
| q->abort(); // We don't want to send buffered data |
| disconnectFromHost(); |
| return; |
| } |
| if (schannelState != SchannelState::Done) |
| break; |
| Q_FALLTHROUGH(); |
| case SchannelState::Done: |
| // connectionEncrypted is already true if we come here from a renegotiation |
| if (!connectionEncrypted) { |
| connectionEncrypted = true; // all is done |
| emit q->encrypted(); |
| } |
| renegotiating = false; |
| if (pendingClose) { |
| pendingClose = false; |
| disconnectFromHost(); |
| } else { |
| transmit(); |
| } |
| break; |
| case SchannelState::Renegotiate: |
| if (!renegotiate()) { |
| disconnectFromHost(); |
| return; |
| } |
| break; |
| } |
| } |
| |
| QList<QSslCipher> QSslSocketBackendPrivate::defaultCiphers() |
| { |
| QList<QSslCipher> ciphers; |
| // @temp (I hope), stolen from qsslsocket_winrt.cpp |
| const QString protocolStrings[] = { QStringLiteral("TLSv1"), QStringLiteral("TLSv1.1"), |
| QStringLiteral("TLSv1.2"), QStringLiteral("TLSv1.3") }; |
| const QSsl::SslProtocol protocols[] = { QSsl::TlsV1_0, QSsl::TlsV1_1, |
| QSsl::TlsV1_2, QSsl::TlsV1_3 }; |
| const int size = ARRAYSIZE(protocols); |
| Q_STATIC_ASSERT(size == ARRAYSIZE(protocolStrings)); |
| ciphers.reserve(size); |
| for (int i = 0; i < size; ++i) { |
| QSslCipher cipher; |
| cipher.d->isNull = false; |
| cipher.d->name = QStringLiteral("Schannel"); |
| cipher.d->protocol = protocols[i]; |
| cipher.d->protocolString = protocolStrings[i]; |
| ciphers.append(cipher); |
| } |
| |
| return ciphers; |
| } |
| |
| QList<QSslError> QSslSocketBackendPrivate::verify(const QList<QSslCertificate> &certificateChain, |
| const QString &hostName) |
| { |
| Q_UNUSED(certificateChain); |
| Q_UNUSED(hostName); |
| |
| Q_UNIMPLEMENTED(); |
| return {}; // @future implement(?) |
| } |
| |
| bool QSslSocketBackendPrivate::importPkcs12(QIODevice *device, QSslKey *key, QSslCertificate *cert, |
| QList<QSslCertificate> *caCertificates, |
| const QByteArray &passPhrase) |
| { |
| Q_UNUSED(device); |
| Q_UNUSED(key); |
| Q_UNUSED(cert); |
| Q_UNUSED(caCertificates); |
| Q_UNUSED(passPhrase); |
| // @future: can load into its own certificate store (encountered problems extracting key). |
| Q_UNIMPLEMENTED(); |
| return false; |
| } |
| |
| /* |
| Copied from qsslsocket_mac.cpp, which was copied from qsslsocket_openssl.cpp |
| */ |
| bool QSslSocketBackendPrivate::checkSslErrors() |
| { |
| if (sslErrors.isEmpty()) |
| return true; |
| Q_Q(QSslSocket); |
| |
| emit q->sslErrors(sslErrors); |
| |
| const bool doVerifyPeer = configuration.peerVerifyMode == QSslSocket::VerifyPeer |
| || (configuration.peerVerifyMode == QSslSocket::AutoVerifyPeer |
| && mode == QSslSocket::SslClientMode); |
| const bool doEmitSslError = !verifyErrorsHaveBeenIgnored(); |
| // check whether we need to emit an SSL handshake error |
| if (doVerifyPeer && doEmitSslError) { |
| if (q->pauseMode() & QAbstractSocket::PauseOnSslErrors) { |
| pauseSocketNotifiers(q); |
| paused = true; |
| } else { |
| setErrorAndEmit(QAbstractSocket::SslHandshakeFailedError, |
| sslErrors.constFirst().errorString()); |
| plainSocket->disconnectFromHost(); |
| } |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void QSslSocketBackendPrivate::initializeCertificateStores() |
| { |
| //// helper function which turns a chain into a certificate store |
| auto createStoreFromCertificateChain = [](const QList<QSslCertificate> certChain, const QSslKey &privateKey) { |
| const wchar_t *passphrase = L""; |
| // Need to embed the private key in the certificate |
| QByteArray pkcs12 = _q_makePkcs12(certChain, |
| privateKey, |
| QString::fromWCharArray(passphrase, 0)); |
| CRYPT_DATA_BLOB pfxBlob; |
| pfxBlob.cbData = DWORD(pkcs12.length()); |
| pfxBlob.pbData = reinterpret_cast<unsigned char *>(pkcs12.data()); |
| return QHCertStorePointer(PFXImportCertStore(&pfxBlob, passphrase, 0)); |
| }; |
| |
| if (!configuration.localCertificateChain.isEmpty()) { |
| if (configuration.privateKey.isNull()) { |
| setErrorAndEmit(QAbstractSocket::SslInvalidUserDataError, |
| QSslSocket::tr("Cannot provide a certificate with no key")); |
| return; |
| } |
| if (localCertificateStore == nullptr) { |
| localCertificateStore = createStoreFromCertificateChain(configuration.localCertificateChain, |
| configuration.privateKey); |
| if (localCertificateStore == nullptr) |
| qCWarning(lcSsl, "Failed to load certificate chain!"); |
| } |
| } |
| |
| if (!configuration.caCertificates.isEmpty() && !caCertificateStore) { |
| caCertificateStore = createStoreFromCertificateChain(configuration.caCertificates, |
| {}); // No private key for the CA certs |
| } |
| } |
| |
| bool QSslSocketBackendPrivate::verifyCertContext(CERT_CONTEXT *certContext) |
| { |
| Q_ASSERT(certContext); |
| Q_Q(QSslSocket); |
| |
| const bool isClient = mode == QSslSocket::SslClientMode; |
| |
| // Create a collection of stores so we can pass in multiple stores as additional locations to |
| // search for the certificate chain |
| auto tempCertCollection = QHCertStorePointer(CertOpenStore(CERT_STORE_PROV_COLLECTION, |
| X509_ASN_ENCODING, |
| 0, |
| CERT_STORE_CREATE_NEW_FLAG, |
| nullptr)); |
| if (!tempCertCollection) { |
| #ifdef QSSLSOCKET_DEBUG |
| qCWarning(lcSsl, "Failed to create certificate store collection!"); |
| #endif |
| return false; |
| } |
| |
| if (rootCertOnDemandLoadingAllowed()) { |
| // @future(maybe): following the OpenSSL backend these certificates should be added into |
| // the Ca list, not just included during verification. |
| // That being said, it's not trivial to add the root certificates (if and only if they |
| // came from the system root store). And I don't see this mentioned in our documentation. |
| auto rootStore = QHCertStorePointer(CertOpenSystemStore(0, L"ROOT")); |
| if (!rootStore) { |
| #ifdef QSSLSOCKET_DEBUG |
| qCWarning(lcSsl, "Failed to open the system root CA certificate store!"); |
| #endif |
| return false; |
| } else if (!CertAddStoreToCollection(tempCertCollection.get(), rootStore.get(), 0, 1)) { |
| #ifdef QSSLSOCKET_DEBUG |
| qCWarning(lcSsl, "Failed to add the system root CA certificate store to the certificate store collection!"); |
| #endif |
| return false; |
| } |
| } |
| if (caCertificateStore) { |
| if (!CertAddStoreToCollection(tempCertCollection.get(), caCertificateStore.get(), 0, 1)) { |
| #ifdef QSSLSOCKET_DEBUG |
| qCWarning(lcSsl, "Failed to add the user's CA certificate store to the certificate store collection!"); |
| #endif |
| return false; |
| } |
| } |
| |
| if (!CertAddStoreToCollection(tempCertCollection.get(), certContext->hCertStore, 0, 0)) { |
| #ifdef QSSLSOCKET_DEBUG |
| qCWarning(lcSsl, "Failed to add certificate's origin store to the certificate store collection!"); |
| #endif |
| return false; |
| } |
| |
| CERT_CHAIN_PARA parameters; |
| ZeroMemory(¶meters, sizeof(parameters)); |
| parameters.cbSize = sizeof(CERT_CHAIN_PARA); |
| parameters.RequestedUsage.dwType = USAGE_MATCH_TYPE_AND; |
| parameters.RequestedUsage.Usage.cUsageIdentifier = 1; |
| LPSTR oid = LPSTR(isClient ? szOID_PKIX_KP_SERVER_AUTH |
| : szOID_PKIX_KP_CLIENT_AUTH); |
| parameters.RequestedUsage.Usage.rgpszUsageIdentifier = &oid; |
| |
| configuration.peerCertificate.clear(); |
| configuration.peerCertificateChain.clear(); |
| const CERT_CHAIN_CONTEXT *chainContext = nullptr; |
| auto freeCertChain = qScopeGuard([&chainContext]() { |
| if (chainContext) |
| CertFreeCertificateChain(chainContext); |
| }); |
| BOOL status = CertGetCertificateChain(nullptr, // hChainEngine, default |
| certContext, // pCertContext |
| nullptr, // pTime, 'now' |
| tempCertCollection.get(), // hAdditionalStore, additional cert store |
| ¶meters, // pChainPara |
| CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT, // dwFlags |
| nullptr, // reserved |
| &chainContext // ppChainContext |
| ); |
| if (status == FALSE || !chainContext || chainContext->cChain == 0) { |
| QSslError error(QSslError::UnableToVerifyFirstCertificate); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| return q->state() == QAbstractSocket::ConnectedState; |
| } |
| |
| // Helper-function to get a QSslCertificate given a CERT_CHAIN_ELEMENT |
| static auto getCertificateFromChainElement = [](CERT_CHAIN_ELEMENT *element) { |
| if (!element) |
| return QSslCertificate(); |
| |
| const CERT_CONTEXT *certContext = element->pCertContext; |
| return QSslCertificatePrivate::QSslCertificate_from_CERT_CONTEXT(certContext); |
| }; |
| |
| // Pick a chain to use as the certificate chain, if multiple are available: |
| // According to https://docs.microsoft.com/en-gb/windows/desktop/api/wincrypt/ns-wincrypt-_cert_chain_context |
| // this seems to be the best way to get a trusted chain. |
| CERT_SIMPLE_CHAIN *chain = chainContext->rgpChain[chainContext->cChain - 1]; |
| |
| if (chain->TrustStatus.dwErrorStatus & CERT_TRUST_IS_PARTIAL_CHAIN) { |
| auto error = QSslError(QSslError::SslError::UnableToGetIssuerCertificate, |
| getCertificateFromChainElement(chain->rgpElement[chain->cElement - 1])); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| if (chain->TrustStatus.dwErrorStatus & CERT_TRUST_INVALID_BASIC_CONSTRAINTS) { |
| // @Note: This is actually one of two errors: |
| // "either the certificate cannot be used to issue other certificates, or the chain path length has been exceeded." |
| // But here we are checking the chain's status, so we assume the "issuing" error cannot occur here. |
| auto error = QSslError(QSslError::PathLengthExceeded); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| static const DWORD leftoverCertChainErrorMask = CERT_TRUST_IS_CYCLIC | CERT_TRUST_INVALID_EXTENSION |
| | CERT_TRUST_INVALID_POLICY_CONSTRAINTS | CERT_TRUST_INVALID_NAME_CONSTRAINTS |
| | CERT_TRUST_CTL_IS_NOT_TIME_VALID | CERT_TRUST_CTL_IS_NOT_SIGNATURE_VALID |
| | CERT_TRUST_CTL_IS_NOT_VALID_FOR_USAGE; |
| if (chain->TrustStatus.dwErrorStatus & leftoverCertChainErrorMask) { |
| auto error = QSslError(QSslError::SslError::UnspecifiedError); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| |
| DWORD verifyDepth = chain->cElement; |
| if (configuration.peerVerifyDepth > 0 && DWORD(configuration.peerVerifyDepth) < verifyDepth) |
| verifyDepth = DWORD(configuration.peerVerifyDepth); |
| |
| for (DWORD i = 0; i < verifyDepth; i++) { |
| CERT_CHAIN_ELEMENT *element = chain->rgpElement[i]; |
| QSslCertificate certificate = getCertificateFromChainElement(element); |
| const QList<QSslCertificateExtension> extensions = certificate.extensions(); |
| |
| #ifdef QSSLSOCKET_DEBUG |
| qCDebug(lcSsl) << "issuer:" << certificate.issuerDisplayName() |
| << "\nsubject:" << certificate.subjectDisplayName() |
| << "\nQSslCertificate info:" << certificate |
| << "\nextended error info:" << element->pwszExtendedErrorInfo |
| << "\nerror status:" << element->TrustStatus.dwErrorStatus; |
| #endif |
| |
| configuration.peerCertificateChain.append(certificate); |
| |
| if (certificate.isBlacklisted()) { |
| const auto error = QSslError(QSslError::CertificateBlacklisted, certificate); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| |
| LONG result = CertVerifyTimeValidity(nullptr /*== now */, element->pCertContext->pCertInfo); |
| if (result != 0) { |
| auto error = QSslError(result == -1 ? QSslError::CertificateNotYetValid |
| : QSslError::CertificateExpired, |
| certificate); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| |
| //// Errors |
| if (element->TrustStatus.dwErrorStatus & CERT_TRUST_IS_NOT_TIME_VALID) { |
| // handled right above |
| Q_ASSERT(!sslErrors.isEmpty()); |
| } |
| if (element->TrustStatus.dwErrorStatus & CERT_TRUST_IS_REVOKED) { |
| auto error = QSslError(QSslError::CertificateRevoked, certificate); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| if (element->TrustStatus.dwErrorStatus & CERT_TRUST_IS_NOT_SIGNATURE_VALID) { |
| auto error = QSslError(QSslError::CertificateSignatureFailed, certificate); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| |
| // While netscape shouldn't be relevant now it defined an extension which is |
| // still in use. Schannel does not check this automatically, so we do it here. |
| // It is used to differentiate between client and server certificates. |
| if (netscapeWrongCertType(extensions, isClient)) |
| element->TrustStatus.dwErrorStatus |= CERT_TRUST_IS_NOT_VALID_FOR_USAGE; |
| |
| if (element->TrustStatus.dwErrorStatus & CERT_TRUST_IS_NOT_VALID_FOR_USAGE) { |
| auto error = QSslError(QSslError::InvalidPurpose, certificate); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| if (element->TrustStatus.dwErrorStatus & CERT_TRUST_IS_UNTRUSTED_ROOT) { |
| // Override this error if we have the certificate inside our trusted CAs list. |
| const bool isTrustedRoot = configuration.caCertificates.contains(certificate); |
| if (!isTrustedRoot) { |
| auto error = QSslError(QSslError::CertificateUntrusted, certificate); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| } |
| static const DWORD certRevocationCheckUnavailableError = CERT_TRUST_IS_OFFLINE_REVOCATION |
| | CERT_TRUST_REVOCATION_STATUS_UNKNOWN; |
| if (element->TrustStatus.dwErrorStatus & certRevocationCheckUnavailableError) { |
| // @future(maybe): Do something with this |
| } |
| |
| // Dumping ground of errors that don't fit our specific errors |
| static const DWORD leftoverCertErrorMask = CERT_TRUST_IS_CYCLIC |
| | CERT_TRUST_INVALID_EXTENSION | CERT_TRUST_INVALID_NAME_CONSTRAINTS |
| | CERT_TRUST_INVALID_POLICY_CONSTRAINTS |
| | CERT_TRUST_HAS_EXCLUDED_NAME_CONSTRAINT |
| | CERT_TRUST_HAS_NOT_SUPPORTED_CRITICAL_EXT |
| | CERT_TRUST_HAS_NOT_DEFINED_NAME_CONSTRAINT |
| | CERT_TRUST_HAS_NOT_PERMITTED_NAME_CONSTRAINT |
| | CERT_TRUST_HAS_NOT_SUPPORTED_NAME_CONSTRAINT; |
| if (element->TrustStatus.dwErrorStatus & leftoverCertErrorMask) { |
| auto error = QSslError(QSslError::UnspecifiedError, certificate); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| if (element->TrustStatus.dwErrorStatus & CERT_TRUST_INVALID_BASIC_CONSTRAINTS) { |
| auto it = std::find_if(extensions.cbegin(), extensions.cend(), |
| [](const QSslCertificateExtension &extension) { |
| return extension.name() == QLatin1String("basicConstraints"); |
| }); |
| if (it != extensions.cend()) { |
| // @Note: This is actually one of two errors: |
| // "either the certificate cannot be used to issue other certificates, |
| // or the chain path length has been exceeded." |
| QVariantMap basicConstraints = it->value().toMap(); |
| QSslError error; |
| if (i > 0 && !basicConstraints.value(QLatin1String("ca"), false).toBool()) |
| error = QSslError(QSslError::InvalidPurpose, certificate); |
| else |
| error = QSslError(QSslError::PathLengthExceeded, certificate); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| } |
| if (element->TrustStatus.dwErrorStatus & CERT_TRUST_IS_EXPLICIT_DISTRUST) { |
| auto error = QSslError(QSslError::CertificateBlacklisted, certificate); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| |
| if (element->TrustStatus.dwInfoStatus & CERT_TRUST_IS_SELF_SIGNED) { |
| // If it's self-signed *and* a CA then we can assume it's a root CA certificate |
| // and we can ignore the "self-signed" note: |
| // We check the basicConstraints certificate extension when possible, but this didn't |
| // exist for version 1, so we can only guess in that case |
| const bool isRootCertificateAuthority = isCertificateAuthority(extensions) |
| || certificate.version() == "1"; |
| |
| // Root certificate tends to be signed by themselves, so ignore self-signed status. |
| if (!isRootCertificateAuthority) { |
| auto error = QSslError(QSslError::SelfSignedCertificate, certificate); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| } |
| } |
| |
| if (!configuration.peerCertificateChain.isEmpty()) |
| configuration.peerCertificate = configuration.peerCertificateChain.first(); |
| |
| // @Note: Somewhat copied from qsslsocket_mac.cpp |
| const bool doVerifyPeer = configuration.peerVerifyMode == QSslSocket::VerifyPeer |
| || (configuration.peerVerifyMode == QSslSocket::AutoVerifyPeer |
| && mode == QSslSocket::SslClientMode); |
| // Check the peer certificate itself. First try the subject's common name |
| // (CN) as a wildcard, then try all alternate subject name DNS entries the |
| // same way. |
| if (!configuration.peerCertificate.isNull()) { |
| // but only if we're a client connecting to a server |
| // if we're the server, don't check CN |
| if (mode == QSslSocket::SslClientMode) { |
| const QString peerName(verificationPeerName.isEmpty() ? q->peerName() : verificationPeerName); |
| if (!isMatchingHostname(configuration.peerCertificate, peerName)) { |
| // No matches in common names or alternate names. |
| const QSslError error(QSslError::HostNameMismatch, configuration.peerCertificate); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| } |
| } else if (doVerifyPeer) { |
| // No peer certificate presented. Report as error if the socket |
| // expected one. |
| const QSslError error(QSslError::NoPeerCertificate); |
| sslErrors += error; |
| emit q->peerVerifyError(error); |
| if (q->state() != QAbstractSocket::ConnectedState) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool QSslSocketBackendPrivate::rootCertOnDemandLoadingAllowed() |
| { |
| return allowRootCertOnDemandLoading && s_loadRootCertsOnDemand; |
| } |
| |
| QT_END_NAMESPACE |