Commit 62a83f91 authored by Christian Kandeler's avatar Christian Kandeler
Browse files

SSH: implement host key checking.



Change-Id: I5f10bd801bb5cf43e58193c41e62d9ea2f9cb645
Task-number: QTCREATORBUG-13339
Reviewed-by: default avatarJoerg Bornemann <joerg.bornemann@theqtcompany.com>
parent be4a0306
......@@ -28,7 +28,8 @@ SOURCES = $$PWD/sshsendfacility.cpp \
$$PWD/sftpfilesystemmodel.cpp \
$$PWD/sshkeycreationdialog.cpp \
$$PWD/sshinit.cpp \
$$PWD/sshdirecttcpiptunnel.cpp
$$PWD/sshdirecttcpiptunnel.cpp \
$$PWD/sshhostkeydatabase.cpp
HEADERS = $$PWD/sshsendfacility_p.h \
$$PWD/sshremoteprocess.h \
......@@ -64,7 +65,8 @@ HEADERS = $$PWD/sshsendfacility_p.h \
$$PWD/ssh_global.h \
$$PWD/sshdirecttcpiptunnel_p.h \
$$PWD/sshinit_p.h \
$$PWD/sshdirecttcpiptunnel.h
$$PWD/sshdirecttcpiptunnel.h \
$$PWD/sshhostkeydatabase.h
FORMS = $$PWD/sshkeycreationdialog.ui
......
......@@ -27,6 +27,8 @@ QtcLibrary {
"sshdirecttcpiptunnel.h", "sshdirecttcpiptunnel_p.h", "sshdirecttcpiptunnel.cpp",
"ssherrors.h",
"sshexception_p.h",
"sshhostkeydatabase.cpp",
"sshhostkeydatabase.h",
"sshincomingpacket_p.h", "sshincomingpacket.cpp",
"sshinit_p.h", "sshinit.cpp",
"sshkeycreationdialog.cpp", "sshkeycreationdialog.h", "sshkeycreationdialog.ui",
......
......@@ -65,7 +65,8 @@ namespace QSsh {
const QByteArray ClientId("SSH-2.0-QtCreator\r\n");
SshConnectionParameters::SshConnectionParameters() :
timeout(0), authenticationType(AuthenticationTypePublicKey), port(0)
timeout(0), authenticationType(AuthenticationTypePublicKey), port(0),
hostKeyCheckingMode(SshHostKeyCheckingNone)
{
options |= SshIgnoreDefaultProxy;
options |= SshEnableStrictConformanceChecks;
......@@ -77,6 +78,7 @@ static inline bool equals(const SshConnectionParameters &p1, const SshConnection
&& p1.authenticationType == p2.authenticationType
&& (p1.authenticationType == SshConnectionParameters::AuthenticationTypePassword ?
p1.password == p2.password : p1.privateKeyFile == p2.privateKeyFile)
&& p1.hostKeyCheckingMode == p2.hostKeyCheckingMode
&& p1.timeout == p2.timeout && p1.port == p2.port;
}
......@@ -90,7 +92,6 @@ bool operator!=(const SshConnectionParameters &p1, const SshConnectionParameters
return !equals(p1, p2);
}
// TODO: Mechanism for checking the host key. First connection to host: save, later: compare
SshConnection::SshConnection(const SshConnectionParameters &serverInfo, QObject *parent)
: QObject(parent)
......@@ -411,7 +412,7 @@ void SshConnectionPrivate::handleServerId()
}
}
m_keyExchange.reset(new SshKeyExchange(m_sendFacility));
m_keyExchange.reset(new SshKeyExchange(m_connParams, m_sendFacility));
m_keyExchange->sendKexInitPacket(m_serverId);
m_keyExchangeState = KexInitSent;
}
......@@ -460,7 +461,7 @@ void SshConnectionPrivate::handleKeyExchangeInitPacket()
// Server-initiated re-exchange.
if (m_keyExchangeState == NoKeyExchange) {
m_keyExchange.reset(new SshKeyExchange(m_sendFacility));
m_keyExchange.reset(new SshKeyExchange(m_connParams, m_sendFacility));
m_keyExchange->sendKexInitPacket(m_serverId);
}
......
......@@ -32,6 +32,7 @@
#define SSHCONNECTION_H
#include "ssherrors.h"
#include "sshhostkeydatabase.h"
#include "ssh_global.h"
......@@ -56,6 +57,13 @@ enum SshConnectionOption {
Q_DECLARE_FLAGS(SshConnectionOptions, SshConnectionOption)
enum SshHostKeyCheckingMode {
SshHostKeyCheckingNone,
SshHostKeyCheckingStrict,
SshHostKeyCheckingAllowNoMatch,
SshHostKeyCheckingAllowMismatch
};
class QSSH_EXPORT SshConnectionParameters
{
public:
......@@ -78,6 +86,8 @@ public:
AuthenticationType authenticationType;
quint16 port;
SshConnectionOptions options;
SshHostKeyCheckingMode hostKeyCheckingMode;
SshHostKeyDatabasePtr hostKeyDatabase;
};
QSSH_EXPORT bool operator==(const SshConnectionParameters &p1, const SshConnectionParameters &p2);
......
/****************************************************************************
**
** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia. For licensing terms and
** conditions see http://www.qt.io/licensing. For further information
** use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights. These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
****************************************************************************/
#include "sshhostkeydatabase.h"
#include <QByteArray>
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QHash>
#include <QString>
namespace QSsh {
class SshHostKeyDatabase::SshHostKeyDatabasePrivate
{
public:
QHash<QString, QByteArray> hostKeys;
};
SshHostKeyDatabase::SshHostKeyDatabase() : d(new SshHostKeyDatabasePrivate)
{
}
SshHostKeyDatabase::~SshHostKeyDatabase()
{
delete d;
}
bool SshHostKeyDatabase::load(const QString &filePath, QString *error)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
if (error) {
*error = QCoreApplication::translate("QSsh::Ssh",
"Failed to open key file \"%1\" for reading: %2")
.arg(QDir::toNativeSeparators(filePath), file.errorString());
}
return false;
}
d->hostKeys.clear();
const QByteArray content = file.readAll().trimmed();
if (content.isEmpty())
return true;
foreach (const QByteArray &line, content.split('\n')) {
const QList<QByteArray> &lineData = line.trimmed().split(' ');
if (lineData.count() != 2) {
qDebug("Unexpected line \"%s\" in file \"%s\".", line.constData(),
qPrintable(filePath));
continue;
}
d->hostKeys.insert(QString::fromUtf8(lineData.first()),
QByteArray::fromHex(lineData.last()));
}
return true;
}
bool SshHostKeyDatabase::store(const QString &filePath, QString *error) const
{
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly)) {
if (error) {
*error = QCoreApplication::translate("QSsh::Ssh",
"Failed to open key file \"%1\" for writing: %2")
.arg(QDir::toNativeSeparators(filePath), file.errorString());
}
return false;
}
file.resize(0);
for (auto it = d->hostKeys.constBegin(); it != d->hostKeys.constEnd(); ++it)
file.write(it.key().toUtf8() + ' ' + it.value().toHex() + '\n');
return true;
}
SshHostKeyDatabase::KeyLookupResult SshHostKeyDatabase::matchHostKey(const QString &hostName,
const QByteArray &key) const
{
auto it = d->hostKeys.find(hostName);
if (it == d->hostKeys.constEnd())
return KeyLookupNoMatch;
if (it.value() == key)
return KeyLookupMatch;
return KeyLookupMismatch;
}
void SshHostKeyDatabase::insertHostKey(const QString &hostName, const QByteArray &key)
{
d->hostKeys.insert(hostName, key);
}
} // namespace QSsh
/****************************************************************************
**
** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia. For licensing terms and
** conditions see http://www.qt.io/licensing. For further information
** use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights. These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
****************************************************************************/
#ifndef SSHHOSTKEYDATABASE_H
#define SSHHOSTKEYDATABASE_H
#include "ssh_global.h"
#include <QSharedPointer>
QT_BEGIN_NAMESPACE
class QByteArray;
class QString;
QT_END_NAMESPACE
namespace QSsh {
class SshHostKeyDatabase;
typedef QSharedPointer<SshHostKeyDatabase> SshHostKeyDatabasePtr;
class QSSH_EXPORT SshHostKeyDatabase
{
friend class QSharedPointer<SshHostKeyDatabase>; // To give create() access to our constructor.
public:
enum KeyLookupResult {
KeyLookupMatch,
KeyLookupNoMatch,
KeyLookupMismatch
};
~SshHostKeyDatabase();
bool load(const QString &filePath, QString *error = 0);
bool store(const QString &filePath, QString *error = 0) const;
KeyLookupResult matchHostKey(const QString &hostName, const QByteArray &key) const;
void insertHostKey(const QString &hostName, const QByteArray &key);
private:
SshHostKeyDatabase();
class SshHostKeyDatabasePrivate;
SshHostKeyDatabasePrivate * const d;
};
} // namespace QSsh
#endif // Include guard.
......@@ -76,8 +76,9 @@ namespace {
} // anonymous namespace
SshKeyExchange::SshKeyExchange(SshSendFacility &sendFacility)
: m_sendFacility(sendFacility)
SshKeyExchange::SshKeyExchange(const SshConnectionParameters &connParams,
SshSendFacility &sendFacility)
: m_connParams(connParams), m_sendFacility(sendFacility)
{
}
......@@ -210,8 +211,46 @@ void SshKeyExchange::sendNewKeysPacket(const SshIncomingPacket &dhReply,
"Invalid signature in SSH_MSG_KEXDH_REPLY packet.");
}
checkHostKey(reply.k_s);
m_sendFacility.sendNewKeysPacket();
}
void SshKeyExchange::checkHostKey(const QByteArray &hostKey)
{
if (m_connParams.hostKeyCheckingMode == SshHostKeyCheckingNone) {
if (m_connParams.hostKeyDatabase)
m_connParams.hostKeyDatabase->insertHostKey(m_connParams.host, hostKey);
return;
}
if (!m_connParams.hostKeyDatabase) {
throw SshClientException(SshInternalError,
SSH_TR("Host key database must exist "
"if host key checking is enabled."));
}
switch (m_connParams.hostKeyDatabase->matchHostKey(m_connParams.host, hostKey)) {
case SshHostKeyDatabase::KeyLookupMatch:
return; // Nothing to do.
case SshHostKeyDatabase::KeyLookupMismatch:
if (m_connParams.hostKeyCheckingMode != SshHostKeyCheckingAllowMismatch)
throwHostKeyException();
break;
case SshHostKeyDatabase::KeyLookupNoMatch:
if (m_connParams.hostKeyCheckingMode == SshHostKeyCheckingStrict)
throwHostKeyException();
break;
}
m_connParams.hostKeyDatabase->insertHostKey(m_connParams.host, hostKey);
}
void SshKeyExchange::throwHostKeyException()
{
throw SshServerException(SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE, "Host key changed",
SSH_TR("Host key of machine \"%1\" has changed.")
.arg(m_connParams.host));
}
} // namespace Internal
} // namespace QSsh
......@@ -31,6 +31,8 @@
#ifndef SSHKEYEXCHANGE_P_H
#define SSHKEYEXCHANGE_P_H
#include "sshconnection.h"
#include <QByteArray>
#include <QScopedPointer>
......@@ -48,7 +50,7 @@ class SshIncomingPacket;
class SshKeyExchange
{
public:
SshKeyExchange(SshSendFacility &sendFacility);
SshKeyExchange(const SshConnectionParameters &connParams, SshSendFacility &sendFacility);
~SshKeyExchange();
void sendKexInitPacket(const QByteArray &serverId);
......@@ -68,6 +70,9 @@ public:
QByteArray hMacAlgoServerToClient() const { return m_s2cHMacAlgo; }
private:
void checkHostKey(const QByteArray &hostKey);
Q_NORETURN void throwHostKeyException();
QByteArray m_serverId;
QByteArray m_clientKexInitPayload;
QByteArray m_serverKexInitPayload;
......@@ -80,6 +85,7 @@ private:
QByteArray m_c2sHMacAlgo;
QByteArray m_s2cHMacAlgo;
QScopedPointer<Botan::HashFunction> m_hash;
const SshConnectionParameters m_connParams;
SshSendFacility &m_sendFacility;
};
......
......@@ -32,9 +32,11 @@
#include "idevicefactory.h"
#include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h>
#include <extensionsystem/pluginmanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectexplorerconstants.h>
#include <ssh/sshhostkeydatabase.h>
#include <utils/qtcassert.h>
#include <utils/fileutils.h>
#include <utils/persistentsettings.h>
......@@ -75,6 +77,7 @@ public:
static DeviceManager *clonedInstance;
QList<IDevice::Ptr> devices;
QHash<Core::Id, Core::Id> defaultDevices;
QSsh::SshHostKeyDatabasePtr hostKeyDatabase;
Utils::PersistentSettingsWriter *writer;
};
......@@ -136,6 +139,7 @@ void DeviceManager::save()
QVariantMap data;
data.insert(QLatin1String(DeviceManagerKey), toMap());
d->writer->save(data, Core::ICore::mainWindow());
d->hostKeyDatabase->store(hostKeysFilePath());
}
void DeviceManager::load()
......@@ -308,6 +312,11 @@ bool DeviceManager::isLoaded() const
return d->writer;
}
QSsh::SshHostKeyDatabasePtr DeviceManager::hostKeyDatabase() const
{
return d->hostKeyDatabase;
}
void DeviceManager::setDefaultDevice(Core::Id id)
{
QTC_ASSERT(this != instance(), return);
......@@ -343,6 +352,13 @@ DeviceManager::DeviceManager(bool isInstance) : d(new DeviceManagerPrivate)
if (isInstance) {
QTC_ASSERT(!m_instance, return);
m_instance = this;
d->hostKeyDatabase = QSsh::SshHostKeyDatabasePtr::create();
const QString keyFilePath = hostKeysFilePath();
if (QFileInfo(keyFilePath).exists()) {
QString error;
if (!d->hostKeyDatabase->load(keyFilePath, &error))
Core::MessageManager::write(error);
}
connect(Core::ICore::instance(), SIGNAL(saveSettingsRequested()), SLOT(save()));
}
}
......@@ -415,6 +431,11 @@ IDevice::ConstPtr DeviceManager::fromRawPointer(const IDevice *device) const
return fromRawPointer(const_cast<IDevice *>(device));
}
QString DeviceManager::hostKeysFilePath()
{
return settingsFilePath(QLatin1String("/ssh-hostkeys")).toString();
}
} // namespace ProjectExplorer
......
......@@ -36,6 +36,7 @@
#include <QObject>
namespace QSsh { class SshHostKeyDatabase; }
namespace Utils { class FileName; }
namespace ProjectExplorer {
......@@ -105,6 +106,8 @@ private:
IDevice::Ptr fromRawPointer(IDevice *device) const;
IDevice::ConstPtr fromRawPointer(const IDevice *device) const;
static QString hostKeysFilePath();
QSharedPointer<QSsh::SshHostKeyDatabase> hostKeyDatabase() const;
static Utils::FileName settingsFilePath(const QString &extension);
static Utils::FileName systemSettingsFilePath(const QString &deviceFileRelativePath);
static void copy(const DeviceManager *source, DeviceManager *target, bool deep);
......
......@@ -123,6 +123,7 @@ const char AuthKey[] = "Authentication";
const char KeyFileKey[] = "KeyFile";
const char PasswordKey[] = "Password";
const char TimeoutKey[] = "Timeout";
const char HostKeyCheckingKey[] = "HostKeyChecking";
const char DebugServerKey[] = "DebugServerKey";
......@@ -161,7 +162,9 @@ PortsGatheringMethod::~PortsGatheringMethod() { }
DeviceTester::DeviceTester(QObject *parent) : QObject(parent) { }
IDevice::IDevice() : d(new Internal::IDevicePrivate)
{ }
{
d->sshParameters.hostKeyDatabase = DeviceManager::instance()->hostKeyDatabase();
}
IDevice::IDevice(Core::Id type, Origin origin, MachineType machineType, Core::Id id)
: d(new Internal::IDevicePrivate)
......@@ -171,6 +174,7 @@ IDevice::IDevice(Core::Id type, Origin origin, MachineType machineType, Core::Id
d->machineType = machineType;
QTC_CHECK(origin == ManuallyAdded || id.isValid());
d->id = id.isValid() ? id : newId();
d->sshParameters.hostKeyDatabase = DeviceManager::instance()->hostKeyDatabase();
}
IDevice::IDevice(const IDevice &other) : d(new Internal::IDevicePrivate)
......@@ -322,6 +326,8 @@ void IDevice::fromMap(const QVariantMap &map)
d->sshParameters.password = map.value(QLatin1String(PasswordKey)).toString();
d->sshParameters.privateKeyFile = map.value(QLatin1String(KeyFileKey), defaultPrivateKeyFilePath()).toString();
d->sshParameters.timeout = map.value(QLatin1String(TimeoutKey), DefaultTimeout).toInt();
d->sshParameters.hostKeyCheckingMode = static_cast<QSsh::SshHostKeyCheckingMode>
(map.value(QLatin1String(HostKeyCheckingKey), QSsh::SshHostKeyCheckingNone).toInt());
d->freePorts = Utils::PortList::fromString(map.value(QLatin1String(PortsSpecKey),
QLatin1String("10000-10100")).toString());
......@@ -353,6 +359,7 @@ QVariantMap IDevice::toMap() const
map.insert(QLatin1String(PasswordKey), d->sshParameters.password);
map.insert(QLatin1String(KeyFileKey), d->sshParameters.privateKeyFile);
map.insert(QLatin1String(TimeoutKey), d->sshParameters.timeout);
map.insert(QLatin1String(HostKeyCheckingKey), d->sshParameters.hostKeyCheckingMode);
map.insert(QLatin1String(PortsSpecKey), d->freePorts.toString());
map.insert(QLatin1String(VersionKey), d->version);
......@@ -392,6 +399,7 @@ QSsh::SshConnectionParameters IDevice::sshParameters() const
void IDevice::setSshParameters(const QSsh::SshConnectionParameters &sshParameters)
{
d->sshParameters = sshParameters;
d->sshParameters.hostKeyDatabase = DeviceManager::instance()->hostKeyDatabase();
}
QString IDevice::qmlProfilerHost() const
......
......@@ -63,6 +63,8 @@ GenericLinuxDeviceConfigurationWidget::GenericLinuxDeviceConfigurationWidget(
connect(m_ui->portsLineEdit, SIGNAL(editingFinished()), this, SLOT(handleFreePortsChanged()));
connect(m_ui->createKeyButton, SIGNAL(clicked()), SLOT(createNewKey()));
connect(m_ui->gdbServerLineEdit, SIGNAL(editingFinished()), SLOT(gdbServerEditingFinished()));
connect(m_ui->hostKeyCheckBox, &QCheckBox::toggled, this,
&GenericLinuxDeviceConfigurationWidget::hostKeyCheckingChanged);
initGui();
}
......@@ -158,6 +160,14 @@ void GenericLinuxDeviceConfigurationWidget::createNewKey()
setPrivateKey(dialog.privateKeyFilePath());
}
void GenericLinuxDeviceConfigurationWidget::hostKeyCheckingChanged(bool doCheck)
{
SshConnectionParameters sshParams = device()->sshParameters();
sshParams.hostKeyCheckingMode
= doCheck ? QSsh::SshHostKeyCheckingAllowNoMatch : QSsh::SshHostKeyCheckingNone;
device()->setSshParameters(sshParams);
}
void GenericLinuxDeviceConfigurationWidget::updateDeviceFromUi()
{
hostNameEditingFinished();
......@@ -200,6 +210,7 @@ void GenericLinuxDeviceConfigurationWidget::initGui()
m_ui->timeoutSpinBox->setValue(sshParams.timeout);
m_ui->hostLineEdit->setEnabled(!device()->isAutoDetected());
m_ui->sshPortSpinBox->setEnabled(!device()->isAutoDetected());
m_ui->hostKeyCheckBox->setChecked(sshParams.hostKeyCheckingMode != SshHostKeyCheckingNone);
m_ui->hostLineEdit->setText(sshParams.host);
m_ui->sshPortSpinBox->setValue(sshParams.port);
......
......@@ -62,6 +62,7 @@ private slots:
void handleFreePortsChanged();
void setPrivateKey(const QString &path);
void createNewKey();
void hostKeyCheckingChanged(bool doCheck);
private:
void updateDeviceFromUi();
......
......@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>393</width>
<height>321</height>
<width>556</width>
<height>309</height>
</rect>
</property>
<property name="windowTitle">
......@@ -123,6 +123,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="hostKeyCheckBox">
<property name="text">
<string>&amp;Check host key</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment