diff options
author | Christian Kandeler <christian.kandeler@theqtcompany.com> | 2016-07-20 18:04:56 +0200 |
---|---|---|
committer | Christian Kandeler <christian.kandeler@qt.io> | 2017-05-04 15:36:28 +0000 |
commit | 3ee2445fb143f3b9f8f7a7f80de202c1d811bb01 (patch) | |
tree | 236101db3d0fb0a0eec57d1f63eff1da9b5196d8 | |
parent | fcdc9342b554d30b5323aa026fd55659565ef71c (diff) | |
download | qt-creator-3ee2445fb143f3b9f8f7a7f80de202c1d811bb01.tar.gz |
SSH: Add support for ssh-agent
Task-number: QTCREATORBUG-16245
Change-Id: Ifd30c89d19e547d7657765790b7520e42b3741c3
Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
21 files changed, 748 insertions, 40 deletions
diff --git a/src/libs/ssh/ssh.pro b/src/libs/ssh/ssh.pro index defd894b35..27be1ec187 100644 --- a/src/libs/ssh/ssh.pro +++ b/src/libs/ssh/ssh.pro @@ -33,7 +33,8 @@ SOURCES = $$PWD/sshsendfacility.cpp \ $$PWD/sshhostkeydatabase.cpp \ $$PWD/sshtcpipforwardserver.cpp \ $$PWD/sshtcpiptunnel.cpp \ - $$PWD/sshforwardedtcpiptunnel.cpp + $$PWD/sshforwardedtcpiptunnel.cpp \ + $$PWD/sshagent.cpp HEADERS = $$PWD/sshsendfacility_p.h \ $$PWD/sshremoteprocess.h \ @@ -76,7 +77,8 @@ HEADERS = $$PWD/sshsendfacility_p.h \ $$PWD/sshtcpipforwardserver_p.h \ $$PWD/sshtcpiptunnel_p.h \ $$PWD/sshforwardedtcpiptunnel.h \ - $$PWD/sshforwardedtcpiptunnel_p.h + $$PWD/sshforwardedtcpiptunnel_p.h \ + $$PWD/sshagent_p.h FORMS = $$PWD/sshkeycreationdialog.ui diff --git a/src/libs/ssh/ssh.qbs b/src/libs/ssh/ssh.qbs index bc76933218..54dad5bf75 100644 --- a/src/libs/ssh/ssh.qbs +++ b/src/libs/ssh/ssh.qbs @@ -22,6 +22,7 @@ Project { "sftpoperation.cpp", "sftpoperation_p.h", "sftpoutgoingpacket.cpp", "sftpoutgoingpacket_p.h", "sftppacket.cpp", "sftppacket_p.h", + "sshagent.cpp", "sshagent_p.h", "sshbotanconversions_p.h", "sshcapabilities_p.h", "sshcapabilities.cpp", "sshchannel.cpp", "sshchannel_p.h", diff --git a/src/libs/ssh/sshagent.cpp b/src/libs/ssh/sshagent.cpp new file mode 100644 index 0000000000..1ada40fcd1 --- /dev/null +++ b/src/libs/ssh/sshagent.cpp @@ -0,0 +1,314 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** 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 The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ +#include "sshagent_p.h" + +#include "sshlogging_p.h" +#include "sshpacket_p.h" +#include "sshpacketparser_p.h" +#include "ssh_global.h" + +#include <QTimer> +#include <QtEndian> + +#include <algorithm> + +namespace QSsh { +namespace Internal { + +// https://github.com/openssh/openssh-portable/blob/V_7_2/PROTOCOL.agent +enum PacketType { + SSH_AGENT_FAILURE = 5, + SSH2_AGENTC_REQUEST_IDENTITIES = 11, + SSH2_AGENTC_SIGN_REQUEST = 13, + SSH2_AGENT_IDENTITIES_ANSWER = 12, + SSH2_AGENT_SIGN_RESPONSE = 14, +}; + +// TODO: Remove once we require 5.7, where the endianness functions have a sane input type. +template<typename T> static T fromBigEndian(const QByteArray &ba) +{ + return qFromBigEndian<T>(reinterpret_cast<const uchar *>(ba.constData())); +} + +void SshAgent::refreshKeysImpl() +{ + if (state() != Connected) + return; + const auto keysRequestIt = std::find_if(m_pendingRequests.constBegin(), + m_pendingRequests.constEnd(), [this](const Request &r) { return r.isKeysRequest(); }); + if (keysRequestIt != m_pendingRequests.constEnd()) { + qCDebug(sshLog) << "keys request already pending, not adding another one"; + return; + } + qCDebug(sshLog) << "queueing keys request"; + m_pendingRequests << Request(); + sendNextRequest(); +} + +void SshAgent::requestSignatureImpl(const QByteArray &key, uint token) +{ + if (state() != Connected) + return; + const QByteArray data = m_dataToSign.take(qMakePair(key, token)); + QSSH_ASSERT(!data.isEmpty()); + qCDebug(sshLog) << "queueing signature request"; + m_pendingRequests.enqueue(Request(key, data, token)); + sendNextRequest(); +} + +void SshAgent::sendNextRequest() +{ + if (m_pendingRequests.isEmpty()) + return; + if (m_outgoingPacket.isComplete()) + return; + if (hasError()) + return; + const Request &request = m_pendingRequests.head(); + m_outgoingPacket = request.isKeysRequest() ? generateKeysPacket() : generateSigPacket(request); + sendPacket(); +} + +SshAgent::Packet SshAgent::generateKeysPacket() +{ + qCDebug(sshLog) << "requesting keys from agent"; + Packet p; + p.size = 1; + p.data += char(SSH2_AGENTC_REQUEST_IDENTITIES); + return p; +} + +SshAgent::Packet SshAgent::generateSigPacket(const SshAgent::Request &request) +{ + qCDebug(sshLog) << "requesting signature from agent for key" << request.key << "and token" + << request.token; + Packet p; + p.data += char(SSH2_AGENTC_SIGN_REQUEST); + p.data += AbstractSshPacket::encodeString(request.key); + p.data += AbstractSshPacket::encodeString(request.dataToSign); + p.data += AbstractSshPacket::encodeInt(quint32(0)); + p.size = p.data.count(); + return p; +} + +SshAgent::~SshAgent() +{ + m_agentSocket.disconnect(this); +} + +void SshAgent::storeDataToSign(const QByteArray &key, const QByteArray &data, uint token) +{ + instance().m_dataToSign.insert(qMakePair(key, token), data); +} + +void SshAgent::removeDataToSign(const QByteArray &key, uint token) +{ + instance().m_dataToSign.remove(qMakePair(key, token)); +} + +SshAgent &QSsh::Internal::SshAgent::instance() +{ + static SshAgent agent; + return agent; +} + +SshAgent::SshAgent() +{ + connect(&m_agentSocket, &QLocalSocket::connected, this, &SshAgent::handleConnected); + connect(&m_agentSocket, &QLocalSocket::disconnected, this, &SshAgent::handleDisconnected); + connect(&m_agentSocket, + static_cast<void(QLocalSocket::*)(QLocalSocket::LocalSocketError)>(&QLocalSocket::error), + this, &SshAgent::handleSocketError); + connect(&m_agentSocket, &QLocalSocket::readyRead, this, &SshAgent::handleIncomingData); + QTimer::singleShot(0, this, &SshAgent::connectToServer); +} + +void SshAgent::connectToServer() +{ + const QByteArray serverAddress = qgetenv("SSH_AUTH_SOCK"); + if (serverAddress.isEmpty()) { + qCDebug(sshLog) << "agent failure: socket address unknown"; + m_error = tr("Cannot connect to ssh-agent: SSH_AUTH_SOCK is not set."); + emit errorOccurred(); + return; + } + qCDebug(sshLog) << "connecting to ssh-agent socket" << serverAddress; + m_state = Connecting; + m_agentSocket.connectToServer(QString::fromLocal8Bit(serverAddress)); +} + +void SshAgent::handleConnected() +{ + m_state = Connected; + qCDebug(sshLog) << "connection to ssh-agent established"; + refreshKeys(); +} + +void SshAgent::handleDisconnected() +{ + qCDebug(sshLog) << "lost connection to ssh-agent"; + m_error = tr("Lost connection to ssh-agent for unknown reason."); + setDisconnected(); +} + +void SshAgent::handleSocketError() +{ + qCDebug(sshLog) << "agent socket error" << m_agentSocket.error(); + m_error = m_agentSocket.errorString(); + setDisconnected(); +} + +void SshAgent::handleIncomingData() +{ + qCDebug(sshLog) << "getting data from agent"; + m_incomingData += m_agentSocket.readAll(); + while (!hasError() && !m_incomingData.isEmpty()) { + if (m_incomingPacket.size == 0) { + if (m_incomingData.count() < int(sizeof m_incomingPacket.size)) + break; + m_incomingPacket.size = fromBigEndian<quint32>(m_incomingData); + m_incomingData.remove(0, sizeof m_incomingPacket.size); + } + const int bytesToTake = qMin<quint32>(m_incomingPacket.size - m_incomingPacket.data.count(), + m_incomingData.count()); + m_incomingPacket.data += m_incomingData.left(bytesToTake); + m_incomingData.remove(0, bytesToTake); + if (m_incomingPacket.isComplete()) + handleIncomingPacket(); + else + break; + } +} + +void SshAgent::handleIncomingPacket() +{ + try { + qCDebug(sshLog) << "received packet from agent:" << m_incomingPacket.data.toHex(); + const char messageType = m_incomingPacket.data.at(0); + switch (messageType) { + case SSH2_AGENT_IDENTITIES_ANSWER: + handleIdentitiesPacket(); + break; + case SSH2_AGENT_SIGN_RESPONSE: + handleSignaturePacket(); + break; + case SSH_AGENT_FAILURE: + if (m_pendingRequests.isEmpty()) { + qCWarning(sshLog) << "unexpected failure message from agent"; + } else { + const Request request = m_pendingRequests.dequeue(); + if (request.isSignatureRequest()) { + qCWarning(sshLog) << "agent failed to sign message for key" + << request.key.toHex(); + emit signatureAvailable(request.key, QByteArray(), request.token); + } else { + qCWarning(sshLog) << "agent failed to retrieve key list"; + if (m_keys.isEmpty()) { + m_error = tr("ssh-agent failed to retrieve keys."); + setDisconnected(); + } + } + } + break; + default: + qCWarning(sshLog) << "unexpected message type from agent:" << messageType; + } + } catch (const SshPacketParseException &) { + qCWarning(sshLog()) << "received malformed packet from agent"; + handleProtocolError(); + } + m_incomingPacket.invalidate(); + m_incomingPacket.size = 0; + m_outgoingPacket.invalidate(); + sendNextRequest(); +} + +void SshAgent::handleIdentitiesPacket() +{ + qCDebug(sshLog) << "got keys packet from agent"; + if (m_pendingRequests.isEmpty() || !m_pendingRequests.dequeue().isKeysRequest()) { + qCDebug(sshLog) << "packet was not requested"; + handleProtocolError(); + return; + } + quint32 offset = 1; + const auto keyCount = SshPacketParser::asUint32(m_incomingPacket.data, &offset); + qCDebug(sshLog) << "packet contains" << keyCount << "keys"; + QList<QByteArray> newKeys; + for (quint32 i = 0; i < keyCount; ++i) { + const QByteArray key = SshPacketParser::asString(m_incomingPacket.data, &offset); + quint32 keyOffset = 0; + const QByteArray algoName = SshPacketParser::asString(key, &keyOffset); + SshPacketParser::asString(key, &keyOffset); // rest of key blob + SshPacketParser::asString(m_incomingPacket.data, &offset); // comment + qCDebug(sshLog) << "adding key of type" << algoName; + newKeys << key; + } + + m_keys = newKeys; + emit keysUpdated(); +} + +void SshAgent::handleSignaturePacket() +{ + qCDebug(sshLog) << "got signature packet from agent"; + if (m_pendingRequests.isEmpty()) { + qCDebug(sshLog) << "signature packet was not requested"; + handleProtocolError(); + return; + } + const Request request = m_pendingRequests.dequeue(); + if (!request.isSignatureRequest()) { + qCDebug(sshLog) << "signature packet was not requested"; + handleProtocolError(); + return; + } + const QByteArray signature = SshPacketParser::asString(m_incomingPacket.data, 1); + qCDebug(sshLog) << "signature for key" << request.key.toHex() << "is" << signature.toHex(); + emit signatureAvailable(request.key, signature, request.token); +} + +void SshAgent::handleProtocolError() +{ + m_error = tr("Protocol error when talking to ssh-agent."); + setDisconnected(); +} + +void SshAgent::setDisconnected() +{ + m_state = Unconnected; + m_agentSocket.disconnect(this); + emit errorOccurred(); +} + +void SshAgent::sendPacket() +{ + const quint32 sizeMsb = qToBigEndian(m_outgoingPacket.size); + m_agentSocket.write(reinterpret_cast<const char *>(&sizeMsb), sizeof sizeMsb); + m_agentSocket.write(m_outgoingPacket.data); +} + +} // namespace Internal +} // namespace QSsh diff --git a/src/libs/ssh/sshagent_p.h b/src/libs/ssh/sshagent_p.h new file mode 100644 index 0000000000..346d9aab9d --- /dev/null +++ b/src/libs/ssh/sshagent_p.h @@ -0,0 +1,125 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** 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 The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QByteArray> +#include <QHash> +#include <QList> +#include <QLocalSocket> +#include <QObject> +#include <QPair> +#include <QQueue> +#include <QString> + +namespace QSsh { +namespace Internal { + +class SshAgent : public QObject +{ + Q_OBJECT +public: + enum State { Unconnected, Connecting, Connected, }; + + ~SshAgent(); + static State state() { return instance().m_state; } + static bool hasError() { return !instance().m_error.isEmpty(); } + static QString errorString() { return instance().m_error; } + static QList<QByteArray> publicKeys() { return instance().m_keys; } + + static void refreshKeys() { instance().refreshKeysImpl(); } + static void storeDataToSign(const QByteArray &key, const QByteArray &data, uint token); + static void removeDataToSign(const QByteArray &key, uint token); + static void requestSignature(const QByteArray &key, uint token) { + instance().requestSignatureImpl(key, token); + } + + static SshAgent &instance(); + +signals: + void errorOccurred(); + void keysUpdated(); + + // Empty signature means signing failure. + void signatureAvailable(const QByteArray &key, const QByteArray &signature, uint token); + +private: + struct Request { + Request() { } + Request(const QByteArray &k, const QByteArray &d, uint t) + : key(k), dataToSign(d), token(t) { } + + bool isKeysRequest() const { return !isSignatureRequest(); } + bool isSignatureRequest() const { return !key.isEmpty(); } + + QByteArray key; + QByteArray dataToSign; + uint token; + }; + + struct Packet { + bool isComplete() const { return size != 0 && int(size) == data.count(); } + void invalidate() { size = 0; data.clear(); } + + quint32 size = 0; + QByteArray data; + }; + + SshAgent(); + void connectToServer(); + void refreshKeysImpl(); + void requestSignatureImpl(const QByteArray &key, uint token); + + void sendNextRequest(); + Packet generateKeysPacket(); + Packet generateSigPacket(const Request &request); + + void handleConnected(); + void handleDisconnected(); + void handleSocketError(); + void handleIncomingData(); + void handleIncomingPacket(); + void handleIdentitiesPacket(); + void handleSignaturePacket(); + + void handleProtocolError(); + void setDisconnected(); + + void sendPacket(); + + State m_state = Unconnected; + QString m_error; + QList<QByteArray> m_keys; + QHash<QPair<QByteArray, uint>, QByteArray> m_dataToSign; + QLocalSocket m_agentSocket; + QByteArray m_incomingData; + Packet m_incomingPacket; + Packet m_outgoingPacket; + + QQueue<Request> m_pendingRequests; +}; + +} // namespace Internal +} // namespace QSsh diff --git a/src/libs/ssh/sshconnection.cpp b/src/libs/ssh/sshconnection.cpp index a98529371d..790ea40ebc 100644 --- a/src/libs/ssh/sshconnection.cpp +++ b/src/libs/ssh/sshconnection.cpp @@ -27,6 +27,7 @@ #include "sshconnection_p.h" #include "sftpchannel.h" +#include "sshagent_p.h" #include "sshcapabilities_p.h" #include "sshchannelmanager_p.h" #include "sshcryptofacility_p.h" @@ -270,6 +271,7 @@ void SshConnectionPrivate::setupPacketHandlers() setupPacketHandler(SSH_MSG_USERAUTH_INFO_REQUEST, authReqList, &This::handleUserAuthInfoRequestPacket); } + setupPacketHandler(SSH_MSG_USERAUTH_PK_OK, authReqList, &This::handleUserAuthKeyOkPacket); const StateList connectedList = StateList() << ConnectionEstablished; @@ -299,7 +301,7 @@ void SshConnectionPrivate::setupPacketHandlers() setupPacketHandler(SSH_MSG_CHANNEL_CLOSE, connectedOrClosedList, &This::handleChannelClose); - setupPacketHandler(SSH_MSG_DISCONNECT, StateList() << SocketConnected + setupPacketHandler(SSH_MSG_DISCONNECT, StateList() << SocketConnected << WaitingForAgentKeys << UserAuthServiceRequested << UserAuthRequested << ConnectionEstablished, &This::handleDisconnect); @@ -527,8 +529,18 @@ void SshConnectionPrivate::handleServiceAcceptPacket() SshCapabilities::SshConnectionService); break; case SshConnectionParameters::AuthenticationTypePublicKey: - m_sendFacility.sendUserAuthByPublicKeyRequestPacket(m_connParams.userName.toUtf8(), - SshCapabilities::SshConnectionService); + authenticateWithPublicKey(); + break; + case SshConnectionParameters::AuthenticationTypeAgent: + if (SshAgent::publicKeys().isEmpty()) { + if (m_agentKeysUpToDate) + throw SshClientException(SshAuthenticationError, tr("ssh-agent has no keys.")); + qCDebug(sshLog) << "agent has no keys yet, waiting"; + m_state = WaitingForAgentKeys; + return; + } else { + tryAllAgentKeys(); + } break; } m_state = UserAuthRequested; @@ -596,6 +608,18 @@ void SshConnectionPrivate::handleUserAuthSuccessPacket() void SshConnectionPrivate::handleUserAuthFailurePacket() { + if (!m_pendingKeyChecks.isEmpty()) { + const QByteArray key = m_pendingKeyChecks.dequeue(); + SshAgent::removeDataToSign(key, tokenForAgent()); + qCDebug(sshLog) << "server rejected one of the keys supplied by the agent," + << m_pendingKeyChecks.count() << "keys remaining"; + if (m_pendingKeyChecks.isEmpty() && m_agentKeyToUse.isEmpty()) { + throw SshClientException(SshAuthenticationError, tr("The server rejected all keys " + "known to the ssh-agent.")); + } + return; + } + // TODO: Evaluate "authentications that can continue" field and act on it. if (m_connParams.authenticationType == SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods @@ -608,10 +632,45 @@ void SshConnectionPrivate::handleUserAuthFailurePacket() } m_timeoutTimer.stop(); - const QString errorMsg = m_connParams.authenticationType == SshConnectionParameters::AuthenticationTypePublicKey - ? tr("Server rejected key.") : tr("Server rejected password."); + QString errorMsg; + switch (m_connParams.authenticationType) { + case SshConnectionParameters::AuthenticationTypePublicKey: + case SshConnectionParameters::AuthenticationTypeAgent: + errorMsg = tr("Server rejected key."); + break; + default: + errorMsg = tr("Server rejected password."); + break; + } throw SshClientException(SshAuthenticationError, errorMsg); } + +void SshConnectionPrivate::handleUserAuthKeyOkPacket() +{ + const SshUserAuthPkOkPacket &msg = m_incomingPacket.extractUserAuthPkOk(); + qCDebug(sshLog) << "server accepted key of type" << msg.algoName; + + if (m_pendingKeyChecks.isEmpty()) { + throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected packet", + tr("Server sent unexpected SSH_MSG_USERAUTH_PK_OK packet.")); + } + const QByteArray key = m_pendingKeyChecks.dequeue(); + if (key != msg.keyBlob) { + // The server must answer the requests in the order we sent them. + throw SshServerException(SSH_DISCONNECT_PROTOCOL_ERROR, "Unexpected packet content", + tr("Server sent unexpected key in SSH_MSG_USERAUTH_PK_OK packet.")); + } + const uint token = tokenForAgent(); + if (!m_agentKeyToUse.isEmpty()) { + qCDebug(sshLog) << "another key has already been accepted, ignoring this one"; + SshAgent::removeDataToSign(key, token); + return; + } + m_agentKeyToUse = key; + qCDebug(sshLog) << "requesting signature from agent"; + SshAgent::requestSignature(key, token); +} + void SshConnectionPrivate::handleDebugPacket() { const SshDebug &msg = m_incomingPacket.extractDebug(); @@ -710,6 +769,11 @@ void SshConnectionPrivate::sendData(const QByteArray &data) m_socket->write(data); } +uint SshConnectionPrivate::tokenForAgent() const +{ + return qHash(m_sendFacility.sessionId()); +} + void SshConnectionPrivate::handleSocketDisconnected() { closeConnection(SSH_DISCONNECT_CONNECTION_LOST, SshClosedByServerError, @@ -727,8 +791,10 @@ void SshConnectionPrivate::handleSocketError() void SshConnectionPrivate::handleTimeout() { - closeConnection(SSH_DISCONNECT_BY_APPLICATION, SshTimeoutError, "", - tr("Timeout waiting for reply from server.")); + const QString errorMessage = m_state == WaitingForAgentKeys + ? tr("Timeout waiting for keys from ssh-agent.") + : tr("Timeout waiting for reply from server."); + closeConnection(SSH_DISCONNECT_BY_APPLICATION, SshTimeoutError, "", errorMessage); } void SshConnectionPrivate::sendKeepAlivePacket() @@ -745,6 +811,66 @@ void SshConnectionPrivate::sendKeepAlivePacket() m_timeoutTimer.start(); } +void SshConnectionPrivate::handleAgentKeysUpdated() +{ + m_agentKeysUpToDate = true; + if (m_state == WaitingForAgentKeys) { + m_state = UserAuthRequested; + tryAllAgentKeys(); + } +} + +void SshConnectionPrivate::handleSignatureFromAgent(const QByteArray &key, + const QByteArray &signature, uint token) +{ + if (token != tokenForAgent()) { + qCDebug(sshLog) << "signature is for different connection, ignoring"; + return; + } + QSSH_ASSERT(key == m_agentKeyToUse); + m_agentSignature = signature; + authenticateWithPublicKey(); +} + +void SshConnectionPrivate::tryAllAgentKeys() +{ + const QList<QByteArray> &keys = SshAgent::publicKeys(); + if (keys.isEmpty()) + throw SshClientException(SshAuthenticationError, tr("ssh-agent has no keys.")); + qCDebug(sshLog) << "trying authentication with" << keys.count() + << "public keys received from agent"; + foreach (const QByteArray &key, keys) { + m_sendFacility.sendQueryPublicKeyPacket(m_connParams.userName.toUtf8(), + SshCapabilities::SshConnectionService, key); + m_pendingKeyChecks.enqueue(key); + } +} + +void SshConnectionPrivate::authenticateWithPublicKey() +{ + qCDebug(sshLog) << "sending actual authentication request"; + + QByteArray key; + QByteArray signature; + if (m_connParams.authenticationType == SshConnectionParameters::AuthenticationTypeAgent) { + // Agent is not needed anymore after this point. + disconnect(&SshAgent::instance(), 0, this, 0); + + key = m_agentKeyToUse; + signature = m_agentSignature; + } + + m_sendFacility.sendUserAuthByPublicKeyRequestPacket(m_connParams.userName.toUtf8(), + SshCapabilities::SshConnectionService, key, signature); +} + +void SshConnectionPrivate::setAgentError() +{ + m_error = SshAgentError; + m_errorString = SshAgent::errorString(); + emit error(m_error); +} + void SshConnectionPrivate::connectToHost() { QSSH_ASSERT_AND_RETURN(m_state == SocketUnconnected); @@ -757,15 +883,37 @@ void SshConnectionPrivate::connectToHost() m_errorString.clear(); m_serverId.clear(); m_serverHasSentDataBeforeId = false; + m_agentSignature.clear(); + m_agentKeysUpToDate = false; + m_pendingKeyChecks.clear(); + m_agentKeyToUse.clear(); - try { - if (m_connParams.authenticationType == SshConnectionParameters::AuthenticationTypePublicKey) + switch (m_connParams.authenticationType) { + case SshConnectionParameters::AuthenticationTypePublicKey: + try { createPrivateKey(); - } catch (const SshClientException &ex) { - m_error = ex.error; - m_errorString = ex.errorString; - emit error(m_error); - return; + break; + } catch (const SshClientException &ex) { + m_error = ex.error; + m_errorString = ex.errorString; + emit error(m_error); + return; + } + case SshConnectionParameters::AuthenticationTypeAgent: + if (SshAgent::hasError()) { + setAgentError(); + return; + } + connect(&SshAgent::instance(), &SshAgent::errorOccurred, + this, &SshConnectionPrivate::setAgentError); + connect(&SshAgent::instance(), &SshAgent::keysUpdated, + this, &SshConnectionPrivate::handleAgentKeysUpdated); + SshAgent::refreshKeys(); + connect(&SshAgent::instance(), &SshAgent::signatureAvailable, + this, &SshConnectionPrivate::handleSignatureFromAgent); + break; + default: + break; } connect(m_socket, &QAbstractSocket::connected, diff --git a/src/libs/ssh/sshconnection.h b/src/libs/ssh/sshconnection.h index 071c97c0c7..7779192cdb 100644 --- a/src/libs/ssh/sshconnection.h +++ b/src/libs/ssh/sshconnection.h @@ -65,6 +65,7 @@ public: enum AuthenticationType { AuthenticationTypePassword, AuthenticationTypePublicKey, + AuthenticationTypeAgent, AuthenticationTypeKeyboardInteractive, // Some servers disable "password", others disable "keyboard-interactive". diff --git a/src/libs/ssh/sshconnection_p.h b/src/libs/ssh/sshconnection_p.h index 7c8198bf20..9a7afe3552 100644 --- a/src/libs/ssh/sshconnection_p.h +++ b/src/libs/ssh/sshconnection_p.h @@ -32,6 +32,7 @@ #include <QHash> #include <QList> +#include <QQueue> #include <QObject> #include <QPair> #include <QScopedPointer> @@ -56,6 +57,7 @@ enum SshStateInternal { SocketConnecting, // After connectToHost() SocketConnected, // After socket's connected() signal UserAuthServiceRequested, + WaitingForAgentKeys, UserAuthRequested, ConnectionEstablished // After service has been started // ... @@ -107,6 +109,12 @@ private: void handleTimeout(); void sendKeepAlivePacket(); + void handleAgentKeysUpdated(); + void handleSignatureFromAgent(const QByteArray &key, const QByteArray &signature, uint token); + void tryAllAgentKeys(); + void authenticateWithPublicKey(); + void setAgentError(); + void handleServerId(); void handlePackets(); void handleCurrentPacket(); @@ -118,6 +126,7 @@ private: void handleUserAuthInfoRequestPacket(); void handleUserAuthSuccessPacket(); void handleUserAuthFailurePacket(); + void handleUserAuthKeyOkPacket(); void handleUserAuthBannerPacket(); void handleUnexpectedPacket(); void handleGlobalRequest(); @@ -143,6 +152,8 @@ private: void sendData(const QByteArray &data); + uint tokenForAgent() const; + typedef void (SshConnectionPrivate::*PacketHandler)(); typedef QList<SshStateInternal> StateList; void setupPacketHandlers(); @@ -171,8 +182,12 @@ private: SshConnection *m_conn; quint64 m_lastInvalidMsgSeqNr; QByteArray m_serverId; + QByteArray m_agentSignature; + QQueue<QByteArray> m_pendingKeyChecks; + QByteArray m_agentKeyToUse; bool m_serverHasSentDataBeforeId; bool m_triedAllPasswordBasedMethods; + bool m_agentKeysUpToDate; }; } // namespace Internal diff --git a/src/libs/ssh/sshcryptofacility_p.h b/src/libs/ssh/sshcryptofacility_p.h index db008207d1..2f9b64c44c 100644 --- a/src/libs/ssh/sshcryptofacility_p.h +++ b/src/libs/ssh/sshcryptofacility_p.h @@ -45,13 +45,13 @@ public: QByteArray generateMac(const QByteArray &data, quint32 dataSize) const; quint32 cipherBlockSize() const { return m_cipherBlockSize; } quint32 macLength() const { return m_macLength; } + QByteArray sessionId() const { return m_sessionId; } protected: enum Mode { CbcMode, CtrMode }; SshAbstractCryptoFacility(); void convert(QByteArray &data, quint32 offset, quint32 dataSize) const; - QByteArray sessionId() const { return m_sessionId; } Botan::Keyed_Filter *makeCtrCipherMode(Botan::BlockCipher *cipher, const Botan::InitializationVector &iv, const Botan::SymmetricKey &key); diff --git a/src/libs/ssh/ssherrors.h b/src/libs/ssh/ssherrors.h index 4c85e071cf..caf89819f0 100644 --- a/src/libs/ssh/ssherrors.h +++ b/src/libs/ssh/ssherrors.h @@ -31,7 +31,7 @@ namespace QSsh { enum SshError { SshNoError, SshSocketError, SshTimeoutError, SshProtocolError, SshHostKeyError, SshKeyFileError, SshAuthenticationError, - SshClosedByServerError, SshInternalError + SshClosedByServerError, SshAgentError, SshInternalError }; } // namespace QSsh diff --git a/src/libs/ssh/sshincomingpacket.cpp b/src/libs/ssh/sshincomingpacket.cpp index 5bb8391434..d8c6356a0b 100644 --- a/src/libs/ssh/sshincomingpacket.cpp +++ b/src/libs/ssh/sshincomingpacket.cpp @@ -296,6 +296,23 @@ SshUserAuthInfoRequestPacket SshIncomingPacket::extractUserAuthInfoRequest() con } } +SshUserAuthPkOkPacket SshIncomingPacket::extractUserAuthPkOk() const +{ + Q_ASSERT(isComplete()); + Q_ASSERT(type() == SSH_MSG_USERAUTH_PK_OK); + + try { + SshUserAuthPkOkPacket msg; + quint32 offset = TypeOffset + 1; + msg.algoName= SshPacketParser::asString(m_data, &offset); + msg.keyBlob = SshPacketParser::asString(m_data, &offset); + return msg; + } catch (const SshPacketParseException &) { + throw SSH_SERVER_EXCEPTION(SSH_DISCONNECT_PROTOCOL_ERROR, + "Invalid SSH_MSG_USERAUTH_PK_OK."); + } +} + SshDebug SshIncomingPacket::extractDebug() const { Q_ASSERT(isComplete()); diff --git a/src/libs/ssh/sshincomingpacket_p.h b/src/libs/ssh/sshincomingpacket_p.h index bd7aea4d6e..ab8be6aae3 100644 --- a/src/libs/ssh/sshincomingpacket_p.h +++ b/src/libs/ssh/sshincomingpacket_p.h @@ -76,6 +76,12 @@ struct SshUserAuthBanner QByteArray language; }; +struct SshUserAuthPkOkPacket +{ + QByteArray algoName; + QByteArray keyBlob; +}; + struct SshUserAuthInfoRequestPacket { QString name; @@ -176,6 +182,7 @@ public: SshDisconnect extractDisconnect() const; SshUserAuthBanner extractUserAuthBanner() const; SshUserAuthInfoRequestPacket extractUserAuthInfoRequest() const; + SshUserAuthPkOkPacket extractUserAuthPkOk() const; SshDebug extractDebug() const; SshRequestSuccess extractRequestSuccess() const; SshUnimplemented extractUnimplemented() const; diff --git a/src/libs/ssh/sshoutgoingpacket.cpp b/src/libs/ssh/sshoutgoingpacket.cpp index c74a8950f1..ae505700e0 100644 --- a/src/libs/ssh/sshoutgoingpacket.cpp +++ b/src/libs/ssh/sshoutgoingpacket.cpp @@ -25,9 +25,11 @@ #include "sshoutgoingpacket_p.h" +#include "sshagent_p.h" #include "sshcapabilities_p.h" #include "sshcryptofacility_p.h" #include "sshlogging_p.h" +#include "sshpacketparser_p.h" #include <QtEndian> @@ -117,17 +119,41 @@ void SshOutgoingPacket::generateUserAuthByPasswordRequestPacket(const QByteArray } void SshOutgoingPacket::generateUserAuthByPublicKeyRequestPacket(const QByteArray &user, - const QByteArray &service) + const QByteArray &service, const QByteArray &key, const QByteArray &signature) { init(SSH_MSG_USERAUTH_REQUEST).appendString(user).appendString(service) - .appendString("publickey").appendBool(true) - .appendString(m_encrypter.authenticationAlgorithmName()) - .appendString(m_encrypter.authenticationPublicKey()); - const QByteArray &dataToSign = m_data.mid(PayloadOffset); - appendString(m_encrypter.authenticationKeySignature(dataToSign)); + .appendString("publickey").appendBool(true); + if (!key.isEmpty()) { + appendString(SshPacketParser::asString(key, quint32(0))); + appendString(key); + appendString(signature); + } else { + appendString(m_encrypter.authenticationAlgorithmName()); + appendString(m_encrypter.authenticationPublicKey()); + const QByteArray &dataToSign = m_data.mid(PayloadOffset); + appendString(m_encrypter.authenticationKeySignature(dataToSign)); + } finalize(); } +void SshOutgoingPacket::generateQueryPublicKeyPacket(const QByteArray &user, + const QByteArray &service, const QByteArray &publicKey) +{ + // Name extraction cannot fail, we already verified this when receiving the key + // from the agent. + const QByteArray algoName = SshPacketParser::asString(publicKey, quint32(0)); + SshOutgoingPacket packetToSign(m_encrypter, m_seqNr); + packetToSign.init(SSH_MSG_USERAUTH_REQUEST).appendString(user).appendString(service) + .appendString("publickey").appendBool(true).appendString(algoName) + .appendString(publicKey); + const QByteArray &dataToSign + = encodeString(m_encrypter.sessionId()) + packetToSign.m_data.mid(PayloadOffset); + SshAgent::storeDataToSign(publicKey, dataToSign, qHash(m_encrypter.sessionId())); + init(SSH_MSG_USERAUTH_REQUEST).appendString(user).appendString(service) + .appendString("publickey").appendBool(false).appendString(algoName) + .appendString(publicKey).finalize(); +} + void SshOutgoingPacket::generateUserAuthByKeyboardInteractiveRequestPacket(const QByteArray &user, const QByteArray &service) { diff --git a/src/libs/ssh/sshoutgoingpacket_p.h b/src/libs/ssh/sshoutgoingpacket_p.h index d2d4f63f16..8e3da5aef6 100644 --- a/src/libs/ssh/sshoutgoingpacket_p.h +++ b/src/libs/ssh/sshoutgoingpacket_p.h @@ -53,7 +53,9 @@ public: void generateUserAuthByPasswordRequestPacket(const QByteArray &user, const QByteArray &service, const QByteArray &pwd); void generateUserAuthByPublicKeyRequestPacket(const QByteArray &user, - const QByteArray &service); + const QByteArray &service, const QByteArray &key, const QByteArray &signature); + void generateQueryPublicKeyPacket(const QByteArray &user, const QByteArray &service, + const QByteArray &publicKey); void generateUserAuthByKeyboardInteractiveRequestPacket(const QByteArray &user, const QByteArray &service); void generateUserAuthInfoResponsePacket(const QStringList &responses); diff --git a/src/libs/ssh/sshpacketparser.cpp b/src/libs/ssh/sshpacketparser.cpp index 38f9c5a04d..6251f60244 100644 --- a/src/libs/ssh/sshpacketparser.cpp +++ b/src/libs/ssh/sshpacketparser.cpp @@ -98,6 +98,11 @@ quint64 SshPacketParser::asUint64(const QByteArray &data, quint32 *offset) return val; } +QByteArray SshPacketParser::asString(const QByteArray &data, quint32 offset) +{ + return asString(data, &offset); +} + QByteArray SshPacketParser::asString(const QByteArray &data, quint32 *offset) { const quint32 length = asUint32(data, offset); diff --git a/src/libs/ssh/sshpacketparser_p.h b/src/libs/ssh/sshpacketparser_p.h index 8dd70511d2..b57f22f084 100644 --- a/src/libs/ssh/sshpacketparser_p.h +++ b/src/libs/ssh/sshpacketparser_p.h @@ -62,6 +62,7 @@ public: static quint64 asUint64(const QByteArray &data, quint32 *offset); static quint32 asUint32(const QByteArray &data, quint32 offset); static quint32 asUint32(const QByteArray &data, quint32 *offset); + static QByteArray asString(const QByteArray &data, quint32 offset); static QByteArray asString(const QByteArray &data, quint32 *offset); static QString asUserString(const QByteArray &data, quint32 *offset); static SshNameList asNameList(const QByteArray &data, quint32 *offset); diff --git a/src/libs/ssh/sshsendfacility.cpp b/src/libs/ssh/sshsendfacility.cpp index 491c6981b9..9552d3e020 100644 --- a/src/libs/ssh/sshsendfacility.cpp +++ b/src/libs/ssh/sshsendfacility.cpp @@ -118,9 +118,16 @@ void SshSendFacility::sendUserAuthByPasswordRequestPacket(const QByteArray &user } void SshSendFacility::sendUserAuthByPublicKeyRequestPacket(const QByteArray &user, - const QByteArray &service) + const QByteArray &service, const QByteArray &key, const QByteArray &signature) { - m_outgoingPacket.generateUserAuthByPublicKeyRequestPacket(user, service); + m_outgoingPacket.generateUserAuthByPublicKeyRequestPacket(user, service, key, signature); + sendPacket(); +} + +void SshSendFacility::sendQueryPublicKeyPacket(const QByteArray &user, const QByteArray &service, + const QByteArray &publicKey) +{ + m_outgoingPacket.generateQueryPublicKeyPacket(user, service, publicKey); sendPacket(); } diff --git a/src/libs/ssh/sshsendfacility_p.h b/src/libs/ssh/sshsendfacility_p.h index 0eb92ae0b1..f54a2a9463 100644 --- a/src/libs/ssh/sshsendfacility_p.h +++ b/src/libs/ssh/sshsendfacility_p.h @@ -49,6 +49,8 @@ public: void recreateKeys(const SshKeyExchange &keyExchange); void createAuthenticationKey(const QByteArray &privKeyFileContents); + QByteArray sessionId() const { return m_encrypter.sessionId(); } + QByteArray sendKeyExchangeInitPacket(); void sendKeyDhInitPacket(const Botan::BigInt &e); void sendKeyEcdhInitPacket(const QByteArray &clientQ); @@ -60,7 +62,9 @@ public: void sendUserAuthByPasswordRequestPacket(const QByteArray &user, const QByteArray &service, const QByteArray &pwd); void sendUserAuthByPublicKeyRequestPacket(const QByteArray &user, - const QByteArray &service); + const QByteArray &service, const QByteArray &key, const QByteArray &signature); + void sendQueryPublicKeyPacket(const QByteArray &user, const QByteArray &service, + const QByteArray &publicKey); void sendUserAuthByKeyboardInteractiveRequestPacket(const QByteArray &user, const QByteArray &service); void sendUserAuthInfoResponsePacket(const QStringList &responses); diff --git a/src/plugins/remotelinux/genericlinuxdeviceconfigurationwidget.cpp b/src/plugins/remotelinux/genericlinuxdeviceconfigurationwidget.cpp index 3bcb2c9587..cc46d1bcdd 100644 --- a/src/plugins/remotelinux/genericlinuxdeviceconfigurationwidget.cpp +++ b/src/plugins/remotelinux/genericlinuxdeviceconfigurationwidget.cpp @@ -58,6 +58,8 @@ GenericLinuxDeviceConfigurationWidget::GenericLinuxDeviceConfigurationWidget( this, &GenericLinuxDeviceConfigurationWidget::keyFileEditingFinished); connect(m_ui->keyButton, &QAbstractButton::toggled, this, &GenericLinuxDeviceConfigurationWidget::authenticationTypeChanged); + connect(m_ui->agentButton, &QAbstractButton::toggled, + this, &GenericLinuxDeviceConfigurationWidget::authenticationTypeChanged); connect(m_ui->timeoutSpinBox, &QAbstractSpinBox::editingFinished, this, &GenericLinuxDeviceConfigurationWidget::timeoutEditingFinished); connect(m_ui->timeoutSpinBox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), @@ -89,14 +91,16 @@ void GenericLinuxDeviceConfigurationWidget::authenticationTypeChanged() { SshConnectionParameters sshParams = device()->sshParameters(); const bool usePassword = m_ui->passwordButton->isChecked(); - sshParams.authenticationType = usePassword - ? SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods - : SshConnectionParameters::AuthenticationTypePublicKey; + const bool useKeyFile = m_ui->keyButton->isChecked(); + sshParams.authenticationType + = usePassword ? SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods + : useKeyFile ? SshConnectionParameters::AuthenticationTypePublicKey + : SshConnectionParameters::AuthenticationTypeAgent; device()->setSshParameters(sshParams); m_ui->pwdLineEdit->setEnabled(usePassword); m_ui->passwordLabel->setEnabled(usePassword); - m_ui->keyFileLineEdit->setEnabled(!usePassword); - m_ui->keyLabel->setEnabled(!usePassword); + m_ui->keyFileLineEdit->setEnabled(useKeyFile); + m_ui->keyLabel->setEnabled(useKeyFile); } void GenericLinuxDeviceConfigurationWidget::hostNameEditingFinished() @@ -214,10 +218,18 @@ void GenericLinuxDeviceConfigurationWidget::initGui() const SshConnectionParameters &sshParams = device()->sshParameters(); - if (sshParams.authenticationType != SshConnectionParameters::AuthenticationTypePublicKey) - m_ui->passwordButton->setChecked(true); - else + switch (sshParams.authenticationType) { + case SshConnectionParameters::AuthenticationTypePublicKey: m_ui->keyButton->setChecked(true); + break; + case SshConnectionParameters::AuthenticationTypeAgent: + m_ui->agentButton->setChecked(true); + break; + case SshConnectionParameters::AuthenticationTypePassword: + case SshConnectionParameters::AuthenticationTypeKeyboardInteractive: + case SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods: + m_ui->passwordButton->setChecked(true); + } m_ui->timeoutSpinBox->setValue(sshParams.timeout); m_ui->hostLineEdit->setEnabled(!device()->isAutoDetected()); m_ui->sshPortSpinBox->setEnabled(!device()->isAutoDetected()); diff --git a/src/plugins/remotelinux/genericlinuxdeviceconfigurationwidget.ui b/src/plugins/remotelinux/genericlinuxdeviceconfigurationwidget.ui index 46ad3645f0..e2854e47cc 100644 --- a/src/plugins/remotelinux/genericlinuxdeviceconfigurationwidget.ui +++ b/src/plugins/remotelinux/genericlinuxdeviceconfigurationwidget.ui @@ -66,6 +66,13 @@ </widget> </item> <item> + <widget class="QRadioButton" name="agentButton"> + <property name="text"> + <string>Key via ssh-agent</string> + </property> + </widget> + </item> + <item> <spacer name="horizontalSpacer_4"> <property name="orientation"> <enum>Qt::Horizontal</enum> diff --git a/src/plugins/remotelinux/genericlinuxdeviceconfigurationwizardpages.cpp b/src/plugins/remotelinux/genericlinuxdeviceconfigurationwizardpages.cpp index c9fe3be665..5bd8bf6c89 100644 --- a/src/plugins/remotelinux/genericlinuxdeviceconfigurationwizardpages.cpp +++ b/src/plugins/remotelinux/genericlinuxdeviceconfigurationwizardpages.cpp @@ -64,6 +64,10 @@ GenericLinuxDeviceConfigurationWizardSetupPage::GenericLinuxDeviceConfigurationW this, &QWizardPage::completeChanged); connect(d->ui.passwordButton, &QAbstractButton::toggled, this, &GenericLinuxDeviceConfigurationWizardSetupPage::handleAuthTypeChanged); + connect(d->ui.keyButton, &QAbstractButton::toggled, + this, &GenericLinuxDeviceConfigurationWizardSetupPage::handleAuthTypeChanged); + connect(d->ui.agentButton, &QAbstractButton::toggled, + this, &GenericLinuxDeviceConfigurationWizardSetupPage::handleAuthTypeChanged); } GenericLinuxDeviceConfigurationWizardSetupPage::~GenericLinuxDeviceConfigurationWizardSetupPage() @@ -107,8 +111,9 @@ QString GenericLinuxDeviceConfigurationWizardSetupPage::userName() const SshConnectionParameters::AuthenticationType GenericLinuxDeviceConfigurationWizardSetupPage::authenticationType() const { return d->ui.passwordButton->isChecked() - ? SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods - : SshConnectionParameters::AuthenticationTypePublicKey; + ? SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods + : d->ui.keyButton->isChecked() ? SshConnectionParameters::AuthenticationTypePublicKey + : SshConnectionParameters::AuthenticationTypeAgent; } QString GenericLinuxDeviceConfigurationWizardSetupPage::password() const @@ -143,8 +148,10 @@ QString GenericLinuxDeviceConfigurationWizardSetupPage::defaultPassWord() const void GenericLinuxDeviceConfigurationWizardSetupPage::handleAuthTypeChanged() { - d->ui.passwordLineEdit->setEnabled(authenticationType() != SshConnectionParameters::AuthenticationTypePublicKey); - d->ui.privateKeyPathChooser->setEnabled(!d->ui.passwordLineEdit->isEnabled()); + d->ui.passwordLineEdit->setEnabled(authenticationType() + == SshConnectionParameters::AuthenticationTypeTryAllPasswordBasedMethods); + d->ui.privateKeyPathChooser->setEnabled(authenticationType() + == SshConnectionParameters::AuthenticationTypePublicKey); emit completeChanged(); } diff --git a/src/plugins/remotelinux/genericlinuxdeviceconfigurationwizardsetuppage.ui b/src/plugins/remotelinux/genericlinuxdeviceconfigurationwizardsetuppage.ui index 6a1e8de8af..54ccf0d7b0 100644 --- a/src/plugins/remotelinux/genericlinuxdeviceconfigurationwizardsetuppage.ui +++ b/src/plugins/remotelinux/genericlinuxdeviceconfigurationwizardsetuppage.ui @@ -106,6 +106,13 @@ </widget> </item> <item> + <widget class="QRadioButton" name="agentButton"> + <property name="text"> + <string>Agent</string> + </property> + </widget> + </item> + <item> <spacer name="horizontalSpacer_3"> <property name="orientation"> <enum>Qt::Horizontal</enum> |