Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Userland/Libraries/LibSSH/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ set(SOURCES
IdentificationString.cpp
KeyExchangeData.cpp
Peer.cpp
Session.cpp
)

serenity_lib(LibSSH ssh)
Expand Down
21 changes: 21 additions & 0 deletions Userland/Libraries/LibSSH/Session.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2026, Lucas Chollet <lucas.chollet@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#include "Session.h"

namespace SSH {

ErrorOr<Session> Session::create(u32 sender_channel_id, u32 window_size, u32 maximum_packet_size)
{
Session session;
session.local_channel_id = sender_channel_id;
session.sender_channel_id = sender_channel_id;
session.maximum_packet_size = maximum_packet_size;
session.window = TRY(ByteBuffer::create_zeroed(window_size));
return session;
}

}
25 changes: 25 additions & 0 deletions Userland/Libraries/LibSSH/Session.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2026, Lucas Chollet <lucas.chollet@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#pragma once

#include <AK/ByteBuffer.h>
#include <AK/Error.h>

namespace SSH {

// 6.1. Opening a Session
// https://datatracker.ietf.org/doc/html/rfc4254#section-6.1
struct Session {
static ErrorOr<Session> create(u32 sender_channel_id, u32 window_size, u32 maximum_packet_size);

u32 local_channel_id {};
u32 sender_channel_id {};
u32 maximum_packet_size {};
ByteBuffer window {};
};

}
210 changes: 207 additions & 3 deletions Userland/Services/SSHServer/SSHClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#include <AK/Format.h>
#include <AK/MemoryStream.h>
#include <AK/Random.h>
#include <LibCore/Account.h>
#include <LibCore/Command.h>
#include <LibCore/Socket.h>
#include <LibCrypto/Curves/Ed25519.h>
#include <LibCrypto/Curves/X25519.h>
Expand All @@ -22,7 +24,6 @@ namespace SSH::Server {

ErrorOr<void> SSHClient::handle_data(ByteBuffer& data)
{

switch (m_state) {
case State::Constructed:
return handle_protocol_version(data);
Expand All @@ -36,8 +37,11 @@ ErrorOr<void> SSHClient::handle_data(ByteBuffer& data)
return {};
case State::KeyExchanged:
return handle_service_request(TRY(unpack_generic_message(data)));
case State::WaitingForUserAuthentication:
return handle_user_authentication(TRY(unpack_generic_message(data)));
case State::Authentified:
return Error::from_string_literal("Draw the rest of the owl");
TRY(handle_generic_packet(TRY(unpack_generic_message(data))));
return {};
}
VERIFY_NOT_REACHED();
}
Expand Down Expand Up @@ -262,7 +266,7 @@ ErrorOr<void> SSHClient::handle_service_request(GenericMessage message)
// "

TRY(send_service_accept(service_name));
m_state = State::Authentified;
m_state = State::WaitingForUserAuthentication;
return {};
}

Expand All @@ -281,4 +285,204 @@ ErrorOr<void> SSHClient::send_service_accept(StringView service_name)
return {};
}

// 5. Authentication Requests
// https://datatracker.ietf.org/doc/html/rfc4252#section-5
ErrorOr<void> SSHClient::handle_user_authentication(GenericMessage message)
{
if (message.type != MessageID::USERAUTH_REQUEST)
return Error::from_string_literal("Expected packet of type USERAUTH_REQUEST");

auto username = TRY(decode_string(message.payload));
auto service_name = TRY(decode_string(message.payload));
auto method_name = TRY(decode_string(message.payload));

dbgln_if(SSH_DEBUG, "User authentication username: {:s}", username.bytes());
dbgln_if(SSH_DEBUG, "User authentication service_name: {:s}", service_name.bytes());
dbgln_if(SSH_DEBUG, "User authentication method name: {:s}", method_name.bytes());

if (username != TRY(Core::Account::self(Core::Account::Read::PasswdOnly)).username())
return Error::from_string_literal("Can't authenticate for another user account");

if (method_name == "none"sv.bytes()) {
// FIXME: Implement proper authentication!!!
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Add dbgln("MAJOR SECURITY HOLE: Accepting without auth; add actual auth support soon"); maybe?


m_state = State::Authentified;
TRY(send_user_authentication_success());

// FIXME: Also send a cool banner :^)

return {};
}

return Error::from_string_literal("Unsupported userauth method");
}

