diff --git a/docs/topics/SSHAgent.adoc b/docs/topics/SSHAgent.adoc index 8b385c64c9..d55bb31ff2 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[] diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index f974db170b..0ae44ae3a2 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 diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 2a1dde612b..3947d541a2 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" @@ -538,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 @@ -615,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); @@ -630,10 +646,12 @@ void EditEntryWidget::setSSHAgentSettings() m_sshAgentUi->requireUserConfirmationCheckBox->setChecked(m_sshAgentSettings.useConfirmConstraintWhenAdding()); m_sshAgentUi->lifetimeCheckBox->setChecked(m_sshAgentSettings.useLifetimeConstraintWhenAdding()); m_sshAgentUi->lifetimeSpinBox->setValue(m_sshAgentSettings.lifetimeConstraintDuration()); - m_sshAgentUi->attachmentComboBox->clear(); + QSignalBlocker sshAgent_attachmentComboBox_Blocker(m_sshAgentUi->attachmentComboBox); 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); } void EditEntryWidget::updateSSHAgent() @@ -666,18 +684,25 @@ void EditEntryWidget::updateSSHAgentAttachments() setSSHAgentSettings(); } + QSignalBlocker sshAgent_attachmentComboBox_Blocker(m_sshAgentUi->attachmentComboBox); 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()); + QSignalBlocker sshAgent_externalFileEdit_Blocker(m_sshAgentUi->externalFileEdit); m_sshAgentUi->externalFileEdit->setText(m_sshAgentSettings.fileName()); if (m_sshAgentSettings.selectedType() == "attachment") { @@ -686,6 +711,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(); } @@ -752,6 +787,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() @@ -814,6 +857,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; @@ -960,6 +1020,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()); @@ -1267,6 +1330,7 @@ bool EditEntryWidget::commitEntry() void EditEntryWidget::acceptEntry() { if (commitEntry()) { + m_sshAgentUi->privateKeyTabWidget->setCurrentIndex(0); clear(); emit editFinished(true); } @@ -1386,6 +1450,7 @@ void EditEntryWidget::cancel() } } + m_sshAgentUi->privateKeyTabWidget->setCurrentIndex(0); clear(); emit editFinished(accepted); } @@ -1405,6 +1470,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(); diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index 3fce4d56d0..66645d4201 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(); diff --git a/src/gui/entry/EditEntryWidgetSSHAgent.ui b/src/gui/entry/EditEntryWidgetSSHAgent.ui index 3215c1a274..ef4aad30b9 100644 --- a/src/gui/entry/EditEntryWidgetSSHAgent.ui +++ b/src/gui/entry/EditEntryWidgetSSHAgent.ui @@ -26,13 +26,80 @@ 0 - - + + + + + + Add to agent + + + + + + + Remove from agent + + + + + + + Clear 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 +110,17 @@ - - + + - Add key to agent when database is opened/unlocked + Fingerprint + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + @@ -62,15 +132,15 @@ - + Decrypt - - + + Qt::Vertical @@ -85,138 +155,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 - - - - - - - Clear agent - - - - - - - - - External file - - - - - - - Browser for key file - - - Browse… - - - - - - - Generate - - - - - - - - 0 - 0 - - - - Select attachment file - - - false - - - - - - - - - - Require user confirmation when this key is used - - - - - + + - + Monospace @@ -231,7 +173,7 @@ - + Qt::Horizontal @@ -245,7 +187,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 + + + + + + + Browse for certificate 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 1403838384..355361552a 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_STR : FALSE_STR); + 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_STR : FALSE_STR); + + if (!m_fileNameCertificate.isEmpty()) { + writer.writeTextElement("FileNameCertificate", m_fileNameCertificate); + } else { + writer.writeEmptyElement("FileNameCertificate"); + } + + writer.writeEndElement(); // LocationCertificate writer.writeEndElement(); // EntrySettings writer.writeEndDocument(); @@ -459,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; } @@ -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() > SSH_MAX_LOCAL_KEY_SIZE) { + 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..dbe0eedef3 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; @@ -77,6 +79,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 +109,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 4f85585964..c5673ccdb4 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,84 @@ bool OpenSSHKey::writePrivate(BinaryStream& stream) return true; } +bool OpenSSHKey::parseCertificate(QByteArray& data) +{ + QString stringData = QString::fromLatin1(data); + QStringList elements = stringData.split(QRegularExpression("\\s+"), Qt::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(elements.isEmpty() || elements.size() < 2 || !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()); + + if (m_rawCertificateData.isEmpty()) { + m_error = tr("Base64 decoding failed"); + return false; + } + + 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; + } + + 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"); + 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 a8aa695cec..c793da9b55 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -333,6 +333,61 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co OpenSSHKey keyCopy = key; keyCopy.clearPrivate(); m_addedKeys[keyCopy] = qMakePair(databaseUuid, settings.removeAtDatabaseClose()); + + 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; + } + } + return true; } @@ -360,7 +415,23 @@ 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); + if (key.writeCertificate(certificateStream, false)) { + requestCertificate.write(SSH_AGENTC_REMOVE_IDENTITY); + requestCertificate.write(certificateData); + QByteArray responseCertificateData; + + return (sendMessage(requestData, responseData) && + sendMessage(requestCertificateData, responseCertificateData)); + } + + return (sendMessage(requestData, responseData)); } /**