diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts
index b505dc930..7ea56b093 100644
--- a/share/translations/keepassxc_en.ts
+++ b/share/translations/keepassxc_en.ts
@@ -120,6 +120,14 @@
Use OpenSSH
+
+ SSH_SK_PROVIDER value
+
+
+
+ SSH_SK_PROVIDER override
+
+
ApplicationSettingsWidget
@@ -5472,6 +5480,14 @@ We recommend you use the AppImage available on our downloads page.
Decryption failed: %1
+
+ Unexpected EOF while reading key
+
+
+
+ Unsupported key part
+
+
PasswordEdit
@@ -7624,6 +7640,10 @@ Please consider generating a new key file.
No agent running, cannot list identities.
+
+ Security keys are not supported by the agent or the security key provider is unavailable.
+
+
SearchHelpWidget
diff --git a/src/core/Config.cpp b/src/core/Config.cpp
index 22741e610..0da3191ef 100644
--- a/src/core/Config.cpp
+++ b/src/core/Config.cpp
@@ -170,6 +170,7 @@ static const QHash configStrings = {
{Config::SSHAgent_UseOpenSSH, {QS("SSHAgent/UseOpenSSH"), Roaming, false}},
{Config::SSHAgent_UsePageant, {QS("SSHAgent/UsePageant"), Roaming, false} },
{Config::SSHAgent_AuthSockOverride, {QS("SSHAgent/AuthSockOverride"), Local, {}}},
+ {Config::SSHAgent_SecurityKeyProviderOverride, {QS("SSHAgent/SecurityKeyProviderOverride"), Local, {}}},
// FdoSecrets
{Config::FdoSecrets_Enabled, {QS("FdoSecrets/Enabled"), Roaming, false}},
diff --git a/src/core/Config.h b/src/core/Config.h
index 065aa1677..7fa4e006c 100644
--- a/src/core/Config.h
+++ b/src/core/Config.h
@@ -148,6 +148,7 @@ public:
SSHAgent_UseOpenSSH,
SSHAgent_UsePageant,
SSHAgent_AuthSockOverride,
+ SSHAgent_SecurityKeyProviderOverride,
FdoSecrets_Enabled,
FdoSecrets_ShowNotification,
diff --git a/src/sshagent/AgentSettingsWidget.cpp b/src/sshagent/AgentSettingsWidget.cpp
index 17fe50c3e..1183198ab 100644
--- a/src/sshagent/AgentSettingsWidget.cpp
+++ b/src/sshagent/AgentSettingsWidget.cpp
@@ -55,6 +55,11 @@ void AgentSettingsWidget::loadSettings()
auto sshAuthSockOverride = sshAgent()->authSockOverride();
m_ui->sshAuthSockLabel->setText(sshAuthSock.isEmpty() ? tr("(empty)") : sshAuthSock);
m_ui->sshAuthSockOverrideEdit->setText(sshAuthSockOverride);
+ auto sshSecurityKeyProvider = sshAgent()->securityKeyProvider(false);
+ auto sshSecurityKeyProviderOverride = sshAgent()->securityKeyProviderOverride();
+ m_ui->sshSecurityKeyProviderLabel->setText(sshSecurityKeyProvider.isEmpty() ? tr("(empty)")
+ : sshSecurityKeyProvider);
+ m_ui->sshSecurityKeyProviderOverrideEdit->setText(sshSecurityKeyProviderOverride);
#endif
m_ui->sshAuthSockMessageWidget->setVisible(sshAgentEnabled);
@@ -85,6 +90,8 @@ void AgentSettingsWidget::saveSettings()
{
auto sshAuthSockOverride = m_ui->sshAuthSockOverrideEdit->text();
sshAgent()->setAuthSockOverride(sshAuthSockOverride);
+ auto sshSecurityKeyProviderOverride = m_ui->sshSecurityKeyProviderOverrideEdit->text();
+ sshAgent()->setSecurityKeyProviderOverride(sshSecurityKeyProviderOverride);
#ifdef Q_OS_WIN
sshAgent()->setUsePageant(m_ui->usePageantCheckBox->isChecked());
sshAgent()->setUseOpenSSH(m_ui->useOpenSSHCheckBox->isChecked());
diff --git a/src/sshagent/AgentSettingsWidget.ui b/src/sshagent/AgentSettingsWidget.ui
index b617d9a11..a961736e3 100644
--- a/src/sshagent/AgentSettingsWidget.ui
+++ b/src/sshagent/AgentSettingsWidget.ui
@@ -100,6 +100,16 @@
8
+ -
+
+
+ SSH_AUTH_SOCK override
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
-
@@ -110,15 +120,18 @@
- -
-
-
- SSH_AUTH_SOCK override
+
-
+
+
+ Qt::Vertical
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ 20
+ 40
+
-
+
-
@@ -139,17 +152,42 @@
-
-
-
- Qt::Vertical
+
+
+ SSH_SK_PROVIDER value
-
-
- 20
- 40
-
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
+
+
+ -
+
+
+
+ Monospace
+
+
+
+ (empty)
+
+
+ Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
+
+
+
+ -
+
+
+ SSH_SK_PROVIDER override
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
diff --git a/src/sshagent/OpenSSHKey.cpp b/src/sshagent/OpenSSHKey.cpp
index 2ff683214..e7a2a9b85 100644
--- a/src/sshagent/OpenSSHKey.cpp
+++ b/src/sshagent/OpenSSHKey.cpp
@@ -473,6 +473,8 @@ bool OpenSSHKey::readPublic(BinaryStream& stream)
{ "ecdsa-sha2-nistp384", {STR_PART, STR_PART} },
{ "ecdsa-sha2-nistp521", {STR_PART, STR_PART} },
{ "ssh-ed25519", {STR_PART} },
+ { "sk-ecdsa-sha2-nistp256@openssh.com", {STR_PART, STR_PART, STR_PART} },
+ { "sk-ssh-ed25519@openssh.com", {STR_PART, STR_PART} },
};
// clang-format on
@@ -502,6 +504,8 @@ bool OpenSSHKey::readPrivate(BinaryStream& stream)
{ "ecdsa-sha2-nistp384", {STR_PART, STR_PART, STR_PART} },
{ "ecdsa-sha2-nistp521", {STR_PART, STR_PART, STR_PART} },
{ "ssh-ed25519", {STR_PART, STR_PART} },
+ { "sk-ecdsa-sha2-nistp256@openssh.com", {STR_PART, STR_PART, STR_PART, UINT8_PART, STR_PART, STR_PART} },
+ { "sk-ssh-ed25519@openssh.com", {STR_PART, STR_PART, UINT8_PART, STR_PART, STR_PART} },
};
// clang-format on
diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp
index abee23c39..2aff5f4cb 100644
--- a/src/sshagent/SSHAgent.cpp
+++ b/src/sshagent/SSHAgent.cpp
@@ -61,11 +61,21 @@ QString SSHAgent::authSockOverride() const
return config()->get(Config::SSHAgent_AuthSockOverride).toString();
}
+QString SSHAgent::securityKeyProviderOverride() const
+{
+ return config()->get(Config::SSHAgent_SecurityKeyProviderOverride).toString();
+}
+
void SSHAgent::setAuthSockOverride(QString& authSockOverride)
{
config()->set(Config::SSHAgent_AuthSockOverride, authSockOverride);
}
+void SSHAgent::setSecurityKeyProviderOverride(QString& securityKeyProviderOverride)
+{
+ config()->set(Config::SSHAgent_SecurityKeyProviderOverride, securityKeyProviderOverride);
+}
+
#ifdef Q_OS_WIN
bool SSHAgent::useOpenSSH() const
{
@@ -109,6 +119,21 @@ QString SSHAgent::socketPath(bool allowOverride) const
return socketPath;
}
+QString SSHAgent::securityKeyProvider(bool allowOverride) const
+{
+ QString skProvider;
+
+ if (allowOverride) {
+ skProvider = securityKeyProviderOverride();
+ }
+
+ if (skProvider.isEmpty()) {
+ skProvider = QProcessEnvironment::systemEnvironment().value("SSH_SK_PROVIDER", "internal");
+ }
+
+ return skProvider;
+}
+
const QString SSHAgent::errorString() const
{
return m_error;
@@ -257,10 +282,12 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co
QByteArray requestData;
BinaryStream request(&requestData);
+ bool isSecurityKey = key.type().startsWith("sk-");
- request.write((settings.useLifetimeConstraintWhenAdding() || settings.useConfirmConstraintWhenAdding())
- ? SSH_AGENTC_ADD_ID_CONSTRAINED
- : SSH_AGENTC_ADD_IDENTITY);
+ request.write(
+ (settings.useLifetimeConstraintWhenAdding() || settings.useConfirmConstraintWhenAdding() || isSecurityKey)
+ ? SSH_AGENTC_ADD_ID_CONSTRAINED
+ : SSH_AGENTC_ADD_IDENTITY);
key.writePrivate(request);
if (settings.useLifetimeConstraintWhenAdding()) {
@@ -272,6 +299,12 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co
request.write(SSH_AGENT_CONSTRAIN_CONFIRM);
}
+ if (isSecurityKey) {
+ request.write(SSH_AGENT_CONSTRAIN_EXTENSION);
+ request.writeString(QString("sk-provider@openssh.com"));
+ request.writeString(securityKeyProvider());
+ }
+
QByteArray responseData;
if (!sendMessage(requestData, responseData)) {
return false;
@@ -289,6 +322,11 @@ 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 (isSecurityKey) {
+ m_error +=
+ "\n" + tr("Security keys are not supported by the agent or the security key provider is unavailable.");
+ }
+
return false;
}
diff --git a/src/sshagent/SSHAgent.h b/src/sshagent/SSHAgent.h
index 3b5e9bdf2..032438613 100644
--- a/src/sshagent/SSHAgent.h
+++ b/src/sshagent/SSHAgent.h
@@ -37,8 +37,11 @@ public:
bool isEnabled() const;
void setEnabled(bool enabled);
QString socketPath(bool allowOverride = true) const;
+ QString securityKeyProvider(bool allowOverride = true) const;
QString authSockOverride() const;
+ QString securityKeyProviderOverride() const;
void setAuthSockOverride(QString& authSockOverride);
+ void setSecurityKeyProviderOverride(QString& securityKeyProviderOverride);
#ifdef Q_OS_WIN
bool useOpenSSH() const;
bool usePageant() const;
@@ -74,6 +77,7 @@ private:
const quint8 SSH_AGENT_CONSTRAIN_LIFETIME = 1;
const quint8 SSH_AGENT_CONSTRAIN_CONFIRM = 2;
+ const quint8 SSH_AGENT_CONSTRAIN_EXTENSION = 255;
bool sendMessage(const QByteArray& in, QByteArray& out);
bool sendMessageOpenSSH(const QByteArray& in, QByteArray& out);
diff --git a/tests/TestOpenSSHKey.cpp b/tests/TestOpenSSHKey.cpp
index d63471ba0..1f0053474 100644
--- a/tests/TestOpenSSHKey.cpp
+++ b/tests/TestOpenSSHKey.cpp
@@ -509,3 +509,52 @@ void TestOpenSSHKey::testDecryptUTF8()
QCOMPARE(key.type(), QString("ssh-ed25519"));
QCOMPARE(key.comment(), QString("opensshkey-test-utf8@keepassxc"));
}
+
+void TestOpenSSHKey::testParseECDSASecurityKey()
+{
+ const QString keyString = QString("-----BEGIN OPENSSH PRIVATE KEY-----\n"
+ "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAfwAAACJzay1lY2\n"
+ "RzYS1zaGEyLW5pc3RwMjU2QG9wZW5zc2guY29tAAAACG5pc3RwMjU2AAAAQQQ2Pr1d6zUa\n"
+ "qcmYgjTGQUF9QPkFEo2Q7aQbvyL/0KL9FObuOfzqxs8mDqswXEsXR4g5L6P7vEe6nPqzSW\n"
+ "X9/jJfAAAABHNzaDoAAAD4kyJ795Mie/cAAAAic2stZWNkc2Etc2hhMi1uaXN0cDI1NkBv\n"
+ "cGVuc3NoLmNvbQAAAAhuaXN0cDI1NgAAAEEENj69Xes1GqnJmII0xkFBfUD5BRKNkO2kG7\n"
+ "8i/9Ci/RTm7jn86sbPJg6rMFxLF0eIOS+j+7xHupz6s0ll/f4yXwAAAARzc2g6AQAAAEA4\n"
+ "Dbqd2ub7R1QQRm8nBZWDGJSiNIh58vvJ4EuAh0FnJsRvvASsSDiGuuXqh56wT5xmlnYvbb\n"
+ "nLWO4/1+Mp5PaDAAAAAAAAACJvcGVuc3Noa2V5LXRlc3QtZWNkc2Etc2tAa2VlcGFzc3hj\n"
+ "AQI=\n"
+ "-----END OPENSSH PRIVATE KEY-----\n");
+
+ const QByteArray keyData = keyString.toLatin1();
+
+ OpenSSHKey key;
+ QVERIFY(key.parsePKCS1PEM(keyData));
+ QVERIFY(!key.encrypted());
+ QCOMPARE(key.cipherName(), QString("none"));
+ QCOMPARE(key.type(), QString("sk-ecdsa-sha2-nistp256@openssh.com"));
+ QCOMPARE(key.comment(), QString("opensshkey-test-ecdsa-sk@keepassxc"));
+ QCOMPARE(key.fingerprint(), QString("SHA256:ctOtAsPMqbtumGI41o2oeWfGDah4m1ACILRj+x0gx0E"));
+}
+
+void TestOpenSSHKey::testParseED25519SecurityKey()
+{
+ const QString keyString = QString("-----BEGIN OPENSSH PRIVATE KEY-----\n"
+ "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAABpzay1zc2\n"
+ "gtZWQyNTUxOUBvcGVuc3NoLmNvbQAAACCSIfzsjUBlhsVBfHHlQCUpj1Yt+404RetvfTnd\n"
+ "DJIIqgAAAARzc2g6AAABCN1MUOzdTFDsAAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY2\n"
+ "9tAAAAIJIh/OyNQGWGxUF8ceVAJSmPVi37jThF6299Od0MkgiqAAAABHNzaDoBAAAAgF+0\n"
+ "UB3uNf48T/u9eSHmhfTfqgZZZxQ81UQmlw9Xw1eNZ2F+y+JwbQYK3gLMxro2cv2PHgYqIW\n"
+ "MAHFxdJjUn62D88bywmHaFT7ftu8/4bh38G+aQsmTFW38li97FiLz+Ytz0X9oSCo1jerkC\n"
+ "fYe8pcZZ7zWWSMzRnZKP11QMEkEQAAAAAAAAACRvcGVuc3Noa2V5LXRlc3QtZWQyNTUxOS\n"
+ "1za0BrZWVwYXNzeGMBAgMEBQ==\n"
+ "-----END OPENSSH PRIVATE KEY-----\n");
+
+ const QByteArray keyData = keyString.toLatin1();
+
+ OpenSSHKey key;
+ QVERIFY(key.parsePKCS1PEM(keyData));
+ QVERIFY(!key.encrypted());
+ QCOMPARE(key.cipherName(), QString("none"));
+ QCOMPARE(key.type(), QString("sk-ssh-ed25519@openssh.com"));
+ QCOMPARE(key.comment(), QString("opensshkey-test-ed25519-sk@keepassxc"));
+ QCOMPARE(key.fingerprint(), QString("SHA256:PGtS5WvbnYmNqFIeRbzO6cVP9GLh8eEzENgkHp02XIA"));
+}
diff --git a/tests/TestOpenSSHKey.h b/tests/TestOpenSSHKey.h
index bc3e8f383..b80ff919c 100644
--- a/tests/TestOpenSSHKey.h
+++ b/tests/TestOpenSSHKey.h
@@ -41,6 +41,8 @@ private slots:
void testDecryptOpenSSHAES256CTR();
void testDecryptRSAAES256CTR();
void testDecryptUTF8();
+ void testParseECDSASecurityKey();
+ void testParseED25519SecurityKey();
};
#endif // TESTOPENSSHKEY_H