diff --git a/src/sshagent/AgentSettingsWidget.cpp b/src/sshagent/AgentSettingsWidget.cpp index e06929195..f7d85ce77 100644 --- a/src/sshagent/AgentSettingsWidget.cpp +++ b/src/sshagent/AgentSettingsWidget.cpp @@ -68,7 +68,8 @@ void AgentSettingsWidget::loadSettings() return; } #endif - if (sshAgent()->testConnection()) { + QList> keys; + if (sshAgent()->listIdentities(keys)) { m_ui->sshAuthSockMessageWidget->showMessage(tr("SSH Agent connection is working!"), MessageWidget::Positive); } else { diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index 3bd3c8df3..6bed354f2 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -212,36 +212,6 @@ bool SSHAgent::sendMessagePageant(const QByteArray& in, QByteArray& out) } #endif -/** - * Test if connection to SSH agent is working. - * - * @return true on success - */ -bool SSHAgent::testConnection() -{ - if (!isAgentRunning()) { - m_error = tr("No agent running, cannot test connection."); - return false; - } - - QByteArray requestData; - BinaryStream request(&requestData); - - request.write(SSH_AGENTC_REQUEST_IDENTITIES); - - QByteArray responseData; - if (!sendMessage(requestData, responseData)) { - return false; - } - - if (responseData.length() < 1 || static_cast(responseData[0]) != SSH_AGENT_IDENTITIES_ANSWER) { - m_error = tr("Agent protocol error."); - return false; - } - - return true; -} - /** * Add the identity to the SSH agent. * @@ -328,6 +298,99 @@ bool SSHAgent::removeIdentity(OpenSSHKey& key) return sendMessage(requestData, responseData); } +/** + * Get a list of identities from the SSH agent. + * + * @param list list of keys to append + * @return true on success + */ +bool SSHAgent::listIdentities(QList>& list) +{ + if (!isAgentRunning()) { + m_error = tr("No agent running, cannot list identities."); + return false; + } + + QByteArray requestData; + BinaryStream request(&requestData); + + request.write(SSH_AGENTC_REQUEST_IDENTITIES); + + QByteArray responseData; + if (!sendMessage(requestData, responseData)) { + return false; + } + + BinaryStream response(&responseData); + + quint8 responseType; + if (!response.read(responseType) || responseType != SSH_AGENT_IDENTITIES_ANSWER) { + m_error = tr("Agent protocol error."); + return false; + } + + quint32 nKeys; + if (!response.read(nKeys)) { + m_error = tr("Agent protocol error."); + return false; + } + + for (quint32 i = 0; i < nKeys; i++) { + QByteArray publicData; + QString comment; + + if (!response.readString(publicData)) { + m_error = tr("Agent protocol error."); + return false; + } + + if (!response.readString(comment)) { + m_error = tr("Agent protocol error."); + return false; + } + + OpenSSHKey* key = new OpenSSHKey(); + key->setComment(comment); + + list.append(QSharedPointer(key)); + + BinaryStream publicDataStream(&publicData); + if (!key->readPublic(publicDataStream)) { + m_error = key->errorString(); + return false; + } + } + + return true; +} + +/** + * Check if this identity is loaded in the SSH Agent. + * + * @param key identity to remove + * @param loaded is the key laoded + * @return true on success + */ +bool SSHAgent::checkIdentity(OpenSSHKey& key, bool& loaded) +{ + QList> list; + + if (!listIdentities(list)) { + return false; + } + + loaded = false; + + for (const auto it : list) { + if (*it == key) { + loaded = true; + break; + } + } + + return true; +} + /** * Remove all identities known to this instance */ diff --git a/src/sshagent/SSHAgent.h b/src/sshagent/SSHAgent.h index a70af44c8..44ae88bb4 100644 --- a/src/sshagent/SSHAgent.h +++ b/src/sshagent/SSHAgent.h @@ -47,8 +47,9 @@ public: const QString errorString() const; bool isAgentRunning() const; - bool testConnection(); bool addIdentity(OpenSSHKey& key, KeeAgentSettings& settings); + bool listIdentities(QList>& list); + bool checkIdentity(OpenSSHKey& key, bool& loaded); bool removeIdentity(OpenSSHKey& key); void removeAllIdentities(); void setAutoRemoveOnLock(const OpenSSHKey& key, bool autoRemove); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c3f1c0e22..96cb5267c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -165,6 +165,10 @@ endif() if(WITH_XC_CRYPTO_SSH) add_unit_test(NAME testopensshkey SOURCES TestOpenSSHKey.cpp LIBS ${TEST_LIBRARIES}) + if(NOT WIN32) + add_unit_test(NAME testsshagent SOURCES TestSSHAgent.cpp + LIBS ${TEST_LIBRARIES}) + endif() endif() add_unit_test(NAME testentry SOURCES TestEntry.cpp diff --git a/tests/TestSSHAgent.cpp b/tests/TestSSHAgent.cpp new file mode 100644 index 000000000..4a13d64f8 --- /dev/null +++ b/tests/TestSSHAgent.cpp @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2020 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "TestSSHAgent.h" +#include "TestGlobal.h" +#include "core/Config.h" +#include "crypto/Crypto.h" +#include "sshagent/SSHAgent.h" + +QTEST_GUILESS_MAIN(TestSSHAgent) + +void TestSSHAgent::initTestCase() +{ + QVERIFY(Crypto::init()); + Config::createTempFileInstance(); + + m_agentSocketFile.setAutoRemove(true); + QVERIFY(m_agentSocketFile.open()); + + m_agentSocketFileName = m_agentSocketFile.fileName(); + QVERIFY(!m_agentSocketFileName.isEmpty()); + + // let ssh-agent re-create it as a socket + QVERIFY(m_agentSocketFile.remove()); + + QStringList arguments; + arguments << "-D" + << "-a" << m_agentSocketFileName; + + QElapsedTimer timer; + timer.start(); + + qDebug() << "ssh-agent starting with arguments" << arguments; + m_agentProcess.setProcessChannelMode(QProcess::ForwardedChannels); + m_agentProcess.start("ssh-agent", arguments); + m_agentProcess.closeWriteChannel(); + + if (!m_agentProcess.waitForStarted()) { + QSKIP("ssh-agent could not be started"); + } + + qDebug() << "ssh-agent started as pid" << m_agentProcess.pid(); + + // we need to wait for the agent to open the socket before going into real tests + QFileInfo socketFileInfo(m_agentSocketFileName); + while (!timer.hasExpired(2000)) { + if (socketFileInfo.exists()) { + break; + } + QTest::qWait(10); + } + + QVERIFY(socketFileInfo.exists()); + qDebug() << "ssh-agent initialized in" << timer.elapsed() << "ms"; + + // initialize test key + const QString keyString = QString("-----BEGIN OPENSSH PRIVATE KEY-----\n" + "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n" + "QyNTUxOQAAACDdlO5F2kF2WzedrBAHBi9wBHeISzXZ0IuIqrp0EzeazAAAAKjgCfj94An4\n" + "/QAAAAtzc2gtZWQyNTUxOQAAACDdlO5F2kF2WzedrBAHBi9wBHeISzXZ0IuIqrp0EzeazA\n" + "AAAEBe1iilZFho8ZGAliiSj5URvFtGrgvmnEKdiLZow5hOR92U7kXaQXZbN52sEAcGL3AE\n" + "d4hLNdnQi4iqunQTN5rMAAAAH29wZW5zc2hrZXktdGVzdC1wYXJzZUBrZWVwYXNzeGMBAg\n" + "MEBQY=\n" + "-----END OPENSSH PRIVATE KEY-----\n"); + + const QByteArray keyData = keyString.toLatin1(); + + QVERIFY(m_key.parsePKCS1PEM(keyData)); +} + +void TestSSHAgent::testConfiguration() +{ + SSHAgent agent; + + // default config must not enable agent + QVERIFY(!agent.isEnabled()); + + agent.setEnabled(true); + QVERIFY(agent.isEnabled()); + + // this will either be an empty string or the real ssh-agent socket path, doesn't matter + QString defaultSocketPath = agent.socketPath(false); + + // overridden path must match default before setting an override + QCOMPARE(agent.socketPath(true), defaultSocketPath); + + agent.setAuthSockOverride(m_agentSocketFileName); + + // overridden path must match what we set + QCOMPARE(agent.socketPath(true), m_agentSocketFileName); + + // non-overridden path must match the default + QCOMPARE(agent.socketPath(false), defaultSocketPath); +} + +void TestSSHAgent::testIdentity() +{ + SSHAgent agent; + agent.setEnabled(true); + agent.setAuthSockOverride(m_agentSocketFileName); + + QVERIFY(agent.isAgentRunning()); + + KeeAgentSettings settings; + bool keyInAgent; + + // test adding a key works + QVERIFY(agent.addIdentity(m_key, settings)); + QVERIFY(agent.checkIdentity(m_key, keyInAgent) && keyInAgent); + + // test removing a key works + QVERIFY(agent.removeIdentity(m_key)); + QVERIFY(agent.checkIdentity(m_key, keyInAgent) && !keyInAgent); +} + +void TestSSHAgent::testRemoveOnClose() +{ + SSHAgent agent; + agent.setEnabled(true); + agent.setAuthSockOverride(m_agentSocketFileName); + + QVERIFY(agent.isAgentRunning()); + + KeeAgentSettings settings; + bool keyInAgent; + + settings.setRemoveAtDatabaseClose(true); + QVERIFY(agent.addIdentity(m_key, settings)); + QVERIFY(agent.checkIdentity(m_key, keyInAgent) && keyInAgent); + agent.setEnabled(false); + QVERIFY(agent.checkIdentity(m_key, keyInAgent) && !keyInAgent); +} + +void TestSSHAgent::testLifetimeConstraint() +{ + SSHAgent agent; + agent.setEnabled(true); + agent.setAuthSockOverride(m_agentSocketFileName); + + QVERIFY(agent.isAgentRunning()); + + KeeAgentSettings settings; + bool keyInAgent; + + settings.setUseLifetimeConstraintWhenAdding(true); + settings.setLifetimeConstraintDuration(2); // two seconds + + // identity should be in agent immediately after adding + QVERIFY(agent.addIdentity(m_key, settings)); + QVERIFY(agent.checkIdentity(m_key, keyInAgent) && keyInAgent); + + QElapsedTimer timer; + timer.start(); + + // wait for the identity to time out + while (!timer.hasExpired(5000)) { + QVERIFY(agent.checkIdentity(m_key, keyInAgent)); + + if (!keyInAgent) { + break; + } + + QTest::qWait(100); + } + + QVERIFY(!keyInAgent); +} + +void TestSSHAgent::testConfirmConstraint() +{ + SSHAgent agent; + agent.setEnabled(true); + agent.setAuthSockOverride(m_agentSocketFileName); + + QVERIFY(agent.isAgentRunning()); + + KeeAgentSettings settings; + bool keyInAgent; + + settings.setUseConfirmConstraintWhenAdding(true); + + QVERIFY(agent.addIdentity(m_key, settings)); + + // we can't test confirmation 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::cleanupTestCase() +{ + if (m_agentProcess.state() != QProcess::NotRunning) { + qDebug() << "Killing ssh-agent pid" << m_agentProcess.pid(); + m_agentProcess.terminate(); + m_agentProcess.waitForFinished(); + } + + m_agentSocketFile.remove(); +} diff --git a/tests/TestSSHAgent.h b/tests/TestSSHAgent.h new file mode 100644 index 000000000..6b99e8e65 --- /dev/null +++ b/tests/TestSSHAgent.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2020 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TESTSSHAGENT_H +#define TESTSSHAGENT_H + +#include "crypto/ssh/OpenSSHKey.h" +#include +#include +#include + +class TestSSHAgent : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void testConfiguration(); + void testIdentity(); + void testRemoveOnClose(); + void testLifetimeConstraint(); + void testConfirmConstraint(); + void cleanupTestCase(); + +private: + QTemporaryFile m_agentSocketFile; + QString m_agentSocketFileName; + QProcess m_agentProcess; + OpenSSHKey m_key; +}; + +#endif // TESTSSHAGENT_H