Better dialog for selecting certificates

Instead of just a simple list of "nickname"'s which is kind of like the
signing backend's internal ID for a given certificate, provide some more
context to each key.

This is a slight improvement, not a complete overhaul.
This commit is contained in:
Sune Vuorela 2023-05-03 13:41:14 +00:00 committed by Albert Astals Cid
parent d3e1c32ff7
commit 763328a6ba
9 changed files with 171 additions and 44 deletions

View file

@ -5,8 +5,24 @@
*/
#include "signatureutils.h"
#include <KLocalizedString>
using namespace Okular;
static QString handleEmpty(const QString &string, CertificateInfo::EmptyString empty)
{
if (string.isEmpty()) {
switch (empty) {
case CertificateInfo::EmptyString::Empty:
return {};
case CertificateInfo::EmptyString::TranslatedNotAvailable:
return i18n("Not Available");
}
return {};
}
return string;
}
class EntityInfo
{
public:
@ -77,17 +93,17 @@ void CertificateInfo::setSerialNumber(const QByteArray &serialNumber)
d->serialNumber = serialNumber;
}
QString CertificateInfo::issuerInfo(EntityInfoKey key) const
QString CertificateInfo::issuerInfo(EntityInfoKey key, EmptyString empty) const
{
switch (key) {
case EntityInfoKey::CommonName:
return d->issuerInfo.commonName;
return handleEmpty(d->issuerInfo.commonName, empty);
case EntityInfoKey::DistinguishedName:
return d->issuerInfo.distinguishedName;
return handleEmpty(d->issuerInfo.distinguishedName, empty);
case EntityInfoKey::EmailAddress:
return d->issuerInfo.emailAddress;
return handleEmpty(d->issuerInfo.emailAddress, empty);
case EntityInfoKey::Organization:
return d->issuerInfo.organization;
return handleEmpty(d->issuerInfo.organization, empty);
}
return QString();
}
@ -110,17 +126,17 @@ void CertificateInfo::setIssuerInfo(EntityInfoKey key, const QString &value)
}
}
QString CertificateInfo::subjectInfo(EntityInfoKey key) const
QString CertificateInfo::subjectInfo(EntityInfoKey key, EmptyString empty) const
{
switch (key) {
case EntityInfoKey::CommonName:
return d->subjectInfo.commonName;
return handleEmpty(d->subjectInfo.commonName, empty);
case EntityInfoKey::DistinguishedName:
return d->subjectInfo.distinguishedName;
return handleEmpty(d->subjectInfo.distinguishedName, empty);
case EntityInfoKey::EmailAddress:
return d->subjectInfo.emailAddress;
return handleEmpty(d->subjectInfo.emailAddress, empty);
case EntityInfoKey::Organization:
return d->subjectInfo.organization;
return handleEmpty(d->subjectInfo.organization, empty);
}
return QString();
}

View file