// 5.1. Responses to Authentication Requests
// https://datatracker.ietf.org/doc/html/rfc4252#section-5.1
ErrorOr<void> SSHClient::send_user_authentication_success()
{
AllocatingMemoryStream stream;
TRY(stream.write_value(MessageID::USERAUTH_SUCCESS));
TRY(write_packet(TRY(stream.read_until_eof())));
return {};
}

ErrorOr<void> SSHClient::handle_generic_packet(GenericMessage&& message)
{
switch (message.type) {
case MessageID::CHANNEL_OPEN:
return handle_channel_open_message(message);
case MessageID::CHANNEL_REQUEST:
return handle_channel_request(message);
default:
dbgln_if(SSH_DEBUG, "Unexpected packet: {}", to_underlying(message.type));
return Error::from_string_literal("Unexpected packet type");
}
VERIFY_NOT_REACHED();
}

// 5.1. Opening a Channel
// https://datatracker.ietf.org/doc/html/rfc4254#section-5.1
ErrorOr<void> SSHClient::handle_channel_open_message(GenericMessage& message)
{
auto channel_type = TRY(decode_string(message.payload));
u32 sender_channel_id = TRY(message.payload.read_value<NetworkOrdered<u32>>());
u32 initial_window_size = TRY(message.payload.read_value<NetworkOrdered<u32>>());
u32 maximum_packet_size = TRY(message.payload.read_value<NetworkOrdered<u32>>());

dbgln_if(SSH_DEBUG, "Channel open request with: {:s} - {} - {} - {}",
channel_type.bytes(), sender_channel_id, initial_window_size, maximum_packet_size);

if (channel_type != "session"sv.bytes())
return Error::from_string_literal("Unexpected channel type");

m_sessions.empend(TRY(Session::create(sender_channel_id, initial_window_size, maximum_packet_size)));

TRY(send_channel_open_confirmation(m_sessions.last()));

return {};
}

// 5.1. Opening a Channel
// https://datatracker.ietf.org/doc/html/rfc4254#section-5.1
ErrorOr<void> SSHClient::send_channel_open_confirmation(Session const& session)
{
AllocatingMemoryStream stream;

// byte SSH_MSG_CHANNEL_OPEN_CONFIRMATION
// uint32 recipient channel
// uint32 sender channel
// uint32 initial window size
// uint32 maximum packet size

TRY(stream.write_value(MessageID::CHANNEL_OPEN_CONFIRMATION));
// "The 'recipient channel' is the channel number given in the original
// open request, and 'sender channel' is the channel number allocated by
// the other side."
TRY(stream.write_value<NetworkOrdered<u32>>(session.sender_channel_id));
TRY(stream.write_value<NetworkOrdered<u32>>(session.local_channel_id));

TRY(stream.write_value<NetworkOrdered<u32>>(session.window.size()));
TRY(stream.write_value<NetworkOrdered<u32>>(session.maximum_packet_size));

TRY(write_packet(TRY(stream.read_until_eof())));
return {};
}

ErrorOr<Session*> SSHClient::find_session(u32 sender_channel_id)
{
for (auto& session : m_sessions) {
if (session.sender_channel_id == sender_channel_id)
return &session;
}
return Error::from_string_literal("Session not found");
}

// 5.4. Channel-Specific Requests
// https://datatracker.ietf.org/doc/html/rfc4254#section-5.4
ErrorOr<void> SSHClient::handle_channel_request(GenericMessage& message)
{
auto recipient_channel_id = TRY(message.payload.read_value<NetworkOrdered<u32>>());
auto request_type = TRY(decode_string(message.payload));
auto want_reply = TRY(message.payload.read_value<bool>());

auto& session = *TRY(find_session(recipient_channel_id));

dbgln_if(SSH_DEBUG, "CHANNEL_REQUEST id({}): {:s}", session.local_channel_id, request_type.bytes());

if (request_type == "env"sv.bytes() && !want_reply) {
dbgln("FIXME: Ignored channel request: {:s}", request_type.bytes());
return {};
}

if (request_type == "exec"sv.bytes()) {
auto command = TRY(decode_string(message.payload));

// FIXME: This is a naive implementation, we should stream the result back
// to the user and not block the event loop during the execution of
// the command.
// We should also use the user's shell rather than hardcoding it.

#ifdef AK_OS_SERENITY
auto shell = "/bin/Shell"sv;
#else
auto shell = "/bin/sh"sv;
#endif

Vector<ByteString> args;
args.append(shell);
args.append("-c");
args.append(ByteString(command.bytes()));

Vector<char const*> raw_args;
raw_args.ensure_capacity(args.size() + 1);
for (auto& arg : args)
raw_args.append(arg.characters());

raw_args.append(nullptr);

auto child = TRY(Core::Command::create(shell, raw_args.data()));
auto output = TRY(child->read_all());
auto status = TRY(child->status());

if (status != Core::Command::ProcessResult::DoneWithZeroExitCode)
return Error::from_string_literal("Unable to run command");

TRY(send_channel_success_message(session));
TRY(send_channel_data(session, output.standard_output));
TRY(send_channel_close(session));
return {};
}

return Error::from_string_literal("Unsupported channel request");
}

