Adds option for manually trusting a TLS server.
This commit is contained in:
parent
6225064a00
commit
6ef3d6a41f
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue