diff --git a/src/commands/createcsrforcardkeycommand.cpp b/src/commands/createcsrforcardkeycommand.cpp index d5faa7b63..eb5e796c9 100644 --- a/src/commands/createcsrforcardkeycommand.cpp +++ b/src/commands/createcsrforcardkeycommand.cpp @@ -1,292 +1,294 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/createcsrforcardkeycommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "createcsrforcardkeycommand.h" #include "cardcommand_p.h" #include "dialogs/createcsrforcardkeydialog.h" #include "smartcard/pivcard.h" #include "smartcard/readerstatus.h" #include "utils/filedialog.h" #include "utils/keyparameters.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::Dialogs; using namespace Kleo::SmartCard; using namespace GpgME; using namespace QGpgME; class CreateCSRForCardKeyCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::CreateCSRForCardKeyCommand; CreateCSRForCardKeyCommand *q_func() const { return static_cast(q); } public: explicit Private(CreateCSRForCardKeyCommand *qq, const std::string &keyRef, const std::string &serialNumber, const std::string &appName, QWidget *parent); ~Private(); private: void start(); void slotDialogAccepted(); void slotDialogRejected(); void slotResult(const KeyGenerationResult &result, const QByteArray &request); QUrl saveRequest(const QByteArray &request); void ensureDialogCreated(); private: std::string appName; std::string keyRef; QStringList keyUsages; QPointer dialog; }; CreateCSRForCardKeyCommand::Private *CreateCSRForCardKeyCommand::d_func() { return static_cast(d.get()); } const CreateCSRForCardKeyCommand::Private *CreateCSRForCardKeyCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() CreateCSRForCardKeyCommand::Private::Private(CreateCSRForCardKeyCommand *qq, const std::string &keyRef_, const std::string &serialNumber, const std::string &appName_, QWidget *parent) : CardCommand::Private(qq, serialNumber, parent) , appName(appName_) , keyRef(keyRef_) { } CreateCSRForCardKeyCommand::Private::~Private() { } namespace { QStringList getKeyUsages(const KeyPairInfo &keyInfo) { // note: gpgsm does not support creating CSRs for authentication certificates QStringList usages; if (keyInfo.canCertify()) { usages.push_back(QStringLiteral("cert")); } if (keyInfo.canSign()) { usages.push_back(QStringLiteral("sign")); } if (keyInfo.canEncrypt()) { usages.push_back(QStringLiteral("encrypt")); } return usages; } } void CreateCSRForCardKeyCommand::Private::start() { if (appName != PIVCard::AppName) { qCWarning(KLEOPATRA_LOG) << "CreateCSRForCardKeyCommand does not support card application" << QString::fromStdString(appName); finished(); return; } const auto card = ReaderStatus::instance()->getCard(serialNumber(), appName); if (!card) { error(i18n("Failed to find the smartcard with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } const KeyPairInfo &keyInfo = card->keyInfo(keyRef); keyUsages = getKeyUsages(keyInfo); ensureDialogCreated(); dialog->setWindowTitle(i18n("Certificate Details")); - dialog->setName(card->cardHolder()); + if (!card->cardHolder().isEmpty()) { + dialog->setName(card->cardHolder()); + } dialog->show(); } void CreateCSRForCardKeyCommand::Private::slotDialogAccepted() { const Error err = ReaderStatus::switchCardAndApp(serialNumber(), appName); if (err) { finished(); return; } const auto backend = smime(); if (!backend) { finished(); return; } KeyGenerationJob *const job = backend->keyGenerationJob(); if (!job) { finished(); return; } Job::context(job)->setArmor(true); connect(job, SIGNAL(result(const GpgME::KeyGenerationResult &, const QByteArray &)), q, SLOT(slotResult(const GpgME::KeyGenerationResult &, const QByteArray &))); KeyParameters keyParameters(KeyParameters::CMS); keyParameters.setKeyType(QString::fromStdString(keyRef)); keyParameters.setKeyUsages(keyUsages); keyParameters.setDN(dialog->dn()); keyParameters.setEmail(dialog->email()); if (const Error err = job->start(keyParameters.toString())) { error(i18nc("@info", "Creating a CSR for the card key failed:\n%1", QString::fromUtf8(err.asString())), i18nc("@title", "Error")); finished(); } } void CreateCSRForCardKeyCommand::Private::slotDialogRejected() { canceled(); } void CreateCSRForCardKeyCommand::Private::slotResult(const KeyGenerationResult &result, const QByteArray &request) { if (result.error().isCanceled()) { // do nothing } else if (result.error()) { error(i18nc("@info", "Creating a CSR for the card key failed:\n%1", QString::fromUtf8(result.error().asString())), i18nc("@title", "Error")); } else { const QUrl url = saveRequest(request); if (!url.isEmpty()) { information(xi18nc("@info", "Successfully wrote request to %1." "You should now send the request to the Certification Authority (CA).", url.toLocalFile()), i18nc("@title", "Request Saved")); } } finished(); } namespace { struct SaveToFileResult { QUrl url; QString errorMessage; }; SaveToFileResult saveRequestToFile(const QString &filename, const QByteArray &request, QIODevice::OpenMode mode) { QFile file(filename); if (file.open(mode)) { const auto bytesWritten = file.write(request); if (bytesWritten < request.size()) { return { QUrl(), file.errorString() }; } return { QUrl::fromLocalFile(file.fileName()), QString() }; } return { QUrl(), file.errorString() }; } } QUrl CreateCSRForCardKeyCommand::Private::saveRequest(const QByteArray &request) { const QString proposedFilename = QLatin1String("request_%1.p10").arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd_HHmmss"))); while (true) { const QString filePath = FileDialog::getSaveFileNameEx( parentWidgetOrView(), i18nc("@title", "Save Request"), QStringLiteral("save_csr"), proposedFilename, i18n("PKCS#10 Requests (*.p10)")); if (filePath.isEmpty()) { // user canceled the dialog return QUrl(); } const auto result = saveRequestToFile(filePath, request, QIODevice::NewOnly); if (result.url.isEmpty()) { qCDebug(KLEOPATRA_LOG) << "Writing request to file" << filePath << "failed:" << result.errorMessage; error(xi18nc("@info", "Saving the request failed.%1", result.errorMessage), i18nc("@title", "Error Saving Request")); } else { return result.url; } } } void CreateCSRForCardKeyCommand::Private::ensureDialogCreated() { if (dialog) { return; } dialog = new CreateCSRForCardKeyDialog; applyWindowID(dialog); dialog->setAttribute(Qt::WA_DeleteOnClose); connect(dialog, SIGNAL(accepted()), q, SLOT(slotDialogAccepted())); connect(dialog, SIGNAL(rejected()), q, SLOT(slotDialogRejected())); } CreateCSRForCardKeyCommand::CreateCSRForCardKeyCommand(const std::string &keyRef, const std::string &serialNumber, const std::string &appName, QWidget *parent) : CardCommand(new Private(this, keyRef, serialNumber, appName, parent)) { } CreateCSRForCardKeyCommand::~CreateCSRForCardKeyCommand() { } void CreateCSRForCardKeyCommand::doStart() { d->start(); } void CreateCSRForCardKeyCommand::doCancel() { } #undef d #undef q #include "moc_createcsrforcardkeycommand.cpp" diff --git a/src/dialogs/certificatedetailsinputwidget.cpp b/src/dialogs/certificatedetailsinputwidget.cpp index 74a747c6f..d9de01194 100644 --- a/src/dialogs/certificatedetailsinputwidget.cpp +++ b/src/dialogs/certificatedetailsinputwidget.cpp @@ -1,348 +1,353 @@ /* -*- mode: c++; c-basic-offset:4 -*- dialogs/certificatedetailsinputwidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "certificatedetailsinputwidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Dialogs; namespace { struct Line { QString attr; QString label; QString regex; QLineEdit *edit; bool required; }; QString attributeFromKey(QString key) { return key.remove(QLatin1Char('!')); } QString attributeLabel(const QString &attr) { if (attr.isEmpty()) { return QString(); } const QString label = DNAttributeMapper::instance()->name2label(attr); if (!label.isEmpty()) { return i18nc("Format string for the labels in the \"Your Personal Data\" page", "%1 (%2)", label, attr); } else { return attr; } } QLineEdit * addRow(QGridLayout *l, const QString &label, const QString &preset, QValidator *validator, bool readonly, bool required) { Q_ASSERT(l); QLabel *lb = new QLabel(l->parentWidget()); lb->setText(i18nc("interpunctation for labels", "%1:", label)); QLineEdit *le = new QLineEdit(l->parentWidget()); le->setText(preset); delete le->validator(); if (validator) { if (!validator->parent()) { validator->setParent(le); } le->setValidator(validator); } le->setReadOnly(readonly && le->hasAcceptableInput()); QLabel *reqLB = new QLabel(l->parentWidget()); reqLB->setText(required ? i18n("(required)") : i18n("(optional)")); const int row = l->rowCount(); l->addWidget(lb, row, 0); l->addWidget(le, row, 1); l->addWidget(reqLB, row, 2); return le; } bool hasIntermediateInput(const QLineEdit *le) { QString text = le->text(); int pos = le->cursorPosition(); const QValidator *const v = le->validator(); return v && v->validate(text, pos) == QValidator::Intermediate; } QString requirementsAreMet(const QVector &lines) { for (const Line &line : lines) { const QLineEdit *le = line.edit; if (!le) { continue; } qCDebug(KLEOPATRA_LOG) << "requirementsAreMet(): checking \"" << line.attr << "\" against \"" << le->text() << "\":"; if (le->text().trimmed().isEmpty()) { if (line.required) { if (line.regex.isEmpty()) { return xi18nc("@info", "%1 is required, but empty.", line.label); } else { return xi18nc("@info", "%1 is required, but empty." "Local Admin rule: %2", line.label, line.regex); } } } else if (hasIntermediateInput(le)) { if (line.regex.isEmpty()) { return xi18nc("@info", "%1 is incomplete.", line.label); } else { return xi18nc("@info", "%1 is incomplete." "Local Admin rule: %2", line.label, line.regex); } } else if (!le->hasAcceptableInput()) { if (line.regex.isEmpty()) { return xi18nc("@info", "%1 is invalid.", line.label); } else { return xi18nc("@info", "%1 is invalid." "Local Admin rule: %2", line.label, line.regex); } } } return QString(); } } class CertificateDetailsInputWidget::Private { friend class ::Kleo::Dialogs::CertificateDetailsInputWidget; CertificateDetailsInputWidget *const q; struct { QGridLayout *gridLayout; QVector lines; QLineEdit *dn; QLabel *error; } ui; public: Private(CertificateDetailsInputWidget *qq) : q(qq) { auto mainLayout = new QVBoxLayout(q); ui.gridLayout = new QGridLayout(); mainLayout->addLayout(ui.gridLayout); + createForm(); + mainLayout->addStretch(1); ui.dn = new QLineEdit(); ui.dn->setFrame(false); ui.dn->setAlignment(Qt::AlignCenter); ui.dn->setReadOnly(true); mainLayout->addWidget(ui.dn); ui.error = new QLabel(); { QSizePolicy sizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); sizePolicy.setHorizontalStretch(0); sizePolicy.setVerticalStretch(0); sizePolicy.setHeightForWidth(ui.error->sizePolicy().hasHeightForWidth()); ui.error->setSizePolicy(sizePolicy); } { QPalette palette; QBrush brush(QColor(255, 0, 0, 255)); brush.setStyle(Qt::SolidPattern); palette.setBrush(QPalette::Active, QPalette::WindowText, brush); palette.setBrush(QPalette::Inactive, QPalette::WindowText, brush); QBrush brush1(QColor(114, 114, 114, 255)); brush1.setStyle(Qt::SolidPattern); palette.setBrush(QPalette::Disabled, QPalette::WindowText, brush1); ui.error->setPalette(palette); } ui.error->setTextFormat(Qt::RichText); // set error label to have a fixed height of two lines: ui.error->setText(QStringLiteral("2
1")); ui.error->setFixedHeight(ui.error->minimumSizeHint().height()); ui.error->clear(); mainLayout->addWidget(ui.error); - createForm(); - fixTabOrder(); + // select the preset text in the first line edit + if (!ui.lines.empty()) { + ui.lines.first().edit->selectAll(); + } + + // explicitly update DN and check requirements after setup is complete + updateDN(); + checkRequirements(); } ~Private() { + // remember current attribute values as presets for next certificate + KConfigGroup config(KSharedConfig::openConfig(), "CertificateCreationWizard"); + for ( const Line &line : ui.lines ) { + const QString attr = attributeFromKey(line.attr); + const QString value = line.edit->text().trimmed(); + config.writeEntry(attr, value); + } + config.sync(); } void createForm() { const KEMailSettings emailSettings; const KConfigGroup config(KSharedConfig::openConfig(), "CertificateCreationWizard"); QStringList attrOrder = config.readEntry("DNAttributeOrder", QStringList()); if (attrOrder.empty()) { attrOrder << QStringLiteral("CN!") << QStringLiteral("EMAIL!") << QStringLiteral("L") << QStringLiteral("OU") << QStringLiteral("O!") << QStringLiteral("C!"); } for (const QString &rawKey : attrOrder) { const QString key = rawKey.trimmed().toUpper(); const QString attr = attributeFromKey(key); if (attr.isEmpty()) { continue; } const QString defaultPreset = (attr == QLatin1String("CN")) ? emailSettings.getSetting(KEMailSettings::RealName) : (attr == QLatin1String("EMAIL")) ? emailSettings.getSetting(KEMailSettings::EmailAddress) : QString(); const QString preset = config.readEntry(attr, defaultPreset); const bool required = key.endsWith(QLatin1Char('!')); const bool readonly = config.isEntryImmutable(attr); const QString label = config.readEntry(attr + QLatin1String("_label"), attributeLabel(attr)); const QString regex = config.readEntry(attr + QLatin1String("_regex")); QValidator *validator = nullptr; if (attr == QLatin1String("EMAIL")) { validator = regex.isEmpty() ? Validation::email() : Validation::email(QRegExp(regex)); } else if (!regex.isEmpty()) { validator = new QRegExpValidator(QRegExp(regex), nullptr); } QLineEdit *le = addRow(ui.gridLayout, label, preset, validator, readonly, required); const Line line = { attr, label, regex, le, required }; ui.lines.push_back(line); if (attr != QLatin1String("EMAIL")) { connect(le, &QLineEdit::textChanged, [this] () { updateDN(); }); } connect(le, &QLineEdit::textChanged, [this] () { checkRequirements(); }); } } - void fixTabOrder() - { - QVector widgets; - widgets.reserve(ui.lines.size() + 1); - for ( const Line &line : ui.lines ) { - widgets.push_back(line.edit); - } - widgets.push_back(ui.dn); - kdtools::for_each_adjacent_pair(widgets.begin(), widgets.end(), &QWidget::setTabOrder); - } - void updateDN() { ui.dn->setText(cmsDN()); } QString cmsDN() const { DN dn; for ( const Line &line : ui.lines ) { const QString text = line.edit->text().trimmed(); if (text.isEmpty()) { continue; } QString attr = attributeFromKey(line.attr); if (attr == QLatin1String("EMAIL")) { continue; } if (const char *const oid = oidForAttributeName(attr)) { attr = QString::fromUtf8(oid); } dn.append(DN::Attribute(attr, text)); } return dn.dn(); } void checkRequirements() { const QString error = requirementsAreMet(ui.lines); ui.error->setText(error); Q_EMIT q->validityChanged(error.isEmpty()); } QLineEdit * attributeWidget(const QString &attribute) { for ( const Line &line : ui.lines ) { if (attributeFromKey(line.attr) == attribute) { return line.edit; } } qCWarning(KLEOPATRA_LOG) << "CertificateDetailsInputWidget: No widget for attribute" << attribute; return nullptr; } void setAttributeValue(const QString &attribute, const QString &value) { QLineEdit *w = attributeWidget(attribute); if (w) { w->setText(value); } } QString attributeValue(const QString &attribute) { const QLineEdit *w = attributeWidget(attribute); return w ? w->text().trimmed() : QString(); } }; CertificateDetailsInputWidget::CertificateDetailsInputWidget(QWidget *parent) : QWidget(parent) , d(new Private(this)) { } CertificateDetailsInputWidget::~CertificateDetailsInputWidget() { } void CertificateDetailsInputWidget::setName(const QString &name) { d->setAttributeValue(QStringLiteral("CN"), name); } void CertificateDetailsInputWidget::setEmail(const QString &email) { d->setAttributeValue(QStringLiteral("EMAIL"), email); } QString CertificateDetailsInputWidget::email() const { return d->attributeValue(QStringLiteral("EMAIL")); } QString CertificateDetailsInputWidget::dn() const { return d->ui.dn->text(); }