Schannel: Reject certificate not signed by a configured CA certificate

Not entirely clear why, but when building the certificate chain for a
peer the system certificate store is searched for root certificates.
General expectation is that after calling
`sslConfiguration.setCaCertificates()` the system certificates will
not be taken into consideration.

To work around this behavior, we do a manual check that the root of the
chain is part of the configured CA certificates.

Change-Id: I03666a4d9b0eac39ae97e150b4743120611a11b3
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
(cherry picked from commit ada2c573c1a25f8d96577734968fe317ddfa292a)
This commit is contained in:
Mårten Nordheim 2023-05-10 16:43:41 +02:00
parent e6cc67dcf6
commit 79e6df3e84
7 changed files with 279 additions and 0 deletions

View File

@ -1880,6 +1880,28 @@ bool QSslSocketBackendPrivate::verifyCertContext(CERT_CONTEXT *certContext)
if (configuration.peerVerifyDepth > 0 && DWORD(configuration.peerVerifyDepth) < verifyDepth)
verifyDepth = DWORD(configuration.peerVerifyDepth);
const auto &caCertificates = q->sslConfiguration().caCertificates();
if (!rootCertOnDemandLoadingAllowed()
&& !(chain->TrustStatus.dwErrorStatus & CERT_TRUST_IS_PARTIAL_CHAIN)
&& (q->peerVerifyMode() == QSslSocket::VerifyPeer
|| (isClient && q->peerVerifyMode() == QSslSocket::AutoVerifyPeer))) {
// When verifying a peer Windows "helpfully" builds a chain that
// may include roots from the system store. But we don't want that if
// the user has set their own CA certificates.
// Since Windows claims this is not a partial chain the root is included
// and we have to check that it is one of our configured CAs.
CERT_CHAIN_ELEMENT *element = chain->rgpElement[chain->cElement - 1];
QSslCertificate certificate = getCertificateFromChainElement(element);
if (!caCertificates.contains(certificate)) {
auto error = QSslError(QSslError::CertificateUntrusted, certificate);
sslErrors += error;
emit q->peerVerifyError(error);
if (q->state() != QAbstractSocket::ConnectedState)
return false;
}
}
for (DWORD i = 0; i < verifyDepth; i++) {
CERT_CHAIN_ELEMENT *element = chain->rgpElement[i];
QSslCertificate certificate = getCertificateFromChainElement(element);

View File

@ -0,0 +1,24 @@
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
qt_internal_add_manual_test(tst_manual_ssl_client_auth
SOURCES
tst_manual_ssl_client_auth.cpp
LIBRARIES
Qt::Network
)
qt_internal_add_resource(tst_manual_ssl_client_auth "tst_manual_ssl_client_auth"
PREFIX
"/"
FILES
"certs/127.0.0.1.pem"
"certs/127.0.0.1-key.pem"
"certs/127.0.0.1-client.pem"
"certs/127.0.0.1-client-key.pem"
"certs/accepted-client.pem"
"certs/accepted-client-key.pem"
"certs/rootCA.pem"
BASE
"certs"
)

View File

@ -0,0 +1,4 @@
*
!/.gitignore
!/generate.sh
!/accepted-client.conf

View File

@ -0,0 +1,14 @@
[req]
default_md = sha512
basicConstraints = CA:FALSE
extendedKeyUsage = clientAuth
[req]
distinguished_name = client_distinguished_name
prompt = no
[client_distinguished_name]
C = NO
ST = Oslo
L = Oslo
O = The Qt Project
OU = The Qt Project
CN = Fake Qt Project Client Certificate

View File

@ -0,0 +1,33 @@
#!/bin/bash
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
# Requires mkcert and openssl
warn () { echo "$@" >&2; }
die () { warn "$@"; exit 1; }
command -v mkcert 1>/dev/null 2>&1 || die "Failed to find mkcert"
command -v openssl 1>/dev/null 2>&1 || die "Failed to find openssl"
SCRIPT=$(realpath "$0")
SCRIPTPATH=$(dirname "$SCRIPT")
pushd "$SCRIPTPATH" || die "Unable to pushd to $SCRIPTPATH"
mkcert 127.0.0.1
mkcert -client 127.0.0.1
warn "Remember to run mkcert -install if you haven't already"
# Generate CA
openssl genrsa -out ca-key.pem 2048
openssl req -new -x509 -noenc -days 365 -key ca-key.pem -out rootCA.pem
# Generate accepted client certificate
openssl genrsa -out accepted-client-key.pem 2048
openssl req -new -sha512 -nodes -key accepted-client-key.pem -out accepted-client.csr -config accepted-client.conf
openssl x509 -req -sha512 -days 45 -in accepted-client.csr -CA rootCA.pem -CAkey ca-key.pem -CAcreateserial -out accepted-client.pem
rm accepted-client.csr
rm rootCA.srl
popd || die "Unable to popd"

View File

@ -0,0 +1,18 @@
TEMPLATE = app
TARGET = tst_manual_ssl_client_auth
CONFIG += cmdline
QT = network
SOURCES += tst_manual_ssl_client_auth.cpp
cert_bundle.files = \
certs/127.0.0.1.pem \
certs/127.0.0.1-key.pem \
certs/127.0.0.1-client.pem \
certs/127.0.0.1-client-key.pem \
certs/accepted-client.pem \
certs/accepted-client-key.pem \
certs/rootCA.pem
cert_bundle.base = certs
RESOURCES += cert_bundle

View File

@ -0,0 +1,164 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QtCore/qcoreapplication.h>
#include <QtCore/qthread.h>
#include <QtCore/qfile.h>
#include <QtCore/qdir.h>
#include <QtNetwork/qsslsocket.h>
#include <QtNetwork/qtcpserver.h>
#include <QtNetwork/qsslconfiguration.h>
#include <QtNetwork/qsslkey.h>
// Client and/or server presents a certificate signed by a system-trusted CA
// but the other side presents a certificate signed by a different CA.
constexpr bool TestServerPresentsIncorrectCa = true;
constexpr bool TestClientPresentsIncorrectCa = false;
/* No built-in QSslServer in this branch .... */
class QSslServer : public QTcpServer
{
Q_OBJECT
public:
QSslServer(QObject *parent = nullptr) : QTcpServer(parent) {}
void setSslConfiguration(const QSslConfiguration &config)
{
m_config = config;
}
QSslConfiguration sslConfiguration() const
{
return m_config;
}
protected:
void incomingConnection(qintptr handle) override
{
QSslSocket *socket = new QSslSocket(this);
socket->setSocketDescriptor(handle, QAbstractSocket::ConnectedState);
socket->setSslConfiguration(m_config);
socket->startServerEncryption();
QObject::connect(socket, &QSslSocket::peerVerifyError, this, [this, socket](const QSslError &error) {
emit peerVerifyError(socket, error);
});
emit startedEncryptionHandshake(socket);
if (socket->waitForEncrypted()) {
QTcpServer::addPendingConnection(socket);
emit pendingConnectionAvailable(socket);
} else {
emit errorOccurred(socket, socket->error());
socket->disconnectFromHost();
}
}
signals:
void pendingConnectionAvailable(QSslSocket *socket);
void startedEncryptionHandshake(QSslSocket *socket);
void errorOccurred(QSslSocket *socket, QAbstractSocket::SocketError error);
void peerVerifyError(QSslSocket *socket, const QSslError &error);
private:
QSslConfiguration m_config;
};
class ServerThread : public QThread
{
Q_OBJECT
public:
void run() override
{
QSslServer server;
QSslConfiguration config = server.sslConfiguration();
QList<QSslCertificate> certs = QSslCertificate::fromPath(QStringLiteral(":/rootCA.pem"));
config.setCaCertificates(certs);
config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(":/127.0.0.1.pem"))
.first());
QFile keyFile(QStringLiteral(":/127.0.0.1-key.pem"));
if (!keyFile.open(QIODevice::ReadOnly))
qFatal("Failed to open key file");
config.setPrivateKey(QSslKey(&keyFile, QSsl::Rsa));
config.setPeerVerifyMode(QSslSocket::VerifyPeer);
server.setSslConfiguration(config);
connect(&server, &QSslServer::pendingConnectionAvailable, [&server]() {
QSslSocket *socket = qobject_cast<QSslSocket *>(server.nextPendingConnection());
qDebug() << "[s] newConnection" << socket->peerAddress() << socket->peerPort();
});
connect(&server, &QSslServer::startedEncryptionHandshake, [](QSslSocket *socket) {
qDebug() << "[s] new handshake" << socket->peerAddress() << socket->peerPort();
});
connect(&server, &QSslServer::errorOccurred,
[](QSslSocket *socket, QAbstractSocket::SocketError error) {
qDebug() << "[s] errorOccurred" << socket->peerAddress() << socket->peerPort()
<< error << socket->errorString();
});
connect(&server, &QSslServer::peerVerifyError,
[](QSslSocket *socket, const QSslError &error) {
qDebug() << "[s] peerVerifyError" << socket->peerAddress() << socket->peerPort()
<< error;
});
server.listen(QHostAddress::LocalHost, 24242);
exec();
server.close();
}
};
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
if (!QFileInfo(":/rootCA.pem").exists())
qFatal("rootCA.pem not found. Did you run generate.sh in the certs directory?");
ServerThread serverThread;
serverThread.start();
QSslSocket socket;
QSslConfiguration config = socket.sslConfiguration();
QString certificatePath;
QString keyFileName;
if (TestClientPresentsIncorrectCa) { // true: Present cert signed with incorrect CA: should fail
certificatePath = ":/127.0.0.1-client.pem";
keyFileName = ":/127.0.0.1-client-key.pem";
} else { // false: Use correct CA: should succeed
certificatePath = ":/accepted-client.pem";
keyFileName = ":/accepted-client-key.pem";
}
config.setLocalCertificate(QSslCertificate::fromPath(certificatePath).first());
if (TestServerPresentsIncorrectCa) // true: Verify server using incorrect CA: should fail
config.setCaCertificates(QSslCertificate::fromPath(":/rootCA.pem"));
QFile keyFile(keyFileName);
if (!keyFile.open(QIODevice::ReadOnly))
qFatal("Failed to open key file");
config.setPrivateKey(QSslKey(&keyFile, QSsl::Rsa));
socket.setSslConfiguration(config);
QObject::connect(&socket, &QSslSocket::encrypted, []() { qDebug() << "[c] encrypted"; });
QObject::connect(&socket, &QSslSocket::errorOccurred,
[&socket](QAbstractSocket::SocketError error) {
qDebug() << "[c] errorOccurred" << error << socket.errorString();
qApp->quit();
});
QObject::connect(&socket, qOverload<const QList<QSslError> &>(&QSslSocket::sslErrors),
[](const QList<QSslError> &errors) {
qDebug() << "[c] sslErrors" << errors;
});
QObject::connect(&socket, &QSslSocket::connected, []() { qDebug() << "[c] connected"; });
socket.connectToHostEncrypted(QStringLiteral("127.0.0.1"), 24242);
const int res = app.exec();
serverThread.quit();
serverThread.wait();
return res;
}
#include "tst_manual_ssl_client_auth.moc"