diff --git a/meson.build b/meson.build index 34809cc..fda967f 100644 --- a/meson.build +++ b/meson.build @@ -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', diff --git a/src/Connection.cxx b/src/Connection.cxx index 8d536ce..a427960 100644 --- a/src/Connection.cxx +++ b/src/Connection.cxx @@ -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()) { diff --git a/src/Connection.hxx b/src/Connection.hxx index d6a64eb..d64f8f5 100644 --- a/src/Connection.hxx +++ b/src/Connection.hxx @@ -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 { diff --git a/src/Instance.cxx b/src/Instance.cxx index 48baa97..d8a3533 100644 --- a/src/Instance.cxx +++ b/src/Instance.cxx @@ -27,9 +27,9 @@ #include Instance::Instance(const Config &config, - std::unique_ptr _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 @@ -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()); diff --git a/src/Instance.hxx b/src/Instance.hxx index e907957..68831e3 100644 --- a/src/Instance.hxx +++ b/src/Instance.hxx @@ -4,6 +4,7 @@ #pragma once +#include "key/List.hxx" #include "event/Loop.hxx" #include "event/ShutdownListener.hxx" #include "event/SignalEvent.hxx" @@ -39,7 +40,7 @@ class Instance final const RootLogger logger; - std::unique_ptr host_key; + KeyList host_keys; #ifdef ENABLE_TRANSLATION const char *const translation_server; @@ -66,7 +67,7 @@ class Instance final public: Instance(const Config &config, - std::unique_ptr _host_key, + KeyList &&_host_key, UniqueSocketDescriptor spawner_socket); ~Instance() noexcept; diff --git a/src/Main.cxx b/src/Main.cxx index 0860038..7858665 100644 --- a/src/Main.cxx +++ b/src/Main.cxx @@ -45,24 +45,25 @@ LoadOptionalKeyFile(const char *path) return LoadKeyFile(fd); } -static std::unique_ptr -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::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::Generate{})); #ifdef HAVE_OPENSSL - return std::make_unique(ECDSAKey::Generate{}); -#else - // TODO - std::terminate(); + keys.Add(std::make_unique(ECDSAKey::Generate{})); #endif // HAVE_OPENSSL } + + return keys; } int @@ -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), }; diff --git a/src/key/List.cxx b/src/key/List.cxx new file mode 100644 index 0000000..3381b69 --- /dev/null +++ b/src/key/List.cxx @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright CM4all GmbH +// author: Max Kellermann + +#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) 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; +} diff --git a/src/key/List.hxx b/src/key/List.hxx new file mode 100644 index 0000000..da56b9a --- /dev/null +++ b/src/key/List.hxx @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright CM4all GmbH +// author: Max Kellermann + +#pragma once + +#include +#include +#include + +class Key; + +class KeyList { + std::map> 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) 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; +}; diff --git a/src/ssh/Connection.cxx b/src/ssh/Connection.cxx index e48aae0..a68150f 100644 --- a/src/ssh/Connection.cxx +++ b/src/ssh/Connection.cxx @@ -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" @@ -36,8 +37,8 @@ SerializeKex(Serializer &s, std::span 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, @@ -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 = "chacha20-poly1305@openssh.com"sv, .encryption_algorithms_server_to_client = "chacha20-poly1305@openssh.com"sv, .mac_algorithms_client_to_server = "hmac-sha2-256,hmac-sha2-512"sv, @@ -124,7 +125,7 @@ Connection::SendECDHKexInitReply(std::span 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); @@ -159,7 +160,7 @@ Connection::SendECDHKexInitReply(std::span 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)); @@ -180,6 +181,28 @@ Connection::HandleKexInit(std::span 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(); } diff --git a/src/ssh/Connection.hxx b/src/ssh/Connection.hxx index e9176ed..4a8b1a3 100644 --- a/src/ssh/Connection.hxx +++ b/src/ssh/Connection.hxx @@ -13,6 +13,7 @@ #include #include +class KeyList; class Key; namespace SSH { @@ -22,7 +23,9 @@ class Cipher; class Connection : BufferedSocketHandler { - const Key &host_key; + const KeyList &host_keys; + + const Key *host_key; BufferedSocket socket; @@ -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 {