QSslSocket (OpenSSL) improve alert messages handling

1. Add a new verification callback. This gives an option
to report errors directly from this callback (by emitting
handshakeInterruptedOnError()). This allows an application
to explain to its peer why the handshake was interrupted (by
sending a corresponding alert message).
2. This also means we want to notice such alerts (in Qt,
from the application's point of view, they are mostly
informational only, no interaction is required). So we
also introduce a new 'info callback', that can notice alert
messages read or written. We also introduce two new enums
describing the level and type of an alert message. QSslSocket
gets three new signals (for incoming/outgoing alerts and
verification errors found early).
3. In case we requested a certificate, but the peer provided
none, we would previously abruptly close the connection without
a proper alert message (and such a situation is not handled
by any verification callbacks, since there is no certificate(s)
to verify essentially). So we now introduce a new verification
option that maps to what OpenSSL calls 'SSL_VERIFY_FAIL_IF_NO_PEER_CERT'.
This way, the proper alert will be generated.

Fixes: QTBUG-68419
Change-Id: I5d1e9298b4040a2d4f867f5b1a3567a2253927b8
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
bb10
Timur Pocheptsov 2019-10-30 11:49:07 +01:00
parent 4c89005ebe
commit e5438e8ded
12 changed files with 683 additions and 24 deletions

View File

@ -222,7 +222,9 @@ bool QSslConfiguration::operator==(const QSslConfiguration &other) const
d->nextNegotiatedProtocol == other.d->nextNegotiatedProtocol &&
d->nextProtocolNegotiationStatus == other.d->nextProtocolNegotiationStatus &&
d->dtlsCookieEnabled == other.d->dtlsCookieEnabled &&
d->ocspStaplingEnabled == other.d->ocspStaplingEnabled;
d->ocspStaplingEnabled == other.d->ocspStaplingEnabled &&
d->reportFromCallback == other.d->reportFromCallback &&
d->missingCertIsFatal == other.d->missingCertIsFatal;
}
/*!
@ -267,7 +269,9 @@ bool QSslConfiguration::isNull() const
d->nextAllowedProtocols.isEmpty() &&
d->nextNegotiatedProtocol.isNull() &&
d->nextProtocolNegotiationStatus == QSslConfiguration::NextProtocolNegotiationNone &&
d->ocspStaplingEnabled == false);
d->ocspStaplingEnabled == false &&
d->reportFromCallback == false &&
d->missingCertIsFatal == false);
}
/*!
@ -1190,6 +1194,89 @@ bool QSslConfiguration::ocspStaplingEnabled() const
return d->ocspStaplingEnabled;
}
/*!
\since 6.0
Returns true if a verification callback will emit QSslSocket::handshakeInterruptedOnError()
early, before concluding the handshake.
\note This function always returns false for all backends but OpenSSL.
\sa setHandshakeMustInterruptOnError(), QSslSocket::handshakeInterruptedOnError(), QSslSocket::continueInterruptedHandshake()
*/
bool QSslConfiguration::handshakeMustInterruptOnError() const
{
return d->reportFromCallback;
}
/*!
\since 6.0
If \a interrupt is true and the underlying backend supports this option,
errors found during certificate verification are reported immediately
by emitting QSslSocket::handshakeInterruptedOnError(). This allows
to stop the unfinished handshake and send a proper alert message to
a peer. No special action is required from the application in this case.
QSslSocket will close the connection after sending the alert message.
If the application after inspecting the error wants to continue the
handshake, it must call QSslSocket::continueInterruptedHandshake()
from its slot function. The signal-slot connection must be direct.
\note When interrupting handshake is enabled, errors that would otherwise
be reported by QSslSocket::peerVerifyError() are instead only reported by
QSslSocket::handshakeInterruptedOnError().
\note Even if the handshake was continued, these errors will be
reported when emitting QSslSocket::sslErrors() signal (and thus must
be ignored in the corresponding function slot).
\sa handshakeMustInterruptOnError(), QSslSocket::handshakeInterruptedOnError(), QSslSocket::continueInterruptedHandshake()
*/
void QSslConfiguration::setHandshakeMustInterruptOnError(bool interrupt)
{
#if QT_CONFIG(openssl)
d->reportFromCallback = interrupt;
#else
qCWarning(lcSsl, "This operation requires OpenSSL as TLS backend");
#endif
}
/*!
\since 6.0
Returns true if errors with code QSslError::NoPeerCertificate
cannot be ignored.
\note Always returns false for all TLS backends but OpenSSL.
\sa QSslSocket::ignoreSslErrors(), setMissingCertificateIsFatal()
*/
bool QSslConfiguration::missingCertificateIsFatal() const
{
return d->missingCertIsFatal;
}
/*!
\since 6.0
If \a cannotRecover is true, and verification mode in use is
QSslSocket::VerifyPeer or QSslSocket::AutoVerifyPeer (for a
client-side socket), the missing peer's certificate would be
treated as an unrecoverable error that cannot be ignored. A proper
alert message will be sent to the peer before closing the connection.
\note Only available if Qt was configured and built with OpenSSL backend.
\sa QSslSocket::ignoreSslErrors(), QSslSocket::PeerVerifyMode, missingCertificateIsFatal()
*/
void QSslConfiguration::setMissingCertificateIsFatal(bool cannotRecover)
{
#if QT_CONFIG(openssl)
d->missingCertIsFatal = cannotRecover;
#else
qCWarning(lcSsl, "Handling a missing certificate as a fatal error requires an OpenSSL backend");
#endif // openssl
}
/*! \internal
*/
bool QSslConfigurationPrivate::peerSessionWasShared(const QSslConfiguration &configuration) {

View File

@ -172,6 +172,12 @@ public:
static void setDefaultDtlsConfiguration(const QSslConfiguration &configuration);
#endif // dtls
bool handshakeMustInterruptOnError() const;
void setHandshakeMustInterruptOnError(bool interrupt);
bool missingCertificateIsFatal() const;
void setMissingCertificateIsFatal(bool cannotRecover);
void setOcspStaplingEnabled(bool enable);
bool ocspStaplingEnabled() const;

View File

@ -149,6 +149,14 @@ public:
const bool ocspStaplingEnabled = false;
#endif
#if QT_CONFIG(openssl)
bool reportFromCallback = false;
bool missingCertIsFatal = false;
#else
const bool reportFromCallback = false;
const bool missingCertIsFatal = false;
#endif // openssl
// in qsslsocket.cpp:
static QSslConfiguration defaultConfiguration();
static void setDefaultConfiguration(const QSslConfiguration &configuration);

View File

@ -56,6 +56,7 @@ QT_BEGIN_NAMESPACE
// defined in qsslsocket_openssl.cpp:
extern int q_X509Callback(int ok, X509_STORE_CTX *ctx);
extern "C" int q_X509CallbackDirect(int ok, X509_STORE_CTX *ctx);
extern QString getErrorsFromOpenSsl();
#if QT_CONFIG(dtls)
@ -571,11 +572,20 @@ init_context:
if (sslContext->sslConfiguration.peerVerifyMode() == QSslSocket::VerifyNone) {
q_SSL_CTX_set_verify(sslContext->ctx, SSL_VERIFY_NONE, nullptr);
} else {
q_SSL_CTX_set_verify(sslContext->ctx, SSL_VERIFY_PEER,
#if QT_CONFIG(dtls)
isDtls ? dtlscallbacks::q_X509DtlsCallback :
#endif // dtls
q_X509Callback);
auto verificationCallback =
#if QT_CONFIG(dtls)
isDtls ? dtlscallbacks::q_X509DtlsCallback :
#endif // dtls
q_X509Callback;
if (!isDtls && configuration.handshakeMustInterruptOnError())
verificationCallback = q_X509CallbackDirect;
auto verificationMode = SSL_VERIFY_PEER;
if (!isDtls && sslContext->sslConfiguration.missingCertificateIsFatal())
verificationMode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
q_SSL_CTX_set_verify(sslContext->ctx, verificationMode, verificationCallback);
}
#if QT_CONFIG(dtls)

View File

@ -227,6 +227,74 @@
\sa QSslSocket::peerVerifyMode()
*/
/*!
\enum QAlertLevel
\brief Describes the level of an alert message
\relates QSslSocket
\since 6.0
\ingroup network
\ingroup ssl
\inmodule QtNetwork
This enum describes the level of an alert message that was sent
or received.
\value Warning Non-fatal alert message
\value Fatal Fatal alert message, the underlying backend will
handle such an alert properly and close the connection.
\value Unknown An alert of unknown level of severity.
*/
/*!
\enum QAlertType
\brief Enumerates possible codes that an alert message can have
\relates QSslSocket
\since 6.0
\ingroup network
\ingroup ssl
\inmodule QtNetwork
See \l{https://tools.ietf.org/html/rfc8446#page-85}{RFC 8446, section 6}
for the possible values and their meaning.
\value CloseNotify,
\value UnexpectedMessage
\value BadRecordMac
\value RecordOverflow
\value DecompressionFailure
\value HandshakeFailure
\value NoCertificate
\value BadCertificate
\value UnsupportedCertificate
\value CertificateRevoked
\value CertificateExpired
\value CertificateUnknown
\value IllegalParameter
\value UnknownCa
\value AccessDenied
\value DecodeError
\value DecryptError
\value ExportRestriction
\value ProtocolVersion
\value InsufficientSecurity
\value InternalError
\value InappropriateFallback
\value UserCancelled
\value NoRenegotiation
\value MissingExtension
\value UnsupportedExtension
\value CertificateUnobtainable
\value UnrecognizedName
\value BadCertificateStatusResponse
\value BadCertificateHashValue
\value UnknownPskIdentity
\value CertificateRequired
\value NoApplicationProtocol
\value UnknownAlertMessage
*/
/*!
\fn void QSslSocket::encrypted()
@ -322,6 +390,48 @@
\sa QSslPreSharedKeyAuthenticator
*/
/*!
\fn void QSslSocket::alertSent(QAlertLevel level, QAlertType type, const QString &description)
QSslSocket emits this signal if an alert message was sent to a peer. \a level
describes if it was a warning or a fatal error. \a type gives the code
of the alert message. When a textual description of the alert message is
available, it is supplied in \a description.
\note This signal is mostly informational and can be used for debugging
purposes, normally it does not require any actions from the application.
\note Not all backends support this functionality.
\sa alertReceived(), QAlertLevel, QAlertType
*/
/*!
\fn void QSslSocket::alertReceived(QAlertLevel level, QAlertType type, const QString &description)
QSslSocket emits this signal if an alert message was received from a peer.
\a level tells if the alert was fatal or it was a warning. \a type is the
code explaining why the alert was sent. When a textual description of
the alert message is available, it is supplied in \a description.
\note The signal is mostly for informational and debugging purposes and does not
require any handling in the application. If the alert was fatal, underlying
backend will handle it and close the connection.
\note Not all backends support this functionality.
\sa alertSent(), QAlertLevel, QAlertType
*/
/*!
\fn void QSslSocket::handshakeInterruptedOnError(const QSslError &error)
QSslSocket emits this signal if a certificate verification error was
found and if early error reporting was enabled in QSslConfiguration.
An application is expected to inspect the \a error and decide if
it wants to continue the handshake, or abort it and send an alert message
to the peer. The signal-slot connection must be direct.
\sa continueInterruptedHandshake(), sslErrors(), QSslConfiguration::setHandshakeMustInterruptOnError()
*/
#include "qssl_p.h"
#include "qsslsocket.h"
#include "qsslcipher.h"
@ -977,7 +1087,10 @@ void QSslSocket::setSslConfiguration(const QSslConfiguration &configuration)
#if QT_CONFIG(ocsp)
d->configuration.ocspStaplingEnabled = configuration.ocspStaplingEnabled();
#endif
#if QT_CONFIG(openssl)
d->configuration.reportFromCallback = configuration.handshakeMustInterruptOnError();
d->configuration.missingCertIsFatal = configuration.missingCertificateIsFatal();
#endif // openssl
// if the CA certificates were set explicitly (either via
// QSslConfiguration::setCaCertificates() or QSslSocket::setCaCertificates(),
// we cannot load the certificates on demand
@ -2043,6 +2156,23 @@ void QSslSocket::ignoreSslErrors(const QList<QSslError> &errors)
d->ignoreErrorsList = errors;
}
/*!
\since 6.0
If an application wants to conclude a handshake even after receiving
handshakeInterruptedOnError() signal, it must call this function.
This call must be done from a slot function attached to the signal.
The signal-slot connection must be direct.
\sa handshakeInterruptedOnError(), QSslConfiguration::setHandshakeMustInterruptOnError()
*/
void QSslSocket::continueInterruptedHandshake()
{
Q_D(QSslSocket);
d->handshakeInterrupted = false;
}
/*!
\internal
*/
@ -2445,6 +2575,10 @@ void QSslConfigurationPrivate::deepCopyDefaultConfiguration(QSslConfigurationPri
#if QT_CONFIG(ocsp)
ptr->ocspStaplingEnabled = global->ocspStaplingEnabled;
#endif
#if QT_CONFIG(openssl)
ptr->reportFromCallback = global->reportFromCallback;
ptr->missingCertIsFatal = global->missingCertIsFatal;
#endif
}
/*!

View File

@ -63,6 +63,49 @@ class QSslEllipticCurve;
class QSslPreSharedKeyAuthenticator;
class QOcspResponse;
enum class QAlertLevel {
Warning,
Fatal,
Unknown
};
enum class QAlertType {
CloseNotify,
UnexpectedMessage = 10,
BadRecordMac = 20,
RecordOverflow = 22,
DecompressionFailure = 30, // reserved
HandshakeFailure = 40,
NoCertificate = 41, // reserved
BadCertificate = 42,
UnsupportedCertificate = 43,
CertificateRevoked = 44,
CertificateExpired = 45,
CertificateUnknown = 46,
IllegalParameter = 47,
UnknownCa = 48,
AccessDenied = 49,
DecodeError = 50,
DecryptError = 51,
ExportRestriction = 60, // reserved
ProtocolVersion = 70,
InsufficientSecurity = 71,
InternalError = 80,
InappropriateFallback = 86,
UserCancelled = 90,
NoRenegotiation = 100,
MissingExtension = 109,
UnsupportedExtension = 110,
CertificateUnobtainable = 111, // reserved
UnrecognizedName = 112,
BadCertificateStatusResponse = 113,
BadCertificateHashValue = 114, // reserved
UnknownPskIdentity = 115,
CertificateRequired = 116,
NoApplicationProtocol = 120,
UnknownAlertMessage = 255
};
class QSslSocketPrivate;
class Q_NETWORK_EXPORT QSslSocket : public QTcpSocket
{
@ -201,6 +244,7 @@ public:
static QString sslLibraryBuildVersionString();
void ignoreSslErrors(const QList<QSslError> &errors);
void continueInterruptedHandshake();
public Q_SLOTS:
void startClientEncryption();
@ -214,6 +258,9 @@ Q_SIGNALS:
void modeChanged(QSslSocket::SslMode newMode);
void encryptedBytesWritten(qint64 totalBytes);
void preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator *authenticator);
void alertSent(QAlertLevel level, QAlertType type, const QString &description);
void alertReceived(QAlertLevel level, QAlertType type, const QString &description);
void handshakeInterruptedOnError(const QSslError &error);
protected:
qint64 readData(char *data, qint64 maxlen) override;

View File

@ -97,6 +97,123 @@
QT_BEGIN_NAMESPACE
namespace {
QAlertLevel tlsAlertLevel(int value)
{
if (const char *typeString = q_SSL_alert_type_string(value)) {
// Documented to return 'W' for warning, 'F' for fatal,
// 'U' for unknown.
switch (typeString[0]) {
case 'W':
return QAlertLevel::Warning;
case 'F':
return QAlertLevel::Fatal;
default:;
}
}
return QAlertLevel::Unknown;
}
QString tlsAlertDescription(int value)
{
QString description = QLatin1String(q_SSL_alert_desc_string_long(value));
if (!description.size())
description = QLatin1String("no description provided");
return description;
}
QAlertType tlsAlertType(int value)
{
// In case for some reason openssl gives us a value,
// which is not in our enum actually, we leave it to
// an application to handle (supposedly they have
// if or switch-statements).
return QAlertType(value & 0xff);
}
} // Unnamed namespace
extern "C"
{
void qt_AlertInfoCallback(const SSL *connection, int from, int value)
{
// Passed to SSL_set_info_callback()
// https://www.openssl.org/docs/man1.1.1/man3/SSL_set_info_callback.html
if (!connection) {
#ifdef QSSLSOCKET_DEBUG
qCWarning(lcSsl, "Invalid 'connection' parameter (nullptr)");
#endif // QSSLSOCKET_DEBUG
return;
}
const auto offset = QSslSocketBackendPrivate::s_indexForSSLExtraData
+ QSslSocketBackendPrivate::socketOffsetInExData;
auto privateSocket =
static_cast<QSslSocketBackendPrivate *>(q_SSL_get_ex_data(connection, offset));
if (!privateSocket) {
// SSL_set_ex_data can fail:
#ifdef QSSLSOCKET_DEBUG
qCWarning(lcSsl, "No external data (socket backend) found for parameter 'connection'");
#endif // QSSLSOCKET_DEBUG
return;
}
if (!(from & SSL_CB_ALERT)) {
// We only want to know about alerts (at least for now).
return;
}
if (from & SSL_CB_WRITE)
privateSocket->alertMessageSent(value);
else
privateSocket->alertMessageReceived(value);
}
int q_X509CallbackDirect(int ok, X509_STORE_CTX *ctx)
{
// Passed to SSL_CTX_set_verify()
// https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_verify.html
// Returns 0 to abort verification, 1 to continue.
// This is a new, experimental verification callback, reporting
// errors immediately and returning 0 or 1 depending on an application
// either ignoring or not ignoring verification errors as they come.
if (!ctx) {
qCWarning(lcSsl, "Invalid store context (nullptr)");
return 0;
}
if (!ok) {
// "Whenever a X509_STORE_CTX object is created for the verification of the
// peer's certificate during a handshake, a pointer to the SSL object is
// stored into the X509_STORE_CTX object to identify the connection affected.
// To retrieve this pointer the X509_STORE_CTX_get_ex_data() function can be
// used with the correct index."
SSL *ssl = static_cast<SSL *>(q_X509_STORE_CTX_get_ex_data(ctx, q_SSL_get_ex_data_X509_STORE_CTX_idx()));
if (!ssl) {
qCWarning(lcSsl, "No external data (SSL) found in X509 store object");
return 0;
}
const auto offset = QSslSocketBackendPrivate::s_indexForSSLExtraData
+ QSslSocketBackendPrivate::socketOffsetInExData;
auto privateSocket = static_cast<QSslSocketBackendPrivate *>(q_SSL_get_ex_data(ssl, offset));
if (!privateSocket) {
qCWarning(lcSsl, "No external data (QSslSocketBackendPrivate) found in SSL object");
return 0;
}
return privateSocket->emitErrorFromCallback(ctx);
}
return 1;
}
} // extern "C"
Q_GLOBAL_STATIC(QRecursiveMutex, qt_opensslInitMutex)
bool QSslSocketPrivate::s_libraryLoaded = false;
@ -404,12 +521,15 @@ int q_X509Callback(int ok, X509_STORE_CTX *ctx)
// Not found on store? Try SSL and its external data then. According to the OpenSSL's
// documentation:
//
// "Whenever a X509_STORE_CTX object is created for the verification of the peers certificate
// during a handshake, a pointer to the SSL object is stored into the X509_STORE_CTX object
// to identify the connection affected. To retrieve this pointer the X509_STORE_CTX_get_ex_data()
// function can be used with the correct index."
// "Whenever a X509_STORE_CTX object is created for the verification of the
// peer's certificate during a handshake, a pointer to the SSL object is
// stored into the X509_STORE_CTX object to identify the connection affected.
// To retrieve this pointer the X509_STORE_CTX_get_ex_data() function can be
// used with the correct index."
const auto offset = QSslSocketBackendPrivate::s_indexForSSLExtraData
+ QSslSocketBackendPrivate::errorOffsetInExData;
if (SSL *ssl = static_cast<SSL *>(q_X509_STORE_CTX_get_ex_data(ctx, q_SSL_get_ex_data_X509_STORE_CTX_idx())))
errors = ErrorListPtr(q_SSL_get_ex_data(ssl, QSslSocketBackendPrivate::s_indexForSSLExtraData + 1));
errors = ErrorListPtr(q_SSL_get_ex_data(ssl, offset));
}
if (!errors) {
@ -1195,18 +1315,32 @@ bool QSslSocketBackendPrivate::startHandshake()
if (inSetAndEmitError)
return false;
pendingFatalAlert = false;
errorsReportedFromCallback = false;
QVector<QSslErrorEntry> lastErrors;
q_SSL_set_ex_data(ssl, s_indexForSSLExtraData + 1, &lastErrors);
int result = (mode == QSslSocket::SslClientMode) ? q_SSL_connect(ssl) : q_SSL_accept(ssl);
q_SSL_set_ex_data(ssl, s_indexForSSLExtraData + 1, nullptr);
q_SSL_set_ex_data(ssl, s_indexForSSLExtraData + errorOffsetInExData, &lastErrors);
if (!lastErrors.isEmpty())
// SSL_set_ex_data can fail, but see the callback's code - we handle this there.
q_SSL_set_ex_data(ssl, s_indexForSSLExtraData + socketOffsetInExData, this);
q_SSL_set_info_callback(ssl, qt_AlertInfoCallback);
int result = (mode == QSslSocket::SslClientMode) ? q_SSL_connect(ssl) : q_SSL_accept(ssl);
q_SSL_set_ex_data(ssl, s_indexForSSLExtraData + errorOffsetInExData, nullptr);
// Note, unlike errors as external data on SSL object, we do not unset
// a callback/ex-data if alert notifications are enabled: an alert can
// arrive after the handshake, for example, this happens when the server
// does not find a ClientCert or does not like it.
if (!lastErrors.isEmpty() || errorsReportedFromCallback)
storePeerCertificates();
for (const auto &currentError : qAsConst(lastErrors)) {
emit q->peerVerifyError(_q_OpenSSL_to_QSslError(currentError.code,
configuration.peerCertificateChain.value(currentError.depth)));
if (q->state() != QAbstractSocket::ConnectedState)
break;
if (!errorsReportedFromCallback) {
for (const auto &currentError : qAsConst(lastErrors)) {
emit q->peerVerifyError(_q_OpenSSL_to_QSslError(currentError.code,
configuration.peerCertificateChain.value(currentError.depth)));
if (q->state() != QAbstractSocket::ConnectedState)
break;
}
}
errorList << lastErrors;
@ -1230,6 +1364,10 @@ bool QSslSocketBackendPrivate::startHandshake()
{
const ScopedBool bg(inSetAndEmitError, true);
setErrorAndEmit(QAbstractSocket::SslHandshakeFailedError, errorString);
if (pendingFatalAlert) {
trySendFatalAlert();
pendingFatalAlert = false;
}
}
q->abort();
}
@ -1699,6 +1837,88 @@ bool QSslSocketBackendPrivate::checkOcspStatus()
#endif // ocsp
void QSslSocketBackendPrivate::alertMessageSent(int value)
{
Q_Q(QSslSocket);
const auto level = tlsAlertLevel(value);
if (level == QAlertLevel::Fatal && !connectionEncrypted) {
// Note, this logic is handshake-time only:
pendingFatalAlert = true;
}
emit q->alertSent(level, tlsAlertType(value), tlsAlertDescription(value));
}
void QSslSocketBackendPrivate::alertMessageReceived(int value)
{
Q_Q(QSslSocket);
emit q->alertReceived(tlsAlertLevel(value), tlsAlertType(value), tlsAlertDescription(value));
}
int QSslSocketBackendPrivate::emitErrorFromCallback(X509_STORE_CTX *ctx)
{
// Returns 0 to abort verification, 1 to continue despite error (as
// OpenSSL expects from the verification callback).
Q_Q(QSslSocket);
Q_ASSERT(ctx);
using ScopedBool = QScopedValueRollback<bool>;
// While we are not setting, we are emitting and in general -
// we want to prevent accidental recursive startHandshake()
// calls:
const ScopedBool bg(inSetAndEmitError, true);
X509 *x509 = q_X509_STORE_CTX_get_current_cert(ctx);
if (!x509) {
qCWarning(lcSsl, "Could not obtain the certificate (that failed to verify)");
return 0;
}
const QSslCertificate certificate = QSslCertificatePrivate::QSslCertificate_from_X509(x509);
const auto errorAndDepth = QSslErrorEntry::fromStoreContext(ctx);
const QSslError tlsError = _q_OpenSSL_to_QSslError(errorAndDepth.code, certificate);
errorsReportedFromCallback = true;
handshakeInterrupted = true;
emit q->handshakeInterruptedOnError(tlsError);
// Conveniently so, we also can access 'lastErrors' external data set
// in startHandshake, we store it for the case an application later
// wants to check errors (ignored or not):
const auto offset = QSslSocketBackendPrivate::s_indexForSSLExtraData
+ QSslSocketBackendPrivate::errorOffsetInExData;
if (auto errorList = static_cast<QVector<QSslErrorEntry>*>(q_SSL_get_ex_data(ssl, offset)))
errorList->append(errorAndDepth);
// An application is expected to ignore this error (by calling ignoreSslErrors)
// in its directly connected slot:
return !handshakeInterrupted;
}
void QSslSocketBackendPrivate::trySendFatalAlert()
{
Q_ASSERT(pendingFatalAlert);
pendingFatalAlert = false;
QVarLengthArray<char, 4096> data;
int pendingBytes = 0;
while (plainSocket->isValid() && (pendingBytes = q_BIO_pending(writeBio)) > 0
&& plainSocket->openMode() != QIODevice::NotOpen) {
// Read encrypted data from the write BIO into a buffer.
data.resize(pendingBytes);
const int bioReadBytes = q_BIO_read(writeBio, data.data(), pendingBytes);
// Write encrypted data from the buffer to the socket.
qint64 actualWritten = plainSocket->write(data.constData(), bioReadBytes);
if (actualWritten < 0)
return;
plainSocket->flush();
}
}
void QSslSocketBackendPrivate::disconnectFromHost()
{
if (ssl) {

View File

@ -131,6 +131,10 @@ public:
SSL_SESSION *session;
QVector<QSslErrorEntry> errorList;
static int s_indexForSSLExtraData; // index used in SSL_get_ex_data to get the matching QSslSocketBackendPrivate
enum ExDataOffset {
errorOffsetInExData = 1,
socketOffsetInExData = 2
};
bool inSetAndEmitError = false;
@ -157,6 +161,15 @@ public:
bool checkOcspStatus();
#endif
void alertMessageSent(int encoded);
void alertMessageReceived(int encoded);
int emitErrorFromCallback(X509_STORE_CTX *ctx);
void trySendFatalAlert();
bool pendingFatalAlert = false;
bool errorsReportedFromCallback = false;
// This decription will go to setErrorAndEmit(SslHandshakeError, ocspErrorDescription)
QString ocspErrorDescription;
// These will go to sslErrors()

View File

@ -156,6 +156,10 @@ DEFINEFUNC(void, OPENSSL_sk_free, OPENSSL_STACK *a, a, return, DUMMYARG)
DEFINEFUNC2(void *, OPENSSL_sk_value, OPENSSL_STACK *a, a, int b, b, return nullptr, return)
DEFINEFUNC(int, SSL_session_reused, SSL *a, a, return 0, return)
DEFINEFUNC2(unsigned long, SSL_CTX_set_options, SSL_CTX *ctx, ctx, unsigned long op, op, return 0, return)
using info_callback = void (*) (const SSL *ssl, int type, int val);
DEFINEFUNC2(void, SSL_set_info_callback, SSL *ssl, ssl, info_callback cb, cb, return, return)
DEFINEFUNC(const char *, SSL_alert_type_string, int value, value, return nullptr, return)
DEFINEFUNC(const char *, SSL_alert_desc_string_long, int value, value, return nullptr, return)
#ifdef TLS1_3_VERSION
DEFINEFUNC2(int, SSL_CTX_set_ciphersuites, SSL_CTX *ctx, ctx, const char *str, str, return 0, return)
DEFINEFUNC2(void, SSL_set_psk_use_session_callback, SSL *ssl, ssl, q_SSL_psk_use_session_cb_func_t callback, callback, return, DUMMYARG)
@ -839,7 +843,9 @@ bool q_resolveOpenSslSymbols()
RESOLVEFUNC(OPENSSL_sk_value)
RESOLVEFUNC(DH_get0_pqg)
RESOLVEFUNC(SSL_CTX_set_options)
RESOLVEFUNC(SSL_set_info_callback)
RESOLVEFUNC(SSL_alert_type_string)
RESOLVEFUNC(SSL_alert_desc_string_long)
#ifdef TLS1_3_VERSION
RESOLVEFUNC(SSL_CTX_set_ciphersuites)
RESOLVEFUNC(SSL_set_psk_use_session_callback)

View File

@ -719,6 +719,10 @@ int q_OCSP_id_cmp(OCSP_CERTID *a, OCSP_CERTID *b);
void *q_CRYPTO_malloc(size_t num, const char *file, int line);
#define q_OPENSSL_malloc(num) q_CRYPTO_malloc(num, "", 0)
void q_SSL_set_info_callback(SSL *ssl, void (*cb) (const SSL *ssl, int type, int val));
const char *q_SSL_alert_type_string(int value);
const char *q_SSL_alert_desc_string_long(int value);
QT_END_NAMESPACE
#endif

View File

@ -208,6 +208,7 @@ protected:
bool paused;
bool flushTriggered;
QVector<QOcspResponse> ocspResponses;
bool handshakeInterrupted = false;
};
#if QT_CONFIG(securetransport) || QT_CONFIG(schannel)

View File

@ -73,7 +73,7 @@ typedef QSharedPointer<QSslSocket> QSslSocketPtr;
#else
#define FLUKE_CERTIFICATE_ERROR QSslError::CertificateUntrusted
#endif
#endif // QT_NO_SSL
#endif // QT_NO_OPENSSL
// Detect ALPN (Application-Layer Protocol Negotiation) support
#undef ALPN_SUPPORTED // Undef the variable first to be safe
@ -263,6 +263,10 @@ private slots:
void unsupportedProtocols();
void oldErrorsOnSocketReuse();
#if QT_CONFIG(openssl)
void alertMissingCertificate();
void alertInvalidCertificate();
#endif // openssl
void setEmptyDefaultConfiguration(); // this test should be last
@ -337,6 +341,8 @@ tst_QSslSocket::tst_QSslSocket()
qRegisterMetaType<QSslError>("QSslError");
qRegisterMetaType<QAbstractSocket::SocketState>("QAbstractSocket::SocketState");
qRegisterMetaType<QAbstractSocket::SocketError>("QAbstractSocket::SocketError");
qRegisterMetaType<QAlertLevel>("QAlertLevel");
qRegisterMetaType<QAlertType>("QAlertType");
#ifndef QT_NO_OPENSSL
qRegisterMetaType<QSslPreSharedKeyAuthenticator *>();
@ -1210,6 +1216,8 @@ public:
signals:
void socketError(QAbstractSocket::SocketError);
void gotAlert(QAlertLevel level, QAlertType type, const QString &message);
void alertSent(QAlertLevel level, QAlertType type, const QString &message);
protected:
void incomingConnection(qintptr socketDescriptor)
@ -1221,6 +1229,8 @@ protected:
if (ignoreSslErrors)
connect(socket, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(ignoreErrorSlot()));
connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SIGNAL(socketError(QAbstractSocket::SocketError)));
connect(socket, &QSslSocket::alertReceived, this, &SslServer::gotAlert);
connect(socket, &QSslSocket::alertSent, this, &SslServer::alertSent);
QFile file(m_keyFile);
QVERIFY(file.open(QIODevice::ReadOnly));
@ -4418,6 +4428,119 @@ void tst_QSslSocket::oldErrorsOnSocketReuse()
#endif // QT_NO_SSL
#if QT_CONFIG(openssl)
void (QSslSocket::*const tlsErrorSignal)(const QList<QSslError> &) = &QSslSocket::sslErrors;
void (QAbstractSocket::*const socketErrorSignal)(QAbstractSocket::SocketError) = &QAbstractSocket::error;
void tst_QSslSocket::alertMissingCertificate()
{
// In this test we want a server to abort the connection due to the failing
// client authentication. The server expected to send an alert before closing
// the connection, and the client expected to receive this alert and report it.
QFETCH_GLOBAL(const bool, setProxy);
if (setProxy) // Not what we test here, bail out.
return;
SslServer server;
if (!server.listen(QHostAddress::LocalHost))
QSKIP("SslServer::listen() returned false");
// We want a certificate request to be sent to the client:
server.peerVerifyMode = QSslSocket::VerifyPeer;
// The only way we can force OpenSSL to send an alert - is to use
// a special option (so we fail before handshake is finished):
server.config.setMissingCertificateIsFatal(true);
QSslSocket clientSocket;
connect(&clientSocket, tlsErrorSignal, [&clientSocket](const QList<QSslError> &errors){
qDebug() << "ERR";
clientSocket.ignoreSslErrors(errors);
});
QSignalSpy serverSpy(&server, &SslServer::alertSent);
QSignalSpy clientSpy(&clientSocket, &QSslSocket::alertReceived);
clientSocket.connectToHostEncrypted(server.serverAddress().toString(), server.serverPort());
QTestEventLoop runner;
QTimer::singleShot(500, [&runner](){
runner.exitLoop();
});
int waitFor = 2;
auto earlyQuitter = [&runner, &waitFor](QAbstractSocket::SocketError) {
if (!--waitFor)
runner.exitLoop();
};
// Presumably, RemoteHostClosedError for the client and SslHandshakeError
// for the server:
connect(&clientSocket, socketErrorSignal, earlyQuitter);
connect(&server, &SslServer::socketError, earlyQuitter);
runner.enterLoopMSecs(1000);
QVERIFY(serverSpy.count() > 0);
QVERIFY(clientSpy.count() > 0);
QVERIFY(server.socket && !server.socket->isEncrypted());
QVERIFY(!clientSocket.isEncrypted());
}
void tst_QSslSocket::alertInvalidCertificate()
{
// In this test a client will not ignore verification errors,
// it also will do 'early' checks, meaning the reported and
// not ignored _during_ the hanshake, not after. This ensures
// OpenSSL sends an alert.
QFETCH_GLOBAL(const bool, setProxy);
if (setProxy) // Not what we test here, bail out.
return;
SslServer server;
if (!server.listen(QHostAddress::LocalHost))
QSKIP("SslServer::listen() returned false");
QSslSocket clientSocket;
auto configuration = QSslConfiguration::defaultConfiguration();
configuration.setHandshakeMustInterruptOnError(true);
QVERIFY(configuration.handshakeMustInterruptOnError());
clientSocket.setSslConfiguration(configuration);
QSignalSpy serverSpy(&server, &SslServer::gotAlert);
QSignalSpy clientSpy(&clientSocket, &QSslSocket::alertSent);
QSignalSpy interruptedSpy(&clientSocket, &QSslSocket::handshakeInterruptedOnError);
clientSocket.connectToHostEncrypted(server.serverAddress().toString(), server.serverPort());
QTestEventLoop runner;
QTimer::singleShot(500, [&runner](){
runner.exitLoop();
});
int waitFor = 2;
auto earlyQuitter = [&runner, &waitFor](QAbstractSocket::SocketError) {
if (!--waitFor)
runner.exitLoop();
};
// Presumably, RemoteHostClosedError for the server and SslHandshakeError
// for the client:
connect(&clientSocket, socketErrorSignal, earlyQuitter);
connect(&server, &SslServer::socketError, earlyQuitter);
runner.enterLoopMSecs(1000);
QVERIFY(serverSpy.count() > 0);
QVERIFY(clientSpy.count() > 0);
QVERIFY(interruptedSpy.count() > 0);
QVERIFY(server.socket && !server.socket->isEncrypted());
QVERIFY(!clientSocket.isEncrypted());
}
#endif // openssl
QTEST_MAIN(tst_QSslSocket)
#include "tst_qsslsocket.moc"