Adds improved client certificate management, adds server certificate management.

This commit is contained in:
Felix (xq) Queißner 2020-06-16 00:41:57 +02:00
parent 5bb3f3f92e
commit 33c91102a5
30 changed files with 872 additions and 41 deletions

View File

@ -144,11 +144,11 @@ ln -s /path/to/kristall .
### 0.3 release
- [ ] TLS Handling
- [ ] Allow user to ignore TLS errors
- [ ] Enable TOFU for HTTPS/Gemini
- [ ] Enable TOFU for HTTPS
- [ ] Enable Client Certificate Management
- [ ] Add option: "Transient certificates survive an application reboot and are stored on disk""
- [ ] Add management for client certificates
- [ ] Rename/delete certificates
- [x] Rename/delete certificates
- [ ] Rename/delete/merge groups
- [ ] Import/export PEM certificates and keys
- [ ] Ask if the client certificate should be disabled when switching host and/or protocol
@ -165,13 +165,14 @@ ln -s /path/to/kristall .
- [ ] Improve style import
- [ ] Direct preview instead of importing it as a preset. Allow user to save preset then manually
- [ ] Handle network errors like timeout and such
### Unspecced
- [ ] Survive full torture suite
- [ ] Correctly parse mime parameters
- [ ] Correctly parse charset (0013, 0014)
- [ ] Correctly parse other params (0015)
- [ ] Correctly parse undefined params (0016)
- [ ] Make Kristall survive gemini://egsam.pitr.ca/
### Unspecced
- [ ] Recognize home directories with /~home and such and add "substyles"
- [ ] [Add favicon support](gemini://mozz.us/files/rfc_gemini_favicon.gmi)
- [ ] Add auto-generated "favicons"

View File

@ -6,6 +6,8 @@
* Added this changelog to the software itself
* Fixed bug: Status bar label now does elide links that are too long instead of resizing the window.
* Fixed bug: Gopher end-of-file marker is now better detected.
* Adds support for server certificate handling for gemini://
*
## 0.2
* Implement Ctrl+D/*Add to favourites* menu item

View File

@ -52,6 +52,7 @@ BrowserTab::BrowserTab(MainWindow * mainWindow) :
connect(&gemini_client, &GeminiClient::transientCertificateRequested, this, &BrowserTab::on_transientCertificateRequested);
connect(&gemini_client, &GeminiClient::authorisedCertificateRequested, this, &BrowserTab::on_authorisedCertificateRequested);
connect(&gemini_client, &GeminiClient::certificateRejected, this, &BrowserTab::on_certificateRejected);
connect(&gemini_client, &GeminiClient::networkError, this, &BrowserTab::on_networkError);
connect(&gopher_client, &GopherClient::requestComplete, this, &BrowserTab::on_requestComplete);
connect(&gopher_client, &GopherClient::requestFailed, this, &BrowserTab::on_requestFailed);
@ -268,6 +269,11 @@ void BrowserTab::on_requestFailed(const QString &reason)
this->setErrorMessage(QString("Request failed:\n%1").arg(reason));
}
void BrowserTab::on_networkError(const QString &reason)
{
this->setErrorMessage(QString("Network error:\n%1").arg(reason));
}
void BrowserTab::on_requestComplete(const QByteArray &data, const QString &mime)
{
qDebug() << "Loaded" << data.length() << "bytes of type" << mime;

View File

@ -71,6 +71,8 @@ private slots:
void on_requestFailed(QString const & reason);
void on_networkError(QString const & reason);
void on_protocolViolation(QString const & reason);
void on_inputRequired(QString const & query);

View File

@ -3,7 +3,10 @@
#include "kristall.hpp"
#include "newidentitiydialog.hpp"
#include <QCryptographicHash>
#include <QMessageBox>
CertificateManagementDialog::CertificateManagementDialog(QWidget *parent) :
QDialog(parent),
@ -15,7 +18,13 @@ CertificateManagementDialog::CertificateManagementDialog(QWidget *parent) :
this->ui->certificates->setModel(&global_identities);
this->ui->certificates->expandAll();
on_certificates_clicked(QModelIndex { });
connect(
this->ui->certificates->selectionModel(),
&QItemSelectionModel::currentChanged,
this,
&CertificateManagementDialog::on_certificates_selected
);
on_certificates_selected(QModelIndex { }, QModelIndex { });
}
CertificateManagementDialog::~CertificateManagementDialog()
@ -23,11 +32,12 @@ CertificateManagementDialog::~CertificateManagementDialog()
delete ui;
}
void CertificateManagementDialog::on_certificates_clicked(const QModelIndex &index)
void CertificateManagementDialog::on_certificates_selected(QModelIndex const& index, QModelIndex const & previous)
{
Q_UNUSED(previous);
selected_identity = global_identities.getMutableIdentity(index);
this->ui->delete_cert_button->setEnabled(selected_identity != nullptr);
this->ui->export_cert_button->setEnabled(selected_identity != nullptr);
if(selected_identity != nullptr)
@ -43,6 +53,7 @@ void CertificateManagementDialog::on_certificates_clicked(const QModelIndex &ind
);
this->ui->cert_notes->setPlainText(cert.user_notes);
this->ui->delete_cert_button->setEnabled(true);
}
else
{
@ -52,6 +63,12 @@ void CertificateManagementDialog::on_certificates_clicked(const QModelIndex &ind
this->ui->cert_expiration_date->setDateTime(QDateTime { });
this->ui->cert_livetime->setText("");
this->ui->cert_fingerprint->setPlainText("");
if(auto group_name = global_identities.group(index); not group_name.isEmpty()) {
this->ui->delete_cert_button->setEnabled(global_identities.canDeleteGroup(group_name));
} else {
this->ui->delete_cert_button->setEnabled(false);
}
}
}
@ -68,3 +85,67 @@ void CertificateManagementDialog::on_cert_display_name_textChanged(const QString
this->selected_identity->display_name = this->ui->cert_display_name->text();
}
}
void CertificateManagementDialog::on_delete_cert_button_clicked()
{
auto index = this->ui->certificates->currentIndex();
if(global_identities.getMutableIdentity(index) != nullptr)
{
auto answer = QMessageBox::question(
this,
"Kristall",
"Do you really want to delete this certificate?\r\n\r\nYou will not be able to restore the identity after this!",
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No
);
if(answer != QMessageBox::Yes)
return;
if(not global_identities.destroyIdentity(index)) {
QMessageBox::warning(this, "Kristall", "Could not destroy identity!");
}
}
else if(auto group_name = global_identities.group(index); not group_name.isEmpty()) {
auto answer = QMessageBox::question(
this,
"Kristall",
QString("Do you want to delete the group '%1'").arg(group_name)
);
if(answer != QMessageBox::Yes)
return;
if(not global_identities.deleteGroup(group_name)) {
QMessageBox::warning(this, "Kristall", "Could not delete group!");
}
}
}
void CertificateManagementDialog::on_export_cert_button_clicked()
{
}
void CertificateManagementDialog::on_import_cert_button_clicked()
{
}
void CertificateManagementDialog::on_create_cert_button_clicked()
{
NewIdentitiyDialog dialog { this };
dialog.setGroupName(global_identities.group(this->ui->certificates->currentIndex()));
if(dialog.exec() != QDialog::Accepted)
return;
auto id = dialog.createIdentity();
if(not id.isValid())
return;
id.is_persistent = true;
global_identities.addCertificate(
dialog.groupName(),
id);
}

View File

@ -18,12 +18,19 @@ public:
~CertificateManagementDialog();
private slots:
void on_certificates_clicked(const QModelIndex &index);
void on_cert_notes_textChanged();
void on_cert_display_name_textChanged(const QString &arg1);
void on_delete_cert_button_clicked();
void on_export_cert_button_clicked();
void on_import_cert_button_clicked();
void on_create_cert_button_clicked();
private:
void on_certificates_selected(const QModelIndex &index, QModelIndex const & previous);
private:
Ui::CertificateManagementDialog *ui;

View File

@ -103,6 +103,10 @@
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/delete-alert.svg</normaloff>:/icons/delete-alert.svg</iconset>
</property>
</widget>
</item>
</layout>

View File

@ -2,6 +2,7 @@
#include <cassert>
#include <QDebug>
#include <QSslConfiguration>
#include "kristall.hpp"
GeminiClient::GeminiClient(QObject *parent) : QObject(parent)
{
@ -11,13 +12,10 @@ GeminiClient::GeminiClient(QObject *parent) : QObject(parent)
connect(&socket, QOverload<const QList<QSslError> &>::of(&QSslSocket::sslErrors), this, &GeminiClient::sslErrors);
connect(&socket, QOverload<QAbstractSocket::SocketError>::of(&QSslSocket::error), this, &GeminiClient::socketError);
QSslConfiguration ssl_config;
ssl_config.setProtocol(QSsl::TlsV1_2);
// ssl_config.setLocalCertificate(QSslCertificate::1
ssl_config.setCaCertificates(QList<QSslCertificate> { });
socket.setSslConfiguration(ssl_config);
}
GeminiClient::~GeminiClient()
@ -76,6 +74,8 @@ void GeminiClient::disableClientCertificate()
void GeminiClient::socketEncrypted()
{
qDebug() << "Pub key =" << socket.peerCertificate().publicKey().toPem();
QString request = target_url.toString(QUrl::FormattingOptions(QUrl::FullyEncoded)) + "\r\n";
QByteArray request_bytes = request.toUtf8();
@ -255,13 +255,59 @@ void GeminiClient::socketDisconnected()
}
}
void GeminiClient::sslErrors(const QList<QSslError> &errors)
static bool isTrustRelated(QSslError::SslError err)
{
for(auto const & error : errors) {
qWarning() << error.errorString() ;
switch(err)
{
case QSslError::CertificateUntrusted: return true;
case QSslError::SelfSignedCertificate: return true;
case QSslError::UnableToGetLocalIssuerCertificate: return true;
default: return false;
}
}
void GeminiClient::sslErrors(QList<QSslError> const & errors)
{
QList<QSslError> remaining_errors = errors;
QList<QSslError> ignored_errors;
int i = 0;
while(i < remaining_errors.size())
{
auto const & err = remaining_errors.at(i);
bool ignore = false;
if(isTrustRelated(err.error()))
{
if(global_trust.isTrusted(target_url, socket.peerCertificate()))
{
ignore = true;
}
}
else if(err.error() == QSslError::UnableToVerifyFirstCertificate)
{
ignore = true;
}
if(ignore) {
ignored_errors.append(err);
remaining_errors.removeAt(0);
} else {
i += 1;
}
}
socket.ignoreSslErrors(errors);
socket.ignoreSslErrors(ignored_errors);
qDebug() << "ignoring" << ignored_errors.size() << "out of" << errors.size();
for(auto const & error : remaining_errors) {
qWarning() << int(error.error()) << error.errorString();
}
if(remaining_errors.size() > 0) {
emit this->networkError(remaining_errors.first().errorString());
}
}
void GeminiClient::socketError(QAbstractSocket::SocketError socketError)
@ -272,6 +318,7 @@ void GeminiClient::socketError(QAbstractSocket::SocketError socketError)
if(socketError == QAbstractSocket::RemoteHostClosedError) {
socket.close();
} else {
qWarning() << socketError << socket.errorString();
// qWarning() << socketError << socket.errorString();
emit this->networkError(socket.errorString());
}
}

View File

@ -70,6 +70,8 @@ signals:
void certificateRejected(CertificateRejection reason, QString const & info);
void networkError(QString const & reason);
private slots:
void socketEncrypted();

View File

@ -38,5 +38,6 @@
<file>icons/shield-outline.svg</file>
<file>icons/shield-lock.svg</file>
<file>icons/certificate.svg</file>
<file>icons/delete-alert.svg</file>
</qresource>
</RCC>

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M17 4V6H3V4H6.5L7.5 3H12.5L13.5 4H17M4 19V7H16V19C16 20.1 15.1 21 14 21H6C4.9 21 4 20.1 4 19M19 16H21V18H19V16M19 9H21V14H19V9Z" /></svg>

After

Width:  |  Height:  |  Size: 422 B

View File

@ -7,6 +7,7 @@
IdentityCollection::IdentityCollection(QObject *parent)
: QAbstractItemModel(parent)
{
}
void IdentityCollection::load(QSettings &settings)
@ -95,28 +96,24 @@ void IdentityCollection::save(QSettings &settings) const
settings.endArray();
}
bool IdentityCollection::addGroup(const QString &group_name)
{
GroupNode * group;
return internalAddGroup(group_name, group);
}
bool IdentityCollection::addCertificate(const QString &group_name, const CryptoIdentity &crypto_id)
{
// Don't allow saving transient certificates
if(not crypto_id.is_persistent)
return false;
this->beginResetModel();
GroupNode * group;
internalAddGroup(group_name, group);
GroupNode * group = nullptr;
for(auto const & grp : root.children)
{
auto * g = static_cast<GroupNode*>(grp.get());
if(g->title == group_name) {
group = g;
break;
}
}
if(group == nullptr) {
group = new GroupNode();
group->title = group_name;
this->root.children.emplace_back(group);
}
QModelIndex parent_index = createIndex(group->index, 0, group);
beginInsertRows(parent_index, group->children.size(), group->children.size() + 1);
auto id = std::make_unique<IdentityNode>();
id->identity = crypto_id;
@ -124,7 +121,7 @@ bool IdentityCollection::addCertificate(const QString &group_name, const CryptoI
this->relayout();
this->endResetModel();
this->endInsertRows();
return true;
}
@ -171,6 +168,77 @@ QStringList IdentityCollection::groups() const
return result;
}
QString IdentityCollection::group(const QModelIndex &index) const
{
if (!index.isValid())
return QString { };
Node const *item = static_cast<Node const*>(index.internalPointer());
switch(item->type) {
case Node::Root: return QString { };
case Node::Group: return static_cast<GroupNode const *>(item)->title;
case Node::Identity: return static_cast<IdentityNode const *>(item)->parent->as<GroupNode>().title;
default: return QString { };
}
}
bool IdentityCollection::destroyIdentity(const QModelIndex &index)
{
if (!index.isValid())
return false;
Node * childItem = static_cast<Node *>(index.internalPointer());
Node * parent = childItem->parent;
if (parent == &root)
return false;
beginRemoveRows(this->parent(index), index.row(), index.row() + 1);
parent->children.erase(parent->children.begin() + childItem->index);
endRemoveRows();
return true;
}
bool IdentityCollection::canDeleteGroup(const QString &group_name)
{
for(auto const & group_node : root.children)
{
auto & group = group_node->as<GroupNode>();
if((group.children.size() == 0) and (group.title == group_name))
return true;
}
return false;
}
bool IdentityCollection::deleteGroup(const QString &group_name)
{
size_t index = 0;
for(auto it = root.children.begin(); it != root.children.end(); it++, index++)
{
auto & group = it->get()->as<GroupNode>();
if(group.title == group_name) {
if(group.children.size() > 0) {
qDebug() << "cannot delete non-empty group" << group_name;
return false;
}
beginRemoveRows(QModelIndex { }, index, index + 1);
root.children.erase(it);
endRemoveRows();
return true;
}
}
return false;
}
QModelIndex IdentityCollection::index(int row, int column, const QModelIndex &parent) const
{
if (not hasIndex(row, column, parent))
@ -265,7 +333,7 @@ void IdentityCollection::relayout()
group.parent = &root;
group.index = i;
qDebug() << "group[" << group.index << "]" << group.as<GroupNode>().title;
// qDebug() << "group[" << group.index << "]" << group.as<GroupNode>().title;
for(size_t j = 0; j < group.children.size(); j++)
{
@ -274,7 +342,33 @@ void IdentityCollection::relayout()
id.index = j;
assert(id.children.size() == 0);
qDebug() << "id[" << id.index << "]" << id.as<IdentityNode>().identity.display_name;
// qDebug() << "id[" << id.index << "]" << id.as<IdentityNode>().identity.display_name;
}
}
}
bool IdentityCollection::internalAddGroup(const QString &group_name, GroupNode * & group)
{
for(auto const & grp : root.children)
{
auto * g = static_cast<GroupNode*>(grp.get());
if(g->title == group_name) {
group = g;
return false;
}
}
auto parent = QModelIndex { };
beginInsertRows(parent, this->root.children.size(), this->root.children.size() + 1);
group = new GroupNode();
group->title = group_name;
this->root.children.emplace_back(group);
this->relayout();
endInsertRows();
return true;
}

View File

@ -48,6 +48,8 @@ public:
void save(QSettings & settings) const;
bool addGroup(QString const & group);
bool addCertificate(QString const & group, CryptoIdentity const & id);
CryptoIdentity getIdentity(QModelIndex const & index) const;
@ -56,6 +58,14 @@ public:
QStringList groups() const;
//! Returns the group name of the index.
QString group(QModelIndex const & index) const;
bool destroyIdentity(QModelIndex const & index);
bool canDeleteGroup(QString const & group_name);
bool deleteGroup(QString const & group_name);
public:
// Header:
// QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
@ -73,6 +83,8 @@ public:
private:
void relayout();
bool internalAddGroup(QString const & group_name, GroupNode * & out_group);
private:
RootNode root;
};

View File

@ -5,9 +5,11 @@
#include <QClipboard>
#include "identitycollection.hpp"
#include "ssltrust.hpp"
extern QSettings global_settings;
extern IdentityCollection global_identities;
extern QClipboard * global_clipboard;
extern SslTrust global_trust;
#endif // KRISTALL_HPP

View File

@ -1,4 +1,4 @@
QT += core gui
QT += core gui svg
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets network multimedia multimediawidgets
@ -30,6 +30,13 @@ win32-msvc {
INCLUDEPATH += "C:\Program Files\OpenSSL\include"
}
android: include(/home/felix/projects/android-hass/android-sdk/android_openssl/openssl.pri)
# android {
# INCLUDEPATH += /home/felix/projects/android-hass/android-sdk/android_openssl/static/include
# LIBS += -L /home/felix/projects/android-hass/android-sdk/android_openssl/static/lib/arm/
# }
INCLUDEPATH += $$PWD/../lib/luis-l-gist/
DEPENDPATH += $$PWD/../lib/luis-l-gist/
@ -58,7 +65,10 @@ SOURCES += \
plaintextrenderer.cpp \
protocolsetup.cpp \
settingsdialog.cpp \
ssltrust.cpp \
tabbrowsinghistory.cpp \
trustedhost.cpp \
trustedhostcollection.cpp \
webclient.cpp
HEADERS += \
@ -86,7 +96,10 @@ HEADERS += \
plaintextrenderer.hpp \
protocolsetup.hpp \
settingsdialog.hpp \
ssltrust.hpp \
tabbrowsinghistory.hpp \
trustedhost.hpp \
trustedhostcollection.hpp \
webclient.hpp
FORMS += \

View File

@ -10,6 +10,7 @@
IdentityCollection global_identities;
QSettings global_settings { "xqTechnologies", "Kristall" };
QClipboard * global_clipboard;
SslTrust global_trust;
int main(int argc, char *argv[])
{
@ -28,6 +29,10 @@ int main(int argc, char *argv[])
global_identities.load(global_settings);
global_settings.endGroup();
global_settings.beginGroup("Trusted Servers");
global_trust.load(global_settings);
global_settings.endGroup();
MainWindow w(&app);
auto urls = cli_parser.positionalArguments();

View File

@ -149,6 +149,10 @@ void MainWindow::saveSettings()
global_identities.save(global_settings);
global_settings.endGroup();
global_settings.beginGroup("Trusted Servers");
global_trust.save(global_settings);
global_settings.endGroup();
global_settings.beginGroup("Theme");
this->current_style.save(global_settings);
global_settings.endGroup();
@ -245,6 +249,7 @@ void MainWindow::on_actionSettings_triggered()
dialog.setStartPage(global_settings.value("start_page").toString());
dialog.setProtocols(this->protocols);
dialog.setUiTheme(global_settings.value("theme").toString());
dialog.setSslTrust(global_trust);
if(dialog.exec() != QDialog::Accepted)
return;
@ -253,6 +258,7 @@ void MainWindow::on_actionSettings_triggered()
global_settings.setValue("start_page", url.toString());
}
global_trust = dialog.sslTrust();
global_settings.setValue("theme", dialog.uiTheme());
this->protocols = dialog.protocols();

View File

@ -45,6 +45,11 @@ QString NewIdentitiyDialog::groupName() const
return this->ui->group->currentText();
}
void NewIdentitiyDialog::setGroupName(const QString &name)
{
this->ui->group->setCurrentText(name);
}
void NewIdentitiyDialog::updateUI()
{
bool is_ok = true;
@ -59,16 +64,18 @@ void NewIdentitiyDialog::updateUI()
void NewIdentitiyDialog::on_group_editTextChanged(const QString &arg1)
{
qDebug() << arg1;
Q_UNUSED(arg1);
this->updateUI();
}
void NewIdentitiyDialog::on_display_name_textChanged(const QString &arg1)
{
Q_UNUSED(arg1);
this->updateUI();
}
void NewIdentitiyDialog::on_common_name_textChanged(const QString &arg1)
{
Q_UNUSED(arg1);
this->updateUI();
}

View File

@ -22,6 +22,7 @@ public:
CryptoIdentity createIdentity() const;
QString groupName() const;
void setGroupName(QString const & name);
private slots:
void on_group_editTextChanged(const QString &arg1);

View File

@ -7,6 +7,7 @@
#include <QInputDialog>
#include <QFileDialog>
#include <QMessageBox>
#include <QDebug>
#include "kristall.hpp"
@ -85,6 +86,22 @@ SettingsDialog::SettingsDialog(QWidget *parent) :
this->on_presets_currentIndexChanged(-1);
}
this->ui->trust_level->clear();
this->ui->trust_level->addItem("Trust on first encounter", QVariant::fromValue<int>(SslTrust::TrustOnFirstUse));
this->ui->trust_level->addItem("Trust everything", QVariant::fromValue<int>(SslTrust::TrustEverything));
this->ui->trust_level->addItem("Manually verify fingerprints", QVariant::fromValue<int>(SslTrust::TrustNoOne));
this->ui->trusted_hosts->setModel(&this->current_trust.trusted_hosts);
this->ui->trusted_hosts->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
this->ui->trusted_hosts->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
this->ui->trusted_hosts->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
connect(
this->ui->trusted_hosts->selectionModel(),
&QItemSelectionModel::currentChanged,
this,
&SettingsDialog::on_trusted_server_selection);
}
SettingsDialog::~SettingsDialog()
@ -201,6 +218,27 @@ void SettingsDialog::setUiTheme(const QString &theme)
}
SslTrust SettingsDialog::sslTrust() const
{
return this->current_trust;
}
void SettingsDialog::setSslTrust(const SslTrust &trust)
{
this->current_trust = trust;
this->ui->trust_level->setCurrentIndex(
this->ui->trust_level->findData(QVariant::fromValue<int>(trust.trust_level))
);
if(trust.enable_ca)
this->ui->trust_enable_ca->setChecked(true);
else
this->ui->trust_disable__ca->setChecked(true);
this->ui->trusted_hosts->resizeColumnsToContents();
}
void SettingsDialog::reloadStylePreview()
{
auto const document = R"gemini(# H1 Header
@ -295,6 +333,15 @@ void SettingsDialog::updateColor(QColor &input)
}
}
void SettingsDialog::on_trusted_server_selection(const QModelIndex &current, const QModelIndex &previous)
{
if(auto host = this->current_trust.trusted_hosts.get(current); host) {
this->ui->trust_revoke_selected->setEnabled(true);
} else {
this->ui->trust_revoke_selected->setEnabled(false);
}
}
void SettingsDialog::on_std_change_color_clicked()
{
updateColor(current_style.standard_color);
@ -531,3 +578,23 @@ void SettingsDialog::on_preset_export_clicked()
this->predefined_styles.value(name).save(export_settings);
export_settings.sync();
}
void SettingsDialog::on_trust_enable_ca_clicked()
{
this->current_trust.enable_ca = true;
}
void SettingsDialog::on_trust_disable__ca_clicked()
{
this->current_trust.enable_ca = false;
}
void SettingsDialog::on_trust_level_currentIndexChanged(int index)
{
this->current_trust.trust_level = SslTrust::TrustLevel(this->ui->trust_level->itemData(index).toInt());
}
void SettingsDialog::on_trust_revoke_selected_clicked()
{
this->current_trust.trusted_hosts.remove(this->ui->trusted_hosts->currentIndex());
}

View File

@ -6,6 +6,7 @@
#include "geminirenderer.hpp"
#include "protocolsetup.hpp"
#include "documentstyle.hpp"
#include "ssltrust.hpp"
namespace Ui {
class SettingsDialog;
@ -34,6 +35,9 @@ public:
QString uiTheme() const;
void setUiTheme(QString const & theme);
SslTrust sslTrust() const;
void setSslTrust(SslTrust const & trust);
private slots:
void on_std_change_font_clicked();
@ -89,6 +93,14 @@ private slots:
void on_preset_export_clicked();
void on_trust_enable_ca_clicked();
void on_trust_disable__ca_clicked();
void on_trust_level_currentIndexChanged(int index);
void on_trust_revoke_selected_clicked();
private:
void reloadStylePreview();
@ -96,6 +108,8 @@ private:
void updateColor(QColor & input);
void on_trusted_server_selection(QModelIndex const & current, QModelIndex const & previous);
private:
Ui::SettingsDialog *ui;
@ -103,6 +117,8 @@ private:
std::unique_ptr<QTextDocument> preview_document;
QMap<QString, DocumentStyle> predefined_styles;
SslTrust current_trust;
};
#endif // SETTINGSDIALOG_HPP

View File

@ -24,6 +24,10 @@
<number>0</number>
</property>
<widget class="QWidget" name="generic">
<attribute name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/settings.svg</normaloff>:/icons/settings.svg</iconset>
</attribute>
<attribute name="title">
<string>Generic</string>
</attribute>
@ -240,6 +244,10 @@
</layout>
</widget>
<widget class="QWidget" name="style_tab">
<attribute name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/palette.svg</normaloff>:/icons/palette.svg</iconset>
</attribute>
<attribute name="title">
<string>Style</string>
</attribute>
@ -794,6 +802,103 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="gem_trust_page">
<attribute name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/certificate.svg</normaloff>:/icons/certificate.svg</iconset>
</attribute>
<attribute name="title">
<string>Gemini TLS</string>
</attribute>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_23">
<property name="text">
<string>Trust Level</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="trust_level"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_24">
<property name="text">
<string>Certificate Authorities</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QRadioButton" name="trust_enable_ca">
<property name="text">
<string>Use local certificate authorities</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup_2</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="trust_disable__ca">
<property name="text">
<string>Don't use local certificate authorities</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup_2</string>
</attribute>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_25">
<property name="text">
<string>Trusted Hosts</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTableView" name="trusted_hosts">
<property name="cornerButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QToolButton" name="trust_revoke_selected">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Revoke trust</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
<item>
@ -846,9 +951,10 @@
</connection>
</connections>
<buttongroups>
<buttongroup name="buttonGroup"/>
<buttongroup name="gophermapBtnGroup"/>
<buttongroup name="textHighlightsBtnGroup"/>
<buttongroup name="gophermapBtnGroup"/>
<buttongroup name="textRenderingBtnGroup"/>
<buttongroup name="buttonGroup_2"/>
<buttongroup name="buttonGroup"/>
</buttongroups>
</ui>

76
src/ssltrust.cpp Normal file
View File

@ -0,0 +1,76 @@
#include "ssltrust.hpp"
#include <QDebug>
void SslTrust::load(QSettings &settings)
{
trust_level = TrustLevel(settings.value("trust_level", int(TrustOnFirstUse)).toInt());
enable_ca = settings.value("enable_ca", QVariant::fromValue(false)).toBool();
trusted_hosts.clear();
int size = settings.beginReadArray("trusted_hosts");
for(int i = 0; i < size; i++)
{
settings.setArrayIndex(i);
auto key_type = QSsl::KeyAlgorithm(settings.value("key_type").toInt());
auto key_value = settings.value("key_bits").toByteArray();
TrustedHost host;
host.host_name = settings.value("host_name").toString();
host.trusted_at = settings.value("trusted_at").toDateTime();
host.public_key = QSslKey(key_value, key_type, QSsl::Der, QSsl::PublicKey);
trusted_hosts.insert(host);
}
settings.endArray();
}
void SslTrust::save(QSettings &settings) const
{
settings.setValue("trust_level", int(trust_level));
settings.setValue("enable_ca", enable_ca);
auto all = trusted_hosts.getAll();
settings.beginWriteArray("trusted_hosts", all.size());
for(int i = 0; i < all.size(); i++)
{
settings.setArrayIndex(i);
settings.setValue("host_name", all.at(i).host_name);
settings.setValue("trusted_at", all.at(i).trusted_at);
settings.setValue("key_type", int(all.at(i).public_key.algorithm()));
settings.setValue("key_bits", all.at(i).public_key.toDer());
}
settings.endArray();
}
bool SslTrust::isTrusted(QUrl const & url, const QSslCertificate &certificate)
{
if(trust_level == TrustEverything)
return true;
if(auto host_or_none = trusted_hosts.get(url.host()); host_or_none)
{
if(host_or_none->public_key == certificate.publicKey())
return true;
qDebug() << "certificate mismatch for" << url;
return false;
}
else
{
if(trust_level == TrustOnFirstUse)
{
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;
}
return false;
}
}

37
src/ssltrust.hpp Normal file
View File

@ -0,0 +1,37 @@
#ifndef SSLTRUST_HPP
#define SSLTRUST_HPP
#include <QSslCertificate>
#include <QSslKey>
#include <QSettings>
#include "trustedhostcollection.hpp"
struct SslTrust
{
enum TrustLevel {
TrustOnFirstUse = 0, // default
TrustEverything = 1, // not recommended
TrustNoOne = 2, // approve every fingerprint by hand
};
SslTrust() = default;
SslTrust(SslTrust const &) = default;
SslTrust(SslTrust &&) = default;
SslTrust & operator=(SslTrust const &) = default;
SslTrust & operator=(SslTrust &&) = default;
TrustLevel trust_level = TrustOnFirstUse;
TrustedHostCollection trusted_hosts;
bool enable_ca = false;
void load(QSettings & settings);
void save(QSettings & settings) const;
bool isTrusted(QUrl const & url, QSslCertificate const & certificate);
};
#endif // SSLTRUST_HPP

2
src/trustedhost.cpp Normal file
View File

@ -0,0 +1,2 @@
#include "trustedhost.hpp"

15
src/trustedhost.hpp Normal file
View File

@ -0,0 +1,15 @@
#ifndef TRUSTEDHOST_HPP
#define TRUSTEDHOST_HPP
#include <QSslKey>
#include <QUrl>
#include <QDateTime>
struct TrustedHost
{
QString host_name;
QSslKey public_key;
QDateTime trusted_at;
};
#endif // TRUSTEDHOST_HPP

View File

@ -0,0 +1,151 @@
#include "trustedhostcollection.hpp"
TrustedHostCollection::TrustedHostCollection(QObject *parent)
: QAbstractTableModel(parent)
{
}
TrustedHostCollection::TrustedHostCollection(const TrustedHostCollection & other) :
items(other.items)
{
assert(other.parent() == nullptr);
}
TrustedHostCollection::TrustedHostCollection(TrustedHostCollection &&other) :
items(std::move(other.items))
{
assert(other.parent() == nullptr);
}
QVariant TrustedHostCollection::headerData(int section, Qt::Orientation orientation, int role) const
{
if(orientation == Qt::Vertical)
return QVariant { };
if(role == Qt::DisplayRole)
{
switch(section)
{
case 0: return "Host Name";
case 1: return "First Seen";
case 2: return "Key Type";
}
}
return QVariant { };
}
int TrustedHostCollection::rowCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return items.size();
}
int TrustedHostCollection::columnCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return 3;
}
QVariant TrustedHostCollection::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant { };
if(index.row() < 0 or index.row() >= items.size())
return QVariant { };
auto const & host = items.at(index.row());
if(role == Qt::DisplayRole)
{
switch(index.column())
{
case 0: return host.host_name;
case 1: return host.trusted_at.toString();
case 2: switch(host.public_key.algorithm())
{
case QSsl::Rsa: return "RSA";
case QSsl::Ec: return "EC";
case QSsl::Dh: return "DH";
case QSsl::Dsa: return "DSA";
case QSsl::Opaque: return "Opaque";
default: return "Unforseen";
}
}
}
return QVariant { };
}
TrustedHostCollection &TrustedHostCollection::operator=(const TrustedHostCollection & other)
{
beginResetModel();
this->items = other.items;
endResetModel();
return *this;
}
TrustedHostCollection &TrustedHostCollection::operator=(TrustedHostCollection && other)
{
beginResetModel();
this->items = std::move(other.items);
endResetModel();
return *this;
}
void TrustedHostCollection::clear()
{
beginResetModel();
this->items.clear();
endResetModel();
}
bool TrustedHostCollection::insert(const TrustedHost &host)
{
for(auto const & item : items)
{
if(item.host_name == host.host_name)
return false;
}
beginInsertRows(QModelIndex { }, items.size(), items.size() + 1);
items.append(host);
endInsertRows();
return true;
}
std::optional<TrustedHost> TrustedHostCollection::get(QString const & host_name) const
{
for(auto const & item : items)
{
if(item.host_name == host_name)
return item;
}
return std::nullopt;
}
std::optional<TrustedHost> TrustedHostCollection::get(const QModelIndex &index) const
{
if(not index.isValid())
return std::nullopt;
if(index.row() < 0 or index.row() >= items.size())
return std::nullopt;
return items.at(index.row());
}
void TrustedHostCollection::remove(const QModelIndex &index)
{
if(not index.isValid())
return;
if(index.row() < 0 or index.row() >= items.size())
return;
beginRemoveRows(QModelIndex{}, index.row(), index.row() + 1);
items.removeAt(index.row());
endRemoveRows();
}
QVector<TrustedHost> TrustedHostCollection::getAll() const
{
return items;
}

View File

@ -0,0 +1,48 @@
#ifndef TRUSTEDHOSTCOLLECTION_HPP
#define TRUSTEDHOSTCOLLECTION_HPP
#include <QAbstractTableModel>
#include "trustedhost.hpp"
#include <optional>
class TrustedHostCollection : public QAbstractTableModel
{
Q_OBJECT
public:
explicit TrustedHostCollection(QObject *parent = nullptr);
TrustedHostCollection(TrustedHostCollection const &);
TrustedHostCollection(TrustedHostCollection &&);
// Header:
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
// Basic functionality:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
public:
TrustedHostCollection & operator=(TrustedHostCollection const &);
TrustedHostCollection & operator=(TrustedHostCollection &&);
void clear();
bool insert(TrustedHost const & host);
std::optional<TrustedHost> get(QString const & host_name) const;
std::optional<TrustedHost> get(QModelIndex const & index) const;
void remove(QModelIndex const & index);
QVector<TrustedHost> getAll() const;
private:
QVector<TrustedHost> items;
};
#endif // TRUSTEDHOSTCOLLECTION_HPP

View File

@ -35,6 +35,8 @@ bool WebClient::startRequest(const QUrl &url)
connect(this->current_reply, &QNetworkReply::readyRead, this, &WebClient::on_data);
connect(this->current_reply, &QNetworkReply::finished, this, &WebClient::on_finished);
connect(this->current_reply, &QNetworkReply::errorOccurred, this, &WebClient::on_networkError);
connect(this->current_reply, &QNetworkReply::sslErrors, this, &WebClient::on_sslErrors);
return true;
}
@ -71,7 +73,7 @@ void WebClient::on_finished()
{
auto mime = this->current_reply->header(QNetworkRequest::ContentTypeHeader).toString();
qDebug() << this->current_reply->url() << mime;
// qDebug() << this->current_reply->url() << mime;
emit this->requestComplete(this->body, mime);
@ -80,3 +82,15 @@ void WebClient::on_finished()
this->current_reply->deleteLater();
this->current_reply = nullptr;
}
void WebClient::on_networkError(QNetworkReply::NetworkError code)
{
qDebug() << code << this->current_reply->errorString();
}
void WebClient::on_sslErrors(const QList<QSslError> &errors)
{
for(auto const & err : errors)
qDebug() << err;
this->current_reply->ignoreSslErrors();
}

View File

@ -3,6 +3,7 @@
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
class WebClient: public QObject
{
@ -26,9 +27,13 @@ signals:
void requestFailed(QString const & message);
void networkError(QString const & message);
private slots:
void on_data();
void on_finished();
void on_networkError(QNetworkReply::NetworkError code);
void on_sslErrors(const QList<QSslError> &errors);
private:
QNetworkAccessManager manager;