Skip to content

Commit

Permalink
ssh/Connection: support multiple host keys
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxKellermann committed Oct 24, 2023
1 parent 79c33de commit 9557241
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 32 deletions.
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ executable(
'src/ssh/KexProposal.cxx',
'src/ssh/KexState.cxx',
'src/ssh/TerminalMode.cxx',
'src/key/List.cxx',
'src/key/Curve25519Key.cxx',
'src/key/Ed25519Key.cxx',
'src/key/Parser.cxx',
Expand Down
4 changes: 2 additions & 2 deletions src/Connection.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ using std::string_view_literals::operator""sv;

Connection::Connection(Instance &_instance, Listener &_listener,
UniqueSocketDescriptor _fd,
const Key &_host_key)
const KeyList &_host_keys)
:SSH::CConnection(_instance.GetEventLoop(), std::move(_fd),
_host_key),
_host_keys),
instance(_instance), listener(_listener),
logger(instance.GetLogger())
{
Expand Down
2 changes: 1 addition & 1 deletion src/Connection.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class Connection final
public:
Connection(Instance &_instance, Listener &_listener,
UniqueSocketDescriptor fd,
const Key &_host_key);
const KeyList &_host_keys);
~Connection() noexcept;

Listener &GetListener() const noexcept {
Expand Down
6 changes: 3 additions & 3 deletions src/Instance.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
#include <unistd.h>

Instance::Instance(const Config &config,
std::unique_ptr<Key> _host_key,
KeyList &&_host_keys,
UniqueSocketDescriptor spawner_socket)
:host_key(std::move(_host_key)),
:host_keys(std::move(_host_keys)),
#ifdef ENABLE_TRANSLATION
translation_server(config.translation_server.empty() ? nullptr : config.translation_server.c_str()),
#endif
Expand Down Expand Up @@ -116,7 +116,7 @@ void
Instance::AddConnection(Listener &listener, UniqueSocketDescriptor fd) noexcept
{
try {
auto *c = new Connection(*this, listener, std::move(fd), *host_key);
auto *c = new Connection(*this, listener, std::move(fd), host_keys);
connections.push_front(*c);
} catch (...) {
logger(1, std::current_exception());
Expand Down
5 changes: 3 additions & 2 deletions src/Instance.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#pragma once

#include "key/List.hxx"
#include "event/Loop.hxx"
#include "event/ShutdownListener.hxx"
#include "event/SignalEvent.hxx"
Expand Down Expand Up @@ -39,7 +40,7 @@ class Instance final

const RootLogger logger;

std::unique_ptr<Key> host_key;
KeyList host_keys;

#ifdef ENABLE_TRANSLATION
const char *const translation_server;
Expand All @@ -66,7 +67,7 @@ class Instance final

public:
Instance(const Config &config,
std::unique_ptr<Key> _host_key,
KeyList &&_host_key,
UniqueSocketDescriptor spawner_socket);
~Instance() noexcept;

Expand Down
33 changes: 16 additions & 17 deletions src/Main.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,25 @@ LoadOptionalKeyFile(const char *path)
return LoadKeyFile(fd);
}

static std::unique_ptr<Key>
LoadHostKey(bool use_ed25519_host_key)
static KeyList
LoadHostKeys()
{
if (auto key = LoadOptionalKeyFile(use_ed25519_host_key
? "/etc/cm4all/lukko/host_ed25519_key"
: "/etc/cm4all/lukko/host_ecdsa_key"))
return key;

if (use_ed25519_host_key) {
return std::make_unique<Ed25519Key>(Ed25519Key::Generate{});
} else {
KeyList keys;

if (auto key = LoadOptionalKeyFile("/etc/cm4all/lukko/host_ed25519_key"))
keys.Add(std::move(key));

if (auto key = LoadOptionalKeyFile("/etc/cm4all/lukko/host_ecdsa_key"))
keys.Add(std::move(key));

if (keys.empty()) {
keys.Add(std::make_unique<Ed25519Key>(Ed25519Key::Generate{}));
#ifdef HAVE_OPENSSL
return std::make_unique<ECDSAKey>(ECDSAKey::Generate{});
#else
// TODO
std::terminate();
keys.Add(std::make_unique<ECDSAKey>(ECDSAKey::Generate{}));
#endif // HAVE_OPENSSL
}

return keys;
}

int
Expand All @@ -81,15 +82,13 @@ try {
LoadConfigFile(config, "/etc/cm4all/lukko/lukko.conf");
config.Check();

const bool use_ed25519_host_key = true;

SetupProcess();

auto spawner_socket = LaunchSpawnServer(config.spawn, nullptr);

Instance instance{
config,
LoadHostKey(use_ed25519_host_key),
LoadHostKeys(),
std::move(spawner_socket),
};

Expand Down
36 changes: 36 additions & 0 deletions src/key/List.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright CM4all GmbH
// author: Max Kellermann <[email protected]>

#include "List.hxx"
#include "Key.hxx"
#include "util/IterableSplitString.hxx"

KeyList::KeyList() noexcept = default;
KeyList::~KeyList() noexcept = default;

void
KeyList::Add(std::unique_ptr<Key> key) noexcept
{
auto [it, inserted] = keys.try_emplace(key->GetAlgorithm(), std::move(key));
if (inserted) {
if (!algorithms.empty())
algorithms.push_back(',');
algorithms.append(it->first);
}
}

const Key *
KeyList::Choose(std::string_view peer_algorithms) const noexcept
{
for (const std::string_view a : IterableSplitString(peer_algorithms, ',')) {
if (a.empty())
continue;

const auto i = keys.find(a);
if (i != keys.end())
return i->second.get();
}

return nullptr;
}
50 changes: 50 additions & 0 deletions src/key/List.hxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright CM4all GmbH
// author: Max Kellermann <[email protected]>

#pragma once

#include <map>
#include <memory>
#include <string>

class Key;

class KeyList {
std::map<std::string_view, std::unique_ptr<Key>> keys;

std::string algorithms;

public:
KeyList() noexcept;
~KeyList() noexcept;

KeyList(KeyList &&) noexcept = default;
KeyList &operator=(KeyList &&) noexcept = default;

bool empty() const noexcept {
return keys.empty();
}

void Add(std::unique_ptr<Key> key) noexcept;

/**
* @return a comma-separated list of available server host key
* algorithms
*/
std::string_view GetAlgorithms() const noexcept {
return algorithms;
}

/**
* Choose a host key based on the list of algorithms received
* in KEXINIT from the peer.
*
* @param peer_algorithms the "server_host_key_algorithms"
* string from the peer's KEXINIT packet, i.e. a
* comma-separated list of acceptable server host key
* algorithms
*/
[[gnu::pure]]
const Key *Choose(std::string_view peer_algorithms) const noexcept;
};
33 changes: 28 additions & 5 deletions src/ssh/Connection.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "ssh/MakePacket.hxx"
#include "ssh/Deserializer.hxx"
#include "key/Key.hxx"
#include "key/List.hxx"
#include "system/Error.hxx"
#include "system/Urandom.hxx"
#include "net/UniqueSocketDescriptor.hxx"
Expand All @@ -36,8 +37,8 @@ SerializeKex(Serializer &s, std::span<const std::byte, KEX_COOKIE_SIZE> cookie,
}

Connection::Connection(EventLoop &event_loop, UniqueSocketDescriptor _fd,
const Key &_host_key)
:host_key(_host_key),
const KeyList &_host_keys)
:host_keys(_host_keys),
socket(event_loop)
{
socket.Init(_fd.Release(), FD_TCP,
Expand Down Expand Up @@ -96,7 +97,7 @@ Connection::SendKexInit()

const KexProposal proposal{
.kex_algorithms = "curve25519-sha256"sv,
.server_host_key_algorithms = host_key.GetAlgorithm(),
.server_host_key_algorithms = host_keys.GetAlgorithms(),
.encryption_algorithms_client_to_server = "[email protected]"sv,
.encryption_algorithms_server_to_client = "[email protected]"sv,
.mac_algorithms_client_to_server = "hmac-sha2-256,hmac-sha2-512"sv,
Expand Down Expand Up @@ -124,7 +125,7 @@ Connection::SendECDHKexInitReply(std::span<const std::byte> client_ephemeral_pub

const auto kex_host_key_length = s.PrepareLength();
const auto kex_host_key_mark = s.Mark();
host_key.SerializeKex(s);
host_key->SerializeKex(s);
s.CommitLength(kex_host_key_length);
const auto server_host_key_blob = s.Since(kex_host_key_mark);

Expand Down Expand Up @@ -159,7 +160,7 @@ Connection::SendECDHKexInitReply(std::span<const std::byte> client_ephemeral_pub
const auto hash = std::span{hash_buffer}.first(hashlen);

const auto signature_length = s.PrepareLength();
host_key.Sign(s, hash);
host_key->Sign(s, hash);
s.CommitLength(signature_length);

SendPacket(std::move(s));
Expand All @@ -180,6 +181,28 @@ Connection::HandleKexInit(std::span<const std::byte> payload)
{
client_kexinit = payload;

Deserializer d{payload};
d.ReadN(16); // cookie
d.ReadString(); // kex_algorithms
const auto server_host_key_algorithms = d.ReadString(); // server_host_key_algorithms
d.ReadString(); // encryption_algorithms_client_to_server
d.ReadString(); // encryption_algorithms_server_to_client
d.ReadString(); // mac_algorithms_client_to_server
d.ReadString(); // mac_algorithms_server_to_client
d.ReadString(); // compression_algorithms_client_to_server
d.ReadString(); // compression_algorithms_server_to_client
d.ReadString(); // languages_client_to_server
d.ReadString(); // languages_server_to_client
d.ReadBool(); // first_kex_packet_follows
d.ReadU32(); // reserved

host_key = host_keys.Choose(server_host_key_algorithms);
if (host_key == nullptr)
throw Disconnect{
DisconnectReasonCode::KEY_EXCHANGE_FAILED,
"No supported host key"sv,
};

SendKexInit();
}

Expand Down
7 changes: 5 additions & 2 deletions src/ssh/Connection.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <span>
#include <string>

class KeyList;
class Key;

namespace SSH {
Expand All @@ -22,7 +23,9 @@ class Cipher;

class Connection : BufferedSocketHandler
{
const Key &host_key;
const KeyList &host_keys;

const Key *host_key;

BufferedSocket socket;

Expand Down Expand Up @@ -56,7 +59,7 @@ protected:

public:
Connection(EventLoop &event_loop, UniqueSocketDescriptor fd,
const Key &_host_key);
const KeyList &_host_keys);
~Connection() noexcept;

auto &GetEventLoop() const noexcept {
Expand Down

0 comments on commit 9557241

Please sign in to comment.