From 046df4dad6978b709834b0fad845bbe7d2714cf9 Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Sat, 27 Apr 2024 23:24:11 +0200 Subject: [PATCH] SSH Agent: Add support for certificates (#5486) --- share/translations/keepassxc_en.ts | 52 +++ src/gui/entry/EditEntryWidget.cpp | 98 +++++ src/gui/entry/EditEntryWidget.h | 6 + src/gui/entry/EditEntryWidgetSSHAgent.ui | 459 ++++++++++++++--------- src/sshagent/KeeAgentSettings.cpp | 167 ++++++++- src/sshagent/KeeAgentSettings.h | 20 + src/sshagent/OpenSSHKey.cpp | 85 +++++ src/sshagent/OpenSSHKey.h | 6 + src/sshagent/SSHAgent.cpp | 71 +++- 9 files changed, 778 insertions(+), 186 deletions(-) diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 126deb41b0..1be7981056 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -2669,6 +2669,10 @@ Would you like to correct it? + + Select certificate + + EditEntryWidgetAdvanced @@ -3098,6 +3102,14 @@ Would you like to correct it? seconds + + Use certificate + + + + Certificate + + EditGroupWidget @@ -4835,6 +4847,22 @@ Line %2, column %3 Failed to open private key + + Certificate is an attachment but no attachments provided. + + + + Certificate is empty + + + + File too large to be a certificate + + + + Failed to open certificate + + KeePass1Reader @@ -6204,6 +6232,22 @@ We recommend you use the AppImage available on our downloads page. Unexpected EOF when writing private key + + Invalid certificate file, expecting an OpenSSH certificate + + + + Unsupported certificate file + + + + Can't write certificate as it is empty + + + + Unexpected EOF when writing certificate + + OpenSSHKeyGenDialog @@ -9253,6 +9297,14 @@ This option is deprecated, use --set-key-file instead. No agent running, cannot list identities. + + Agent refused this identity certificate. Possible reasons include: + + + + Invalid or empty certificate. + + SearchHelpWidget diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 6620b08077..0523026f7c 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -94,6 +94,7 @@ EditEntryWidget::EditEntryWidget(QWidget* parent) , m_autoTypeWindowSequenceGroup(new QButtonGroup(this)) , m_usernameCompleter(new QCompleter(this)) , m_usernameCompleterModel(new QStringListModel(this)) + , m_blockSSHAgentSignals(new bool) { setupMain(); setupAdvanced(); @@ -492,6 +493,12 @@ void EditEntryWidget::setupEntryUpdate() connect(m_sshAgentUi->requireUserConfirmationCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified())); connect(m_sshAgentUi->lifetimeCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified())); connect(m_sshAgentUi->lifetimeSpinBox, SIGNAL(valueChanged(int)), this, SLOT(setModified())); + connect(m_sshAgentUi->attachmentCertificateRadioButton, SIGNAL(toggled(bool)), this, SLOT(setModified())); + connect(m_sshAgentUi->externalCertificateFileRadioButton, SIGNAL(toggled(bool)), this, SLOT(setModified())); + connect(m_sshAgentUi->attachmentCertificateComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(setModified())); + connect(m_sshAgentUi->attachmentCertificateComboBox, SIGNAL(editTextChanged(QString)), this, SLOT(setModified())); + connect(m_sshAgentUi->externalCertificateFileEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified())); + connect(m_sshAgentUi->addCertificateToAgentCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified())); } #endif @@ -568,16 +575,46 @@ void EditEntryWidget::setupSSHAgent() connect(m_sshAgentUi->decryptButton, &QPushButton::clicked, this, &EditEntryWidget::decryptPrivateKey); connect(m_sshAgentUi->copyToClipboardButton, &QPushButton::clicked, this, &EditEntryWidget::copyPublicKey); connect(m_sshAgentUi->generateButton, &QPushButton::clicked, this, &EditEntryWidget::generatePrivateKey); + connect(m_sshAgentUi->attachmentCertificateRadioButton, &QRadioButton::clicked, + this, &EditEntryWidget::updateSSHAgentKeyInfo); + connect(m_sshAgentUi->attachmentCertificateComboBox, static_cast(&QComboBox::currentIndexChanged), + this, &EditEntryWidget::updateSSHAgentAttachmentCertificate); + connect(m_sshAgentUi->externalCertificateFileRadioButton, &QRadioButton::clicked, + this, &EditEntryWidget::updateSSHAgentKeyInfo); + connect(m_sshAgentUi->externalCertificateFileEdit, &QLineEdit::textChanged, + this, &EditEntryWidget::updateSSHAgentKeyInfo); + connect(m_sshAgentUi->browseCertificateButton, &QPushButton::clicked, this, &EditEntryWidget::browseCertificate); connect(m_attachments.data(), &EntryAttachments::modified, this, &EditEntryWidget::updateSSHAgentAttachments); // clang-format on + blockSSHAgentSignals(false); + addPage(tr("SSH Agent"), icons()->icon("utilities-terminal"), m_sshAgentWidget); } +void EditEntryWidget::blockSSHAgentSignals(const bool block) +{ + if (block == m_blockSSHAgentSignals) { + return; + } + m_blockSSHAgentSignals = block; + + m_sshAgentUi->attachmentRadioButton->blockSignals(block); + m_sshAgentUi->attachmentComboBox->blockSignals(block); + m_sshAgentUi->externalFileRadioButton->blockSignals(block); + m_sshAgentUi->externalFileEdit->blockSignals(block); + m_attachments.data()->blockSignals(block); + m_sshAgentUi->attachmentCertificateRadioButton->blockSignals(block); + m_sshAgentUi->attachmentCertificateComboBox->blockSignals(block); + m_sshAgentUi->externalCertificateFileRadioButton->blockSignals(block); + m_sshAgentUi->externalCertificateFileEdit->blockSignals(block); +} + void EditEntryWidget::setSSHAgentSettings() { + blockSSHAgentSignals(); m_sshAgentUi->addKeyToAgentCheckBox->setChecked(m_sshAgentSettings.addAtDatabaseOpen()); m_sshAgentUi->removeKeyFromAgentCheckBox->setChecked(m_sshAgentSettings.removeAtDatabaseClose()); m_sshAgentUi->requireUserConfirmationCheckBox->setChecked(m_sshAgentSettings.useConfirmConstraintWhenAdding()); @@ -587,10 +624,14 @@ void EditEntryWidget::setSSHAgentSettings() m_sshAgentUi->addToAgentButton->setEnabled(false); m_sshAgentUi->removeFromAgentButton->setEnabled(false); m_sshAgentUi->copyToClipboardButton->setEnabled(false); + m_sshAgentUi->addCertificateToAgentCheckBox->setChecked(m_sshAgentSettings.useCertificate()); // AlexpFr redondant ? + m_sshAgentUi->attachmentCertificateComboBox->clear(); // AlexpFr: why ? + blockSSHAgentSignals(false); } void EditEntryWidget::updateSSHAgent() { + blockSSHAgentSignals(); m_sshAgentSettings.reset(); m_sshAgentSettings.fromEntry(m_entry); setSSHAgentSettings(); @@ -602,6 +643,7 @@ void EditEntryWidget::updateSSHAgent() } updateSSHAgentAttachments(); + blockSSHAgentSignals(false); } void EditEntryWidget::updateSSHAgentAttachment() @@ -618,16 +660,21 @@ void EditEntryWidget::updateSSHAgentAttachments() m_sshAgentSettings.reset(); setSSHAgentSettings(); } + blockSSHAgentSignals(); m_sshAgentUi->attachmentComboBox->clear(); m_sshAgentUi->attachmentComboBox->addItem(""); + m_sshAgentUi->attachmentCertificateComboBox->clear(); + m_sshAgentUi->attachmentCertificateComboBox->addItem(""); + for (const QString& fileName : m_attachments->keys()) { if (fileName == "KeeAgent.settings") { continue; } m_sshAgentUi->attachmentComboBox->addItem(fileName); + m_sshAgentUi->attachmentCertificateComboBox->addItem(fileName); } m_sshAgentUi->attachmentComboBox->setCurrentText(m_sshAgentSettings.attachmentName()); @@ -639,11 +686,23 @@ void EditEntryWidget::updateSSHAgentAttachments() m_sshAgentUi->externalFileRadioButton->setChecked(true); } + m_sshAgentUi->attachmentCertificateComboBox->setCurrentText(m_sshAgentSettings.attachmentNameCertificate()); + m_sshAgentUi->externalCertificateFileEdit->setText(m_sshAgentSettings.fileNameCertificate()); + + if (m_sshAgentSettings.selectedCertificateType() == "attachment") { + m_sshAgentUi->attachmentCertificateRadioButton->setChecked(true); + } else { + m_sshAgentUi->externalCertificateFileRadioButton->setChecked(true); + } + + blockSSHAgentSignals(false); + updateSSHAgentKeyInfo(); } void EditEntryWidget::updateSSHAgentKeyInfo() { + blockSSHAgentSignals(); m_sshAgentUi->addToAgentButton->setEnabled(false); m_sshAgentUi->removeFromAgentButton->setEnabled(false); m_sshAgentUi->copyToClipboardButton->setEnabled(false); @@ -655,6 +714,7 @@ void EditEntryWidget::updateSSHAgentKeyInfo() OpenSSHKey key; if (!getOpenSSHKey(key)) { + blockSSHAgentSignals(false); return; } @@ -687,6 +747,7 @@ void EditEntryWidget::updateSSHAgentKeyInfo() sshAgent()->setAutoRemoveOnLock(key, m_sshAgentUi->removeKeyFromAgentCheckBox->isChecked()); } + blockSSHAgentSignals(false); } void EditEntryWidget::toKeeAgentSettings(KeeAgentSettings& settings) const @@ -710,6 +771,14 @@ void EditEntryWidget::toKeeAgentSettings(KeeAgentSettings& settings) const // we don't use this either but we don't want it to dirty flag the config settings.setSaveAttachmentToTempFile(m_sshAgentSettings.saveAttachmentToTempFile()); + + settings.setUseCertificate(m_sshAgentUi->addCertificateToAgentCheckBox->isChecked()); + settings.setSelectedCertificateType(m_sshAgentUi->attachmentCertificateRadioButton->isChecked() ? "attachment" : "file"); + settings.setAttachmentCertificateName(m_sshAgentUi->attachmentCertificateComboBox->currentText()); + settings.setFileNameCertificate(m_sshAgentUi->externalCertificateFileEdit->text()); + + // we don't use this either but we don't want it to dirty flag the config + settings.setSaveAttachmentCertificateToTempFile(m_sshAgentSettings.saveAttachmentCertificateToTempFile()); } void EditEntryWidget::updateTotp() @@ -721,6 +790,7 @@ void EditEntryWidget::updateTotp() void EditEntryWidget::browsePrivateKey() { + blockSSHAgentSignals(); auto fileName = fileDialog()->getOpenFileName(this, tr("Select private key"), FileDialog::getLastDir("sshagent")); if (!fileName.isEmpty()) { FileDialog::saveLastDir("sshagent", fileName); @@ -728,6 +798,7 @@ void EditEntryWidget::browsePrivateKey() m_sshAgentUi->externalFileRadioButton->setChecked(true); updateSSHAgentKeyInfo(); } + blockSSHAgentSignals(false); } bool EditEntryWidget::getOpenSSHKey(OpenSSHKey& key, bool decrypt) @@ -772,6 +843,25 @@ void EditEntryWidget::addKeyToAgent() } } +void EditEntryWidget::updateSSHAgentAttachmentCertificate() +{ + m_sshAgentUi->attachmentCertificateRadioButton->setChecked(true); + updateSSHAgentKeyInfo(); +} + +void EditEntryWidget::browseCertificate() +{ + blockSSHAgentSignals(); + auto fileName = fileDialog()->getOpenFileName(this, tr("Select certificate"), FileDialog::getLastDir("sshagent")); + if (!fileName.isEmpty()) { + FileDialog::saveLastDir("sshagent", fileName); + m_sshAgentUi->externalCertificateFileEdit->setText(fileName); + m_sshAgentUi->externalCertificateFileRadioButton->setChecked(true); + updateSSHAgentKeyInfo(); + } + blockSSHAgentSignals(false); +} + void EditEntryWidget::removeKeyFromAgent() { OpenSSHKey key; @@ -870,6 +960,7 @@ void EditEntryWidget::loadEntry(Entry* entry, const QString& parentName, QSharedPointer database) { + blockSSHAgentSignals(); m_entry = entry; m_db = std::move(database); m_create = create; @@ -900,6 +991,7 @@ void EditEntryWidget::loadEntry(Entry* entry, showApplyButton(!m_create); setModified(false); + blockSSHAgentSignals(false); } void EditEntryWidget::setForms(Entry* entry, bool restore) @@ -1170,7 +1262,9 @@ bool EditEntryWidget::commitEntry() m_autoTypeAssoc->removeEmpty(); #ifdef WITH_XC_SSHAGENT + blockSSHAgentSignals(); toKeeAgentSettings(m_sshAgentSettings); + blockSSHAgentSignals(false); #endif // Begin entry update @@ -1206,6 +1300,7 @@ bool EditEntryWidget::commitEntry() void EditEntryWidget::acceptEntry() { if (commitEntry()) { + m_sshAgentUi->privateKeyTabWidget->setCurrentIndex(0); clear(); emit editFinished(true); } @@ -1322,12 +1417,14 @@ void EditEntryWidget::cancel() } } + m_sshAgentUi->privateKeyTabWidget->setCurrentIndex(0); clear(); emit editFinished(accepted); } void EditEntryWidget::clear() { + blockSSHAgentSignals(); if (m_entry) { m_entry->disconnect(this); } @@ -1347,6 +1444,7 @@ void EditEntryWidget::clear() m_historyModel->clear(); m_iconsWidget->reset(); hideMessage(); + blockSSHAgentSignals(false); } #ifdef WITH_XC_NETWORKING diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index 0507b5dfbc..e44bb74305 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -125,6 +125,8 @@ private slots: void decryptPrivateKey(); void copyPublicKey(); void generatePrivateKey(); + void updateSSHAgentAttachmentCertificate(); + void browseCertificate(); #endif #ifdef WITH_XC_BROWSER void updateBrowserModified(); @@ -146,6 +148,7 @@ private slots: #endif #ifdef WITH_XC_SSHAGENT void setupSSHAgent(); + void blockSSHAgentSignals(const bool block = true); #endif void setupProperties(); void setupHistory(); @@ -170,6 +173,8 @@ private slots: #ifdef WITH_XC_SSHAGENT KeeAgentSettings m_sshAgentSettings; QString m_pendingPrivateKey; + QPointer m_entryCertificate; + const QScopedPointer m_attachmentsCertificate; #endif const QScopedPointer m_mainUi; const QScopedPointer m_advancedUi; @@ -206,6 +211,7 @@ private slots: QCompleter* const m_usernameCompleter; QStringListModel* const m_usernameCompleterModel; QTimer m_entryModifiedTimer; + bool m_blockSSHAgentSignals = false; Q_DISABLE_COPY(EditEntryWidget) }; diff --git a/src/gui/entry/EditEntryWidgetSSHAgent.ui b/src/gui/entry/EditEntryWidgetSSHAgent.ui index 3fa48baf33..7253e73e8f 100644 --- a/src/gui/entry/EditEntryWidgetSSHAgent.ui +++ b/src/gui/entry/EditEntryWidgetSSHAgent.ui @@ -26,13 +26,73 @@ 0 - - + + + + + + Add to agent + + + + + + + Remove from agent + + + + + + + - Remove key from agent when database is closed/locked + Require user confirmation when this key is used + + + + Public key + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + + + + Monospace + + + + n/a + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -43,14 +103,17 @@ - - + + - Add key to agent when database is opened/unlocked + Fingerprint + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + @@ -62,15 +125,15 @@ - + Decrypt - - + + Qt::Vertical @@ -85,131 +148,10 @@ - - - - Fingerprint - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Copy to clipboard - - - - - - - Public key - - - Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing - - - - - - - Private key - - - - - - Attachment - - - true - - - - - - - Qt::ClickFocus - - - External key file - - - - - - - - - Add to agent - - - - - - - Remove from agent - - - - - - - - - External file - - - - - - - Browser for key file - - - Browse… - - - - - - - Generate - - - - - - - - 0 - 0 - - - - Select attachment file - - - false - - - - - - - - - - Require user confirmation when this key is used - - - - - + + - + Monospace @@ -224,7 +166,7 @@ - + Qt::Horizontal @@ -238,7 +180,204 @@ - + + + + Add key to agent when database is opened/unlocked + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + Remove key from agent when database is closed/locked + + + + + + + + 0 + 0 + + + + 0 + + + + Private key + + + + + + Use certificate + + + + + + + External file + + + + + + + Browser for key file + + + Browse… + + + + + + + + 0 + 0 + + + + Select attachment file + + + false + + + + + + + Attachment + + + true + + + + + + + Qt::ClickFocus + + + External key file + + + + + + + Generate + + + + + + + + Certificate + + + + + + Attachment + + + true + + + + + + + + 0 + 0 + + + + Select attachment file + + + false + + + + + + + Qt::ClickFocus + + + External key file + + + + + + + External file + + + + + + + Browser for key file + + + Browse… + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + + + Copy to clipboard + + + + @@ -275,54 +414,6 @@ - - - - - - - Monospace - - - - n/a - - - Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 10 - - - - diff --git a/src/sshagent/KeeAgentSettings.cpp b/src/sshagent/KeeAgentSettings.cpp index 272fb7edf7..c2f363df23 100644 --- a/src/sshagent/KeeAgentSettings.cpp +++ b/src/sshagent/KeeAgentSettings.cpp @@ -46,7 +46,12 @@ bool KeeAgentSettings::operator==(const KeeAgentSettings& other) const && m_selectedType == other.m_selectedType && m_attachmentName == other.m_attachmentName && m_saveAttachmentToTempFile == other.m_saveAttachmentToTempFile - && m_fileName == other.m_fileName); + && m_fileName == other.m_fileName + && m_selectedCertificateType == other.m_selectedCertificateType + && m_attachmentNameCertificate == other.m_attachmentNameCertificate + && m_saveAttachmentCertificateToTempFile == other.m_saveAttachmentCertificateToTempFile + && m_fileNameCertificate == other.m_fileNameCertificate + && m_useCertificate == other.m_useCertificate); // clang-format on } @@ -83,6 +88,11 @@ void KeeAgentSettings::reset() m_saveAttachmentToTempFile = false; m_fileName.clear(); m_error.clear(); + m_selectedCertificateType = QStringLiteral("file"); + m_attachmentNameCertificate.clear(); + m_saveAttachmentCertificateToTempFile = false; + m_fileNameCertificate.clear(); + m_useCertificate = false; } /** @@ -200,6 +210,61 @@ void KeeAgentSettings::setFileName(const QString& fileName) m_fileName = fileName; } +const QString KeeAgentSettings::fileNameCertificateEnvSubst(QProcessEnvironment environment) const +{ + return Tools::envSubstitute(m_fileNameCertificate, environment); +} + +bool KeeAgentSettings::useCertificate() const +{ + return m_useCertificate; +} + +void KeeAgentSettings::setUseCertificate(bool useCertificate) +{ + m_useCertificate = useCertificate; +} + +const QString KeeAgentSettings::selectedCertificateType() const +{ + return m_selectedCertificateType; +} + +const QString KeeAgentSettings::attachmentNameCertificate() const +{ + return m_attachmentNameCertificate; +} + +bool KeeAgentSettings::saveAttachmentCertificateToTempFile() const +{ + return m_saveAttachmentCertificateToTempFile; +} + +const QString KeeAgentSettings::fileNameCertificate() const +{ + return m_fileNameCertificate; +} + +void KeeAgentSettings::setSelectedCertificateType(const QString& selectedCertificateType) +{ + m_selectedCertificateType = selectedCertificateType; +} + +void KeeAgentSettings::setAttachmentCertificateName(const QString& attachmentCertificateName) +{ + m_attachmentNameCertificate = attachmentCertificateName; +} + +void KeeAgentSettings::setSaveAttachmentCertificateToTempFile(bool saveAttachmentCertificateToTempFile) +{ + m_saveAttachmentCertificateToTempFile = saveAttachmentCertificateToTempFile; +} + +void KeeAgentSettings::setFileNameCertificate(const QString& fileNameCertificate) +{ + m_fileNameCertificate = fileNameCertificate; +} + bool KeeAgentSettings::readBool(QXmlStreamReader& reader) { reader.readNext(); @@ -273,6 +338,29 @@ bool KeeAgentSettings::fromXml(const QByteArray& ba) reader.skipCurrentElement(); } } + } else if (reader.name() == "UseCertificate") { + m_useCertificate = readBool(reader); + } else if (reader.name() == "LocationCertificate") { + while (!reader.error() && reader.readNextStartElement()) { + if (reader.name() == "SelectedCertificateType") { + reader.readNext(); + m_selectedCertificateType = reader.text().toString(); + reader.readNext(); + } else if (reader.name() == "AttachmentCertificateName") { + reader.readNext(); + m_attachmentNameCertificate = reader.text().toString(); + reader.readNext(); + } else if (reader.name() == "SaveAttachmentCertificateToTempFile") { + m_saveAttachmentCertificateToTempFile = readBool(reader); + } else if (reader.name() == "FileNameCertificate") { + reader.readNext(); + m_fileNameCertificate = reader.text().toString(); + reader.readNext(); + } else { + qWarning() << "Skipping location certificate element" << reader.name(); + reader.skipCurrentElement(); + } + } } else { qWarning() << "Skipping element" << reader.name(); reader.skipCurrentElement(); @@ -328,6 +416,27 @@ QByteArray KeeAgentSettings::toXml() const } writer.writeEndElement(); // Location + + writer.writeTextElement("UseCertificate", m_useCertificate ? "true" : "false"); + writer.writeStartElement("LocationCertificate"); + + writer.writeTextElement("SelectedCertificateType", m_selectedCertificateType); + + if (!m_attachmentNameCertificate.isEmpty()) { + writer.writeTextElement("AttachmentCertificateName", m_attachmentNameCertificate); + } else { + writer.writeEmptyElement("AttachmentCertificateName"); + } + + writer.writeTextElement("SaveAttachmentCertificateToTempFile", m_saveAttachmentCertificateToTempFile ? "true" : "false"); + + if (!m_fileNameCertificate.isEmpty()) { + writer.writeTextElement("FileNameCertificate", m_fileNameCertificate); + } else { + writer.writeEmptyElement("FileNameCertificate"); + } + + writer.writeEndElement(); // LocationCertificate writer.writeEndElement(); // EntrySettings writer.writeEndDocument(); @@ -497,5 +606,61 @@ bool KeeAgentSettings::toOpenSSHKey(const QString& username, key.setComment(fileName); } + if (m_useCertificate) { + QString fileCertificateName; + QByteArray certificateData; + + if (m_selectedCertificateType == "attachment") { + if (!attachments) { + m_error = QCoreApplication::translate("KeeAgentSettings", + "Certificate is an attachment but no attachments provided."); + return false; + } + + fileCertificateName = m_attachmentNameCertificate; + certificateData = attachments->value(fileCertificateName); + } else { + QString fileNameCertificateSubst = fileNameCertificateEnvSubst(); + QFileInfo localFileCertificateInfo(fileNameCertificateSubst); + + // resolve relative certificate path from database location + if (localFileCertificateInfo.isRelative()) { + QFileInfo databaseFileCertificateInfo(databasePath); + localFileCertificateInfo = QFileInfo(databaseFileCertificateInfo.absolutePath() + QDir::separator() + fileNameCertificateSubst); + } + + fileCertificateName = localFileCertificateInfo.fileName(); + + QFile localCertificateFile(localFileCertificateInfo.absoluteFilePath()); + + if (localCertificateFile.fileName().isEmpty()) { + m_error = QCoreApplication::translate("KeeAgentSettings", "Certificate is empty"); + return false; + } + + if (localCertificateFile.size() > 1024 * 1024) { + m_error = QCoreApplication::translate("KeeAgentSettings", "File too large to be a certificate"); + return false; + } + + if (!localCertificateFile.open(QIODevice::ReadOnly)) { + m_error = QCoreApplication::translate("KeeAgentSettings", "Failed to open certificate"); + return false; + } + + certificateData = localCertificateFile.readAll(); + } + + if (certificateData.isEmpty()) { + m_error = QCoreApplication::translate("KeeAgentSettings", "Certificate is empty"); + return false; + } + + if (!key.parseCertificate(certificateData)) { + m_error = key.errorString(); + return false; + } + } + return true; } diff --git a/src/sshagent/KeeAgentSettings.h b/src/sshagent/KeeAgentSettings.h index ffc14044ee..113d4f982a 100644 --- a/src/sshagent/KeeAgentSettings.h +++ b/src/sshagent/KeeAgentSettings.h @@ -77,6 +77,19 @@ class KeeAgentSettings void setSaveAttachmentToTempFile(bool); void setFileName(const QString& fileName); + // Certificate + const QString fileNameCertificateEnvSubst(QProcessEnvironment environment = QProcessEnvironment::systemEnvironment()) const; + bool useCertificate() const; + void setUseCertificate(bool UseCertificate); + const QString selectedCertificateType() const; + const QString attachmentNameCertificate() const; + bool saveAttachmentCertificateToTempFile() const; + const QString fileNameCertificate() const; + void setSelectedCertificateType(const QString& certificateType); + void setAttachmentCertificateName(const QString& attachmentCertificateName); + void setSaveAttachmentCertificateToTempFile(bool); + void setFileNameCertificate(const QString& fileNameCertificate); + private: bool readBool(QXmlStreamReader& reader); int readInt(QXmlStreamReader& reader); @@ -94,6 +107,13 @@ class KeeAgentSettings bool m_saveAttachmentToTempFile; QString m_fileName; QString m_error; + + // Certificate + bool m_useCertificate; + QString m_selectedCertificateType; + QString m_attachmentNameCertificate; + bool m_saveAttachmentCertificateToTempFile; + QString m_fileNameCertificate; }; #endif // KEEAGENTSETTINGS_H diff --git a/src/sshagent/OpenSSHKey.cpp b/src/sshagent/OpenSSHKey.cpp index cdcc257013..8db2d5852f 100644 --- a/src/sshagent/OpenSSHKey.cpp +++ b/src/sshagent/OpenSSHKey.cpp @@ -46,6 +46,8 @@ OpenSSHKey::OpenSSHKey(QObject* parent) , m_rawPrivateData(QByteArray()) , m_comment(QString()) , m_error(QString()) + , m_certificateType(QString()) + , m_rawCertificateData(QByteArray()) { } @@ -62,6 +64,8 @@ OpenSSHKey::OpenSSHKey(const OpenSSHKey& other) , m_rawPrivateData(other.m_rawPrivateData) , m_comment(other.m_comment) , m_error(other.m_error) + , m_certificateType(other.m_certificateType) + , m_rawCertificateData(other.m_rawCertificateData) { } @@ -81,6 +85,11 @@ const QString OpenSSHKey::type() const return m_type; } +const QString OpenSSHKey::certificateType() const +{ + return m_certificateType; +} + const QString OpenSSHKey::fingerprint(QCryptographicHash::Algorithm algo) const { if (m_rawPublicData.isEmpty()) { @@ -656,6 +665,82 @@ bool OpenSSHKey::writePrivate(BinaryStream& stream) return true; } +bool OpenSSHKey::parseCertificate(QByteArray& data) +{ + QString stringData = QString::fromLatin1(data); + QStringList elements = stringData.split(QRegularExpression("\\s+"), QString::SkipEmptyParts); + + if (elements.length() >= 2 && elements.length() <= 3) { + m_error = tr("Invalid certificate file, expecting an OpenSSH certificate"); + return false; + } + + QStringList certificateTypeList = { + "ssh-ed25519-cert-v01@openssh.com", + "ssh-rsa-cert-v01@openssh.com", + "ssh-dss-cert-v01@openssh.com", + "sk-ssh-ed25519-cert-v01@openssh.com", + "sk-ssh-rsa-cert-v01@openssh.com", + "sk-ssh-dss-cert-v01@openssh.com" + "rsa-sha2-256-cert-v01@openssh.com", + "sk-rsa-sha2-256-cert-v01@openssh.com", + "rsa-sha2-512-cert-v01@openssh.com", + "sk-rsa-sha2-512-cert-v01@openssh.com", + "ecdsa-sha2-nistp256-cert-v01@openssh.com", + "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com", + "ecdsa-sha2-nistp384-cert-v01@openssh.com", + "sk-ecdsa-sha2-nistp384-cert-v01@openssh.com", + "ecdsa-sha2-nistp521-cert-v01@openssh.com", + "sk-ecdsa-sha2-nistp521-cert-v01@openssh.com", + }; + + if(!certificateTypeList.contains(elements.first())) { + m_error = tr("Unsupported certificate file"); + return false; + } + + m_certificateType = elements.first(); + m_rawCertificateData = QByteArray::fromBase64(elements[1].toLatin1()); + // if (elements.length() == 3) {m_certificateComment = elements.last();} + + return true; +} + +bool OpenSSHKey::writeCertificate(BinaryStream& stream, const bool addCertificate) +{ + if (m_rawCertificateData.isEmpty()) { + m_error = tr("Can't write certificate as it is empty"); + return false; + } + + if (!addCertificate) { + if (!stream.writeString(m_rawCertificateData)) { + m_error = tr("Unexpected EOF when writing certificate"); + return false; + } + return true; + } + + stream.writeString(m_certificateType); + + if (!stream.writeString(m_rawCertificateData)) { + m_error = tr("Unexpected EOF when writing certificate"); + return false; + } + + if (!stream.write(m_rawPrivateData)) { + m_error = tr("Unexpected EOF when writing certificate"); + return false; + } + + if (!stream.writeString(m_comment)) { + m_error = tr("Unexpected EOF when writing certificate"); + return false; + } + + return true; +} + uint qHash(const OpenSSHKey& key) { return qHash(key.fingerprint()); diff --git a/src/sshagent/OpenSSHKey.h b/src/sshagent/OpenSSHKey.h index c2c8319398..f4ba1c21d4 100644 --- a/src/sshagent/OpenSSHKey.h +++ b/src/sshagent/OpenSSHKey.h @@ -62,6 +62,10 @@ class OpenSSHKey : public QObject static const QString TYPE_OPENSSH_PRIVATE; static const QString OPENSSH_CIPHER_SUFFIX; + bool parseCertificate(QByteArray& data); + bool writeCertificate(BinaryStream& stream, const bool addCertificate = true); + const QString certificateType() const; + private: enum KeyPart { @@ -85,6 +89,8 @@ class OpenSSHKey : public QObject QByteArray m_rawPrivateData; QString m_comment; QString m_error; + QString m_certificateType; + QByteArray m_rawCertificateData; }; uint qHash(const OpenSSHKey& key); diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index 5c423cc755..3469117bcc 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -330,6 +330,60 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co return false; } + if (settings.useCertificate()) { + QByteArray requestCertificateData; + BinaryStream requestCertificate(&requestCertificateData); + bool isSecurityCertificate = key.certificateType().startsWith("sk-"); + + requestCertificate.write( + (settings.useLifetimeConstraintWhenAdding() || settings.useConfirmConstraintWhenAdding() || isSecurityCertificate) + ? SSH_AGENTC_ADD_ID_CONSTRAINED + : SSH_AGENTC_ADD_IDENTITY); + + key.writeCertificate(requestCertificate); + + if (settings.useLifetimeConstraintWhenAdding()) { + requestCertificate.write(SSH_AGENT_CONSTRAIN_LIFETIME); + requestCertificate.write(static_cast(settings.lifetimeConstraintDuration())); + } + + if (settings.useConfirmConstraintWhenAdding()) { + requestCertificate.write(SSH_AGENT_CONSTRAIN_CONFIRM); + } + + // To be verified if useful with certificates + if (isSecurityCertificate) { + requestCertificate.write(SSH_AGENT_CONSTRAIN_EXTENSION); + requestCertificate.writeString(QString("sk-provider@openssh.com")); + requestCertificate.writeString(securityKeyProvider()); + } + + QByteArray responseCertificateData; + if (!sendMessage(requestCertificateData, responseCertificateData)) { + return false; + } + + if (responseCertificateData.length() < 1 || static_cast(responseCertificateData[0]) != SSH_AGENT_SUCCESS) { + m_error = + tr("Agent refused this identity certificate. Possible reasons include:") + "\n" + tr("Invalid or empty certificate."); "\n" + tr("The key has already been added."); + + if (settings.useLifetimeConstraintWhenAdding()) { + m_error += "\n" + tr("Restricted lifetime is not supported by the agent (check options)."); + } + + if (settings.useConfirmConstraintWhenAdding()) { + m_error += "\n" + tr("A confirmation request is not supported by the agent (check options)."); + } + + if (isSecurityKey) { + m_error += + "\n" + tr("Security keys are not supported by the agent or the security key provider is unavailable."); + } + + return false; + } + } + OpenSSHKey keyCopy = key; keyCopy.clearPrivate(); m_addedKeys[keyCopy] = qMakePair(databaseUuid, settings.removeAtDatabaseClose()); @@ -360,7 +414,22 @@ bool SSHAgent::removeIdentity(OpenSSHKey& key) request.writeString(keyData); QByteArray responseData; - return sendMessage(requestData, responseData); + + // Try to remove certificate + QByteArray requestCertificateData; + BinaryStream requestCertificate(&requestCertificateData); + + QByteArray certificateData; + BinaryStream certificateStream(&certificateData); + key.writeCertificate(certificateStream, false); + + requestCertificate.write(SSH_AGENTC_REMOVE_IDENTITY); + requestCertificate.write(certificateData); + + QByteArray responseCertificateData; + + return (sendMessage(requestData, responseData) && + sendMessage(requestCertificateData, responseCertificateData)); } /**