diff options
| author | Felix (xq) Queißner <git@mq32.de> | 2020-06-21 21:29:30 +0200 |
|---|---|---|
| committer | Felix (xq) Queißner <git@mq32.de> | 2020-06-21 21:29:30 +0200 |
| commit | 6ef3d6a41f07a2f43a9b69f4e75adbffe634ea09 (patch) | |
| tree | 791ad53823e47ecff837ec6004aa80c8fb1e1445 | |
| parent | 6225064a008eccb9099ed2db49dad04c5f6d0550 (diff) | |
| download | kristall-6ef3d6a41f07a2f43a9b69f4e75adbffe634ea09.tar.gz | |
Adds option for manually trusting a TLS server.
| -rw-r--r-- | BUILDING.md | 1 | ||||
| -rw-r--r-- | ROADMAP.md | 1 | ||||
| -rw-r--r-- | src/browsertab.cpp | 48 | ||||
| -rw-r--r-- | src/browsertab.hpp | 2 | ||||
| -rw-r--r-- | src/certificatemanagementdialog.cpp | 4 | ||||
| -rw-r--r-- | src/error_page/MistrustedHost.gemini | 5 | ||||
| -rw-r--r-- | src/error_page/UntrustedHost.gemini | 10 | ||||
| -rw-r--r-- | src/geminiclient.cpp | 24 | ||||
| -rw-r--r-- | src/geminiclient.hpp | 1 | ||||
| -rw-r--r-- | src/kristall.hpp | 6 | ||||
| -rw-r--r-- | src/main.cpp | 5 | ||||
| -rw-r--r-- | src/protocolhandler.hpp | 3 | ||||
| -rw-r--r-- | src/ssltrust.cpp | 24 | ||||
| -rw-r--r-- | src/ssltrust.hpp | 3 | ||||
| -rw-r--r-- | src/webclient.cpp | 13 |
15 files changed, 129 insertions, 21 deletions
diff --git a/BUILDING.md b/BUILDING.md index dfa727a..b86cfc4 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -74,6 +74,7 @@ Use the `Makefile` to build `build/kristall` instead of the default target. Ther - `qt5_devel` - `qt5_tools` - `libiconv_devel` + - `openssl_devel` (should be preinstalled) 2. Use `make` to build th executable ## Manual Installation @@ -38,6 +38,7 @@ This document contains TODO items for planned Kristall releases as well as some - [ ] Make default protocol configurable - [ ] Ctrl-F search in documents - [ ] Add "view source" option to show original document +- [ ] Implement graphic fingerprint display instead of hex-based one ## Unspecced - [ ] Add option: "Transient certificates survive an application reboot and are stored on disk" diff --git a/src/browsertab.cpp b/src/browsertab.cpp index 0ab0308..c417bf0 100644 --- a/src/browsertab.cpp +++ b/src/browsertab.cpp @@ -34,6 +34,7 @@ #include <QMimeType> #include <QImageReader> #include <QClipboard> +#include <QDesktopServices> #include <QGraphicsPixmapItem> #include <QGraphicsTextItem> @@ -257,6 +258,11 @@ void BrowserTab::on_certificateRequired(const QString &reason) this->updateUI(); } +void BrowserTab::on_hostCertificateLoaded(const QSslCertificate &cert) +{ + this->current_server_certificate = cert; +} + static QByteArray convertToUtf8(QByteArray const & input, QString const & charSet) { QFile temp { "/tmp/raw.dat" }; @@ -510,6 +516,8 @@ File Size: %2 void BrowserTab::on_inputRequired(const QString &query) { + this->network_timeout_timer.stop(); + QInputDialog dialog{this}; dialog.setInputMode(QInputDialog::TextInput); @@ -644,17 +652,18 @@ void BrowserTab::on_fav_button_clicked() toggleIsFavourite(this->ui->fav_button->isChecked()); } -#include <QDesktopServices> - void BrowserTab::on_text_browser_anchorClicked(const QUrl &url) { - qDebug() << url; + static int click_count = 0; + qDebug() << (++click_count) << url; if(url.scheme() == "kristall+ctrl") { if(this->is_internal_location) { QString opt = url.path(); qDebug() << "kristall control action" << opt; + + // this will bypass the TLS security if(opt == "ignore-tls") { auto response = QMessageBox::question( this, @@ -667,6 +676,36 @@ void BrowserTab::on_text_browser_anchorClicked(const QUrl &url) this->startRequest(this->current_location, ProtocolHandler::IgnoreTlsErrors); } } + // + else if(opt == "ignore-tls-safe") { + this->startRequest(this->current_location, ProtocolHandler::IgnoreTlsErrors); + } + // Add this page to the list of trusted hosts and continue + else if(opt == "add-fingerprint") { + auto answer = QMessageBox::question( + this, + "Kristall", + tr("Do you really want to add the server certificate to your list of trusted hosts?\r\nHost: %1") + .arg(this->current_location.host()), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes // that's a sane option here + ); + if(answer != QMessageBox::Yes) { + return; + } + + if(this->current_location.scheme() == "gemini") { + global_gemini_trust.addTrust(this->current_location, this->current_server_certificate); + } + else if(this->current_location.scheme() == "https") { + global_https_trust.addTrust(this->current_location, this->current_server_certificate); + } + else { + assert(false and "missing protocol implementation!"); + } + + this->startRequest(this->current_location, ProtocolHandler::Default); + } } else { QMessageBox::critical( this, @@ -803,12 +842,15 @@ void BrowserTab::addProtocolHandler(std::unique_ptr<ProtocolHandler> &&handler) connect(handler.get(), &ProtocolHandler::inputRequired, this, &BrowserTab::on_inputRequired); connect(handler.get(), &ProtocolHandler::networkError, this, &BrowserTab::on_networkError); connect(handler.get(), &ProtocolHandler::certificateRequired, this, &BrowserTab::on_certificateRequired); + connect(handler.get(), &ProtocolHandler::hostCertificateLoaded, this, &BrowserTab::on_hostCertificateLoaded); this->protocol_handlers.emplace_back(std::move(handler)); } bool BrowserTab::startRequest(const QUrl &url, ProtocolHandler::RequestOptions options) { + this->current_server_certificate = QSslCertificate { }; + this->current_handler = nullptr; for(auto & ptr : this->protocol_handlers) { diff --git a/src/browsertab.hpp b/src/browsertab.hpp index e491f9e..9adaccf 100644 --- a/src/browsertab.hpp +++ b/src/browsertab.hpp @@ -106,6 +106,7 @@ private: // network slots void on_inputRequired(QString const & user_query); void on_networkError(ProtocolHandler::NetworkError error, QString const & reason); void on_certificateRequired(QString const & info); + void on_hostCertificateLoaded(QSslCertificate const & cert); void on_networkTimeout(); @@ -151,6 +152,7 @@ public: QModelIndex current_history_index; std::unique_ptr<QTextDocument> current_document; + QSslCertificate current_server_certificate; QByteArray current_buffer; QString current_mime; diff --git a/src/certificatemanagementdialog.cpp b/src/certificatemanagementdialog.cpp index 3102ac8..5141b30 100644 --- a/src/certificatemanagementdialog.cpp +++ b/src/certificatemanagementdialog.cpp @@ -50,9 +50,7 @@ void CertificateManagementDialog::on_certificates_selected(QModelIndex const& in this->ui->cert_common_name->setText(cert.certificate.subjectInfo(QSslCertificate::CommonName).join(", ")); this->ui->cert_expiration_date->setDateTime(cert.certificate.expiryDate()); this->ui->cert_livetime->setText(QString("%1 days").arg(QDateTime::currentDateTime().daysTo(cert.certificate.expiryDate()))); - this->ui->cert_fingerprint->setPlainText( - QCryptographicHash::hash(cert.certificate.toDer(), QCryptographicHash::Sha256).toHex(':') - ); + this->ui->cert_fingerprint->setPlainText(toFingerprintString(cert.certificate)); this->ui->cert_notes->setPlainText(cert.user_notes); this->ui->cert_host_filter->setText(cert.host_filter); diff --git a/src/error_page/MistrustedHost.gemini b/src/error_page/MistrustedHost.gemini index 7673de3..f32826f 100644 --- a/src/error_page/MistrustedHost.gemini +++ b/src/error_page/MistrustedHost.gemini @@ -2,6 +2,7 @@ The host you tried to visit does not look trustworty anymore. The certificate changed since your last visit. -If you still trust this host, please revoke trust in the settings menu, then reload the page. +Fingerprint: +%1 -> %1 +If you still trust this host, please revoke trust in the settings menu, then reload the page. diff --git a/src/error_page/UntrustedHost.gemini b/src/error_page/UntrustedHost.gemini index 41c21c0..afe0c24 100644 --- a/src/error_page/UntrustedHost.gemini +++ b/src/error_page/UntrustedHost.gemini @@ -1,6 +1,12 @@ # Untrusted Host The host you tried to visit is not trusted by Kristall. If you do trust this server, please add it to the list of trusted certificates! -(which is currently not possible ☹) -> %1 +Fingerprint: +``` +%1 +``` + +=> kristall+ctrl:ignore-tls-safe Continue to site (once) + +=> kristall+ctrl:add-fingerprint Add fingerprint to trusted hosts diff --git a/src/geminiclient.cpp b/src/geminiclient.cpp index 12351ca..6986250 100644 --- a/src/geminiclient.cpp +++ b/src/geminiclient.cpp @@ -9,9 +9,9 @@ GeminiClient::GeminiClient() : ProtocolHandler(nullptr) connect(&socket, &QSslSocket::encrypted, this, &GeminiClient::socketEncrypted); connect(&socket, &QSslSocket::readyRead, this, &GeminiClient::socketReadyRead); connect(&socket, &QSslSocket::disconnected, this, &GeminiClient::socketDisconnected); - connect(&socket, &QSslSocket::stateChanged, [](QSslSocket::SocketState state) { - qDebug() << "Socket state changed to " << state; - }); +// connect(&socket, &QSslSocket::stateChanged, [](QSslSocket::SocketState state) { +// qDebug() << "Socket state changed to " << state; +// }); connect(&socket, QOverload<const QList<QSslError> &>::of(&QSslSocket::sslErrors), this, &GeminiClient::sslErrors); #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) @@ -45,6 +45,8 @@ bool GeminiClient::startRequest(const QUrl &url, RequestOptions options) return false; } + this->is_error_state = false; + this->options = options; QSslConfiguration ssl_config = socket.sslConfiguration(); @@ -55,7 +57,6 @@ bool GeminiClient::startRequest(const QUrl &url, RequestOptions options) ssl_config.setCaCertificates(QSslConfiguration::systemCaCertificates()); socket.setSslConfiguration(ssl_config); - socket.connectToHostEncrypted(url.host(), url.port(1965)); this->buffer.clear(); @@ -113,7 +114,7 @@ void GeminiClient::disableClientCertificate() void GeminiClient::socketEncrypted() { - // qDebug() << "Pub key =" << socket.peerCertificate().publicKey().toPem(); + emit this->hostCertificateLoaded(this->socket.peerCertificate()); QString request = target_url.toString(QUrl::FormattingOptions(QUrl::FullyEncoded)) + "\r\n"; @@ -133,6 +134,8 @@ void GeminiClient::socketEncrypted() void GeminiClient::socketReadyRead() { + if(this->is_error_state) // don't do any further + return; QByteArray response = socket.readAll(); if(is_receiving_body) @@ -287,7 +290,7 @@ void GeminiClient::socketReadyRead() void GeminiClient::socketDisconnected() { - if(is_receiving_body) { + if(this->is_receiving_body and not this->is_error_state) { body.append(socket.readAll()); emit requestComplete(body, mime_type); } @@ -295,6 +298,8 @@ void GeminiClient::socketDisconnected() void GeminiClient::sslErrors(QList<QSslError> const & errors) { + emit this->hostCertificateLoaded(this->socket.peerCertificate()); + if(options & IgnoreTlsErrors) { socket.ignoreSslErrors(errors); return; @@ -317,12 +322,14 @@ void GeminiClient::sslErrors(QList<QSslError> const & errors) ignore = true; break; case SslTrust::Untrusted: + this->is_error_state = true; this->suppress_socket_tls_error = true; - emit this->networkError(UntrustedHost, "The requested host is not trusted."); + emit this->networkError(UntrustedHost, toFingerprintString(socket.peerCertificate())); return; case SslTrust::Mistrusted: + this->is_error_state = true; this->suppress_socket_tls_error = true; - emit this->networkError(MistrustedHost, "The requested host is in the trust store and its signature changed..."); + emit this->networkError(MistrustedHost, toFingerprintString(socket.peerCertificate())); return; } } @@ -360,6 +367,7 @@ void GeminiClient::socketError(QAbstractSocket::SocketError socketError) if(socketError == QAbstractSocket::RemoteHostClosedError) { socket.close(); } else { + this->is_error_state = true; if(not this->suppress_socket_tls_error) { this->emitNetworkError(socketError, socket.errorString()); } diff --git a/src/geminiclient.hpp b/src/geminiclient.hpp index 79514c0..85c4f76 100644 --- a/src/geminiclient.hpp +++ b/src/geminiclient.hpp @@ -42,6 +42,7 @@ private slots: private: bool is_receiving_body; bool suppress_socket_tls_error; + bool is_error_state; QUrl target_url; QSslSocket socket; diff --git a/src/kristall.hpp b/src/kristall.hpp index 8f80045..1f0a65d 100644 --- a/src/kristall.hpp +++ b/src/kristall.hpp @@ -3,6 +3,7 @@ #include <QSettings> #include <QClipboard> +#include <QSslCertificate> #include "identitycollection.hpp" #include "ssltrust.hpp" @@ -44,6 +45,11 @@ struct GenericSettings void save(QSettings & settings) const; }; +//! Converts the certificate to a standardized fingerprint representation +//! also commonly used in browsers: +//! `:`-separated SHA256 hash +QString toFingerprintString(QSslCertificate const & certificate); + extern QSettings global_settings; extern IdentityCollection global_identities; extern QClipboard * global_clipboard; diff --git a/src/main.cpp b/src/main.cpp index f5252b8..742af6d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,6 +15,11 @@ SslTrust global_https_trust; FavouriteCollection global_favourites; GenericSettings global_options; +QString toFingerprintString(QSslCertificate const & certificate) +{ + return QCryptographicHash::hash(certificate.toDer(), QCryptographicHash::Sha256).toHex(':'); +} + int main(int argc, char *argv[]) { QApplication app(argc, argv); diff --git a/src/protocolhandler.hpp b/src/protocolhandler.hpp index 2fc60db..c5bb653 100644 --- a/src/protocolhandler.hpp +++ b/src/protocolhandler.hpp @@ -61,6 +61,9 @@ signals: //! The server wants us to use a client certificate void certificateRequired(QString const & info); + + //! The server uses TLS and has a certificate. + void hostCertificateLoaded(QSslCertificate const & cert); protected: void emitNetworkError(QAbstractSocket::SocketError error_code, QString const & textual_description); }; diff --git a/src/ssltrust.cpp b/src/ssltrust.cpp index f35988e..7ccdf9c 100644 --- a/src/ssltrust.cpp +++ b/src/ssltrust.cpp @@ -47,6 +47,27 @@ void SslTrust::save(QSettings &settings) const settings.endArray(); } +bool SslTrust::addTrust(const QUrl &url, const QSslCertificate &certificate) +{ + if(certificate.isNull()) + return false; + if(auto host_or_none = trusted_hosts.get(url.host()); host_or_none) + { + return false; + } + else + { + TrustedHost host; + host.host_name = url.host(); + host.trusted_at = QDateTime::currentDateTime(); + host.public_key = certificate.publicKey(); + + bool ok = trusted_hosts.insert(host); + assert(ok); + return true; + } +} + bool SslTrust::isTrusted(QUrl const & url, const QSslCertificate &certificate) { return (getTrust(url, certificate) == Trusted); @@ -54,6 +75,9 @@ bool SslTrust::isTrusted(QUrl const & url, const QSslCertificate &certificate) SslTrust::TrustStatus SslTrust::getTrust(const QUrl &url, const QSslCertificate &certificate) { + if(certificate.isNull()) + return Untrusted; + if(trust_level == TrustEverything) return Trusted; diff --git a/src/ssltrust.hpp b/src/ssltrust.hpp index 96a4d83..4214d8a 100644 --- a/src/ssltrust.hpp +++ b/src/ssltrust.hpp @@ -38,6 +38,9 @@ struct SslTrust void load(QSettings & settings); void save(QSettings & settings) const; + //! Adds the certificate to the trust store. Returns `true` on success. + bool addTrust(QUrl const & url, QSslCertificate const & certificate); + bool isTrusted(QUrl const & url, QSslCertificate const & certificate); TrustStatus getTrust(QUrl const & url, QSslCertificate const & certificate); diff --git a/src/webclient.cpp b/src/webclient.cpp index ecbcfef..ed87694 100644 --- a/src/webclient.cpp +++ b/src/webclient.cpp @@ -50,6 +50,8 @@ bool WebClient::startRequest(const QUrl &url, RequestOptions options) request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, false); request.setSslConfiguration(ssl_config); + this->manager.clearAccessCache(); + this->manager.clearConnectionCache(); this->current_reply = manager.get(request); if(this->current_reply == nullptr) return false; @@ -99,6 +101,8 @@ void WebClient::on_data() void WebClient::on_finished() { + emit this->hostCertificateLoaded(this->current_reply->sslConfiguration().peerCertificate()); + auto * const reply = this->current_reply; this->current_reply = nullptr; @@ -159,6 +163,8 @@ void WebClient::on_finished() void WebClient::on_sslErrors(const QList<QSslError> &errors) { + emit this->hostCertificateLoaded(this->current_reply->sslConfiguration().peerCertificate()); + if(options & IgnoreTlsErrors) { this->current_reply->ignoreSslErrors(errors); return; @@ -175,18 +181,19 @@ void WebClient::on_sslErrors(const QList<QSslError> &errors) bool ignore = false; if(SslTrust::isTrustRelated(err.error())) { - switch(global_https_trust.getTrust(this->current_reply->url(), this->current_reply->sslConfiguration().peerCertificate())) + auto cert = this->current_reply->sslConfiguration().peerCertificate(); + switch(global_https_trust.getTrust(this->current_reply->url(), cert)) { case SslTrust::Trusted: ignore = true; break; case SslTrust::Untrusted: this->suppress_socket_tls_error = true; - emit this->networkError(UntrustedHost, "The requested host is not trusted."); + emit this->networkError(UntrustedHost, toFingerprintString(cert)); return; case SslTrust::Mistrusted: this->suppress_socket_tls_error = true; - emit this->networkError(MistrustedHost, "The requested is in the trust store and its signature changed.."); + emit this->networkError(MistrustedHost, toFingerprintString(cert)); return; } } |
