From e4b0746a5d5142cc16c39dca8decc757c5319ad7 Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:21:07 +0100 Subject: [PATCH 01/16] Fix sshAgent casacading calls with QSignalBlocker --- src/gui/entry/EditEntryWidget.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 2a1dde612..f29e0b8af 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -41,6 +41,7 @@ #include "core/PasswordGenerator.h" #include "core/TimeDelta.h" #ifdef WITH_XC_SSHAGENT +#include #include "sshagent/OpenSSHKey.h" #include "sshagent/OpenSSHKeyGenDialog.h" #include "sshagent/SSHAgent.h" @@ -630,6 +631,7 @@ void EditEntryWidget::setSSHAgentSettings() m_sshAgentUi->requireUserConfirmationCheckBox->setChecked(m_sshAgentSettings.useConfirmConstraintWhenAdding()); m_sshAgentUi->lifetimeCheckBox->setChecked(m_sshAgentSettings.useLifetimeConstraintWhenAdding()); m_sshAgentUi->lifetimeSpinBox->setValue(m_sshAgentSettings.lifetimeConstraintDuration()); + QSignalBlocker sshAgent_attachmentComboBox_Blocker(m_sshAgentUi->attachmentComboBox); m_sshAgentUi->attachmentComboBox->clear(); m_sshAgentUi->addToAgentButton->setEnabled(false); m_sshAgentUi->removeFromAgentButton->setEnabled(false); @@ -666,6 +668,7 @@ void EditEntryWidget::updateSSHAgentAttachments() setSSHAgentSettings(); } + QSignalBlocker sshAgent_attachmentComboBox_Blocker(m_sshAgentUi->attachmentComboBox); m_sshAgentUi->attachmentComboBox->clear(); m_sshAgentUi->attachmentComboBox->addItem(""); @@ -678,6 +681,7 @@ void EditEntryWidget::updateSSHAgentAttachments() } m_sshAgentUi->attachmentComboBox->setCurrentText(m_sshAgentSettings.attachmentName()); + QSignalBlocker sshAgent_externalFileEdit_Blocker(m_sshAgentUi->externalFileEdit); m_sshAgentUi->externalFileEdit->setText(m_sshAgentSettings.fileName()); if (m_sshAgentSettings.selectedType() == "attachment") { @@ -960,6 +964,9 @@ void EditEntryWidget::loadEntry(Entry* entry, void EditEntryWidget::setForms(Entry* entry, bool restore) { +#ifdef WITH_XC_SSHAGENT + QSignalBlocker attachmentsBlocker(m_attachments.data()); +#endif m_attachments->copyDataFrom(entry->attachments()); m_customData->copyDataFrom(entry->customData()); @@ -1405,6 +1412,9 @@ void EditEntryWidget::clear() m_mainUi->notesEdit->clear(); m_entryAttributes->clear(); +#ifdef WITH_XC_SSHAGENT + QSignalBlocker attachmentsBlocker(m_attachments.data()); +#endif m_attachments->clear(); m_customData->clear(); m_autoTypeAssoc->clear(); From 5c5d62ae6bbf3468474b043bf947460bde905f4f Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:24:02 +0100 Subject: [PATCH 02/16] SSH Agent: Add support for certificates (#5486) --- src/gui/entry/EditEntryWidget.cpp | 60 +++ src/gui/entry/EditEntryWidget.h | 4 + src/gui/entry/EditEntryWidgetSSHAgent.ui | 537 +++++++++++++---------- src/sshagent/KeeAgentSettings.cpp | 167 ++++++- src/sshagent/KeeAgentSettings.h | 20 + src/sshagent/OpenSSHKey.cpp | 79 ++++ src/sshagent/OpenSSHKey.h | 6 + src/sshagent/SSHAgent.cpp | 71 ++- 8 files changed, 719 insertions(+), 225 deletions(-) diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index f29e0b8af..ec5e6cb62 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -539,6 +539,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 @@ -616,6 +622,15 @@ 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); @@ -636,6 +651,9 @@ 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()); + QSignalBlocker sshAgent_attachmentCertificateComboBox_Blocker(m_sshAgentUi->attachmentCertificateComboBox); + m_sshAgentUi->attachmentCertificateComboBox->clear(); // AlexpFr: why? attachmentCertificateComboBox init in updateSSHAgentAttachments( } void EditEntryWidget::updateSSHAgent() @@ -672,12 +690,17 @@ void EditEntryWidget::updateSSHAgentAttachments() m_sshAgentUi->attachmentComboBox->clear(); m_sshAgentUi->attachmentComboBox->addItem(""); + QSignalBlocker sshAgent_attachmentCertificateComboBox_Blocker(m_sshAgentUi->attachmentCertificateComboBox); + 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()); @@ -690,6 +713,16 @@ void EditEntryWidget::updateSSHAgentAttachments() m_sshAgentUi->externalFileRadioButton->setChecked(true); } + m_sshAgentUi->attachmentCertificateComboBox->setCurrentText(m_sshAgentSettings.attachmentNameCertificate()); + QSignalBlocker sshAgent_externalCertificateFileEdit_Blocker(m_sshAgentUi->externalCertificateFileEdit); + m_sshAgentUi->externalCertificateFileEdit->setText(m_sshAgentSettings.fileNameCertificate()); + + if (m_sshAgentSettings.selectedCertificateType() == "attachment") { + m_sshAgentUi->attachmentCertificateRadioButton->setChecked(true); + } else { + m_sshAgentUi->externalCertificateFileRadioButton->setChecked(true); + } + updateSSHAgentKeyInfo(); } @@ -756,6 +789,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() @@ -818,6 +859,23 @@ void EditEntryWidget::addKeyToAgent() } } +void EditEntryWidget::updateSSHAgentAttachmentCertificate() +{ + m_sshAgentUi->attachmentCertificateRadioButton->setChecked(true); + updateSSHAgentKeyInfo(); +} + +void EditEntryWidget::browseCertificate() +{ + 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(); + } +} + void EditEntryWidget::removeKeyFromAgent() { OpenSSHKey key; @@ -1274,6 +1332,7 @@ bool EditEntryWidget::commitEntry() void EditEntryWidget::acceptEntry() { if (commitEntry()) { + m_sshAgentUi->privateKeyTabWidget->setCurrentIndex(0); clear(); emit editFinished(true); } @@ -1393,6 +1452,7 @@ void EditEntryWidget::cancel() } } + m_sshAgentUi->privateKeyTabWidget->setCurrentIndex(0); clear(); emit editFinished(accepted); } diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index 3fce4d56d..5bfb2b9b1 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -139,6 +139,8 @@ private slots: void decryptPrivateKey(); void copyPublicKey(); void generatePrivateKey(); + void updateSSHAgentAttachmentCertificate(); + void browseCertificate(); #endif #ifdef WITH_XC_BROWSER void updateBrowserModified(); @@ -186,6 +188,8 @@ private: #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; diff --git a/src/gui/entry/EditEntryWidgetSSHAgent.ui b/src/gui/entry/EditEntryWidgetSSHAgent.ui index 3215c1a27..467777c12 100644 --- a/src/gui/entry/EditEntryWidgetSSHAgent.ui +++ b/src/gui/entry/EditEntryWidgetSSHAgent.ui @@ -26,79 +26,35 @@ 0 - - + + + + + + Add to agent + + + + + + + Remove from agent + + + + + + + Clear agent + + + + + + + - Remove key from agent when database is closed/locked - - - - - - - Comment - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Add key to agent when database is opened/unlocked - - - - - - - - Monospace - - - - true - - - - - - - Decrypt - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 10 - - - - - - - - Fingerprint - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Copy to clipboard + Require user confirmation when this key is used @@ -112,108 +68,7 @@ - - - - Private key - - - - - - Attachment - - - true - - - - - - - Qt::ClickFocus - - - External key file - - - - - - - - - Add to agent - - - - - - - Remove from agent - - - - - - - Clear agent - - - - - - - - - External file - - - - - - - Browser for key file - - - Browse… - - - - - - - Generate - - - - - - - - 0 - 0 - - - - Select attachment file - - - false - - - - - - - - - - Require user confirmation when this key is used - - - - + @@ -245,7 +100,291 @@ - + + + + Comment + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Fingerprint + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + Monospace + + + + true + + + + + + + Decrypt + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + + + Monospace + + + + n/a + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 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 + + + + @@ -282,54 +421,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 140383838..1a95ea1f5 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(); @@ -493,5 +602,61 @@ bool KeeAgentSettings::toOpenSSHKey(const QString& username, key.setComment(QString("%1@%2").arg(username, 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 ffc14044e..113d4f982 100644 --- a/src/sshagent/KeeAgentSettings.h +++ b/src/sshagent/KeeAgentSettings.h @@ -77,6 +77,19 @@ public: 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 @@ private: 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 4f8558596..581460942 100644 --- a/src/sshagent/OpenSSHKey.cpp +++ b/src/sshagent/OpenSSHKey.cpp @@ -47,6 +47,8 @@ OpenSSHKey::OpenSSHKey(QObject* parent) , m_rawPrivateData(QByteArray()) , m_comment(QString()) , m_error(QString()) + , m_certificateType(QString()) + , m_rawCertificateData(QByteArray()) { } @@ -63,6 +65,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) { } @@ -82,6 +86,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()) { @@ -660,6 +669,76 @@ 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); + + 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("Invalid or unsupported certificate file"); + return false; + } + + m_certificateType = elements.first(); + m_rawCertificateData = QByteArray::fromBase64(elements[1].toLatin1()); + + 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 c2c831939..f4ba1c21d 100644 --- a/src/sshagent/OpenSSHKey.h +++ b/src/sshagent/OpenSSHKey.h @@ -62,6 +62,10 @@ public: 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 @@ private: 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 a8aa695ce..cb64adaa7 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)); } /** From cf0b9e6298f92961f37fe45bda338cd68637a597 Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Sat, 21 Feb 2026 01:59:25 +0100 Subject: [PATCH 03/16] SSH Agent: Add SSH certificate section in documentation --- docs/topics/SSHAgent.adoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/topics/SSHAgent.adoc b/docs/topics/SSHAgent.adoc index 8b385c64c..d55bb31ff 100644 --- a/docs/topics/SSHAgent.adoc +++ b/docs/topics/SSHAgent.adoc @@ -177,4 +177,10 @@ If you chose to not autoload the key on database unlock, you can manually make t .SSH Agent Load Key from Context Menu image::sshagent_context_menu.png[] + +==== Associate certificate to SSH key +If you have an externally generated OpenSSH certificate file associated with your SSH key, you can configure it in the "Certificate" tab. + +When the key is loaded, if "Use certificate" is checked, both the key and certificate are added to the agent. + // end::content[] From 0cb2caa1464c9af41684d19dc4cea89951e42cba Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Sat, 21 Feb 2026 10:33:21 +0100 Subject: [PATCH 04/16] SSH Agent: Update keepassxc_en.ts --- share/translations/keepassxc_en.ts | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index f974db170..0ae44ae3a 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -3095,6 +3095,10 @@ Would you like to correct it? Failed to decrypt SSH key, ensure password is correct. + + Select certificate + + EditEntryWidgetAdvanced @@ -3528,6 +3532,14 @@ Would you like to correct it? Clear agent + + Use certificate + + + + Certificate + + EditGroupWidget @@ -5347,6 +5359,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 @@ -6844,6 +6872,18 @@ Expect some bugs and minor issues, this version is meant for testing purposes.Failed to read public key: %1 + + Invalid or unsupported certificate file + + + + Can't write certificate as it is empty + + + + Unexpected EOF when writing certificate + + OpenSSHKeyGenDialog @@ -10043,6 +10083,14 @@ This option is deprecated, use --set-key-file instead. All SSH identities removed from agent. + + Agent refused this identity certificate. Possible reasons include: + + + + Invalid or empty certificate. + + SearchHelpWidget From b7168f6c4ef4b97bcc28664dfc1584db344494d1 Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:49:56 +0100 Subject: [PATCH 05/16] SSH Agent : Use TRUE_STR and FALSE_STR (review) --- src/sshagent/KeeAgentSettings.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sshagent/KeeAgentSettings.cpp b/src/sshagent/KeeAgentSettings.cpp index 1a95ea1f5..d6d0e2d6f 100644 --- a/src/sshagent/KeeAgentSettings.cpp +++ b/src/sshagent/KeeAgentSettings.cpp @@ -417,7 +417,7 @@ QByteArray KeeAgentSettings::toXml() const writer.writeEndElement(); // Location - writer.writeTextElement("UseCertificate", m_useCertificate ? "true" : "false"); + writer.writeTextElement("UseCertificate", m_useCertificate ? TRUE_STR : FALSE_STR); writer.writeStartElement("LocationCertificate"); writer.writeTextElement("SelectedCertificateType", m_selectedCertificateType); @@ -428,7 +428,7 @@ QByteArray KeeAgentSettings::toXml() const writer.writeEmptyElement("AttachmentCertificateName"); } - writer.writeTextElement("SaveAttachmentCertificateToTempFile", m_saveAttachmentCertificateToTempFile ? "true" : "false"); + writer.writeTextElement("SaveAttachmentCertificateToTempFile", m_saveAttachmentCertificateToTempFile ? TRUE_STR : FALSE_STR); if (!m_fileNameCertificate.isEmpty()) { writer.writeTextElement("FileNameCertificate", m_fileNameCertificate); From 3d9ef5c4ed6efbfd8a705000e9c8cbe4bd6f3ff3 Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:25:29 +0100 Subject: [PATCH 06/16] SSH Agent: use SSH_MAX_LOCAL_KEY_SIZE define for cert+key size checks - Add #define SSH_MAX_LOCAL_KEY_SIZE (1 MiB) in KeeAgentSettings.h - Replace magic value "1024 * 1024" with this constant --- src/sshagent/KeeAgentSettings.cpp | 4 ++-- src/sshagent/KeeAgentSettings.h | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sshagent/KeeAgentSettings.cpp b/src/sshagent/KeeAgentSettings.cpp index d6d0e2d6f..355361552 100644 --- a/src/sshagent/KeeAgentSettings.cpp +++ b/src/sshagent/KeeAgentSettings.cpp @@ -568,7 +568,7 @@ bool KeeAgentSettings::toOpenSSHKey(const QString& username, return false; } - if (localFile.size() > 1024 * 1024) { + if (localFile.size() > SSH_MAX_LOCAL_KEY_SIZE) { m_error = QCoreApplication::translate("KeeAgentSettings", "File too large to be a private key"); return false; } @@ -634,7 +634,7 @@ bool KeeAgentSettings::toOpenSSHKey(const QString& username, return false; } - if (localCertificateFile.size() > 1024 * 1024) { + if (localCertificateFile.size() > SSH_MAX_LOCAL_KEY_SIZE) { m_error = QCoreApplication::translate("KeeAgentSettings", "File too large to be a certificate"); return false; } diff --git a/src/sshagent/KeeAgentSettings.h b/src/sshagent/KeeAgentSettings.h index 113d4f982..dbe0eedef 100644 --- a/src/sshagent/KeeAgentSettings.h +++ b/src/sshagent/KeeAgentSettings.h @@ -21,6 +21,8 @@ #include +#define SSH_MAX_LOCAL_KEY_SIZE (1024 * 1024) + class Entry; class EntryAttachments; class OpenSSHKey; From 7916050b39557e3e9efb4a222fe0c1690aba8139 Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:40:08 +0100 Subject: [PATCH 07/16] SSA Agent: remove redundant clear() on attachment combo boxes attachmentCertificateComboBox and attachmentComboBox are already cleared in updateSSHAgentAttachments(). --- src/gui/entry/EditEntryWidget.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index ec5e6cb62..3947d541a 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -647,13 +647,11 @@ void EditEntryWidget::setSSHAgentSettings() m_sshAgentUi->lifetimeCheckBox->setChecked(m_sshAgentSettings.useLifetimeConstraintWhenAdding()); m_sshAgentUi->lifetimeSpinBox->setValue(m_sshAgentSettings.lifetimeConstraintDuration()); QSignalBlocker sshAgent_attachmentComboBox_Blocker(m_sshAgentUi->attachmentComboBox); - m_sshAgentUi->attachmentComboBox->clear(); m_sshAgentUi->addToAgentButton->setEnabled(false); m_sshAgentUi->removeFromAgentButton->setEnabled(false); m_sshAgentUi->copyToClipboardButton->setEnabled(false); m_sshAgentUi->addCertificateToAgentCheckBox->setChecked(m_sshAgentSettings.useCertificate()); QSignalBlocker sshAgent_attachmentCertificateComboBox_Blocker(m_sshAgentUi->attachmentCertificateComboBox); - m_sshAgentUi->attachmentCertificateComboBox->clear(); // AlexpFr: why? attachmentCertificateComboBox init in updateSSHAgentAttachments( } void EditEntryWidget::updateSSHAgent() From 808ced63950ccfd1296812eb471f6008dbef9d0a Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:30:13 +0100 Subject: [PATCH 08/16] SSH Agent: Remove unused members m_entryCertificate and m_attachmentsCertificate --- src/gui/entry/EditEntryWidget.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index 5bfb2b9b1..66645d420 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -188,8 +188,6 @@ private: #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; From a4f53473b694d3a3fd3eed2b4158efdfd1760383 Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:47:57 +0100 Subject: [PATCH 09/16] SSH Agent: fix QString concatenation syntax error in error message Removed erroneous semicolon --- src/sshagent/SSHAgent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index cb64adaa7..6cd379eea 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -365,7 +365,7 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co 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."); + 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)."); From 5831029dd93d619ae8e7fc27f94eb9aed1f44ef3 Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:20:25 +0100 Subject: [PATCH 10/16] SSH agent: oops - fix QString concatenation syntax error in error message --- src/sshagent/SSHAgent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index 6cd379eea..a9c54374c 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -365,7 +365,7 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co 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."); + 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)."); From 94428b483b6f7cf48d707edf1ef8a74ca76274ef Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:28:35 +0100 Subject: [PATCH 11/16] SSH Agent: Replace QString::SkipEmptyParts with Qt::SkipEmptyParts --- src/sshagent/OpenSSHKey.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sshagent/OpenSSHKey.cpp b/src/sshagent/OpenSSHKey.cpp index 581460942..db0d2ae78 100644 --- a/src/sshagent/OpenSSHKey.cpp +++ b/src/sshagent/OpenSSHKey.cpp @@ -672,7 +672,7 @@ bool OpenSSHKey::writePrivate(BinaryStream& stream) bool OpenSSHKey::parseCertificate(QByteArray& data) { QString stringData = QString::fromLatin1(data); - QStringList elements = stringData.split(QRegularExpression("\\s+"), QString::SkipEmptyParts); + QStringList elements = stringData.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); QStringList certificateTypeList = { "ssh-ed25519-cert-v01@openssh.com", From 82f6d3ae12f03dc842ab19f30e95399ac4f418d8 Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:39:43 +0100 Subject: [PATCH 12/16] SSH Agent: Fix accessibleName for externalCertificateFileEdit --- src/gui/entry/EditEntryWidgetSSHAgent.ui | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/gui/entry/EditEntryWidgetSSHAgent.ui b/src/gui/entry/EditEntryWidgetSSHAgent.ui index 467777c12..ef4aad30b 100644 --- a/src/gui/entry/EditEntryWidgetSSHAgent.ui +++ b/src/gui/entry/EditEntryWidgetSSHAgent.ui @@ -42,13 +42,13 @@ - - - + + + Clear agent - - - + + + @@ -228,7 +228,7 @@ 0 - + Private key @@ -353,7 +353,7 @@ - Browser for key file + Browse for certificate file Browse… From 94ef87050a698bfbfd0aadaf23c77a63599ba2b8 Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:37:11 +0100 Subject: [PATCH 13/16] SSH Agent: Improve OpenSSH certificate validation - Check elements non-empty before accessing first() - Validate minimum 2 elements (type + nonce) - Reject invalid/unsupported certificate types - Verify Base64 nonce decoding success --- src/sshagent/OpenSSHKey.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/sshagent/OpenSSHKey.cpp b/src/sshagent/OpenSSHKey.cpp index db0d2ae78..fd0c7447a 100644 --- a/src/sshagent/OpenSSHKey.cpp +++ b/src/sshagent/OpenSSHKey.cpp @@ -693,7 +693,7 @@ bool OpenSSHKey::parseCertificate(QByteArray& data) "sk-ecdsa-sha2-nistp521-cert-v01@openssh.com", }; - if(!certificateTypeList.contains(elements.first())) { + if(elements.isEmpty() || elements.size() < 2 || !certificateTypeList.contains(elements.first())) { m_error = tr("Invalid or unsupported certificate file"); return false; } @@ -701,6 +701,11 @@ bool OpenSSHKey::parseCertificate(QByteArray& data) m_certificateType = elements.first(); m_rawCertificateData = QByteArray::fromBase64(elements[1].toLatin1()); + if (m_rawCertificateData.isEmpty()) { + m_error = tr("Base64 decoding failed"); + return false; + } + return true; } From 963bbd0c1731f74fbd422661f6053d19569aaf3e Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:27:41 +0100 Subject: [PATCH 14/16] SSH Agent: Track keys immediately after successful add Move m_addedKeys insertion after key add success but before optional cert add. Ensures keys are always tracked (and auto-removed) even if cert fails. --- src/sshagent/SSHAgent.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index a9c54374c..44619b1c6 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -330,6 +330,10 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co return false; } + OpenSSHKey keyCopy = key; + keyCopy.clearPrivate(); + m_addedKeys[keyCopy] = qMakePair(databaseUuid, settings.removeAtDatabaseClose()); + if (settings.useCertificate()) { QByteArray requestCertificateData; BinaryStream requestCertificate(&requestCertificateData); @@ -384,9 +388,6 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co } } - OpenSSHKey keyCopy = key; - keyCopy.clearPrivate(); - m_addedKeys[keyCopy] = qMakePair(databaseUuid, settings.removeAtDatabaseClose()); return true; } From 3f497f2eed56eae5cfa2e01daead01ad3f30d933 Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:36:13 +0100 Subject: [PATCH 15/16] SSH Agent: Check writeString() return value in OpenSSHKey Validate `stream.writeString(m_certificateType)` success and set m_error on failure, consistent with other write*() calls. Prevents partial stream writes --- src/sshagent/OpenSSHKey.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sshagent/OpenSSHKey.cpp b/src/sshagent/OpenSSHKey.cpp index fd0c7447a..c5673ccdb 100644 --- a/src/sshagent/OpenSSHKey.cpp +++ b/src/sshagent/OpenSSHKey.cpp @@ -724,7 +724,10 @@ bool OpenSSHKey::writeCertificate(BinaryStream& stream, const bool addCertificat return true; } - stream.writeString(m_certificateType); + if (!stream.writeString(m_certificateType)) { + m_error = tr("Unexpected EOF when writing certificate"); + return false; + } if (!stream.writeString(m_rawCertificateData)) { m_error = tr("Unexpected EOF when writing certificate"); From fb0b003aaa57499519fb9d8c26b19e66c011e4be Mon Sep 17 00:00:00 2001 From: Alexandre Petit <62973006+AlexpFr@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:00:06 +0100 Subject: [PATCH 16/16] SSH Agent: Skip cert removal if writeCertificate() fails Avoid sending invalid SSH_AGENTC_REMOVE_IDENTITY to agent when KeePassXC cannot read the certificate data itself. Prevents malformed removal requests for unreadable certificates. --- src/sshagent/SSHAgent.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index 44619b1c6..c793da9b5 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -422,15 +422,16 @@ bool SSHAgent::removeIdentity(OpenSSHKey& key) QByteArray certificateData; BinaryStream certificateStream(&certificateData); - key.writeCertificate(certificateStream, false); + if (key.writeCertificate(certificateStream, false)) { + requestCertificate.write(SSH_AGENTC_REMOVE_IDENTITY); + requestCertificate.write(certificateData); + QByteArray responseCertificateData; - requestCertificate.write(SSH_AGENTC_REMOVE_IDENTITY); - requestCertificate.write(certificateData); + return (sendMessage(requestData, responseData) && + sendMessage(requestCertificateData, responseCertificateData)); + } - QByteArray responseCertificateData; - - return (sendMessage(requestData, responseData) && - sendMessage(requestCertificateData, responseCertificateData)); + return (sendMessage(requestData, responseData)); } /**