ErrorOr<void> SSHClient::send_channel_success_message(Session const& session)
{
AllocatingMemoryStream stream;
TRY(stream.write_value(MessageID::CHANNEL_SUCCESS));
TRY(stream.write_value<NetworkOrdered<u32>>(session.local_channel_id));
TRY(write_packet(TRY(stream.read_until_eof())));
return {};
}

ErrorOr<void> SSHClient::send_channel_data(Session const& session, ByteBuffer const& data)
{
AllocatingMemoryStream stream;
TRY(stream.write_value(MessageID::CHANNEL_DATA));
TRY(stream.write_value<NetworkOrdered<u32>>(session.local_channel_id));
TRY(encode_string(stream, data));
TRY(write_packet(TRY(stream.read_until_eof())));
return {};
}

ErrorOr<void> SSHClient::send_channel_close(Session const& session)
{
AllocatingMemoryStream stream;
TRY(stream.write_value(MessageID::CHANNEL_CLOSE));
TRY(stream.write_value<NetworkOrdered<u32>>(session.local_channel_id));
TRY(write_packet(TRY(stream.read_until_eof())));
return {};
}

} // SSHServer
17 changes: 17 additions & 0 deletions Userland/Services/SSHServer/SSHClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <LibCore/Forward.h>
#include <LibSSH/KeyExchangeData.h>
#include <LibSSH/Peer.h>
#include <LibSSH/Session.h>

namespace SSH::Server {

Expand Down Expand Up @@ -37,6 +38,7 @@ class SSHClient : public Peer {
WaitingForKeyExchange,
WaitingForNewKeysMessage,
KeyExchanged,
WaitingForUserAuthentication,
Authentified,
};

Expand All @@ -53,10 +55,25 @@ class SSHClient : public Peer {
ErrorOr<void> handle_service_request(GenericMessage data);
ErrorOr<void> send_service_accept(StringView);

ErrorOr<void> handle_user_authentication(GenericMessage data);
ErrorOr<void> send_user_authentication_success();

ErrorOr<void> handle_generic_packet(GenericMessage&&);

ErrorOr<void> handle_channel_open_message(GenericMessage&);
ErrorOr<void> send_channel_open_confirmation(Session const&);
ErrorOr<void> handle_channel_request(GenericMessage&);
ErrorOr<void> send_channel_success_message(Session const&);
ErrorOr<void> send_channel_data(Session const&, ByteBuffer const&);
ErrorOr<void> send_channel_close(Session const&);
ErrorOr<Session*> find_session(u32 sender_channel_id);

State m_state { State::Constructed };
Core::TCPSocket& m_tcp_socket;

KeyExchangeData m_key_exchange_data {};

Vector<Session> m_sessions;
};

} // SSHServer
6 changes: 4 additions & 2 deletions Userland/Services/SSHServer/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ static constexpr auto DEFAULT_PORT = 22;

ErrorOr<int> serenity_main(Main::Arguments args)
{
TRY(Core::System::pledge("stdio accept inet unix"));
TRY(Core::System::pledge("stdio accept inet unix rpath proc exec"));
TRY(Core::System::unveil("/etc/passwd", "r"));
TRY(Core::System::unveil("/bin/Shell", "rx"));
TRY(Core::System::unveil(nullptr, nullptr));

Optional<u32> port {};
Expand Down Expand Up @@ -54,6 +56,6 @@ ErrorOr<int> serenity_main(Main::Arguments args)

outln("Listening on {}:{}", tcp_server->local_address().value(), tcp_server->local_port());

TRY(Core::System::pledge("stdio accept"));
TRY(Core::System::pledge("stdio accept rpath proc exec"));
return loop.exec();
}
Loading