@ -45,6 +45,11 @@ public:
EmailAddress,
Organization,
};
/**
* How should certain empty strings be treated
* @since 23.08
*/
enum class EmptyString { /** Empty strings should just be empty*/ Empty, TranslatedNotAvailable /** Empty strings should be a localized version of "Not available" */ };
/**
* Destructor
@ -91,7 +96,7 @@ public:
* Information about the issuer.
* @since 23.08
*/
QString issuerInfo(EntityInfoKey key) const;
QString issuerInfo(EntityInfoKey key, EmptyString empty) const;
/**
* Sets information about the issuer.
@ -103,7 +108,7 @@ public:
* Information about the subject
* @since 23.08
*/
QString subjectInfo(EntityInfoKey key) const;
QString subjectInfo(EntityInfoKey key, EmptyString empty) const;
/**
* Sets information about the subject

View file

@ -76,7 +76,9 @@ bool PDFSettingsWidget::event(QEvent *e)
for (const auto &cert : certs) {
new QTreeWidgetItem(m_tree,
{cert.subjectInfo(Okular::CertificateInfo::EntityInfoKey::CommonName), cert.subjectInfo(Okular::CertificateInfo::EntityInfoKey::EmailAddress), cert.validityEnd().toString(QStringLiteral("yyyy-MM-dd"))});
{cert.subjectInfo(Okular::CertificateInfo::EntityInfoKey::CommonName, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable),
cert.subjectInfo(Okular::CertificateInfo::EntityInfoKey::EmailAddress, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable),
cert.validityEnd().toString(QStringLiteral("yyyy-MM-dd"))});
}
m_pdfsw.defaultLabel->setText(Poppler::getNSSDir());

View file

@ -10,14 +10,6 @@
#include <QDebug>
#include <QInputDialog>
static QString notAvailableIfEmpty(const QString &string)
{
if (string.isEmpty()) {
return i18n("Not Available");
}
return string;
}
static Okular::CertificateInfo::KeyUsageExtensions fromPoppler(Poppler::CertificateInfo::KeyUsageExtensions popplerKu)
{
using namespace Okular;
@ -75,8 +67,8 @@ Okular::CertificateInfo fromPoppler(const Poppler::CertificateInfo &pInfo)
oInfo.setSerialNumber(pInfo.serialNumber());
for (auto key :
{Poppler::CertificateInfo::EntityInfoKey::CommonName, Poppler::CertificateInfo::EntityInfoKey::DistinguishedName, Poppler::CertificateInfo::EntityInfoKey::EmailAddress, Poppler::CertificateInfo::EntityInfoKey::Organization}) {
oInfo.setIssuerInfo(static_cast<Okular::CertificateInfo::EntityInfoKey>(key), notAvailableIfEmpty(pInfo.issuerInfo(key)));
oInfo.setSubjectInfo(static_cast<Okular::CertificateInfo::EntityInfoKey>(key), notAvailableIfEmpty(pInfo.subjectInfo(key)));
oInfo.setIssuerInfo(static_cast<Okular::CertificateInfo::EntityInfoKey>(key), pInfo.issuerInfo(key));
oInfo.setSubjectInfo(static_cast<Okular::CertificateInfo::EntityInfoKey>(key), pInfo.subjectInfo(key));
}
oInfo.setNickName(pInfo.nickName());
oInfo.setValidityStart(pInfo.validityStart());

View file

@ -73,29 +73,29 @@ QString CertificateModel::propertyVisibleValue(CertificateModel::Property p) con
case CertificateModel::SerialNumber:
return QString::fromLatin1(m_certificateInfo.serialNumber().toHex(' '));
case CertificateModel::Issuer:
return m_certificateInfo.issuerInfo(Okular::CertificateInfo::DistinguishedName);
return m_certificateInfo.issuerInfo(Okular::CertificateInfo::DistinguishedName, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable);
case CertificateModel::IssuedOn:
return QLocale().toString(m_certificateInfo.validityStart(), QLocale::LongFormat);
case CertificateModel::ExpiresOn:
return QLocale().toString(m_certificateInfo.validityEnd(), QLocale::LongFormat);
case CertificateModel::Subject:
return m_certificateInfo.subjectInfo(Okular::CertificateInfo::DistinguishedName);
return m_certificateInfo.subjectInfo(Okular::CertificateInfo::DistinguishedName, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable);
case CertificateModel::PublicKey:
return i18n("%1 (%2 bits)", SignatureGuiUtils::getReadablePublicKeyType(m_certificateInfo.publicKeyType()), m_certificateInfo.publicKeyStrength());
case CertificateModel::KeyUsage:
return SignatureGuiUtils::getReadableKeyUsageCommaSeparated(m_certificateInfo.keyUsageExtensions());
case CertificateModel::IssuerName:
return m_certificateInfo.issuerInfo(Okular::CertificateInfo::CommonName);
return m_certificateInfo.issuerInfo(Okular::CertificateInfo::CommonName, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable);
case CertificateModel::IssuerEmail:
return m_certificateInfo.issuerInfo(Okular::CertificateInfo::EmailAddress);
return m_certificateInfo.issuerInfo(Okular::CertificateInfo::EmailAddress, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable);
case CertificateModel::IssuerOrganization:
return m_certificateInfo.issuerInfo(Okular::CertificateInfo::Organization);
return m_certificateInfo.issuerInfo(Okular::CertificateInfo::Organization, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable);
case CertificateModel::SubjectName:
return m_certificateInfo.subjectInfo(Okular::CertificateInfo::CommonName);
return m_certificateInfo.subjectInfo(Okular::CertificateInfo::CommonName, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable);
case CertificateModel::SubjectEmail:
return m_certificateInfo.subjectInfo(Okular::CertificateInfo::EmailAddress);
return m_certificateInfo.subjectInfo(Okular::CertificateInfo::EmailAddress, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable);
case CertificateModel::SubjectOrganization:
return m_certificateInfo.subjectInfo(Okular::CertificateInfo::Organization);
return m_certificateInfo.subjectInfo(Okular::CertificateInfo::Organization, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable);
case CertificateModel::Sha1:
return QString::fromLatin1(QCryptographicHash::hash(m_certificateInfo.certificateData(), QCryptographicHash::Sha1).toHex(' '));
case CertificateModel::Sha256:

View file

@ -132,16 +132,16 @@ CertificateViewer::CertificateViewer(const Okular::CertificateInfo &certInfo, QW
auto issuerBox = new QGroupBox(i18n("Issued By"), generalPage);
auto issuerFormLayout = new QFormLayout(issuerBox);
issuerFormLayout->setLabelAlignment(Qt::AlignLeft);
issuerFormLayout->addRow(i18n("Common Name(CN)"), new QLabel(m_certificateInfo.issuerInfo(Okular::CertificateInfo::CommonName)));
issuerFormLayout->addRow(i18n("EMail"), new QLabel(m_certificateInfo.issuerInfo(Okular::CertificateInfo::EmailAddress)));
issuerFormLayout->addRow(i18n("Organization(O)"), new QLabel(m_certificateInfo.issuerInfo(Okular::CertificateInfo::Organization)));
issuerFormLayout->addRow(i18n("Common Name(CN)"), new QLabel(m_certificateInfo.issuerInfo(Okular::CertificateInfo::CommonName, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable)));
issuerFormLayout->addRow(i18n("EMail"), new QLabel(m_certificateInfo.issuerInfo(Okular::CertificateInfo::EmailAddress, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable)));
issuerFormLayout->addRow(i18n("Organization(O)"), new QLabel(m_certificateInfo.issuerInfo(Okular::CertificateInfo::Organization, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable)));
auto subjectBox = new QGroupBox(i18n("Issued To"), generalPage);
auto subjectFormLayout = new QFormLayout(subjectBox);
subjectFormLayout->setLabelAlignment(Qt::AlignLeft);
subjectFormLayout->addRow(i18n("Common Name(CN)"), new QLabel(m_certificateInfo.subjectInfo(Okular::CertificateInfo::CommonName)));
subjectFormLayout->addRow(i18n("EMail"), new QLabel(m_certificateInfo.subjectInfo(Okular::CertificateInfo::EmailAddress)));
subjectFormLayout->addRow(i18n("Organization(O)"), new QLabel(m_certificateInfo.subjectInfo(Okular::CertificateInfo::Organization)));
subjectFormLayout->addRow(i18n("Common Name(CN)"), new QLabel(m_certificateInfo.subjectInfo(Okular::CertificateInfo::CommonName, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable)));
subjectFormLayout->addRow(i18n("EMail"), new QLabel(m_certificateInfo.subjectInfo(Okular::CertificateInfo::EmailAddress, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable)));
subjectFormLayout->addRow(i18n("Organization(O)"), new QLabel(m_certificateInfo.subjectInfo(Okular::CertificateInfo::Organization, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable)));
auto validityBox = new QGroupBox(i18n("Validity"), generalPage);
auto validityFormLayout = new QFormLayout(validityBox);

View file

@ -377,7 +377,7 @@ public:
documentPassword.clear();
} else {
certNicknameToUse = signInfo->certificate->nickName();
certCommonName = signInfo->certificate->subjectInfo(Okular::CertificateInfo::CommonName);
certCommonName = signInfo->certificate->subjectInfo(Okular::CertificateInfo::CommonName, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable);
passToUse = signInfo->certificatePassword;
documentPassword = signInfo->documentPassword;
}

View file

@ -1,5 +1,7 @@
/*
SPDX-FileCopyrightText: 2018 Chinmoy Ranjan Pradhan <chinmoyrp65@gmail.com>
SPDX-FileCopyrightText: 2023 g10 Code GmbH
SPDX-FileContributor: Sune Stolborg Vuorela <sune@vuorela.dk>
SPDX-License-Identifier: GPL-2.0-or-later
*/
@ -11,16 +13,24 @@
#include "core/page.h"
#include "pageview.h"
#include <QApplication>
#include <QFileDialog>
#include <QFileInfo>
#include <QInputDialog>
#include <QLabel>
#include <QListView>
#include <QMimeDatabase>
#include <QPainter>
#include <QPushButton>
#include <QStandardItemModel>
#include <QVBoxLayout>
#include <KLocalizedString>
#include <KMessageBox>
namespace SignaturePartUtils
{
std::optional<SigningInformation> getCertificateAndPasswordForSigning(PageView *pageView, Okular::Document *doc)
{
const Okular::CertificateStore *certStore = doc->certificateStore();
@ -37,19 +47,44 @@ std::optional<SigningInformation> getCertificateAndPasswordForSigning(PageView *
QString password;
QString documentPassword;
QStringList items;
QStandardItemModel items;
QHash<QString, Okular::CertificateInfo> nickToCert;
int minWidth = -1;
for (const auto &cert : qAsConst(certs)) {
items.append(cert.nickName());
auto item = std::make_unique<QStandardItem>();
QString commonName = cert.subjectInfo(Okular::CertificateInfo::CommonName, Okular::CertificateInfo::EmptyString::Empty);
item->setData(commonName, Qt::UserRole);
QString emailAddress = cert.subjectInfo(Okular::CertificateInfo::EmailAddress, Okular::CertificateInfo::EmptyString::Empty);
item->setData(emailAddress, Qt::UserRole + 1);
minWidth = std::max(minWidth, std::max(cert.nickName().size(), emailAddress.size() + commonName.size()));
item->setData(cert.nickName(), Qt::DisplayRole);
item->setData(cert.subjectInfo(Okular::CertificateInfo::DistinguishedName, Okular::CertificateInfo::EmptyString::Empty), Qt::ToolTipRole);
item->setEditable(false);
items.appendRow(item.release());
nickToCert[cert.nickName()] = cert;
}
bool resok = false;
const QString certNicknameToUse = QInputDialog::getItem(pageView, i18n("Select certificate to sign with"), i18n("Certificates:"), items, 0, false, &resok);
SelectCertificateDialog dialog(pageView);
QFontMetrics fm = dialog.fontMetrics();
dialog.list->setMinimumWidth(fm.averageCharWidth() * (minWidth + 5));
dialog.list->setModel(&items);
dialog.list->setAlternatingRowColors(true);
dialog.list->setSelectionMode(QAbstractItemView::SingleSelection);
QObject::connect(dialog.list->selectionModel(), &QItemSelectionModel::selectionChanged, &dialog, [dialog = &dialog](auto &&, auto &&) {
// One can ctrl-click on the selected item to deselect it, that would
// leave the selection empty, so better prevent the OK button
// from being usable
dialog->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(dialog->list->selectionModel()->hasSelection());
});
dialog.list->selectionModel()->select(items.index(0, 0), QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect);
auto result = dialog.exec();
if (!resok) {
if (result == QDialog::Rejected) {
return std::nullopt;
}
auto certNicknameToUse = dialog.list->selectionModel()->currentIndex().data(Qt::DisplayRole).toString();
// I could not find any case in which i need to enter a password to use the certificate, seems that once you unlcok the firefox/NSS database
// you don't need a password anymore, but still there's code to do that in NSS so we have code to ask for it if needed. What we do is
@ -107,7 +142,7 @@ void signUnsignedSignature(const Okular::FormFieldSignature *form, PageView *pag
Okular::NewSignatureData data;
data.setCertNickname(signingInfo->certificate->nickName());
data.setCertSubjectCommonName(signingInfo->certificate->subjectInfo(Okular::CertificateInfo::CommonName));
data.setCertSubjectCommonName(signingInfo->certificate->subjectInfo(Okular::CertificateInfo::CommonName, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable));
data.setPassword(signingInfo->certificatePassword);
data.setDocumentPassword(signingInfo->documentPassword);
@ -123,4 +158,57 @@ void signUnsignedSignature(const Okular::FormFieldSignature *form, PageView *pag
}
}
SelectCertificateDialog::SelectCertificateDialog(QWidget *parent)
: QDialog(parent)
{
setWindowTitle(i18n("Certificates"));
buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
list = new QListView();
list->setItemDelegate(new KeyDelegate);
auto layout = new QVBoxLayout();
layout->addWidget(new QLabel(i18n("Select certificate to sign with:")));
layout->addWidget(list);
layout->addWidget(buttonBox);
setLayout(layout);
}
QSize KeyDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
auto baseSize = QStyledItemDelegate::sizeHint(option, index);
baseSize.setHeight(baseSize.height() * 2);
return baseSize;
}
void KeyDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
auto style = option.widget ? option.widget->style() : QApplication::style();
QStyledItemDelegate::paint(painter, option, QModelIndex()); // paint the background but without any text on it.
QPalette::ColorGroup cg;
if (option.state & QStyle::State_Active) {
cg = QPalette::Normal;
} else {
cg = QPalette::Inactive;
}
if (option.state & QStyle::State_Selected) {
painter->setPen(QPen {option.palette.brush(cg, QPalette::HighlightedText), 0});
} else {
painter->setPen(QPen {option.palette.brush(cg, QPalette::Text), 0});
}
auto textRect = option.rect;
int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &option, option.widget) + 1;
textRect.adjust(textMargin, 0, -textMargin, 0);
QRect topHalf {textRect.x(), textRect.y(), textRect.width(), textRect.height() / 2};
QRect bottomHalf {textRect.x(), textRect.y() + textRect.height() / 2, textRect.width(), textRect.height() / 2};
style->drawItemText(painter, topHalf, (option.displayAlignment & Qt::AlignVertical_Mask) | Qt::AlignLeft, option.palette, true, index.data(Qt::DisplayRole).toString());
style->drawItemText(painter, bottomHalf, (option.displayAlignment & Qt::AlignVertical_Mask) | Qt::AlignRight, option.palette, true, index.data(Qt::UserRole + 1).toString());
style->drawItemText(painter, bottomHalf, (option.displayAlignment & Qt::AlignVertical_Mask) | Qt::AlignLeft, option.palette, true, index.data(Qt::UserRole).toString());
}
}

