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));
}
/**