diff options
Diffstat (limited to 'plugins/ssh')
| -rw-r--r-- | plugins/ssh/CMakeLists.txt | 35 | ||||
| -rw-r--r-- | plugins/ssh/ssh.c | 728 | ||||
| -rw-r--r-- | plugins/ssh/ssh_config.c | 132 | ||||
| -rw-r--r-- | plugins/ssh/ssh_keys.c | 146 |
4 files changed, 1041 insertions, 0 deletions
diff --git a/plugins/ssh/CMakeLists.txt b/plugins/ssh/CMakeLists.txt new file mode 100644 index 0000000..9c656b8 --- /dev/null +++ b/plugins/ssh/CMakeLists.txt @@ -0,0 +1,35 @@ +add_library(hybbx_plugin_ssh STATIC + ssh.c + ssh_config.c + ssh_keys.c +) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(LIBSSH REQUIRED IMPORTED_TARGET libssh) + +find_package(Threads REQUIRED) + +target_include_directories(hybbx_plugin_ssh + PRIVATE ${CMAKE_SOURCE_DIR}/include +) + +target_compile_definitions(hybbx_plugin_ssh + PRIVATE HYBBX_PLUGIN_BUILD +) + +target_link_libraries(hybbx_plugin_ssh + PRIVATE + hybbx_core + Threads::Threads + PkgConfig::LIBSSH +) + +hybbx_link_plugin_instances(hybbx_plugin_ssh HYBBX_HAVE_PLUGIN_SSH) +hybbx_apply_hardening(hybbx_plugin_ssh) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_ssh + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/ssh/ssh.c b/plugins/ssh/ssh.c new file mode 100644 index 0000000..442653d --- /dev/null +++ b/plugins/ssh/ssh.c @@ -0,0 +1,728 @@ +/* + * ssh — libssh transport plugin (port 3232). INI: [transport.ssh]. + * + * Links dynamically against libssh (LGPL-2.1+). See share/THIRD_PARTY_NOTICES.txt. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/socket.h" +#include "hybbx/security_ban.h" +#include "hybbx/ssh.h" +#include "hybbx/traffic.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <libssh/callbacks.h> +#include <libssh/libssh.h> +#include <libssh/server.h> + +#include <arpa/inet.h> +#include <errno.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <poll.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define SSH_CLIENT_POLL_MS 30000 +#define SSH_READ_BUF 512 + +typedef struct ssh_client { + int fd; + ssh_session session; + ssh_channel channel; + ssh_event event; + hybbx_session_t *hbx_session; + int ssh_authenticated; + int auth_attempts; + hybbx_result_t last_rc; + volatile int shell_ready; + struct ssh_server_callbacks_struct server_cb; + struct ssh_channel_callbacks_struct channel_cb; +} ssh_client_t; + +typedef struct ssh_client_ctx { + ssh_client_t *client; +} ssh_client_ctx_t; + +extern const hybbx_transport_plugin_t hybbx_plugin_ssh; + +static hybbx_service_t *g_service; +static hybbx_ssh_config_t g_config; +static char g_hostkey_path[HYBBX_PATH_MAX]; +static pthread_t g_accept_thread; +static int g_listen_v4 = -1; +static int g_listen_v6 = -1; +static volatile int g_ssh_running = 0; + +static hybbx_result_t ssh_plugin_stop(void); + +static int set_socket_options(int fd, int family) +{ + int on = 1; + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0) { + return -1; + } + + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) != 0) { + return -1; + } + + hybbx_socket_nosigpipe(fd); + +#ifdef IPV6_V6ONLY + if (family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) != 0) { + return -1; + } + } +#else + (void)family; +#endif + + return 0; +} + +static int create_listen_socket(int family, const char *bind_addr, unsigned port) +{ + int fd; + int rc; + + fd = socket(family, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + if (set_socket_options(fd, family) != 0) { + close(fd); + return -1; + } + + if (family == AF_INET6) { + struct sockaddr_in6 addr6; + + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons((uint16_t)port); + + if (inet_pton(AF_INET6, bind_addr, &addr6.sin6_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)); + } else { + struct sockaddr_in addr4; + + memset(&addr4, 0, sizeof(addr4)); + addr4.sin_family = AF_INET; + addr4.sin_port = htons((uint16_t)port); + + if (inet_pton(AF_INET, bind_addr, &addr4.sin_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)); + } + + if (rc != 0) { + close(fd); + return -1; + } + + if (listen(fd, 16) != 0) { + close(fd); + return -1; + } + + return fd; +} + +/* + * SSH wire authentication only — accepts any username/password so the client + * can open a shell channel. HyBBX login (guest auto-login or /login prompt) + * follows [auth] in hybbx.ini, same as telnet. + */ +static int ssh_auth_password(ssh_session session, const char *user, + const char *password, void *userdata) +{ + ssh_client_t *client = (ssh_client_t *)userdata; + + (void)session; + (void)user; + (void)password; + + if (client == NULL) { + return SSH_AUTH_DENIED; + } + + client->auth_attempts++; + client->ssh_authenticated = 1; + return SSH_AUTH_SUCCESS; +} + +static hybbx_result_t ssh_plugin_stop(void); + +static int ssh_channel_pty_request(ssh_session session, ssh_channel channel, + const char *term, int cols, int rows, + int py, int px, void *userdata) +{ + (void)session; + (void)channel; + (void)term; + (void)cols; + (void)rows; + (void)py; + (void)px; + (void)userdata; + return SSH_OK; +} + +static int ssh_channel_shell_request(ssh_session session, ssh_channel channel, + void *userdata) +{ + ssh_client_t *client = (ssh_client_t *)userdata; + + (void)session; + (void)channel; + + if (client == NULL) { + return SSH_ERROR; + } + + client->shell_ready = 1; + return SSH_OK; +} + +static ssh_channel ssh_channel_open(ssh_session session, void *userdata) +{ + ssh_client_t *client = (ssh_client_t *)userdata; + + (void)session; + + if (client == NULL) { + return NULL; + } + + client->channel = ssh_channel_new(session); + if (client->channel == NULL) { + return NULL; + } + + memset(&client->channel_cb, 0, sizeof(client->channel_cb)); + client->channel_cb.userdata = client; + client->channel_cb.channel_pty_request_function = ssh_channel_pty_request; + client->channel_cb.channel_shell_request_function = ssh_channel_shell_request; + ssh_callbacks_init(&client->channel_cb); + ssh_set_channel_callbacks(client->channel, &client->channel_cb); + + return client->channel; +} + +static hybbx_result_t ssh_plugin_write(hybbx_session_t *session, + const char *data, size_t len) +{ + ssh_client_t *client; + size_t i; + int rc; + + if (session == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + client = (ssh_client_t *)session->transport_data; + if (client == NULL || client->channel == NULL) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < len; i++) { + char out[2]; + size_t out_len = 1; + + out[0] = data[i]; + if (data[i] == '\n' && (i == 0 || data[i - 1] != '\r')) { + out[0] = '\r'; + out[1] = '\n'; + out_len = 2; + } + + rc = ssh_channel_write(client->channel, out, out_len); + if (rc < 0) { + return HYBBX_ERR_IO; + } + } + + return HYBBX_OK; +} + +static void ssh_send_busy(ssh_channel channel) +{ + static const char msg[] = "All nodes in use. Try later.\r\n"; + + if (channel == NULL) { + return; + } + + (void)ssh_channel_write(channel, msg, sizeof(msg) - 1u); +} + +static int ssh_wait_auth_and_shell(ssh_client_t *client) +{ + int ticks = 0; + + while (g_ssh_running) { + if (client->ssh_authenticated && client->channel != NULL && + client->shell_ready) { + return 0; + } + + if (client->auth_attempts >= 5 || ticks >= 300) { + return -1; + } + + if (ssh_event_dopoll(client->event, 100) == SSH_ERROR) { + return -1; + } + ticks++; + } + + return -1; +} + +static void ssh_on_user_data(const uint8_t *data, size_t len, void *ctx) +{ + ssh_client_ctx_t *cctx = (ssh_client_ctx_t *)ctx; + hybbx_result_t rc; + + if (cctx == NULL || cctx->client == NULL || + cctx->client->hbx_session == NULL || data == NULL || len == 0) { + return; + } + + rc = hybbx_session_handle_input(cctx->client->hbx_session, data, len); + if (rc != HYBBX_OK) { + cctx->client->last_rc = rc; + } +} + +static int ssh_plugin_service_request(ssh_session session, const char *service, + void *userdata) +{ + (void)session; + (void)userdata; + + if (service == NULL) { + return SSH_ERROR; + } + + if (strcmp(service, "ssh-userauth") == 0 || + strcmp(service, "ssh-connection") == 0) { + return SSH_OK; + } + + return SSH_ERROR; +} + +static void *ssh_client_thread(void *arg) +{ + ssh_client_t *client = (ssh_client_t *)arg; + ssh_bind sshbind = NULL; + ssh_client_ctx_t cctx; + uint8_t buf[SSH_READ_BUF]; + hybbx_result_t rc; + char remote[64]; + + if (client == NULL || g_service == NULL) { + free(client); + return NULL; + } + + remote[0] = '\0'; + (void)hybbx_socket_peer_name(client->fd, remote, sizeof(remote)); + + sshbind = ssh_bind_new(); + if (sshbind == NULL) { + goto cleanup; + } + + if (ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_HOSTKEY, + g_hostkey_path) != SSH_OK) { + hybbx_log_warn("[ssh] host key load failed: %s", g_hostkey_path); + goto cleanup; + } + + client->session = ssh_new(); + if (client->session == NULL) { + goto cleanup; + } + + if (ssh_bind_accept_fd(sshbind, client->session, client->fd) != SSH_OK) { + goto cleanup; + } + + memset(&client->server_cb, 0, sizeof(client->server_cb)); + client->server_cb.userdata = client; + client->server_cb.auth_password_function = ssh_auth_password; + client->server_cb.service_request_function = ssh_plugin_service_request; + client->server_cb.channel_open_request_session_function = ssh_channel_open; + ssh_callbacks_init(&client->server_cb); + ssh_set_server_callbacks(client->session, &client->server_cb); + ssh_set_auth_methods(client->session, SSH_AUTH_METHOD_PASSWORD); + + if (ssh_server_init_kex(client->session) != SSH_OK) { + goto cleanup; + } + + if (ssh_handle_key_exchange(client->session) != SSH_OK) { + goto cleanup; + } + + client->event = ssh_event_new(); + if (client->event == NULL) { + goto cleanup; + } + + if (ssh_event_add_session(client->event, client->session) != SSH_OK) { + goto cleanup; + } + + if (ssh_wait_auth_and_shell(client) != 0) { + goto cleanup; + } + + rc = hybbx_session_open(g_service, &hybbx_plugin_ssh, client, + &client->hbx_session); + if (rc != HYBBX_OK || client->hbx_session == NULL) { + if (rc == HYBBX_ERR_BUSY) { + ssh_send_busy(client->channel); + } + goto cleanup; + } + + if (remote[0] != '\0') { + (void)hybbx_session_set_remote(client->hbx_session, remote); + } + + /* + * Telnet clients echo locally when the server sends WONT ECHO; SSH PTY + * clients do not. Mirror telnet UX by enabling HyBBX input echo unless + * [traffic] input_echo=yes already turned it on. + */ + { + const hybbx_traffic_config_t *traffic = hybbx_traffic_config_get(); + + if (traffic == NULL || !traffic->input_echo) { + (void)hybbx_session_set_input_echo(client->hbx_session, 1); + } + } + + cctx.client = client; + client->last_rc = HYBBX_OK; + + while (g_ssh_running && ssh_channel_is_open(client->channel)) { + int n; + int ev; + + ev = ssh_event_dopoll(client->event, SSH_CLIENT_POLL_MS); + if (ev == SSH_ERROR) { + break; + } + + if (ev == 0) { + rc = hybbx_session_tick(client->hbx_session); + if (rc == HYBBX_SESSION_END) { + client->last_rc = HYBBX_SESSION_END; + break; + } + } + + while ((n = ssh_channel_read(client->channel, buf, sizeof(buf), 0)) > 0) { + ssh_on_user_data((const uint8_t *)buf, (size_t)n, &cctx); + if (client->last_rc == HYBBX_SESSION_END) { + break; + } + } + if (client->last_rc == HYBBX_SESSION_END) { + break; + } + if (n < 0 && !ssh_channel_is_open(client->channel)) { + break; + } + + rc = hybbx_session_tick(client->hbx_session); + if (rc == HYBBX_SESSION_END) { + client->last_rc = HYBBX_SESSION_END; + break; + } + } + +cleanup: + if (client->hbx_session != NULL) { + hybbx_session_close(client->hbx_session); + client->hbx_session = NULL; + } + + if (client->channel != NULL) { + if (ssh_channel_is_open(client->channel)) { + ssh_channel_send_eof(client->channel); + ssh_channel_close(client->channel); + } + ssh_channel_free(client->channel); + client->channel = NULL; + } + + if (client->event != NULL) { + ssh_event_free(client->event); + client->event = NULL; + } + + if (client->session != NULL) { + ssh_disconnect(client->session); + ssh_free(client->session); + client->session = NULL; + } + + if (sshbind != NULL) { + ssh_bind_free(sshbind); + sshbind = NULL; + } + + if (client->fd >= 0) { + client->fd = -1; + } + + free(client); + return NULL; +} + +static void accept_client(int client_fd) +{ + ssh_client_t *client; + pthread_t thread; + pthread_attr_t attr; + + client = calloc(1, sizeof(*client)); + if (client == NULL) { + close(client_fd); + return; + } + + client->fd = client_fd; + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + + if (pthread_create(&thread, &attr, ssh_client_thread, client) != 0) { + close(client_fd); + free(client); + } + + pthread_attr_destroy(&attr); +} + +static void *ssh_accept_thread(void *arg) +{ + (void)arg; + + while (g_ssh_running) { + struct pollfd fds[2]; + nfds_t nfds = 0; + int i; + int ready; + + memset(fds, 0, sizeof(fds)); + + if (g_listen_v4 >= 0) { + fds[nfds].fd = g_listen_v4; + fds[nfds].events = POLLIN; + nfds++; + } + + if (g_listen_v6 >= 0) { + fds[nfds].fd = g_listen_v6; + fds[nfds].events = POLLIN; + nfds++; + } + + if (nfds == 0) { + break; + } + + ready = poll(fds, nfds, 500); + if (ready < 0) { + if (errno == EINTR) { + continue; + } + break; + } + + if (ready == 0) { + continue; + } + + for (i = 0; i < (int)nfds; i++) { + if (fds[i].revents & POLLIN) { + int client_fd = accept(fds[i].fd, NULL, NULL); + + if (client_fd >= 0) { + if (!hybbx_security_ban_accept_fd(client_fd)) { + close(client_fd); + continue; + } + accept_client(client_fd); + } + } + } + } + + return NULL; +} + +static hybbx_result_t ssh_plugin_init(hybbx_service_t *service) +{ + int rc; + + g_service = service; + + rc = ssh_init(); + if (rc < 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static void ssh_plugin_shutdown(void) +{ + ssh_plugin_stop(); + ssh_finalize(); +} + +static hybbx_result_t ssh_plugin_start(const char *config) +{ + char keys_resolved[HYBBX_PATH_MAX]; + hybbx_result_t rc; + + if (g_ssh_running) { + return HYBBX_ERR_BUSY; + } + + rc = hybbx_ssh_config_parse(config, &g_config); + if (rc != HYBBX_OK) { + return rc; + } + + if (hybbx_path_resolve(keys_resolved, sizeof(keys_resolved), + g_config.hostkey_dir) != HYBBX_OK) { + hybbx_strlcpy(keys_resolved, g_config.hostkey_dir, + sizeof(keys_resolved)); + } + + rc = hybbx_ssh_keys_ensure(keys_resolved, g_hostkey_path, + sizeof(g_hostkey_path)); + if (rc != HYBBX_OK) { + return rc; + } + + g_listen_v4 = -1; + g_listen_v6 = -1; + + if (g_config.ipv4) { + g_listen_v4 = create_listen_socket(AF_INET, g_config.bind_v4, + g_config.port); + if (g_listen_v4 < 0) { + hybbx_socket_log_bind_failure("ssh", g_config.bind_v4, + g_config.port); + return HYBBX_ERR_IO; + } + } + + if (g_config.ipv6) { + g_listen_v6 = create_listen_socket(AF_INET6, g_config.bind_v6, + g_config.port); + if (g_listen_v6 < 0) { + hybbx_log_warn("[ssh] IPv6 bind [%s]:%u skipped (%s)", + g_config.bind_v6, g_config.port, strerror(errno)); + } + } + + if (g_listen_v4 < 0 && g_listen_v6 < 0) { + return HYBBX_ERR_IO; + } + + g_ssh_running = 1; + + if (pthread_create(&g_accept_thread, NULL, ssh_accept_thread, NULL) != 0) { + g_ssh_running = 0; + if (g_listen_v4 >= 0) { + close(g_listen_v4); + g_listen_v4 = -1; + } + if (g_listen_v6 >= 0) { + close(g_listen_v6); + g_listen_v6 = -1; + } + return HYBBX_ERR_IO; + } + + { + char msg[256]; + size_t pos = 0; + + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, "[ssh] listening"); + if (g_listen_v4 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv4 %s:%u", + g_config.bind_v4, g_config.port); + } + if (g_listen_v6 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv6 [%s]:%u", + g_config.bind_v6, g_config.port); + } + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " (libssh)"); + hybbx_log_info("%s", msg); + } + + return HYBBX_OK; +} + +static hybbx_result_t ssh_plugin_stop(void) +{ + if (!g_ssh_running) { + return HYBBX_OK; + } + + g_ssh_running = 0; + + if (g_listen_v4 >= 0) { + shutdown(g_listen_v4, SHUT_RDWR); + close(g_listen_v4); + g_listen_v4 = -1; + } + + if (g_listen_v6 >= 0) { + shutdown(g_listen_v6, SHUT_RDWR); + close(g_listen_v6); + g_listen_v6 = -1; + } + + pthread_join(g_accept_thread, NULL); + hybbx_log_info("[ssh] stop"); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_ssh = { + .name = "ssh", + .kind = HYBBX_TRANSPORT_SSH, + .version = 1, + .init = ssh_plugin_init, + .shutdown = ssh_plugin_shutdown, + .start = ssh_plugin_start, + .stop = ssh_plugin_stop, + .write = ssh_plugin_write, +}; diff --git a/plugins/ssh/ssh_config.c b/plugins/ssh/ssh_config.c new file mode 100644 index 0000000..77fb7db --- /dev/null +++ b/plugins/ssh/ssh_config.c @@ -0,0 +1,132 @@ +#include "hybbx/ssh.h" +#include "hybbx/util.h" + +#include <stdlib.h> +#include <string.h> + +static unsigned int parse_port(const char *value) +{ + char *end; + unsigned long port; + + if (value == NULL || value[0] == '\0') { + return HYBBX_SSH_DEFAULT_PORT; + } + + port = strtoul(value, &end, 10); + if (end == value || *end != '\0' || port == 0 || port > 65535u) { + return HYBBX_SSH_DEFAULT_PORT; + } + + return (unsigned int)port; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (scratch != NULL && scratch_len > 0) { + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + return value; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +void hybbx_ssh_config_defaults(hybbx_ssh_config_t *config) +{ + if (config == NULL) { + return; + } + + memset(config, 0, sizeof(*config)); + hybbx_strlcpy(config->bind_v4, HYBBX_SSH_DEFAULT_BIND_V4, + sizeof(config->bind_v4)); + hybbx_strlcpy(config->bind_v6, HYBBX_SSH_DEFAULT_BIND_V6, + sizeof(config->bind_v6)); + hybbx_strlcpy(config->hostkey_dir, HYBBX_SSH_DEFAULT_HOSTKEY_DIR, + sizeof(config->hostkey_dir)); + config->port = HYBBX_SSH_DEFAULT_PORT; + config->ipv4 = 1; + config->ipv6 = 1; +} + +hybbx_result_t hybbx_ssh_config_parse(const char *config, + hybbx_ssh_config_t *out) +{ + char scratch[HYBBX_SSH_BIND_V6_MAX]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_ssh_config_defaults(out); + + if (config == NULL || config[0] == '\0') { + return HYBBX_OK; + } + + value = find_kv(config, "port", scratch, sizeof(scratch)); + out->port = parse_port(value); + + value = find_kv(config, "bind", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + if (strchr(value, ':') != NULL) { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } else { + hybbx_strlcpy(out->bind_v4, value, sizeof(out->bind_v4)); + } + } + + value = find_kv(config, "bind6", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } + + value = find_kv(config, "hostkey_dir", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->hostkey_dir, value, sizeof(out->hostkey_dir)); + } + + value = find_kv(config, "ipv4", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv4 = hybbx_parse_bool(value, 1); + } + + value = find_kv(config, "ipv6", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv6 = hybbx_parse_bool(value, 1); + } + + return HYBBX_OK; +} diff --git a/plugins/ssh/ssh_keys.c b/plugins/ssh/ssh_keys.c new file mode 100644 index 0000000..656b9d4 --- /dev/null +++ b/plugins/ssh/ssh_keys.c @@ -0,0 +1,146 @@ +#include "hybbx/ssh.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <libssh/libssh.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> + +static hybbx_result_t mkdir_keys_dir(const char *keys_dir) +{ + char parent[HYBBX_PATH_MAX]; + struct stat st; + + if (keys_dir == NULL || keys_dir[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (stat(keys_dir, &st) == 0) { + return S_ISDIR(st.st_mode) ? HYBBX_OK : HYBBX_ERR_IO; + } + + if (hybbx_path_dirname(keys_dir, parent, sizeof(parent)) == HYBBX_OK && + parent[0] != '\0' && strcmp(parent, keys_dir) != 0 && + stat(parent, &st) != 0) { + if (mkdir_keys_dir(parent) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + } + + if (mkdir(keys_dir, 0700) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static hybbx_result_t generate_ed25519_keypair(const char *priv_path, + const char *pub_path) +{ + ssh_key key = NULL; + int rc; + + rc = ssh_pki_generate(SSH_KEYTYPE_ED25519, 0, &key); + if (rc != SSH_OK || key == NULL) { + hybbx_log_warn("[ssh] Ed25519 generate failed (rc=%d)", rc); + return HYBBX_ERR_IO; + } + + rc = ssh_pki_export_privkey_file(key, NULL, NULL, NULL, priv_path); + if (rc != SSH_OK) { + hybbx_log_warn("[ssh] export private key failed: %s", + ssh_get_error(key)); + ssh_key_free(key); + return HYBBX_ERR_IO; + } + + rc = ssh_pki_export_pubkey_file(key, pub_path); + ssh_key_free(key); + if (rc != SSH_OK) { + hybbx_log_warn("[ssh] export public key failed"); + return HYBBX_ERR_IO; + } + + (void)chmod(priv_path, 0600); + (void)chmod(pub_path, 0644); + return HYBBX_OK; +} + +static int hostkey_needs_rotation(const char *priv_path) +{ + struct stat st; + time_t now; + time_t age; + + if (stat(priv_path, &st) != 0) { + return 0; + } + + now = time(NULL); + if (now <= st.st_mtime) { + return 0; + } + + age = now - st.st_mtime; + return age > (time_t)HYBBX_SSH_HOSTKEY_VALID_DAYS * 86400L; +} + +hybbx_result_t hybbx_ssh_keys_ensure(const char *keys_dir, + char *hostkey_path, + size_t hostkey_path_len) +{ + char resolved_dir[HYBBX_PATH_MAX]; + char priv_path[HYBBX_PATH_MAX]; + char pub_path[HYBBX_PATH_MAX]; + struct stat st; + + if (keys_dir == NULL || hostkey_path == NULL || hostkey_path_len == 0) { + return HYBBX_ERR_INVALID; + } + + if (ssh_init() < 0) { + return HYBBX_ERR_IO; + } + + if (hybbx_path_resolve(resolved_dir, sizeof(resolved_dir), + keys_dir) != HYBBX_OK) { + hybbx_strlcpy(resolved_dir, keys_dir, sizeof(resolved_dir)); + } + + if (mkdir_keys_dir(resolved_dir) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (hybbx_path_join(priv_path, sizeof(priv_path), resolved_dir, + HYBBX_SSH_HOSTKEY_ED25519) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (hybbx_path_join(pub_path, sizeof(pub_path), resolved_dir, + HYBBX_SSH_HOSTKEY_ED25519 ".pub") != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (stat(priv_path, &st) == 0 && hostkey_needs_rotation(priv_path)) { + hybbx_log_warn("[ssh] host key older than %u days — rotating %s", + HYBBX_SSH_HOSTKEY_VALID_DAYS, priv_path); + (void)unlink(priv_path); + (void)unlink(pub_path); + } + + if (stat(priv_path, &st) != 0) { + if (generate_ed25519_keypair(priv_path, pub_path) != HYBBX_OK) { + hybbx_log_warn("[ssh] failed to generate host key in %s", + resolved_dir); + return HYBBX_ERR_IO; + } + hybbx_log_info("[ssh] generated host key %s (valid %u days)", priv_path, + HYBBX_SSH_HOSTKEY_VALID_DAYS); + } + + hybbx_strlcpy(hostkey_path, priv_path, hostkey_path_len); + return HYBBX_OK; +} |
