SSH Agent: Add support for OpenSSH 8.2 FIDO/U2F keys

Closes #4334
This commit is contained in:
Toni Spets 2020-02-16 14:44:18 +02:00 committed by Jonathan White
parent c07a57d141
commit 860fcfd78d
10 changed files with 183 additions and 19 deletions

View File

@ -120,6 +120,14 @@
<source>Use OpenSSH</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>SSH_SK_PROVIDER value</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>SSH_SK_PROVIDER override</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ApplicationSettingsWidget</name>
@ -5472,6 +5480,14 @@ We recommend you use the AppImage available on our downloads page.</source>
<source>Decryption failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unexpected EOF while reading key</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unsupported key part</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PasswordEdit</name>
@ -7624,6 +7640,10 @@ Please consider generating a new key file.</source>
<source>No agent running, cannot list identities.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Security keys are not supported by the agent or the security key provider is unavailable.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SearchHelpWidget</name>

View File

@ -170,6 +170,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> 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}},

View File

@ -148,6 +148,7 @@ public:
SSHAgent_UseOpenSSH,
SSHAgent_UsePageant,
SSHAgent_AuthSockOverride,
SSHAgent_SecurityKeyProviderOverride,
FdoSecrets_Enabled,
FdoSecrets_ShowNotification,

View File

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

View File

@ -100,6 +100,16 @@
<property name="verticalSpacing">
<number>8</number>
</property>
<item row="1" column="0">
<widget class="QLabel" name="sshAuthSockOverrideLabel">
<property name="text">
<string>SSH_AUTH_SOCK override</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="sshAuthSockValueLabel">
<property name="text">
@ -110,15 +120,18 @@
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="sshAuthSockOverrideLabel">
<property name="text">
<string>SSH_AUTH_SOCK override</string>
<item row="4" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</widget>
</spacer>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="sshAuthSockOverrideEdit"/>
@ -139,17 +152,42 @@
</widget>
</item>
<item row="2" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
<widget class="QLabel" name="sshSecurityKeyProviderValueLabel">
<property name="text">
<string>SSH_SK_PROVIDER value</string>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</spacer>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="sshSecurityKeyProviderLabel">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="text">
<string>(empty)</string>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="sshSecurityKeyProviderOverrideLabel">
<property name="text">
<string>SSH_SK_PROVIDER override</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="sshSecurityKeyProviderOverrideEdit"/>
</item>
</layout>
</item>

View File

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

View File

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

View File

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

View File

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

View File

@ -41,6 +41,8 @@ private slots:
void testDecryptOpenSSHAES256CTR();
void testDecryptRSAAES256CTR();
void testDecryptUTF8();
void testParseECDSASecurityKey();
void testParseED25519SecurityKey();
};
#endif // TESTOPENSSHKEY_H