This commit is contained in:
Alexandre Petit 2026-03-10 09:51:12 +05:30 committed by GitHub
commit 60d50cac47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 793 additions and 227 deletions

View file

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

View file

@ -3095,6 +3095,10 @@ Would you like to correct it?</source>
<source>Failed to decrypt SSH key, ensure password is correct.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Select certificate</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>EditEntryWidgetAdvanced</name>
@ -3528,6 +3532,14 @@ Would you like to correct it?</source>
<source>Clear agent</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Use certificate</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Certificate</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>EditGroupWidget</name>
@ -5347,6 +5359,22 @@ Line %2, column %3</source>
<source>Failed to open private key</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Certificate is an attachment but no attachments provided.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Certificate is empty</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>File too large to be a certificate</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to open certificate</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>KeePass1Reader</name>
@ -6844,6 +6872,18 @@ Expect some bugs and minor issues, this version is meant for testing purposes.</
<source>Failed to read public key: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Invalid or unsupported certificate file</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Can&apos;t write certificate as it is empty</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unexpected EOF when writing certificate</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>OpenSSHKeyGenDialog</name>
@ -10043,6 +10083,14 @@ This option is deprecated, use --set-key-file instead.</source>
<source>All SSH identities removed from agent.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Agent refused this identity certificate. Possible reasons include:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Invalid or empty certificate.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SearchHelpWidget</name>

View file

@ -41,6 +41,7 @@
#include "core/PasswordGenerator.h"
#include "core/TimeDelta.h"
#ifdef WITH_XC_SSHAGENT
#include <QSignalBlocker>
#include "sshagent/OpenSSHKey.h"
#include "sshagent/OpenSSHKeyGenDialog.h"
#include "sshagent/SSHAgent.h"
@ -544,6 +545,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
@ -621,6 +628,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,10 +652,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()
@ -672,18 +690,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") {
@ -692,6 +717,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();
}
@ -758,6 +793,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()
@ -820,6 +863,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;
@ -966,6 +1026,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());
@ -1244,6 +1307,7 @@ bool EditEntryWidget::commitEntry()
void EditEntryWidget::acceptEntry()
{
if (commitEntry()) {
m_sshAgentUi->privateKeyTabWidget->setCurrentIndex(0);
clear();
emit editFinished(true);
}
@ -1363,6 +1427,7 @@ void EditEntryWidget::cancel()
}
}
m_sshAgentUi->privateKeyTabWidget->setCurrentIndex(0);
clear();
emit editFinished(accepted);
}
@ -1382,6 +1447,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();

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

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">
<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>Browse for certificate 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_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;
}

View file

@ -21,6 +21,8 @@
#include <QProcessEnvironment>
#define SSH_MAX_LOCAL_KEY_SIZE (1024 * 1024)
class Entry;
class EntryAttachments;
class OpenSSHKey;
@ -77,6 +79,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 +109,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,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());

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

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