SSH Agent: Add support for certificates (#5486)

This commit is contained in:
Alexandre Petit 2026-02-18 21:24:02 +01:00
parent e4b0746a5d
commit 5c5d62ae6b
No known key found for this signature in database
8 changed files with 719 additions and 225 deletions

View file

@ -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<void (QComboBox::*)(int)>(&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);
}

View file

@ -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<Entry> m_entryCertificate;
const QScopedPointer<EntryAttachments> m_attachmentsCertificate;
#endif
const QScopedPointer<Ui::EditEntryWidgetMain> m_mainUi;
const QScopedPointer<Ui::EditEntryWidgetAdvanced> m_advancedUi;

View file

@ -26,79 +26,35 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item row="1" column="1" colspan="4">
<widget class="QCheckBox" name="removeKeyFromAgentCheckBox">
<item row="6" column="3">
<layout class="QHBoxLayout" name="agentActionsLayout" stretch="0,0,0">
<item>
<widget class="QPushButton" name="addToAgentButton">
<property name="text">
<string>Add to agent</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeFromAgentButton">
<property name="text">
<string>Remove from agent</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="clearAgentButton">
<property name="text">
<string>Clear agent</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="1" colspan="3">
<widget class="QCheckBox" name="requireUserConfirmationCheckBox">
<property name="text">
<string>Remove key from agent when database is closed/locked</string>
</property>
</widget>
</item>
<item row="14" column="1">
<widget class="QLabel" name="commentLabel">
<property name="text">
<string>Comment</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="1" colspan="4">
<widget class="QCheckBox" name="addKeyToAgentCheckBox">
<property name="text">
<string>Add key to agent when database is opened/unlocked</string>
</property>
</widget>
</item>
<item row="15" column="3" colspan="2">
<widget class="QPlainTextEdit" name="publicKeyEdit">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="14" column="4">
<widget class="QPushButton" name="decryptButton">
<property name="text">
<string>Decrypt</string>
</property>
</widget>
</item>
<item row="4" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item row="13" column="1">
<widget class="QLabel" name="fingerprintLabel">
<property name="text">
<string>Fingerprint</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="16" column="3" colspan="2">
<widget class="QPushButton" name="copyToClipboardButton">
<property name="text">
<string>Copy to clipboard</string>
<string>Require user confirmation when this key is used</string>
</property>
</widget>
</item>
@ -112,108 +68,7 @@
</property>
</widget>
</item>
<item row="5" column="1" colspan="4">
<widget class="QGroupBox" name="privateKeyGroupBox">
<property name="title">
<string>Private key</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QRadioButton" name="attachmentRadioButton">
<property name="text">
<string>Attachment</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="QLineEdit" name="externalFileEdit">
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
<property name="accessibleName">
<string>External key file</string>
</property>
</widget>
</item>
<item row="4" column="3">
<layout class="QHBoxLayout" name="agentActionsLayout" stretch="0,0,0">
<item>
<widget class="QPushButton" name="addToAgentButton">
<property name="text">
<string>Add to agent</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeFromAgentButton">
<property name="text">
<string>Remove from agent</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="clearAgentButton">
<property name="text">
<string>Clear agent</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QRadioButton" name="externalFileRadioButton">
<property name="text">
<string>External file</string>
</property>
</widget>
</item>
<item row="3" column="4">
<widget class="QPushButton" name="browseButton">
<property name="accessibleName">
<string>Browser for key file</string>
</property>
<property name="text">
<string extracomment="Button for opening file dialog">Browse…</string>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QPushButton" name="generateButton">
<property name="text">
<string>Generate</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QComboBox" name="attachmentComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="accessibleName">
<string>Select attachment file</string>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="1" colspan="4">
<widget class="QCheckBox" name="requireUserConfirmationCheckBox">
<property name="text">
<string>Require user confirmation when this key is used</string>
</property>
</widget>
</item>
<item row="14" column="3">
<item row="14" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="commentTextLabel">
@ -245,7 +100,291 @@
</item>
</layout>
</item>
<item row="3" column="1" colspan="4">
<item row="14" column="1">
<widget class="QLabel" name="commentLabel">
<property name="text">
<string>Comment</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="13" column="1">
<widget class="QLabel" name="fingerprintLabel">
<property name="text">
<string>Fingerprint</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="15" column="2" colspan="2">
<widget class="QPlainTextEdit" name="publicKeyEdit">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="14" column="3">
<widget class="QPushButton" name="decryptButton">
<property name="text">
<string>Decrypt</string>
</property>
</widget>
</item>
<item row="12" column="2">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item row="13" column="2" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="fingerprintTextLabel">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="text">
<string>n/a</string>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="0" column="1" colspan="3">
<widget class="QCheckBox" name="addKeyToAgentCheckBox">
<property name="text">
<string>Add key to agent when database is opened/unlocked</string>
</property>
</widget>
</item>
<item row="4" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1" colspan="3">
<widget class="QCheckBox" name="removeKeyFromAgentCheckBox">
<property name="text">
<string>Remove key from agent when database is closed/locked</string>
</property>
</widget>
</item>
<item row="5" column="1" colspan="3">
<widget class="QTabWidget" name="privateKeyTabWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="privateKeyTabWidgetPage1" native="true">
<attribute name="title">
<string>Private key</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<item row="4" column="0">
<widget class="QCheckBox" name="addCertificateToAgentCheckBox">
<property name="text">
<string>Use certificate</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QRadioButton" name="externalFileRadioButton">
<property name="text">
<string>External file</string>
</property>
</widget>
</item>
<item row="3" column="4">
<widget class="QPushButton" name="browseButton">
<property name="accessibleName">
<string>Browser for key file</string>
</property>
<property name="text">
<string extracomment="Button for opening file dialog">Browse…</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QComboBox" name="attachmentComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="accessibleName">
<string>Select attachment file</string>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QRadioButton" name="attachmentRadioButton">
<property name="text">
<string>Attachment</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="QLineEdit" name="externalFileEdit">
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
<property name="accessibleName">
<string>External key file</string>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QPushButton" name="generateButton">
<property name="text">
<string>Generate</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="privateKeyTabWidgetPage2">
<attribute name="title">
<string>Certificate</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QRadioButton" name="attachmentCertificateRadioButton">
<property name="text">
<string>Attachment</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="attachmentCertificateComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="accessibleName">
<string>Select attachment file</string>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="externalCertificateFileEdit">
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
<property name="accessibleName">
<string>External key file</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="externalCertificateFileRadioButton">
<property name="text">
<string>External file</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="browseCertificateButton">
<property name="accessibleName">
<string>Browser for key file</string>
</property>
<property name="text">
<string extracomment="Button for opening file dialog">Browse…</string>
</property>
</widget>
</item>
<item row="2" column="1">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item row="16" column="2" colspan="2">
<widget class="QPushButton" name="copyToClipboardButton">
<property name="text">
<string>Copy to clipboard</string>
</property>
</widget>
</item>
<item row="3" column="1" colspan="3">
<layout class="QHBoxLayout" name="removeKeyLayout">
<item>
<widget class="QCheckBox" name="lifetimeCheckBox">
@ -282,54 +421,6 @@
</item>
</layout>
</item>
<item row="13" column="3" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="fingerprintTextLabel">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="text">
<string>n/a</string>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="12" column="3">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<tabstops>

View file

@ -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;
}

View file

@ -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

View file

@ -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());

View file

@ -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);

View file

@ -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<quint32>(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<quint8>(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));
}
/**