View file

@ -1,5 +1,7 @@
/*
SPDX-FileCopyrightText: 2018 Chinmoy Ranjan Pradhan <chinmoyrp65@gmail.com>
SPDX-FileCopyrightText: 2023 g10 Code GmbH
SPDX-FileContributor: Sune Stolborg Vuorela <sune@vuorela.dk>
SPDX-License-Identifier: GPL-2.0-or-later
*/
@ -7,12 +9,16 @@
#ifndef OKULAR_SIGNATUREPARTUTILS_H
#define OKULAR_SIGNATUREPARTUTILS_H
#include <QDialog>
#include <QStyledItemDelegate>
#include <memory>
#include <optional>
#include "gui/signatureguiutils.h"
class PageView;
class QListView;
class QDialogButtonBox;
namespace SignaturePartUtils
{
@ -30,6 +36,24 @@ std::optional<SigningInformation> getCertificateAndPasswordForSigning(PageView *
QString getFileNameForNewSignedFile(PageView *pageView, Okular::Document *doc);
void signUnsignedSignature(const Okular::FormFieldSignature *form, PageView *pageView, Okular::Document *doc);
class KeyDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
using QStyledItemDelegate::QStyledItemDelegate;
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const final;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const final;
};
class SelectCertificateDialog : public QDialog
{
Q_OBJECT
public:
QListView *list;
QDialogButtonBox *buttonBox;
explicit SelectCertificateDialog(QWidget *parent);
};
}
#endif