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