Adds option for manually trusting a TLS server.

This commit is contained in:
Felix (xq) Queißner 2020-06-21 21:29:30 +02:00
parent 6225064a00
commit 6ef3d6a41f
15 changed files with 129 additions and 21 deletions

View File

@ -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

View File

@ -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"

View File

@ -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)
{

View File

@ -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;

View File

@ -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);

View File

@ -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.

View File

@ -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

View File

@ -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());
}

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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);
};

View File

@ -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;

View File

@ -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);

View File

@ -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;
}
}