mirror of
https://github.com/SerenityOS/serenity.git
synced 2026-11-07 04:29:30 -10:00
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:
1 parent
0315b8c33c
commit
5f37b60744
6 files changed
+139
-5
No files matched your search
@@ -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*
|
||||
```
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 };
|
||||
|
||||
|
||||
Reference in new issue
Block a user