Работа с сетью в Qt

SSL/TLS в Qt: настройка QSslSocket и работа с сертификатами

Введение

Любой сетевой обмен, который пересекает недоверенную сеть, должен быть зашифрован — это требование сегодня является базовым, а не опциональным. В Qt шифрование TCP-соединений реализуется через QSslSocket, наследника QTcpSocket, который добавляет TLS-хэндшейк, проверку сертификатов и контроль версий протокола. В статье разберём базовую настройку клиентского TLS-соединения и типичные проблемы с сертификатами.

Концепция

QSslSocket выполняет TLS-хэндшейк после вызова connectToHostEncrypted(). Об успешном завершении хэндшейка сигнализирует encrypted(), а о проблемах с сертификатом сервера — sslErrors(const QList<QSslError> &). По умолчанию, если есть ошибки сертификата (например, самоподписанный сертификат или истёкший срок действия) и приложение не обработало sslErrors, соединение будет прервано — это защита от атак «человек посередине» (MITM).

Для тестовых сред с самоподписанными сертификатами разработчики иногда игнорируют все ошибки SSL через socket->ignoreSslErrors(), но это крайне небезопасно для продакшена — правильнее добавить конкретный сертификат в список доверенных через QSslConfiguration::setCaCertificates().

Пример кода: SSL/TLS в Qt

#include <QCoreApplication>
#include <QSslSocket>
#include <QSslConfiguration>
#include <QDebug>

class SecureClient : public QObject
{
    Q_OBJECT
public:
    explicit SecureClient(QObject *parent = nullptr) : QObject(parent)
    {
        connect(&m_socket, &QSslSocket::encrypted, this, &SecureClient::onEncrypted);
        connect(&m_socket, &QSslSocket::sslErrors, this, &SecureClient::onSslErrors);
        connect(&m_socket, &QSslSocket::readyRead, this, &SecureClient::onReadyRead);
    }

    void connectToServer(const QString &host, quint16 port)
    {
        qDebug() << "Поддерживаемые версии TLS:" << QSslSocket::supportedProtocols();
        m_socket.connectToHostEncrypted(host, port);
    }

private slots:
    void onEncrypted()
    {
        qDebug() << "TLS-соединение установлено, протокол:"
                  << m_socket.sessionProtocol();
        QSslCertificate cert = m_socket.peerCertificate();
        qDebug() << "Сертификат сервера выдан для:" << cert.subjectInfo(QSslCertificate::CommonName);

        m_socket.write("GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n");
    }

    void onSslErrors(const QList<QSslError> &errors)
    {
        for (const QSslError &error : errors) {
            qWarning() << "Ошибка SSL:" << error.errorString();
        }
        // В продакшене НЕ игнорировать ошибки бездумно. Здесь — только для отладки
        // с самоподписанным сертификатом конкретного известного сервера.
        // m_socket.ignoreSslErrors();
    }

    void onReadyRead()
    {
        qDebug() << m_socket.readAll();
    }

private:
    QSslSocket m_socket;
};

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    SecureClient client;
    client.connectToServer("example.com", 443);
    return app.exec();
}

#include "main.moc"

Пояснения к коду

После вызова connectToHostEncrypted() сокет выполняет и TCP-подключение, и TLS-хэндшейк. В обработчике onEncrypted() можно получить информацию о согласованном протоколе через sessionProtocol() и о сертификате сервера через peerCertificate(). Метод onSslErrors() показывает, как перехватывать ошибки сертификата — в данном примере они только логируются, а закомментированный вызов ignoreSslErrors() показывает, где было бы место для обхода ошибок в контролируемой тестовой среде.

Подводные камни

  • ignoreSslErrors() без аргументов игнорирует ВСЕ ошибки SSL, включая критичные (например, отозванный сертификат или несовпадение домена) — используйте версию с конкретным списком ожидаемых ошибок ignoreSslErrors(const QList<QSslError>&), если нужно сделать исключение только для известной ситуации.
  • Отсутствие системных корневых сертификатов на embedded-платформах. На некоторых встраиваемых Linux-сборках нет стандартного хранилища CA-сертификатов — в этом случае QSslSocket не сможет проверить даже валидный сертификат, и нужно явно подключать бандл сертификатов (например, из ca-certificates).
  • Устаревшие версии TLS. По умолчанию Qt может разрешать старые протоколы (TLS 1.0/1.1) для совместимости — для безопасных систем стоит явно ограничить QSslConfiguration::setProtocol(QSsl::TlsV1_2OrLater).
  • Несовпадение библиотеки OpenSSL во время сборки и выполнения. На Windows и Linux при динамической линковке OpenSSL версия библиотеки в системе должна быть совместима с той, под которую собран Qt — иначе QSslSocket::supportedProtocols() может вернуть пустой список.