SSHServer+LibSSH: Read the host keys from /etc/ssh/

This will allow SSHServer to have a stable private key without using
--unsafe-stub-private-key. We already had support for reading public
keys and this commit adds support for openssh's proprietary private key
file format. It is unfortunately not standardized, and while I
considered using a custom file format I think that being compatible with
`ssh-keygen` is better for the moment.
This commit is contained in:
Lucas Chollet committed 2026-08-22 21:49:12 +02:00
1 parent 0315b8c33c
commit 5f37b60744
6 files changed
+139 -5

No files matched your search

+30 -1
View File
@@ -23,10 +23,39 @@ ssh-keygen -t ed25519 -f ~/.ssh/serenity_ed25519
To allow SSH access, you need to install your public key into the SerenityOS image so the SSH server can authenticate your client.
The SSH server looks for authorized keys in `/home/anon/.config/ssh/authorized_keys`.
The SSH server looks for authorized keys in `$HOME/.config/ssh/authorized_keys`.
The simplest way to do it is to add these lines to `sync-local.sh`:
```bash
mkdir -p mnt/home/anon/.config/ssh/
cat path/to/serenity_ed25519.pub >> mnt/home/anon/.config/ssh/authorized_keys
```
## Server Identity
Every SSH server has its own identity, represented by a host key pair.
If this identity changes between two connections, clients will detect it and abort as this is one of the symptoms of a man-in-the-middle attack.
SSHServer only supports generating ephemeral keys, so this will cause clients to warn about the server's changed identity every time the server is restarted.
To fix this issue, we can manually generate a key and provide it to the server.
### Generating a Key Pair
The steps are similar to those in the section above, first generate a key pair.
This key doesn't have to live in your `.ssh` directory, so I recommend to put it somewhere else.
As an example:
```bash
ssh-keygen -t ed25519 -f path/to/serenity/ssh_keys/host_ed25519
```
### Installing the key pair in SerenityOS
Then add this to `sync-local.sh` to copy the key pair to SerenityOS's image.
```bash
mkdir -p mnt/etc/ssh/
cp path/to/serenity/ssh_keys/host_ed25519* mnt/etc/ssh/
chown root:root mnt/etc/ssh/host_ed25519*
chmod u=r,g=,o= mnt/etc/ssh/host_ed25519*
```
+18
View File
@@ -141,3 +141,21 @@ TEST_CASE(typed_blob_from_string)
"\x20\x11\xe4\xab\xa4\xa0\xbf\xf3\x59\xfc\xc9\x82\xf0"
"\x01\xff\xd3\xdb\xa3\x62"sv.bytes());
}
TEST_CASE(typed_blob_from_openssh_private_key)
{
static constexpr auto private_key = R"(-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACAk5SqGXG9YD3p4TFQL+VgbpbWJSdrArrtYaem2zQ3JsAAAAJi1cqI0tXKi
NAAAAAtzc2gtZWQyNTUxOQAAACAk5SqGXG9YD3p4TFQL+VgbpbWJSdrArrtYaem2zQ3JsA
AAAEA1dZAYEAZ0Yv/uh+u0pxJyWMNs9Lu6eOnWUlNK6fy7DCTlKoZcb1gPenhMVAv5WBul
tYlJ2sCuu1hp6bbNDcmwAAAAEXRoaXMgaXMgYSBjb21tZW50AQIDBA==
-----END OPENSSH PRIVATE KEY-----
)"sv;
auto typed_blob = TRY_OR_FAIL(SSH::TypedBlob::read_from_openssh_private_key(private_key));
EXPECT_EQ(typed_blob.type, SSH::TypedBlob::Type::SSH_ED25519);
EXPECT_EQ(typed_blob.key.bytes(), "\x35\x75\x90\x18\x10\x06\x74\x62\xFF\xEE\x87\xEB\xB4"
"\xA7\x12\x72\x58\xC3\x6C\xF4\xBB\xBA\x78\xE9\xD6\x52"
"\x53\x4A\xE9\xFC\xBB\x0C"sv.bytes());
}
+57
View File
@@ -7,6 +7,8 @@
#include "DataTypes.h"
#include <AK/Base64.h>
#include <AK/Endian.h>
#include <LibCrypto/ASN1/PEM.h>
#include <LibCrypto/Curves/Ed25519.h>
namespace SSH {
@@ -148,4 +150,59 @@ ErrorOr<TypedBlob> TypedBlob::read_from_string(StringView input)
return blob;
}
// The format is documented at [1] source code at [2]
// [1] https://coolaj86.com/articles/the-openssh-private-key-format/
// [2] https://github.com/openssh/openssh-portable/blob/master/sshkey.c
ErrorOr<TypedBlob> TypedBlob::read_from_openssh_private_key(StringView input)
{
auto buffer = Crypto::decode_pem(input.bytes());
FixedMemoryStream inner_stream { buffer.bytes() };
static constexpr auto magic_bytes = "openssh-key-v1\0"sv;
Array<u8, magic_bytes.length()> data;
TRY(inner_stream.read_until_filled(data));
if (data != magic_bytes.bytes())
return Error::from_string_literal("Unsupported private key file");
auto cipher_name = TRY(decode_string(inner_stream));
if (cipher_name != "none"sv.bytes())
return Error::from_string_literal("Unsupported cipher name");
auto kdfname = TRY(decode_string(inner_stream));
if (kdfname != "none"sv.bytes())
return Error::from_string_literal("Unsupported kdf name");
auto number_of_kdf = TRY(inner_stream.read_value<NetworkOrdered<u32>>());
if (number_of_kdf != 0)
return Error::from_string_literal("Key has kdf");
auto number_of_keys = TRY(inner_stream.read_value<NetworkOrdered<u32>>());
if (number_of_keys != 1)
return Error::from_string_literal("File has more than one key");
auto public_key = TRY(TypedBlob::decode(inner_stream));
[[maybe_unused]] auto private_key_length = TRY(inner_stream.read_value<NetworkOrdered<u32>>());
[[maybe_unused]] u64 maybe_checksum = TRY(inner_stream.read_value<NetworkOrdered<u64>>());
auto key_type = TRY(decode_string(inner_stream));
if (key_type != type_to_string(public_key.type).bytes())
return Error::from_string_literal("Invalid private key type");
auto embedded_public_key = TRY(decode_string(inner_stream));
if (embedded_public_key != public_key.key)
return Error::from_string_literal("Mismatch between public and embedded public key");
auto private_key = TRY(decode_string(inner_stream));
if (private_key.span().slice(32) != public_key.key)
return Error::from_string_literal("Mismatch between public and second embedded public key");
if (TRY(Crypto::Curves::Ed25519::generate_public_key(private_key.bytes().trim(32))) != public_key.key)
return Error::from_string_literal("Mismatch between public and private key");
// The file still has a comment + padding.
return TypedBlob { .type = Type::SSH_ED25519, .key = TRY(ByteBuffer::copy(private_key.bytes().trim(32))) };
}
} // SSH
+1
View File
@@ -33,6 +33,7 @@ struct TypedBlob {
static ErrorOr<TypedBlob> decode(FixedMemoryStream& stream);
static ErrorOr<TypedBlob> read_from_string(StringView);
static ErrorOr<TypedBlob> read_from_openssh_private_key(StringView);
Type type { Type::SSH_ED25519 };
ByteBuffer key {};
@@ -48,11 +48,41 @@ static ErrorOr<Vector<TypedBlob>> read_blobs_from_file(NonnullOwnPtr<Core::File>
return blobs;
}
ErrorOr<void> ServerConfiguration::load_server_keys_from_file() const
{
auto private_key_file = TRY(Core::File::open("/etc/ssh/host_ed25519"sv, Core::File::OpenMode::Read));
auto content = TRY(private_key_file->read_until_eof());
auto private_key = TRY(TypedBlob::read_from_openssh_private_key(content));
VERIFY(private_key.type == TypedBlob::Type::SSH_ED25519);
auto public_key_file = TRY(Core::File::open("/etc/ssh/host_ed25519.pub"sv, Core::File::OpenMode::Read));
auto blobs = TRY(read_blobs_from_file(move(public_key_file)));
VERIFY(blobs.size() == 1);
auto public_key = blobs.take_first();
VERIFY(public_key.type == TypedBlob::Type::SSH_ED25519);
auto computed_public_key = TRY(Crypto::Curves::Ed25519::generate_public_key(private_key.key));
if (computed_public_key != public_key.key)
return Error::from_string_literal("Corrupted host key");
m_ssh_ed25519_server_private_key = move(private_key);
m_ssh_ed25519_server_public_key = move(public_key);
return {};
}
void ServerConfiguration::ensure_ssh_ed25519_keys() const
{
if (m_ssh_ed25519_server_public_key.key.is_empty()
|| m_ssh_ed25519_server_private_key.key.is_empty()) {
if (!m_use_unsafe_stubbed_private_key) {
auto maybe_error = load_server_keys_from_file();
if (!maybe_error.is_error())
return;
dbgln("Unable to use the host key from /etc/ssh/: {}", maybe_error.error());
}
if (m_use_unsafe_stubbed_private_key) {
auto stub = MUST(ByteBuffer ::create_uninitialized(32));
stub.bytes().fill(0x42);
@@ -12,9 +12,6 @@
namespace SSH::Server {
// FIXME: This generates a brand new host key every single time
// a server is started. We should store in on the disk to
// make it persistent.
class ServerConfiguration {
public:
static ServerConfiguration& the();
@@ -40,9 +37,11 @@ public:
private:
ErrorOr<StringView> user_authorized_keys_file(Core::Account const&) const;
void ensure_ssh_ed25519_keys() const;
ErrorOr<void> load_server_keys_from_file() const;
mutable TypedBlob m_ssh_ed25519_server_public_key;
mutable TypedBlob m_ssh_ed25519_server_private_key;
void ensure_ssh_ed25519_keys() const;
bool m_use_unsafe_stubbed_private_key { false };