aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFelix (xq) Queißner <git@mq32.de>2020-06-21 21:29:30 +0200
committerFelix (xq) Queißner <git@mq32.de>2020-06-21 21:29:30 +0200
commit6ef3d6a41f07a2f43a9b69f4e75adbffe634ea09 (patch)
tree791ad53823e47ecff837ec6004aa80c8fb1e1445
parent6225064a008eccb9099ed2db49dad04c5f6d0550 (diff)
downloadkristall-6ef3d6a41f07a2f43a9b69f4e75adbffe634ea09.tar.gz
Adds option for manually trusting a TLS server.
-rw-r--r--BUILDING.md1
-rw-r--r--ROADMAP.md1
-rw-r--r--src/browsertab.cpp48
-rw-r--r--src/browsertab.hpp2
-rw-r--r--src/certificatemanagementdialog.cpp4
-rw-r--r--src/error_page/MistrustedHost.gemini5
-rw-r--r--src/error_page/UntrustedHost.gemini10
-rw-r--r--src/geminiclient.cpp24
-rw-r--r--src/geminiclient.hpp1
-rw-r--r--src/kristall.hpp6
-rw-r--r--src/main.cpp5
-rw-r--r--src/protocolhandler.hpp3
-rw-r--r--src/ssltrust.cpp24
-rw-r--r--src/ssltrust.hpp3
-rw-r--r--src/webclient.cpp13
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
diff --git a/ROADMAP.md b/ROADMAP.md
index b3a3eb3..5216acd 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -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;
}
}