From c642e6bbd36f6640a1e50ff7e7ad296248454e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20Vit=C3=A9?= Date: Thu, 6 Feb 2025 22:09:12 +0100 Subject: [PATCH 1/7] tests: Test KeeAgentSettings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change adds testing all KeeAgentSettings fields including their XML conversions by separately: - verifying the default value, - change the current to something else, - convert the KeeAgentSettings object to XML, - convert it back to a second KeeAgentSettings object, - compare both objects to be equal and - verify that the new value landed in the field of the second KeeAgentSettings object. Signed-off-by: Konrad Vité --- tests/TestSSHAgent.cpp | 69 ++++++++++++++++++++++++++++++++++++++++++ tests/TestSSHAgent.h | 1 + 2 files changed, 70 insertions(+) diff --git a/tests/TestSSHAgent.cpp b/tests/TestSSHAgent.cpp index be3ddd2e5..5becddd5d 100644 --- a/tests/TestSSHAgent.cpp +++ b/tests/TestSSHAgent.cpp @@ -110,6 +110,75 @@ void TestSSHAgent::testConfiguration() QCOMPARE(agent.socketPath(false), defaultSocketPath); } +void TestSSHAgent::testKeeAgentSettings() +{ + KeeAgentSettings settings; + KeeAgentSettings settings2; + + QVERIFY(settings2.fromXml(settings.toXml())); + QVERIFY(settings == settings2); + + QVERIFY(!settings.allowUseOfSshKey()); + settings.setAllowUseOfSshKey(true); + QVERIFY(settings2.fromXml(settings.toXml())); + QVERIFY(settings2.allowUseOfSshKey()); + QVERIFY(settings == settings2); + + QVERIFY(!settings.addAtDatabaseOpen()); + settings.setAddAtDatabaseOpen(true); + QVERIFY(settings2.fromXml(settings.toXml())); + QVERIFY(settings2.addAtDatabaseOpen()); + QVERIFY(settings == settings2); + + QVERIFY(!settings.removeAtDatabaseClose()); + settings.setRemoveAtDatabaseClose(true); + QVERIFY(settings2.fromXml(settings.toXml())); + QVERIFY(settings2.removeAtDatabaseClose()); + QVERIFY(settings == settings2); + + QVERIFY(!settings.useConfirmConstraintWhenAdding()); + settings.setUseConfirmConstraintWhenAdding(true); + QVERIFY(settings2.fromXml(settings.toXml())); + QVERIFY(settings2.useConfirmConstraintWhenAdding()); + QVERIFY(settings == settings2); + + QVERIFY(!settings.useLifetimeConstraintWhenAdding()); + settings.setUseLifetimeConstraintWhenAdding(true); + QVERIFY(settings2.fromXml(settings.toXml())); + QVERIFY(settings2.useLifetimeConstraintWhenAdding()); + QVERIFY(settings == settings2); + + QVERIFY(settings.lifetimeConstraintDuration() == 600); + settings.setLifetimeConstraintDuration(120); + QVERIFY(settings2.fromXml(settings.toXml())); + QVERIFY(settings2.lifetimeConstraintDuration()); + QVERIFY(settings == settings2); + + QVERIFY(settings.fileName().isEmpty()); + settings.setFileName("dummy.pkey"); + QVERIFY(settings2.fromXml(settings.toXml())); + QVERIFY(settings2.fileName() == "dummy.pkey"); + QVERIFY(settings == settings2); + + QVERIFY(settings.selectedType() == "file"); + settings.setSelectedType(QStringLiteral("attachment")); + QVERIFY(settings2.fromXml(settings.toXml())); + QVERIFY(settings2.selectedType() == "attachment"); + QVERIFY(settings == settings2); + + QVERIFY(settings.attachmentName().isEmpty()); + settings.setAttachmentName("dummy.pkey"); + QVERIFY(settings2.fromXml(settings.toXml())); + QVERIFY(settings2.attachmentName() == "dummy.pkey"); + QVERIFY(settings == settings2); + + QVERIFY(!settings.saveAttachmentToTempFile()); + settings.setSaveAttachmentToTempFile(true); + QVERIFY(settings2.fromXml(settings.toXml())); + QVERIFY(settings2.saveAttachmentToTempFile()); + QVERIFY(settings == settings2); +} + void TestSSHAgent::testIdentity() { SSHAgent agent; diff --git a/tests/TestSSHAgent.h b/tests/TestSSHAgent.h index 36b9b2bff..a51f4a1be 100644 --- a/tests/TestSSHAgent.h +++ b/tests/TestSSHAgent.h @@ -30,6 +30,7 @@ class TestSSHAgent : public QObject private slots: void initTestCase(); void testConfiguration(); + void testKeeAgentSettings(); void testIdentity(); void testRemoveOnClose(); void testLifetimeConstraint(); From 872e914520c4b075163ea8af470793ac5b18372a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20Vit=C3=A9?= Date: Thu, 6 Feb 2025 22:09:24 +0100 Subject: [PATCH 2/7] ssh-agent: Implement destination constraints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change implements loading ssh-agent destination constraints from KeeAgent.settings into the ssh-agent. For now there is no UI so configuration must be done in KeePass2/KeeAgent or manually. The ssh-agent constrain extension is described at [1]. However, I found it partly misleading: - in the constaint array each constraint is enveloped where in the keyspec arrays the keyspec are just appended to the constraint. - each constraint and host has an additional string field reserved for future use. The actual structure has been obtained from openssh ssh-add source code [2]. [1]: https://www.openssh.com/agent-restrict.html [2]: https://github.com/openssh/openssh-portable/blob/3ad669f81aabbd2ba9fbd472903f680f598e1e99/authfd.c#L538 Signed-off-by: Konrad Vité --- share/translations/keepassxc_en.ts | 4 + src/sshagent/KeeAgentSettings.cpp | 178 +++++++++++++++++++++++++++++ src/sshagent/KeeAgentSettings.h | 27 +++++ src/sshagent/SSHAgent.cpp | 58 ++++++++++ src/sshagent/SSHAgent.h | 10 +- tests/TestSSHAgent.cpp | 118 +++++++++++++++++++ tests/TestSSHAgent.h | 1 + 7 files changed, 395 insertions(+), 1 deletion(-) diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 7d4dee1ee..3463122c4 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -9839,6 +9839,10 @@ This option is deprecated, use --set-key-file instead. All SSH identities removed from agent. + + Destination constraints are invalid or not supported by the agent (check options). + + SearchHelpWidget diff --git a/src/sshagent/KeeAgentSettings.cpp b/src/sshagent/KeeAgentSettings.cpp index a794eac93..0d3084742 100644 --- a/src/sshagent/KeeAgentSettings.cpp +++ b/src/sshagent/KeeAgentSettings.cpp @@ -35,6 +35,29 @@ KeeAgentSettings::KeeAgentSettings() reset(); } +bool KeeAgentSettings::KeySpec::operator==(const KeeAgentSettings::KeySpec& other) const +{ + return (key == other.key && isCertificateAuthority == other.isCertificateAuthority); +} + +QByteArray KeeAgentSettings::KeySpec::getKeyBlob() const +{ + // In KeeAgent the key data is the second word in the string. First is the + // key type. Third is the key comment which is optional. + auto words = key.split(" "); + if (words.length() >= 2) { + return QByteArray::fromBase64(words[1].toLatin1(), QByteArray::Base64Encoding); + } else { + return QByteArray(); + } +} + +bool KeeAgentSettings::DestinationConstraint::operator==(const KeeAgentSettings::DestinationConstraint& other) const +{ + return (fromHost == other.fromHost && fromHostKeys == other.fromHostKeys && toUser == other.toUser + && toHost == other.toHost && toHostKeys == other.toHostKeys); +} + bool KeeAgentSettings::operator==(const KeeAgentSettings& other) const { // clang-format off @@ -43,6 +66,8 @@ bool KeeAgentSettings::operator==(const KeeAgentSettings& other) const && m_useConfirmConstraintWhenAdding == other.m_useConfirmConstraintWhenAdding && m_useLifetimeConstraintWhenAdding == other.m_useLifetimeConstraintWhenAdding && m_lifetimeConstraintDuration == other.m_lifetimeConstraintDuration + && m_useDestinationConstraintsWhenAdding == other.m_useDestinationConstraintsWhenAdding + && m_destinationConstraints == other.m_destinationConstraints && m_selectedType == other.m_selectedType && m_attachmentName == other.m_attachmentName && m_saveAttachmentToTempFile == other.m_saveAttachmentToTempFile @@ -77,6 +102,8 @@ void KeeAgentSettings::reset() m_useConfirmConstraintWhenAdding = false; m_useLifetimeConstraintWhenAdding = false; m_lifetimeConstraintDuration = 600; + m_useDestinationConstraintsWhenAdding = false; + m_destinationConstraints.clear(); m_selectedType = QStringLiteral("file"); m_attachmentName.clear(); @@ -125,6 +152,16 @@ int KeeAgentSettings::lifetimeConstraintDuration() const return m_lifetimeConstraintDuration; } +bool KeeAgentSettings::useDestinationConstraintsWhenAdding() const +{ + return m_useDestinationConstraintsWhenAdding; +} + +QList KeeAgentSettings::destinationConstraints() const +{ + return m_destinationConstraints; +} + const QString KeeAgentSettings::selectedType() const { return m_selectedType; @@ -180,6 +217,16 @@ void KeeAgentSettings::setLifetimeConstraintDuration(int lifetimeConstraintDurat m_lifetimeConstraintDuration = lifetimeConstraintDuration; } +void KeeAgentSettings::setUseDestinationConstraintsWhenAdding(bool useDestinationConstraintsWhenAdding) +{ + m_useDestinationConstraintsWhenAdding = useDestinationConstraintsWhenAdding; +} + +void KeeAgentSettings::setDestinationConstraints(const QList& destinationConstraints) +{ + m_destinationConstraints = destinationConstraints; +} + void KeeAgentSettings::setSelectedType(const QString& selectedType) { m_selectedType = selectedType; @@ -229,6 +276,8 @@ bool KeeAgentSettings::fromXml(const QByteArray& ba) QXmlStreamReader reader; reader.addData(ba); + reset(); + if (reader.error() || !reader.readNextStartElement()) { m_error = reader.errorString(); return false; @@ -273,6 +322,88 @@ bool KeeAgentSettings::fromXml(const QByteArray& ba) reader.skipCurrentElement(); } } + if (!reader.error()) + reader.readNext(); + } else if (reader.name() == "UseDestinationConstraintWhenAdding") { + m_useDestinationConstraintsWhenAdding = readBool(reader); + } else if (reader.name() == "DestinationConstraints") { + while (!reader.error() && reader.readNextStartElement()) { + if (reader.name() == "Constraint") { + KeeAgentSettings::DestinationConstraint constraint; + while (!reader.error() && reader.readNextStartElement()) { + if (reader.name() == "FromHostKeys" || reader.name() == "ToHostKeys") { + QString section = reader.name().toString(); + while (!reader.error() && reader.readNextStartElement()) { + if (reader.name() == "KeySpec") { + KeeAgentSettings::KeySpec keyspec; + while (!reader.error() && reader.readNextStartElement()) { + if (reader.name() == "HostKey") { + reader.readNext(); + keyspec.key = reader.text().toString(); + reader.readNext(); + } else if (reader.name() == "IsCA") { + keyspec.isCertificateAuthority = readBool(reader); + } else { + qWarning() << "Skipping KeySpec element" << reader.name(); + reader.skipCurrentElement(); + } + } + + if (keyspec.getKeyBlob().isEmpty()) { + return false; + } + + if (section == "FromHostKeys") { + constraint.fromHostKeys.append(std::move(keyspec)); + } else { + constraint.toHostKeys.append(std::move(keyspec)); + } + if (!reader.error()) + reader.readNext(); + } else { + qWarning() << "Skipping " << section << " element" << reader.name(); + reader.skipCurrentElement(); + } + } + if (!reader.error()) + reader.readNext(); + } else if (reader.name() == "FromHost") { + reader.readNext(); + constraint.fromHost = reader.text().toString(); + reader.readNext(); + } else if (reader.name() == "ToUser") { + reader.readNext(); + constraint.toUser = reader.text().toString(); + reader.readNext(); + } else if (reader.name() == "ToHost") { + reader.readNext(); + constraint.toHost = reader.text().toString(); + reader.readNext(); + } else { + qWarning() << "Skipping Constraint element" << reader.name(); + reader.skipCurrentElement(); + } + } + + if ((constraint.fromHost.isEmpty() && !constraint.fromHostKeys.isEmpty()) + || (!constraint.fromHost.isEmpty() && constraint.fromHostKeys.isEmpty())) { + return false; + } + if (constraint.toHost.isEmpty() || constraint.toHostKeys.isEmpty()) { + return false; + } + + m_destinationConstraints.append(std::move(constraint)); + + if (!reader.error()) + reader.readNext(); + } else { + qWarning() << "Skipping DestinationConstraints element" << reader.name(); + reader.skipCurrentElement(); + } + } + if (!reader.error()) + reader.readNext(); } else { qWarning() << "Skipping element" << reader.name(); reader.skipCurrentElement(); @@ -309,6 +440,53 @@ QByteArray KeeAgentSettings::toXml() const writer.writeTextElement("UseConfirmConstraintWhenAdding", m_useConfirmConstraintWhenAdding ? "true" : "false"); writer.writeTextElement("UseLifetimeConstraintWhenAdding", m_useLifetimeConstraintWhenAdding ? "true" : "false"); writer.writeTextElement("LifetimeConstraintDuration", QString::number(m_lifetimeConstraintDuration)); + writer.writeTextElement("UseDestinationConstraintWhenAdding", + m_useDestinationConstraintsWhenAdding ? "true" : "false"); + + writer.writeStartElement("DestinationConstraints"); + + foreach (const auto& constraint, m_destinationConstraints) { + writer.writeStartElement("Constraint"); + + if (constraint.fromHost.isEmpty()) { + writer.writeEmptyElement("FromHost"); + } else { + writer.writeTextElement("FromHost", constraint.fromHost); + } + + writer.writeStartElement("FromHostKeys"); + foreach (const auto& keyspec, constraint.fromHostKeys) { + writer.writeStartElement("KeySpec"); + writer.writeTextElement("HostKey", keyspec.key); + writer.writeTextElement("IsCA", keyspec.isCertificateAuthority ? "true" : "false"); + writer.writeEndElement(); // KeySpec + } + writer.writeEndElement(); // FromHostKeys + + if (constraint.toUser.isEmpty()) { + writer.writeEmptyElement("ToUser"); + } else { + writer.writeTextElement("ToUser", constraint.toUser); + } + if (constraint.toHost.isEmpty()) { + writer.writeEmptyElement("ToHost"); + } else { + writer.writeTextElement("ToHost", constraint.toHost); + } + + writer.writeStartElement("ToHostKeys"); + foreach (const auto& keyspec, constraint.toHostKeys) { + writer.writeStartElement("KeySpec"); + writer.writeTextElement("HostKey", keyspec.key); + writer.writeTextElement("IsCA", keyspec.isCertificateAuthority ? "true" : "false"); + writer.writeEndElement(); // KeySpec + } + writer.writeEndElement(); // ToHostKeys + + writer.writeEndElement(); // Constraint + } + + writer.writeEndElement(); // DestinationConstraints writer.writeStartElement("Location"); writer.writeTextElement("SelectedType", m_selectedType); diff --git a/src/sshagent/KeeAgentSettings.h b/src/sshagent/KeeAgentSettings.h index ffc14044e..d3a6da16e 100644 --- a/src/sshagent/KeeAgentSettings.h +++ b/src/sshagent/KeeAgentSettings.h @@ -29,6 +29,27 @@ class QXmlStreamReader; class KeeAgentSettings { public: + struct KeySpec + { + QString key; + bool isCertificateAuthority; + + bool operator==(const KeySpec& other) const; + + QByteArray getKeyBlob() const; + }; + + struct DestinationConstraint + { + QString fromHost; + QList fromHostKeys; + QString toUser; + QString toHost; + QList toHostKeys; + + bool operator==(const DestinationConstraint& other) const; + }; + KeeAgentSettings(); bool operator==(const KeeAgentSettings& other) const; bool operator!=(const KeeAgentSettings& other) const; @@ -58,6 +79,8 @@ public: bool useConfirmConstraintWhenAdding() const; bool useLifetimeConstraintWhenAdding() const; int lifetimeConstraintDuration() const; + bool useDestinationConstraintsWhenAdding() const; + QList destinationConstraints() const; const QString selectedType() const; const QString attachmentName() const; @@ -71,6 +94,8 @@ public: void setUseConfirmConstraintWhenAdding(bool useConfirmConstraintWhenAdding); void setUseLifetimeConstraintWhenAdding(bool useLifetimeConstraintWhenAdding); void setLifetimeConstraintDuration(int lifetimeConstraintDuration); + void setUseDestinationConstraintsWhenAdding(bool useDestinationConstraintsWhenAdding); + void setDestinationConstraints(const QList& destinationConstraints); void setSelectedType(const QString& type); void setAttachmentName(const QString& attachmentName); @@ -87,6 +112,8 @@ private: bool m_useConfirmConstraintWhenAdding; bool m_useLifetimeConstraintWhenAdding; int m_lifetimeConstraintDuration; + bool m_useDestinationConstraintsWhenAdding; + QList m_destinationConstraints; // location QString m_selectedType; diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index a8aa695ce..ead599c77 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -305,6 +305,12 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co request.writeString(securityKeyProvider()); } + if (settings.useDestinationConstraintsWhenAdding()) { + request.write(SSH_AGENT_CONSTRAIN_EXTENSION); + request.writeString(QString("restrict-destination-v00@openssh.com")); + encodeDestinationConstraints(settings.destinationConstraints(), request); + } + QByteArray responseData; if (!sendMessage(requestData, responseData)) { return false; @@ -322,6 +328,10 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co m_error += "\n" + tr("A confirmation request is not supported by the agent (check options)."); } + if (settings.useDestinationConstraintsWhenAdding()) { + m_error += "\n" + tr("Destination constraints are invalid or 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."); @@ -336,6 +346,54 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co return true; } +bool SSHAgent::encodeDestinationConstraints(const QList& constraints, + BinaryStream& out) +{ + QByteArray data; + BinaryStream stream(&data); + + foreach (const auto& constraint, constraints) { + encodeDestinationConstraint(constraint, stream); + } + + out.writeString(data); + return true; +} + +bool SSHAgent::encodeDestinationConstraint(const KeeAgentSettings::DestinationConstraint& constraint, BinaryStream& out) +{ + QByteArray data; + BinaryStream stream(&data); + + encodeDestinationConstraintHost("", constraint.fromHost, constraint.fromHostKeys, stream); + encodeDestinationConstraintHost(constraint.toUser, constraint.toHost, constraint.toHostKeys, stream); + stream.writeString(QString("")); // reserved + + out.writeString(data); + return true; +} + +bool SSHAgent::encodeDestinationConstraintHost(const QString user, + const QString hostname, + const QList& keys, + BinaryStream& out) +{ + QByteArray data; + BinaryStream stream(&data); + + stream.writeString(user); + stream.writeString(hostname); + stream.writeString(QString("")); // reserved + + foreach (const auto& key, keys) { + stream.writeString(key.getKeyBlob()); + stream.write(static_cast(key.isCertificateAuthority)); + } + + out.writeString(data); + return true; +} + /** * Remove an identity from the SSH agent. * diff --git a/src/sshagent/SSHAgent.h b/src/sshagent/SSHAgent.h index d3eeb4ebc..0001b8167 100644 --- a/src/sshagent/SSHAgent.h +++ b/src/sshagent/SSHAgent.h @@ -21,9 +21,9 @@ #include +#include "KeeAgentSettings.h" #include "OpenSSHKey.h" -class KeeAgentSettings; class Database; class SSHAgent : public QObject @@ -91,6 +91,14 @@ private: const quint32 AGENT_COPYDATA_ID = 0x804e50ba; #endif + bool encodeDestinationConstraints(const QList& constraints, + BinaryStream& out); + bool encodeDestinationConstraint(const KeeAgentSettings::DestinationConstraint& constraint, BinaryStream& out); + bool encodeDestinationConstraintHost(const QString user, + const QString hostname, + const QList& keys, + BinaryStream& out); + QHash> m_addedKeys; QString m_error; }; diff --git a/tests/TestSSHAgent.cpp b/tests/TestSSHAgent.cpp index 5becddd5d..152cb3fd6 100644 --- a/tests/TestSSHAgent.cpp +++ b/tests/TestSSHAgent.cpp @@ -27,6 +27,52 @@ QTEST_GUILESS_MAIN(TestSSHAgent) +static const QList githubKeys = { + { + .key = "ssh-rsa " + "AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+" + "VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/" + "BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/" + "hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+" + "5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+" + "wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+" + "bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/" + "YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=", + .isCertificateAuthority = false, + }, + { + .key = "ecdsa-sha2-nistp256 " + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N" + "87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=", + .isCertificateAuthority = false, + }, + { + .key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl", + .isCertificateAuthority = false, + }}; + +static const QList gitlabKeys = { + { + .key = "ssh-rsa " + "AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++" + "J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/" + "ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/" + "StKDHMoX4/OKyIzuS0q/" + "T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+" + "siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9", + .isCertificateAuthority = false, + }, + { + .key = "ecdsa-sha2-nistp256 " + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3H" + "w9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=", + .isCertificateAuthority = false, + }, + { + .key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf", + .isCertificateAuthority = false, + }}; + void TestSSHAgent::initTestCase() { QVERIFY(Crypto::init()); @@ -177,6 +223,41 @@ void TestSSHAgent::testKeeAgentSettings() QVERIFY(settings2.fromXml(settings.toXml())); QVERIFY(settings2.saveAttachmentToTempFile()); QVERIFY(settings == settings2); + + QVERIFY(!settings.useDestinationConstraintsWhenAdding()); + settings.setUseDestinationConstraintsWhenAdding(true); + QVERIFY(settings2.fromXml(settings.toXml())); + QVERIFY(settings2.useDestinationConstraintsWhenAdding()); + QVERIFY(settings == settings2); + + QList destinationConstraints; + + // ssh-add -h github.com + destinationConstraints.append({ + .fromHost = "", + .fromHostKeys = {}, + .toUser = "", + .toHost = "github.com", + .toHostKeys = githubKeys, + }); + QVERIFY(settings.destinationConstraints().isEmpty()); + settings.setDestinationConstraints(destinationConstraints); + QVERIFY(settings2.fromXml(settings.toXml())); + QVERIFY(settings2.destinationConstraints() == destinationConstraints); + QVERIFY(settings == settings2); + + // ssh-add -h github.com -h "github.com>git@gitlab.com" + destinationConstraints.append({ + .fromHost = "github.com", + .fromHostKeys = githubKeys, + .toUser = "git", + .toHost = "gitlab.com", + .toHostKeys = gitlabKeys, + }); + settings.setDestinationConstraints(destinationConstraints); + QVERIFY(settings2.fromXml(settings.toXml())); + QVERIFY(settings2.destinationConstraints() == destinationConstraints); + QVERIFY(settings == settings2); } void TestSSHAgent::testIdentity() @@ -281,6 +362,43 @@ void TestSSHAgent::testConfirmConstraint() QVERIFY(agent.checkIdentity(m_key, keyInAgent) && !keyInAgent); } +void TestSSHAgent::testDestinationConstraints() +{ + SSHAgent agent; + agent.setEnabled(true); + agent.setAuthSockOverride(m_agentSocketFileName); + + QVERIFY(agent.isAgentRunning()); + + KeeAgentSettings settings; + bool keyInAgent; + + // ssh-add -h github.com -h "github.com>git@gitlab.com" + settings.setUseDestinationConstraintsWhenAdding(true); + settings.setDestinationConstraints({{ + .fromHost = "", + .fromHostKeys = {}, + .toUser = "", + .toHost = "github.com", + .toHostKeys = githubKeys, + }, + { + .fromHost = "github.com", + .fromHostKeys = githubKeys, + .toUser = "git", + .toHost = "gitlab.com", + .toHostKeys = gitlabKeys, + }}); + + QVERIFY(agent.addIdentity(m_key, settings, m_uuid)); + + // we can't test destination constraints itself is working but we can test the agent accepts the key + QVERIFY(agent.checkIdentity(m_key, keyInAgent) && keyInAgent); + + QVERIFY(agent.removeIdentity(m_key)); + QVERIFY(agent.checkIdentity(m_key, keyInAgent) && !keyInAgent); +} + void TestSSHAgent::testToOpenSSHKey() { KeeAgentSettings settings; diff --git a/tests/TestSSHAgent.h b/tests/TestSSHAgent.h index a51f4a1be..bdf697998 100644 --- a/tests/TestSSHAgent.h +++ b/tests/TestSSHAgent.h @@ -35,6 +35,7 @@ private slots: void testRemoveOnClose(); void testLifetimeConstraint(); void testConfirmConstraint(); + void testDestinationConstraints(); void testToOpenSSHKey(); void testKeyGenRSA(); void testKeyGenECDSA(); From 25f9d2214c53189f73955ea0203b1dbe9871bd61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20Vit=C3=A9?= Date: Thu, 6 Feb 2025 22:09:49 +0100 Subject: [PATCH 3/7] TestSSHAgent: Skip testDestinationConstraints() on OpenSSH < 8.9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Konrad Vité --- tests/TestSSHAgent.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/TestSSHAgent.cpp b/tests/TestSSHAgent.cpp index 152cb3fd6..58a09d6ed 100644 --- a/tests/TestSSHAgent.cpp +++ b/tests/TestSSHAgent.cpp @@ -24,6 +24,7 @@ #include "sshagent/SSHAgent.h" #include +#include QTEST_GUILESS_MAIN(TestSSHAgent) @@ -364,6 +365,21 @@ void TestSSHAgent::testConfirmConstraint() void TestSSHAgent::testDestinationConstraints() { + // ssh-agent does not support destination constraints before OpenSSH + // version 8.9. Therefore we want to skip this test on older versions. + // Unfortunately ssh-agent does not give us any way to retrieve its version + // number neither via protocol nor on the command line. Therefore we use + // the version number of the SSH client and assume it to be the same. + QProcess ssh; + ssh.setReadChannel(QProcess::StandardError); + ssh.start("ssh", QStringList() << "-V"); + ssh.waitForFinished(); + auto ssh_version = QString::fromUtf8(ssh.readLine()); + ssh_version.remove(QRegExp("^OpenSSH_")); + if (QVersionNumber ::fromString(ssh_version) < QVersionNumber(8, 9)) { + QSKIP("Test requires ssh-agent >= 8.9"); + } + SSHAgent agent; agent.setEnabled(true); agent.setAuthSockOverride(m_agentSocketFileName); From e8690c4ce81747dbd763a34bdbb275363eb25c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20Vit=C3=A9?= Date: Thu, 6 Feb 2025 22:08:19 +0100 Subject: [PATCH 4/7] ssh-agent: Add global option to enable or disable destination contraints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Konrad Vité --- share/translations/keepassxc_en.ts | 8 +++ src/core/Config.cpp | 1 + src/core/Config.h | 1 + src/sshagent/AgentSettingsWidget.cpp | 23 +++++++ src/sshagent/AgentSettingsWidget.h | 1 + src/sshagent/AgentSettingsWidget.ui | 96 +++++++++++++++------------- src/sshagent/SSHAgent.cpp | 14 +++- src/sshagent/SSHAgent.h | 2 + 8 files changed, 101 insertions(+), 45 deletions(-) diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 3463122c4..70a4c4864 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -156,6 +156,14 @@ SSH Agent connection is working! + + Enable destination constraints + + + + Destination contrains can have unexpected side effects. Make sure to read the <a href="https://keepassxc.org/docs/KeePassXC_UserGuide#_using_destination_constraints">documentation</a>. + + ApplicationSettingsWidget diff --git a/src/core/Config.cpp b/src/core/Config.cpp index a876b36f2..2968057e5 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -183,6 +183,7 @@ static const QHash configStrings = { {Config::SSHAgent_Enabled, {QS("SSHAgent/Enabled"), Roaming, false}}, {Config::SSHAgent_UseOpenSSH, {QS("SSHAgent/UseOpenSSH"), Roaming, false}}, {Config::SSHAgent_UsePageant, {QS("SSHAgent/UsePageant"), Roaming, true} }, + {Config::SSHAgent_EnableDestinationConstraints, {QS("SSHAgent/EnableDestinationConstraints"), Roaming, false} }, {Config::SSHAgent_AuthSockOverride, {QS("SSHAgent/AuthSockOverride"), Local, {}}}, {Config::SSHAgent_SecurityKeyProviderOverride, {QS("SSHAgent/SecurityKeyProviderOverride"), Local, {}}}, diff --git a/src/core/Config.h b/src/core/Config.h index 5911d78a1..f78710e14 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -162,6 +162,7 @@ public: SSHAgent_Enabled, SSHAgent_UseOpenSSH, SSHAgent_UsePageant, + SSHAgent_EnableDestinationConstraints, SSHAgent_AuthSockOverride, SSHAgent_SecurityKeyProviderOverride, diff --git a/src/sshagent/AgentSettingsWidget.cpp b/src/sshagent/AgentSettingsWidget.cpp index bdad6e30e..8ddccbe68 100644 --- a/src/sshagent/AgentSettingsWidget.cpp +++ b/src/sshagent/AgentSettingsWidget.cpp @@ -35,7 +35,21 @@ AgentSettingsWidget::AgentSettingsWidget(QWidget* parent) m_ui->sshAuthSockMessageWidget->setVisible(sshAgent()->isEnabled()); m_ui->sshAuthSockMessageWidget->setCloseButtonVisible(false); m_ui->sshAuthSockMessageWidget->setAutoHideTimeout(-1); + + m_ui->destinationConstraintsMessageWidget->setCloseButtonVisible(false); + m_ui->destinationConstraintsMessageWidget->setAutoHideTimeout(-1); + m_ui->destinationConstraintsMessageWidget->showMessage( + tr("Destination contrains can have unexpected side effects. " + "Make sure to read the " + "documentation."), + MessageWidget::Warning); + m_ui->destinationConstraintsMessageWidget->setVisible(sshAgent()->enableDestinationConstraints()); + connect(m_ui->enableSSHAgentCheckBox, SIGNAL(stateChanged(int)), SLOT(toggleSettingsEnabled())); + connect(m_ui->enableDestinationConstraintsCheckBox, + SIGNAL(stateChanged(int)), + SLOT(toggleDestinationConstraintsEnabled())); } AgentSettingsWidget::~AgentSettingsWidget() @@ -66,6 +80,9 @@ void AgentSettingsWidget::loadSettings() m_ui->sshAuthSockMessageWidget->setVisible(sshAgentEnabled); + auto destinationConstraintsEnabled = sshAgent()->enableDestinationConstraints(); + m_ui->enableDestinationConstraintsCheckBox->setChecked(destinationConstraintsEnabled); + if (sshAgentEnabled) { #ifndef Q_OS_WIN if (sshAuthSock.isEmpty() && sshAuthSockOverride.isEmpty()) { @@ -98,6 +115,7 @@ void AgentSettingsWidget::saveSettings() sshAgent()->setUsePageant(m_ui->usePageantRadioButton->isChecked() || m_ui->useBothRadioButton->isChecked()); sshAgent()->setUseOpenSSH(m_ui->useOpenSSHRadioButton->isChecked() || m_ui->useBothRadioButton->isChecked()); #endif + sshAgent()->setEnableDestinationConstraints(m_ui->enableDestinationConstraintsCheckBox->isChecked()); sshAgent()->setEnabled(m_ui->enableSSHAgentCheckBox->isChecked()); } @@ -105,3 +123,8 @@ void AgentSettingsWidget::toggleSettingsEnabled() { m_ui->agentConfigPageBody->setEnabled(m_ui->enableSSHAgentCheckBox->isChecked()); } + +void AgentSettingsWidget::toggleDestinationConstraintsEnabled() +{ + m_ui->destinationConstraintsMessageWidget->setVisible(m_ui->enableDestinationConstraintsCheckBox->isChecked()); +} diff --git a/src/sshagent/AgentSettingsWidget.h b/src/sshagent/AgentSettingsWidget.h index 67c41ffde..265d2c9a1 100644 --- a/src/sshagent/AgentSettingsWidget.h +++ b/src/sshagent/AgentSettingsWidget.h @@ -39,6 +39,7 @@ public slots: void loadSettings(); void saveSettings(); void toggleSettingsEnabled(); + void toggleDestinationConstraintsEnabled(); private: QScopedPointer m_ui; diff --git a/src/sshagent/AgentSettingsWidget.ui b/src/sshagent/AgentSettingsWidget.ui index abd056c8a..99ae5e062 100644 --- a/src/sshagent/AgentSettingsWidget.ui +++ b/src/sshagent/AgentSettingsWidget.ui @@ -7,7 +7,7 @@ 0 0 400 - 300 + 443 @@ -93,6 +93,16 @@ + + + + Enable destination constraints + + + + + + @@ -107,42 +117,29 @@ 8 - - + + - SSH_AUTH_SOCK override + SSH_SK_PROVIDER override Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - SSH_AUTH_SOCK value - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - + + + + SSH_SK_PROVIDER value + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + @@ -158,10 +155,36 @@ - - + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + - SSH_SK_PROVIDER value + SSH_AUTH_SOCK value + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + SSH_AUTH_SOCK override Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter @@ -183,19 +206,6 @@ - - - - SSH_SK_PROVIDER override - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index ead599c77..ac639d80b 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -98,6 +98,16 @@ void SSHAgent::setUsePageant(bool usePageant) } #endif +bool SSHAgent::enableDestinationConstraints() const +{ + return config()->get(Config::SSHAgent_EnableDestinationConstraints).toBool(); +} + +void SSHAgent::setEnableDestinationConstraints(bool enableDestinationConstraints) +{ + config()->set(Config::SSHAgent_EnableDestinationConstraints, enableDestinationConstraints); +} + QString SSHAgent::socketPath(bool allowOverride) const { QString socketPath; @@ -305,7 +315,7 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co request.writeString(securityKeyProvider()); } - if (settings.useDestinationConstraintsWhenAdding()) { + if (enableDestinationConstraints() && settings.useDestinationConstraintsWhenAdding()) { request.write(SSH_AGENT_CONSTRAIN_EXTENSION); request.writeString(QString("restrict-destination-v00@openssh.com")); encodeDestinationConstraints(settings.destinationConstraints(), request); @@ -328,7 +338,7 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co m_error += "\n" + tr("A confirmation request is not supported by the agent (check options)."); } - if (settings.useDestinationConstraintsWhenAdding()) { + if (enableDestinationConstraints() && settings.useDestinationConstraintsWhenAdding()) { m_error += "\n" + tr("Destination constraints are invalid or not supported by the agent (check options)."); } diff --git a/src/sshagent/SSHAgent.h b/src/sshagent/SSHAgent.h index 0001b8167..5c02dabf3 100644 --- a/src/sshagent/SSHAgent.h +++ b/src/sshagent/SSHAgent.h @@ -48,6 +48,8 @@ public: void setUseOpenSSH(bool useOpenSSH); void setUsePageant(bool usePageant); #endif + bool enableDestinationConstraints() const; + void setEnableDestinationConstraints(bool enableDestinationConstraints); const QString errorString() const; bool isAgentRunning() const; From 76860ce57a23b9799c0e98cdaf2aa5fd109fda05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20Vit=C3=A9?= Date: Fri, 7 Feb 2025 21:59:16 +0100 Subject: [PATCH 5/7] docs: Add SSH Agent destination constraints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Konrad Vité --- docs/topics/SSHAgent.adoc | 81 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/docs/topics/SSHAgent.adoc b/docs/topics/SSHAgent.adoc index 5539e0e67..aff0bcc20 100644 --- a/docs/topics/SSHAgent.adoc +++ b/docs/topics/SSHAgent.adoc @@ -177,4 +177,85 @@ 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[] + + +=== Using destination constraints +SSH Agent destination constraints may be used to restrict the usage of an SSH key to specific hosts or to specific destinations. +This is especially useful when forwarding the SSH agent to machines that are not fully trustworthy. +The feature requires OpenSSH 8.9 or later on the client and server. +Please refer to https://www.openssh.com/agent-restrict.html for more details. + +To enable support for SSH Agent destination constraints, follow the steps below: + + 1. Select _Tools > Settings_ from the menu + 2. Select _SSH Agent_ category on the left sidebar + 3. Check _Enable destination contraints_ + +As of now KeepassXC lacks a UI for configuring the destination constraints on a specific SSH key. +However, the settings format is compatible to the Keepass plugin https://lechnology.com/software/keeagent/[KeeAgent]. +Therefore you could either load your database in _Keepass_ with _KeeAgent_ and use the _KeeAgent_ UI to configure the destination constraints. + +Alternatively you could alter the settings file manually: + + 1. Create a new entry, or open an existing entry in edit mode. + 2. Go to the advanced category and _Save_ the _KeeAgent.settings_ file to your filesystem + 3. Edit the file with your editor (see below) + 4. Add the edited file back to the SSH key entry in the database, overwriting the old version. + 5. Go to the SSH agent category and _Remove from agent_ and then _Add to agent_ + +The settings file is an UTF-16 encoded XML file consisting a large `EntrySettings` block. +This block contains an `UseDestinationConstraintWhenAdding` tag the is set to `false` by default. +You must set that to `true` to enable destination constraints on the specific SSH key entry. + The `DestinationConstraints` is a list of zero or more `Constraint` blocks. + Each `Constraint` block contains the origin and destination host both identified by their name (`FromHost` / `ToHost`) and their host keys (`FromHostKeys` / `ToHostKeys`). + An empty origin host (`FromHost` / `FromHostKeys`) depicts the machine the SSH agent running on. + The destination can be further restricted to a specific user by setting the user name in `ToUser`. +The host keys are in the format `$algorithm $pubkey` and can be retrieved with the command `ssh-keyscan $HOST` or from the `known_hosts` file. + +NOTE: Both return the format `$hostname $algorithm $pubkey`. Therefore the hostname must be omitted. + +The following example corresponds to the command + `ssh-add -h 'perseus@cetus.example.org' -h 'cetus.example.org>github.com' ~/.ssh/id_ed25519`: + + + + … + true + + + + + perseus + cetus.example.org + + + ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAZEGN4X9luxQr0Q+xEiB8NLK+E2m2/9DeT98hhiT8wn + false + + + + + cetus.example.org + + + ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAZEGN4X9luxQr0Q+xEiB8NLK+E2m2/9DeT98hhiT8wn + false + + + + github.com + + + ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg= + false + + + ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg= + false + + + + + + // end::content[] From 6063508e7a084a268d7098952252b623f3dbf782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20Vit=C3=A9?= Date: Wed, 12 Feb 2025 16:16:23 +0100 Subject: [PATCH 6/7] SSH Agent: Reload settings when KeeAgent.settings attachment changed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Konrad Vité --- src/gui/entry/EditEntryWidget.cpp | 6 ++++++ src/sshagent/KeeAgentSettings.cpp | 14 +++++++++++++- src/sshagent/KeeAgentSettings.h | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index d2213a669..39850d9b5 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -661,6 +661,12 @@ void EditEntryWidget::updateSSHAgentAttachments() setSSHAgentSettings(); } + if (KeeAgentSettings::inEntryAttachments(m_attachments.data())) { + m_sshAgentSettings.reset(); + m_sshAgentSettings.fromEntryAttachments(m_attachments.data()); + setSSHAgentSettings(); + } + m_sshAgentUi->attachmentComboBox->clear(); m_sshAgentUi->attachmentComboBox->addItem(""); diff --git a/src/sshagent/KeeAgentSettings.cpp b/src/sshagent/KeeAgentSettings.cpp index 0d3084742..ccf48f912 100644 --- a/src/sshagent/KeeAgentSettings.cpp +++ b/src/sshagent/KeeAgentSettings.cpp @@ -533,7 +533,19 @@ bool KeeAgentSettings::inEntryAttachments(const EntryAttachments* attachments) */ bool KeeAgentSettings::fromEntry(const Entry* entry) { - const auto attachments = entry->attachments(); + return KeeAgentSettings::fromEntryAttachments(entry->attachments()); +} + +/** + * Read settings from entry attachments as an XML attachment. + * + * Sets error string on error. + * + * @param entry EntryAttachments to read the attachment from + * @return true if XML document was loaded + */ +bool KeeAgentSettings::fromEntryAttachments(const EntryAttachments* attachments) +{ if (attachments->hasKey("KeeAgent.settings")) { return fromXml(attachments->value("KeeAgent.settings")); } diff --git a/src/sshagent/KeeAgentSettings.h b/src/sshagent/KeeAgentSettings.h index d3a6da16e..4b63019c3 100644 --- a/src/sshagent/KeeAgentSettings.h +++ b/src/sshagent/KeeAgentSettings.h @@ -61,6 +61,7 @@ public: static bool inEntryAttachments(const EntryAttachments* attachments); bool fromEntry(const Entry* entry); + bool fromEntryAttachments(const EntryAttachments* attachments); void toEntry(Entry* entry) const; bool keyConfigured() const; bool toOpenSSHKey(const Entry* entry, OpenSSHKey& key, bool decrypt); From 9ec4deb83820789f8380aabff11de6ef056fcc0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20Vit=C3=A9?= Date: Wed, 12 Feb 2025 16:18:52 +0100 Subject: [PATCH 7/7] EditEntryWidget: SSH Agent: Load destination constraints into temporary settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using the "Add to agent" button in the SSH Agent category of the entry settings a default KeeAgentSettings structure will be used and filled-in with the current state of the UI elements. As destination contraints are not present in the UI for now those have not been loaded and thus the key was loaded into the SSH agent with no constraints at all. This change fixes it by loading the attachment first before overwriting with the data from the UI elements. Signed-off-by: Konrad Vité --- src/gui/entry/EditEntryWidget.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 39850d9b5..66f27e5d1 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -734,6 +734,12 @@ void EditEntryWidget::updateSSHAgentKeyInfo() void EditEntryWidget::toKeeAgentSettings(KeeAgentSettings& settings) const { + // set from attachment to load settings aren't supported by the UI (e.g. + // destination constraints) + if (KeeAgentSettings::inEntryAttachments(m_attachments.data())) { + settings.fromEntryAttachments(m_attachments.data()); + } + settings.setAddAtDatabaseOpen(m_sshAgentUi->addKeyToAgentCheckBox->isChecked()); settings.setRemoveAtDatabaseClose(m_sshAgentUi->removeKeyFromAgentCheckBox->isChecked()); settings.setUseConfirmConstraintWhenAdding(m_sshAgentUi->requireUserConfirmationCheckBox->isChecked());