From e20317e1e8a48e29387772665b5e90a9f1971825 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Sun, 3 May 2026 18:30:16 -0500 Subject: [PATCH 01/13] feat: adds basic SSH Server Daemon - still testing --- .github/workflows/build.yml | 26 + .github/workflows/release-on-tag.yml | 7 + CMakeLists.txt | 8 + build.ps1 | 10 +- build.sh | 8 +- common/noxssh_common.c | 55 + common/noxssh_common.h | 24 +- common/noxssh_server.c | 1872 ++++++++++++++++++++++++++ common/noxssh_server.h | 113 ++ docs/SERVER_EMBEDDED.md | 49 + scripts/ci_smoke_sshd.ps1 | 28 + scripts/ci_smoke_sshd.sh | 27 + server/CMakeLists.txt | 19 + server/daemon_runtime.c | 235 ++++ server/daemon_runtime.h | 31 + server/embedded_adapter.h | 44 + server/main.c | 88 ++ tests/CMakeLists.txt | 12 + tests/test_server_init.c | 11 + 19 files changed, 2663 insertions(+), 4 deletions(-) create mode 100644 common/noxssh_server.c create mode 100644 common/noxssh_server.h create mode 100644 docs/SERVER_EMBEDDED.md create mode 100644 scripts/ci_smoke_sshd.ps1 create mode 100644 scripts/ci_smoke_sshd.sh create mode 100644 server/CMakeLists.txt create mode 100644 server/daemon_runtime.c create mode 100644 server/daemon_runtime.h create mode 100644 server/embedded_adapter.h create mode 100644 server/main.c create mode 100644 tests/CMakeLists.txt create mode 100644 tests/test_server_init.c diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f29325e..245dfa0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,6 +47,32 @@ jobs: mkdir -p dist/debian cp build/*.deb dist/debian/ + - name: Verify daemon binary + shell: bash + run: | + set -euo pipefail + test -f bin/noxssh + test -f bin/noxsshd || test -f bin/noxsshd.exe + + - name: CTest + if: runner.os != 'Windows' + run: cd build && ctest --output-on-failure + + - name: CTest (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: cd build; ctest -C Release --output-on-failure + + - name: Smoke noxsshd (Unix) + if: runner.os != 'Windows' + shell: bash + run: bash scripts/ci_smoke_sshd.sh + + - name: Smoke noxsshd (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: ./scripts/ci_smoke_sshd.ps1 + - name: Upload binaries uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml index f299c06..efbf029 100644 --- a/.github/workflows/release-on-tag.yml +++ b/.github/workflows/release-on-tag.yml @@ -127,6 +127,13 @@ jobs: cp "${deb}" "release-assets/noxssh-${{ matrix.artifact_suffix }}-v${VERSION}.deb" done + - name: Verify client and daemon binaries + shell: bash + run: | + set -euo pipefail + test -f bin/noxssh + test -f bin/noxsshd || test -f bin/noxsshd.exe + - name: Create release archives shell: bash run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 4fb92b6..c3c8f67 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,7 @@ add_subdirectory(noxtls EXCLUDE_FROM_ALL) add_library(netnox_ssh common/noxssh_common.c + common/noxssh_server.c ) target_include_directories(netnox_ssh PUBLIC @@ -74,6 +75,13 @@ target_compile_definitions(netnox_ssh NOXTLS_FEATURE_AES_CTR=1 ) add_subdirectory(client) +add_subdirectory(server) + +option(NOXSSH_BUILD_TESTS "Build noxssh unit tests" ON) +if(NOXSSH_BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() install(FILES README.md diff --git a/build.ps1 b/build.ps1 index d712db7..b72c309 100644 --- a/build.ps1 +++ b/build.ps1 @@ -16,7 +16,15 @@ if (-not (Test-Path $BinDir)) { Write-Host "Configuring CMake project..." cmake -S $Root -B $BuildDir -DCMAKE_BUILD_TYPE=$Config -Write-Host "Building noxssh ($Config)..." +Write-Host "Building noxssh (client) and noxsshd (server), $Config..." cmake --build $BuildDir --config $Config +$client = Join-Path $BinDir "noxssh.exe" +$daemon = Join-Path $BinDir "noxsshd.exe" +if (-not (Test-Path $client)) { + Write-Warning "Expected client binary: $client" +} +if (-not (Test-Path $daemon)) { + Write-Warning "Expected server binary: $daemon" +} Write-Host "Build complete. Binaries are in: $BinDir" diff --git a/build.sh b/build.sh index 08cda06..d71a6c9 100755 --- a/build.sh +++ b/build.sh @@ -13,4 +13,10 @@ git -C "${ROOT_DIR}" submodule update --init --recursive cmake -S "${ROOT_DIR}" -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" cmake --build "${BUILD_DIR}" -printf 'Build complete. Binaries are in: %s\n' "${BIN_DIR}" +printf 'Build complete (noxssh client + noxsshd server). Binaries are in: %s\n' "${BIN_DIR}" +if [ ! -f "${BIN_DIR}/noxssh" ]; then + printf 'Warning: expected %s/noxssh\n' "${BIN_DIR}" >&2 +fi +if [ ! -f "${BIN_DIR}/noxsshd" ]; then + printf 'Warning: expected %s/noxsshd\n' "${BIN_DIR}" >&2 +fi diff --git a/common/noxssh_common.c b/common/noxssh_common.c index 1ebe4d2..dfe5edf 100644 --- a/common/noxssh_common.c +++ b/common/noxssh_common.c @@ -2046,6 +2046,51 @@ static netnox_return_t netnox_ssh_negotiate_userauth_service(netnox_ssh_client_t return NETNOX_RETURN_SUCCESS; } +/** + * @brief Perform service request/accept for ssh-connection (RFC 4254). + * @internal + */ +static netnox_return_t netnox_ssh_negotiate_connection_service(netnox_ssh_client_t * client) +{ + uint8_t payload[256]; + uint32_t payload_len = 0u; + uint32_t offset = 1u; + const uint8_t * svc = NULL; + uint32_t svc_len = 0u; + + if(client == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + SSH_DEBUG("negotiate_connection: sending service request ssh-connection"); + if(netnox_ssh_send_service_request(client, NETNOX_SSH_SERVICE_CONNECTION) != NETNOX_RETURN_SUCCESS) { + SSH_DEBUG("negotiate_connection: send_service_request failed"); + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_wait_for_message(client, + NETNOX_SSH_MSG_SERVICE_ACCEPT, + 0xFFu, + payload, + (uint32_t)sizeof(payload), + &payload_len) != NETNOX_RETURN_SUCCESS) { + SSH_DEBUG("negotiate_connection: wait_for_message SERVICE_ACCEPT failed"); + return NETNOX_RETURN_FAILED; + } + if(payload[0] != NETNOX_SSH_MSG_SERVICE_ACCEPT) { + SSH_DEBUG("negotiate_connection: unexpected message %u", (unsigned)payload[0]); + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_string_view(payload, payload_len, &offset, &svc, &svc_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(svc_len != strlen(NETNOX_SSH_SERVICE_CONNECTION) || + memcmp(svc, NETNOX_SSH_SERVICE_CONNECTION, svc_len) != 0) { + return NETNOX_RETURN_FAILED; + } + + client->connection_service_ready = 1u; + return NETNOX_RETURN_SUCCESS; +} + /** * @brief Send password-based userauth request. * @internal @@ -2483,6 +2528,15 @@ netnox_return_t netnox_ssh_client_open_session(netnox_ssh_client_t * client) client->local_channel_id = 0u; client->local_window_size = NETNOX_SSH_CHANNEL_WINDOW_SIZE; + if(client->connection_service_ready == 0u) { + SSH_DEBUG("open_session: requesting ssh-connection service"); + if(netnox_ssh_negotiate_connection_service(client) != NETNOX_RETURN_SUCCESS) { + SSH_DEBUG("open_session: negotiate_connection_service failed"); + return NETNOX_RETURN_FAILED; + } + SSH_DEBUG("open_session: ssh-connection service accepted"); + } + if(netnox_ssh_send_channel_open_session(client) != NETNOX_RETURN_SUCCESS) { SSH_DEBUG("open_session: send CHANNEL_OPEN(session) failed"); return NETNOX_RETURN_FAILED; @@ -2946,6 +3000,7 @@ netnox_return_t netnox_ssh_client_close(netnox_ssh_client_t * client) client->key_exchange_complete = 0u; client->pending_newkeys_len = 0u; client->userauth_service_ready = 0u; + client->connection_service_ready = 0u; client->authenticated = 0u; client->channel_open = 0u; client->local_channel_id = 0u; diff --git a/common/noxssh_common.h b/common/noxssh_common.h index e8e2af4..970ff0e 100644 --- a/common/noxssh_common.h +++ b/common/noxssh_common.h @@ -58,11 +58,24 @@ typedef struct netnox_interface_s netnox_interface_t; #define NETNOX_SSH_DEFAULT_PORT (22u) /** @brief Default SSH client identification string. */ #define NETNOX_SSH_DEFAULT_CLIENT_IDENT "SSH-2.0-noxssh_0.1" +/** @brief Default SSH server identification string. */ +#define NETNOX_SSH_DEFAULT_SERVER_IDENT "SSH-2.0-noxsshd_0.1" + +/** @brief SSH message number for disconnect. */ +#define NETNOX_SSH_MSG_DISCONNECT (1u) +/** @brief SSH message number for ignore. */ +#define NETNOX_SSH_MSG_IGNORE (2u) +/** @brief SSH message number for ext-info (RFC 8308). */ +#define NETNOX_SSH_MSG_EXT_INFO (7u) +/** @brief SSH message number for global request. */ +#define NETNOX_SSH_MSG_GLOBAL_REQUEST (80u) +/** @brief SSH message number for request success (global). */ +#define NETNOX_SSH_MSG_REQUEST_SUCCESS (81u) +/** @brief SSH message number for request failure (global). */ +#define NETNOX_SSH_MSG_REQUEST_FAILURE (82u) /** @brief SSH message number for service request. */ #define NETNOX_SSH_MSG_SERVICE_REQUEST (5u) -/** @brief SSH message number for ignore/no-op packets. */ -#define NETNOX_SSH_MSG_IGNORE (2u) /** @brief SSH message number for service accept. */ #define NETNOX_SSH_MSG_SERVICE_ACCEPT (6u) /** @brief SSH message number for key exchange initialization. */ @@ -85,6 +98,8 @@ typedef struct netnox_interface_s netnox_interface_t; #define NETNOX_SSH_MSG_CHANNEL_OPEN_CONFIRMATION (91u) /** @brief SSH message number for channel open failure. */ #define NETNOX_SSH_MSG_CHANNEL_OPEN_FAILURE (92u) +/** @brief SSH message number for channel window adjust. */ +#define NETNOX_SSH_MSG_CHANNEL_WINDOW_ADJUST (93u) /** @brief SSH message number for channel data. */ #define NETNOX_SSH_MSG_CHANNEL_DATA (94u) /** @brief SSH message number for channel extended data (stderr). */ @@ -118,6 +133,10 @@ typedef struct netnox_interface_s netnox_interface_t; #define NETNOX_SSH_CHANNEL_REQ_SHELL "shell" /** @brief SHA-256 fingerprint size in bytes. */ #define NETNOX_SSH_HOST_KEY_FINGERPRINT_LEN (32u) +/** @brief Channel request type for subsystem (e.g. SFTP). */ +#define NETNOX_SSH_CHANNEL_REQ_SUBSYSTEM "subsystem" +/** @brief SFTP subsystem name (RFC draft / de-facto standard). */ +#define NETNOX_SSH_SUBSYSTEM_SFTP "sftp" /** Callback to send data over the transport (socket/stream). Returns bytes sent or negative on error. */ typedef int32_t (*netnox_ssh_transport_send_t)(void * user_data, const uint8_t * data, uint32_t len); @@ -145,6 +164,7 @@ typedef struct uint8_t connected; uint8_t kexinit_exchanged; uint8_t userauth_service_ready; + uint8_t connection_service_ready; uint8_t authenticated; uint8_t channel_open; uint8_t key_exchange_complete; diff --git a/common/noxssh_server.c b/common/noxssh_server.c new file mode 100644 index 0000000..bf828be --- /dev/null +++ b/common/noxssh_server.c @@ -0,0 +1,1872 @@ +/***************************************************************************** +* Copyright (c) [2019] - [2026], Argenox Technologies LLC +* SPDX-License-Identifier: GPL-2.0-or-later OR NoxTLS-Commercial +* +* @file noxssh_server.c +* @brief SSH-2 server: curve25519-sha256, ssh-ed25519, aes128-ctr, hmac-sha2-256. +*/ + +#include +#include +#include +#include + +#ifdef _WIN32 +/* Windows: one-shot exec uses _popen only. */ +#else +#include +#include +#include + +extern char ** environ; +#endif + +#include "noxssh_server.h" +#include "../noxtls/noxtls-lib/pkc/x25519/noxtls_x25519.h" +#include "../noxtls/noxtls-lib/pkc/ed25519/noxtls_ed25519.h" +#include "../noxtls/noxtls-lib/mdigest/noxtls_sha.h" +#include "../noxtls/noxtls-lib/encryption/aes/noxtls_aes.h" +#include "../noxtls/noxtls-lib/mdigest/noxtls_hash.h" + +#define NETNOX_SSH_CHAR_CR ((char)'\r') +#define NETNOX_SSH_CHAR_LF ((char)'\n') +#define NETNOX_SSH_MIN_PADDING_LEN (4u) +#define NETNOX_SSH_PACKET_BLOCK_SIZE (8u) +#define NETNOX_SSH_KEXINIT_COOKIE_LEN (16u) +#define NETNOX_SSH_KEXINIT_PAYLOAD_MAX_LEN (1024u) +#define NETNOX_SSH_AES_BLOCK_LEN (16u) +#define NETNOX_SSH_MAC_LEN (32u) + +static uint8_t g_srv_prng_seeded; + +static int netnox_ssh_srv_debug(void) +{ + const char * env = getenv("NETNOX_SSH_DEBUG"); + if(env == NULL || env[0] == '\0') { + return 0; + } + if(env[0] >= '1' && env[0] <= '3' && env[1] == '\0') { + return (int)(env[0] - '0'); + } + { + int level = atoi(env); + if(level < 0) { + level = 0; + } else if(level > 3) { + level = 3; + } else if(level == 0) { + level = 1; + } + return level; + } +} +#define SRVDBG(fmt, ...) \ + do { if(netnox_ssh_srv_debug() >= 1) (void)fprintf(stderr, "noxsshd: " fmt "\n", ##__VA_ARGS__); } while(0) + +static void netnox_ssh_write_u32_be(uint8_t * out, uint32_t value) +{ + out[0] = (uint8_t)((value >> 24) & 0xFFu); + out[1] = (uint8_t)((value >> 16) & 0xFFu); + out[2] = (uint8_t)((value >> 8) & 0xFFu); + out[3] = (uint8_t)(value & 0xFFu); +} + +static uint32_t netnox_ssh_read_u32_be(const uint8_t * in) +{ + return ((uint32_t)in[0] << 24) | + ((uint32_t)in[1] << 16) | + ((uint32_t)in[2] << 8) | + (uint32_t)in[3]; +} + +static void netnox_ssh_fill_random(uint8_t * out, uint32_t len) +{ + uint32_t i; + if(out == NULL || len == 0u) { + return; + } + if(g_srv_prng_seeded == 0u) { + srand((unsigned int)time(NULL)); + g_srv_prng_seeded = 1u; + } + for(i = 0u; i < len; i++) { + out[i] = (uint8_t)(rand() & 0xFF); + } +} + +static netnox_return_t netnox_ssh_copy_string(char * dst, uint16_t dst_len, const char * src) +{ + size_t src_len; + if(dst == NULL || src == NULL || dst_len == 0u) { + return NETNOX_RETURN_BAD_PARAM; + } + src_len = strlen(src); + if(src_len > (size_t)(dst_len - 1u)) { + return NETNOX_RETURN_BAD_PARAM; + } + memcpy(dst, src, src_len); + dst[src_len] = '\0'; + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t netnox_ssh_hmac_sha256(const uint8_t * key, + uint32_t key_len, + const uint8_t * data, + uint32_t data_len, + uint8_t * mac_out) +{ + noxtls_sha_ctx_t ctx; + uint8_t ipad[64]; + uint8_t opad[64]; + uint8_t inner_hash[32]; + uint32_t i; + + if(key == NULL || data == NULL || mac_out == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + if(noxtls_sha_init(&ctx, NOXTLS_HASH_SHA_256) != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + memset(ipad, 0x36, sizeof(ipad)); + memset(opad, 0x5C, sizeof(opad)); + if(key_len > 64u) { + noxtls_sha_update(&ctx, (uint8_t *)key, key_len); + noxtls_sha_finish(&ctx, inner_hash); + key = inner_hash; + key_len = 32u; + } + for(i = 0u; i < key_len; i++) { + ipad[i] ^= key[i]; + opad[i] ^= key[i]; + } + noxtls_sha_init(&ctx, NOXTLS_HASH_SHA_256); + noxtls_sha_update(&ctx, ipad, 64u); + noxtls_sha_update(&ctx, (uint8_t *)data, data_len); + noxtls_sha_finish(&ctx, inner_hash); + noxtls_sha_init(&ctx, NOXTLS_HASH_SHA_256); + noxtls_sha_update(&ctx, opad, 64u); + noxtls_sha_update(&ctx, inner_hash, 32u); + noxtls_sha_finish(&ctx, mac_out); + return NETNOX_RETURN_SUCCESS; +} + +static void netnox_ssh_ctr_increment_blocks(uint8_t * counter_16, uint32_t n_blocks) +{ + int i; + uint64_t carry = (uint64_t)n_blocks; + for(i = 15; i >= 0 && carry > 0u; i--) { + carry += (uint64_t)counter_16[i]; + counter_16[i] = (uint8_t)(carry & 0xFFu); + carry >>= 8; + } +} + +static netnox_return_t netnox_ssh_send_all(netnox_ssh_server_t * s, const uint8_t * data, uint32_t len) +{ + uint32_t sent_total = 0u; + if(s == NULL || data == NULL || s->send_cb == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + while(sent_total < len) { + int32_t sent = s->send_cb(s->io_user_data, data + sent_total, len - sent_total); + if(sent <= 0) { + return NETNOX_RETURN_FAILED; + } + sent_total += (uint32_t)sent; + } + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t netnox_ssh_recv_exact(netnox_ssh_server_t * s, uint8_t * data, uint32_t len) +{ + uint32_t recv_total = 0u; + if(s == NULL || data == NULL || s->recv_cb == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + while(recv_total < len) { + int32_t got = s->recv_cb(s->io_user_data, data + recv_total, len - recv_total); + if(got <= 0) { + return NETNOX_RETURN_FAILED; + } + recv_total += (uint32_t)got; + } + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t netnox_ssh_recv_line(netnox_ssh_server_t * s, char * out, uint16_t out_len) +{ + uint16_t used = 0u; + if(s == NULL || out == NULL || out_len == 0u || s->recv_cb == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + while(used < (uint16_t)(out_len - 1u)) { + int32_t rc; + uint8_t ch = 0u; + rc = s->recv_cb(s->io_user_data, &ch, 1u); + if(rc <= 0) { + return NETNOX_RETURN_FAILED; + } + if((char)ch == NETNOX_SSH_CHAR_LF) { + break; + } + if((char)ch != NETNOX_SSH_CHAR_CR) { + out[used++] = (char)ch; + } + } + if(used == (uint16_t)(out_len - 1u)) { + uint8_t ch = 0u; + while(1) { + int32_t rc = s->recv_cb(s->io_user_data, &ch, 1u); + if(rc <= 0) { + return NETNOX_RETURN_FAILED; + } + if((char)ch == NETNOX_SSH_CHAR_LF) { + break; + } + } + out[0] = '\0'; + return NETNOX_RETURN_SUCCESS; + } + out[used] = '\0'; + return NETNOX_RETURN_SUCCESS; +} + +static int netnox_ssh_is_ident_line(const char * line) +{ + if(line == NULL) { + return 0; + } + return (strncmp(line, "SSH-", 4u) == 0) ? 1 : 0; +} + +static netnox_return_t netnox_ssh_payload_append_namelist(uint8_t * payload, + uint32_t * payload_len, + uint32_t payload_max, + const char * list) +{ + uint32_t list_len; + if(payload == NULL || payload_len == NULL || list == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + list_len = (uint32_t)strlen(list); + if((*payload_len + 4u + list_len) > payload_max) { + return NETNOX_RETURN_BAD_PARAM; + } + netnox_ssh_write_u32_be(&payload[*payload_len], list_len); + *payload_len += 4u; + if(list_len > 0u) { + memcpy(&payload[*payload_len], list, list_len); + *payload_len += list_len; + } + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t netnox_ssh_payload_append_u8(uint8_t * payload, + uint32_t * payload_len, + uint32_t payload_max, + uint8_t value) +{ + if(payload == NULL || payload_len == NULL || (*payload_len + 1u) > payload_max) { + return NETNOX_RETURN_BAD_PARAM; + } + payload[*payload_len] = value; + *payload_len += 1u; + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t netnox_ssh_payload_append_u32(uint8_t * payload, + uint32_t * payload_len, + uint32_t payload_max, + uint32_t value) +{ + if(payload == NULL || payload_len == NULL || (*payload_len + 4u) > payload_max) { + return NETNOX_RETURN_BAD_PARAM; + } + netnox_ssh_write_u32_be(&payload[*payload_len], value); + *payload_len += 4u; + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t netnox_ssh_payload_append_string(uint8_t * payload, + uint32_t * payload_len, + uint32_t payload_max, + const uint8_t * str_data, + uint32_t str_len) +{ + if(payload == NULL || payload_len == NULL || (str_len > 0u && str_data == NULL)) { + return NETNOX_RETURN_BAD_PARAM; + } + if((*payload_len + 4u + str_len) > payload_max) { + return NETNOX_RETURN_BAD_PARAM; + } + netnox_ssh_write_u32_be(&payload[*payload_len], str_len); + *payload_len += 4u; + if(str_len > 0u) { + memcpy(&payload[*payload_len], str_data, str_len); + *payload_len += str_len; + } + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t netnox_ssh_payload_read_u32(const uint8_t * payload, + uint32_t payload_len, + uint32_t * offset, + uint32_t * out_value) +{ + if(payload == NULL || offset == NULL || out_value == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + if((*offset + 4u) > payload_len) { + return NETNOX_RETURN_BAD_PARAM; + } + *out_value = netnox_ssh_read_u32_be(&payload[*offset]); + *offset += 4u; + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t netnox_ssh_payload_read_string_view(const uint8_t * payload, + uint32_t payload_len, + uint32_t * offset, + const uint8_t ** out_data, + uint32_t * out_len) +{ + uint32_t str_len; + if(payload == NULL || offset == NULL || out_data == NULL || out_len == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + if(netnox_ssh_payload_read_u32(payload, payload_len, offset, &str_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_BAD_PARAM; + } + if((*offset + str_len) > payload_len) { + return NETNOX_RETURN_BAD_PARAM; + } + *out_data = &payload[*offset]; + *out_len = str_len; + *offset += str_len; + return NETNOX_RETURN_SUCCESS; +} + +static int netnox_ssh_namelist_contains(const uint8_t * list, uint32_t list_len, const char * token) +{ + uint32_t start = 0u; + uint32_t token_len; + if(list == NULL || token == NULL) { + return 0; + } + token_len = (uint32_t)strlen(token); + if(token_len == 0u) { + return 0; + } + while(start < list_len) { + uint32_t end = start; + while(end < list_len && list[end] != (uint8_t)',') { + end++; + } + if((end - start) == token_len && memcmp(&list[start], token, token_len) == 0) { + return 1; + } + if(end >= list_len) { + break; + } + start = end + 1u; + } + return 0; +} + +static netnox_return_t netnox_ssh_validate_client_kexinit(const uint8_t * payload, uint32_t payload_len) +{ + uint32_t offset = 0u; + const uint8_t * field = NULL; + uint32_t field_len = 0u; + + if(payload == NULL || payload_len < (1u + NETNOX_SSH_KEXINIT_COOKIE_LEN)) { + return NETNOX_RETURN_BAD_PARAM; + } + if(payload[0] != NETNOX_SSH_MSG_KEXINIT) { + return NETNOX_RETURN_FAILED; + } + offset = 1u + NETNOX_SSH_KEXINIT_COOKIE_LEN; + if(netnox_ssh_payload_read_string_view(payload, payload_len, &offset, &field, &field_len) != NETNOX_RETURN_SUCCESS || + netnox_ssh_namelist_contains(field, field_len, NETNOX_SSH_REQUIRED_KEX_ALG) == 0) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_string_view(payload, payload_len, &offset, &field, &field_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_namelist_contains(field, field_len, NETNOX_SSH_REQUIRED_HOST_KEY_ALG) == 0) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_string_view(payload, payload_len, &offset, &field, &field_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_namelist_contains(field, field_len, NETNOX_SSH_REQUIRED_CIPHER_ALG) == 0) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_string_view(payload, payload_len, &offset, &field, &field_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_namelist_contains(field, field_len, NETNOX_SSH_REQUIRED_CIPHER_ALG) == 0) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_string_view(payload, payload_len, &offset, &field, &field_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_namelist_contains(field, field_len, NETNOX_SSH_REQUIRED_MAC_ALG) == 0) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_string_view(payload, payload_len, &offset, &field, &field_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_namelist_contains(field, field_len, NETNOX_SSH_REQUIRED_MAC_ALG) == 0) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_string_view(payload, payload_len, &offset, &field, &field_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_namelist_contains(field, field_len, NETNOX_SSH_REQUIRED_COMPRESSION_ALG) == 0) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_string_view(payload, payload_len, &offset, &field, &field_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_namelist_contains(field, field_len, NETNOX_SSH_REQUIRED_COMPRESSION_ALG) == 0) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_string_view(payload, payload_len, &offset, &field, &field_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_string_view(payload, payload_len, &offset, &field, &field_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t netnox_ssh_buf_append(uint8_t * out, + uint32_t * out_len, + uint32_t out_max, + const uint8_t * in, + uint32_t in_len) +{ + if(out == NULL || out_len == NULL || (in_len > 0u && in == NULL)) { + return NETNOX_RETURN_BAD_PARAM; + } + if((*out_len + in_len) > out_max) { + return NETNOX_RETURN_BAD_PARAM; + } + if(in_len > 0u) { + memcpy(&out[*out_len], in, in_len); + *out_len += in_len; + } + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t netnox_ssh_buf_append_mpint_be(uint8_t * out, + uint32_t * out_len, + uint32_t out_max, + const uint8_t * in, + uint32_t in_len) +{ + uint32_t first = 0u; + uint32_t value_len; + uint8_t zero = 0u; + + if(out == NULL || out_len == NULL || in == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + while(first < in_len && in[first] == 0u) { + first++; + } + if(first == in_len) { + return netnox_ssh_payload_append_u32(out, out_len, out_max, 0u); + } + value_len = in_len - first; + if((in[first] & 0x80u) != 0u) { + if(netnox_ssh_payload_append_u32(out, out_len, out_max, value_len + 1u) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_BAD_PARAM; + } + if(netnox_ssh_buf_append(out, out_len, out_max, &zero, 1u) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_BAD_PARAM; + } + } else { + if(netnox_ssh_payload_append_u32(out, out_len, out_max, value_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_BAD_PARAM; + } + } + return netnox_ssh_buf_append(out, out_len, out_max, &in[first], value_len); +} + +static netnox_return_t netnox_ssh_buf_append_mpint_be_32(uint8_t * out, + uint32_t * out_len, + uint32_t out_max, + const uint8_t * in) +{ + uint8_t zero = 0u; + if(out == NULL || out_len == NULL || in == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + if((*out_len + 4u + 33u) > out_max) { + return NETNOX_RETURN_BAD_PARAM; + } + if((in[0] & 0x80u) != 0u) { + netnox_ssh_write_u32_be(&out[*out_len], 33u); + *out_len += 4u; + out[*out_len] = zero; + *out_len += 1u; + memcpy(&out[*out_len], in, 32u); + *out_len += 32u; + } else { + netnox_ssh_write_u32_be(&out[*out_len], 32u); + *out_len += 4u; + memcpy(&out[*out_len], in, 32u); + *out_len += 32u; + } + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t srv_compute_exchange_hash(netnox_ssh_server_t * s, + const uint8_t * host_key_blob, + uint32_t host_key_blob_len, + const uint8_t * client_pub, + uint32_t client_pub_len, + const uint8_t * server_pub, + uint32_t server_pub_len, + const uint8_t * shared_secret_raw, + uint32_t shared_secret_raw_len, + uint8_t out_hash[32]) +{ + uint8_t h_input[4096]; + uint32_t h_input_len = 0u; + noxtls_sha_ctx_t sha_ctx; + noxtls_return_t noxrc; + + if(s == NULL || host_key_blob == NULL || client_pub == NULL || server_pub == NULL || + shared_secret_raw == NULL || out_hash == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + if(netnox_ssh_payload_append_string(h_input, &h_input_len, (uint32_t)sizeof(h_input), + (const uint8_t *)s->client_ident, + (uint32_t)strlen(s->client_ident)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_string(h_input, &h_input_len, (uint32_t)sizeof(h_input), + (const uint8_t *)s->server_ident, + (uint32_t)strlen(s->server_ident)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_string(h_input, &h_input_len, (uint32_t)sizeof(h_input), + s->kexinit_client_payload, + s->kexinit_client_payload_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_string(h_input, &h_input_len, (uint32_t)sizeof(h_input), + s->kexinit_server_payload, + s->kexinit_server_payload_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_string(h_input, &h_input_len, (uint32_t)sizeof(h_input), + host_key_blob, host_key_blob_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_string(h_input, &h_input_len, (uint32_t)sizeof(h_input), + client_pub, client_pub_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_string(h_input, &h_input_len, (uint32_t)sizeof(h_input), + server_pub, server_pub_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_buf_append_mpint_be(h_input, &h_input_len, (uint32_t)sizeof(h_input), + shared_secret_raw, shared_secret_raw_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + noxrc = noxtls_sha_init(&sha_ctx, NOXTLS_HASH_SHA_256); + if(noxrc != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + noxrc = noxtls_sha_update(&sha_ctx, h_input, h_input_len); + if(noxrc != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + noxrc = noxtls_sha_finish(&sha_ctx, out_hash); + if(noxrc != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t srv_derive_key_block(netnox_ssh_server_t * s, + uint8_t selector, + uint8_t * out, + uint32_t out_len) +{ + uint8_t seed[1024]; + uint32_t seed_len = 0u; + uint8_t digest[32]; + noxtls_sha_ctx_t sha_ctx; + noxtls_return_t noxrc; + uint32_t produced = 0u; + uint32_t take; + int use_mpint = 1; + uint8_t k_enc[4u + 33u]; + uint32_t k_enc_len = 0u; + + if(s == NULL || out == NULL || out_len == 0u || s->session_id_len == 0u) { + return NETNOX_RETURN_BAD_PARAM; + } + { + const char * env = getenv("NETNOX_SSH_KEY_DERIV_MPINT"); + if(env != NULL && env[0] == '0' && env[1] == '\0') { + use_mpint = 0; + } + } + if(use_mpint) { + if(netnox_ssh_buf_append_mpint_be(k_enc, &k_enc_len, (uint32_t)sizeof(k_enc), + s->shared_secret_raw, (uint32_t)sizeof(s->shared_secret_raw)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_buf_append(seed, &seed_len, (uint32_t)sizeof(seed), k_enc, k_enc_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + } else { + if(netnox_ssh_buf_append(seed, &seed_len, (uint32_t)sizeof(seed), + s->shared_secret_raw, (uint32_t)sizeof(s->shared_secret_raw)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + } + if(netnox_ssh_buf_append(seed, &seed_len, (uint32_t)sizeof(seed), s->session_id, s->session_id_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_buf_append(seed, &seed_len, (uint32_t)sizeof(seed), &selector, 1u) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_buf_append(seed, &seed_len, (uint32_t)sizeof(seed), s->session_id, s->session_id_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + noxrc = noxtls_sha_init(&sha_ctx, NOXTLS_HASH_SHA_256); + if(noxrc != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + noxrc = noxtls_sha_update(&sha_ctx, seed, seed_len); + if(noxrc != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + noxrc = noxtls_sha_finish(&sha_ctx, digest); + if(noxrc != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + take = (out_len < (uint32_t)sizeof(digest)) ? out_len : (uint32_t)sizeof(digest); + memcpy(out, digest, take); + produced = take; + while(produced < out_len) { + uint8_t cont_in[2048]; + uint32_t cont_len = 0u; + if(use_mpint) { + k_enc_len = 0u; + if(netnox_ssh_buf_append_mpint_be(k_enc, &k_enc_len, (uint32_t)sizeof(k_enc), + s->shared_secret_raw, (uint32_t)sizeof(s->shared_secret_raw)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_buf_append(cont_in, &cont_len, (uint32_t)sizeof(cont_in), k_enc, k_enc_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + } else { + if(netnox_ssh_buf_append(cont_in, &cont_len, (uint32_t)sizeof(cont_in), + s->shared_secret_raw, (uint32_t)sizeof(s->shared_secret_raw)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + } + if(netnox_ssh_buf_append(cont_in, &cont_len, (uint32_t)sizeof(cont_in), s->session_id, s->session_id_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_buf_append(cont_in, &cont_len, (uint32_t)sizeof(cont_in), out, produced) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + noxrc = noxtls_sha_init(&sha_ctx, NOXTLS_HASH_SHA_256); + if(noxrc != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + noxrc = noxtls_sha_update(&sha_ctx, cont_in, cont_len); + if(noxrc != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + noxrc = noxtls_sha_finish(&sha_ctx, digest); + if(noxrc != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + take = (out_len - produced) < (uint32_t)sizeof(digest) ? (out_len - produced) : (uint32_t)sizeof(digest); + memcpy(out + produced, digest, take); + produced += take; + } + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t srv_derive_transport_keys(netnox_ssh_server_t * s) +{ + if(s == NULL || s->session_id_len == 0u) { + return NETNOX_RETURN_BAD_PARAM; + } + if(srv_derive_key_block(s, (uint8_t)'A', s->c2s_iv, (uint32_t)sizeof(s->c2s_iv)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(srv_derive_key_block(s, (uint8_t)'B', s->s2c_iv, (uint32_t)sizeof(s->s2c_iv)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(srv_derive_key_block(s, (uint8_t)'C', s->c2s_key, (uint32_t)sizeof(s->c2s_key)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(srv_derive_key_block(s, (uint8_t)'D', s->s2c_key, (uint32_t)sizeof(s->s2c_key)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(srv_derive_key_block(s, (uint8_t)'E', s->c2s_mac_key, (uint32_t)sizeof(s->c2s_mac_key)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(srv_derive_key_block(s, (uint8_t)'F', s->s2c_mac_key, (uint32_t)sizeof(s->s2c_mac_key)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + memcpy(s->c2s_counter, s->c2s_iv, NETNOX_SSH_AES_BLOCK_LEN); + memcpy(s->s2c_counter, s->s2c_iv, NETNOX_SSH_AES_BLOCK_LEN); + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t srv_send_packet(netnox_ssh_server_t * s, + const uint8_t * payload, + uint32_t payload_len); +static netnox_return_t srv_recv_packet(netnox_ssh_server_t * s, + uint8_t * payload_out, + uint32_t payload_max, + uint32_t * payload_len_out); + +static netnox_return_t srv_build_host_key_blob(netnox_ssh_server_t * s, + uint8_t * out, + uint32_t out_max, + uint32_t * out_len) +{ + static const char alg[] = "ssh-ed25519"; + uint32_t len = 0u; + if(s == NULL || out == NULL || out_len == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + *out_len = 0u; + if(netnox_ssh_payload_append_string(out, &len, out_max, + (const uint8_t *)alg, (uint32_t)strlen(alg)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_string(out, &len, out_max, s->host_ed25519_pk, 32u) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + *out_len = len; + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t srv_build_kexinit_payload(uint8_t * payload, + uint32_t * payload_len, + uint32_t payload_max) +{ + uint32_t i; + netnox_return_t rc; + if(payload == NULL || payload_len == NULL || payload_max < (1u + NETNOX_SSH_KEXINIT_COOKIE_LEN)) { + return NETNOX_RETURN_BAD_PARAM; + } + payload[0] = NETNOX_SSH_MSG_KEXINIT; + *payload_len = 1u; + netnox_ssh_fill_random(&payload[*payload_len], NETNOX_SSH_KEXINIT_COOKIE_LEN); + *payload_len += NETNOX_SSH_KEXINIT_COOKIE_LEN; + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, NETNOX_SSH_KEX_ALG_LIST); + if(rc != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, NETNOX_SSH_HOST_KEY_ALG_LIST); + if(rc != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, NETNOX_SSH_CIPHER_ALG_LIST); + if(rc != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, NETNOX_SSH_CIPHER_ALG_LIST); + if(rc != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, NETNOX_SSH_MAC_ALG_LIST); + if(rc != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, NETNOX_SSH_MAC_ALG_LIST); + if(rc != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, NETNOX_SSH_COMPRESSION_ALG_LIST); + if(rc != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, NETNOX_SSH_COMPRESSION_ALG_LIST); + if(rc != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, ""); + if(rc != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, ""); + if(rc != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if((*payload_len + 1u + 4u) > payload_max) { + return NETNOX_RETURN_FAILED; + } + payload[*payload_len] = 0u; + *payload_len += 1u; + netnox_ssh_write_u32_be(&payload[*payload_len], 0u); + *payload_len += 4u; + for(i = 0u; i < *payload_len; i++) { + (void)payload[i]; + } + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t srv_send_packet(netnox_ssh_server_t * s, + const uint8_t * payload, + uint32_t payload_len) +{ + uint32_t padding_len = NETNOX_SSH_MIN_PADDING_LEN; + uint32_t packet_len; + uint32_t plain_len; + uint32_t n_blocks; + uint32_t block_size; + uint8_t plain[4u + NETNOX_SSH_MAX_PACKET_LEN]; + uint8_t enc[4u + NETNOX_SSH_MAX_PACKET_LEN]; + uint8_t mac_buf[NETNOX_SSH_MAC_LEN]; + uint8_t seq_be[4]; + netnox_return_t send_rc; + noxtls_return_t aes_rc; + + if(s == NULL || payload == NULL || payload_len == 0u) { + return NETNOX_RETURN_BAD_PARAM; + } + block_size = (s->key_exchange_complete != 0u) ? NETNOX_SSH_AES_BLOCK_LEN : NETNOX_SSH_PACKET_BLOCK_SIZE; + while(((1u + payload_len + padding_len + 4u) % block_size) != 0u) { + padding_len++; + } + packet_len = 1u + payload_len + padding_len; + plain_len = 4u + packet_len; + if(packet_len > NETNOX_SSH_MAX_PACKET_LEN) { + return NETNOX_RETURN_BAD_PARAM; + } + netnox_ssh_write_u32_be(&plain[0], packet_len); + plain[4] = (uint8_t)padding_len; + memcpy(&plain[5], payload, payload_len); + netnox_ssh_fill_random(&plain[5u + payload_len], padding_len); + + if(s->key_exchange_complete != 0u) { + uint8_t mac_input[4u + 4u + NETNOX_SSH_MAX_PACKET_LEN]; + uint32_t enc_mac_len = plain_len + NETNOX_SSH_MAC_LEN; + netnox_ssh_write_u32_be(seq_be, s->send_seq); + memcpy(mac_input, seq_be, 4u); + memcpy(mac_input + 4u, plain, plain_len); + if(netnox_ssh_hmac_sha256(s->s2c_mac_key, (uint32_t)sizeof(s->s2c_mac_key), + mac_input, 4u + plain_len, mac_buf) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + aes_rc = aes_encrypt_ctr(s->s2c_key, plain, plain_len, s->s2c_counter, enc, AES_128_BIT); + if(aes_rc != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + n_blocks = (plain_len + NETNOX_SSH_AES_BLOCK_LEN - 1u) / NETNOX_SSH_AES_BLOCK_LEN; + netnox_ssh_ctr_increment_blocks(s->s2c_counter, n_blocks); + { + uint8_t combined[128]; + uint32_t total = s->pending_newkeys_len + enc_mac_len; + if(total > 0u && total <= (uint32_t)sizeof(combined)) { + uint32_t off = 0u; + if(s->pending_newkeys_len > 0u) { + memcpy(combined, s->pending_newkeys, s->pending_newkeys_len); + off = s->pending_newkeys_len; + s->pending_newkeys_len = 0u; + } + memcpy(combined + off, enc, plain_len); + memcpy(combined + off + plain_len, mac_buf, NETNOX_SSH_MAC_LEN); + if(netnox_ssh_send_all(s, combined, total) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + } else { + if(s->pending_newkeys_len > 0u) { + if(netnox_ssh_send_all(s, s->pending_newkeys, s->pending_newkeys_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + s->pending_newkeys_len = 0u; + } + memcpy(combined, enc, plain_len); + memcpy(combined + plain_len, mac_buf, NETNOX_SSH_MAC_LEN); + if(netnox_ssh_send_all(s, combined, enc_mac_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + } + } + s->send_seq++; + s->encrypted_bytes_sent += (uint64_t)enc_mac_len; + return NETNOX_RETURN_SUCCESS; + } + if(payload_len == 1u && payload[0] == NETNOX_SSH_MSG_NEWKEYS) { + send_rc = netnox_ssh_send_all(s, plain, plain_len); + if(send_rc == NETNOX_RETURN_SUCCESS) { + s->send_seq++; + } + return send_rc; + } + send_rc = netnox_ssh_send_all(s, plain, plain_len); + if(send_rc == NETNOX_RETURN_SUCCESS) { + s->send_seq++; + } + return send_rc; +} + +static netnox_return_t srv_recv_packet(netnox_ssh_server_t * s, + uint8_t * payload_out, + uint32_t payload_max, + uint32_t * payload_len_out) +{ + uint8_t len_buf[4]; + uint8_t packet[NETNOX_SSH_MAX_PACKET_LEN]; + uint32_t packet_len; + uint32_t payload_len; + uint8_t padding_len; + + if(s == NULL || payload_out == NULL || payload_len_out == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + + if(s->key_exchange_complete != 0u) { + uint8_t enc[4u + NETNOX_SSH_MAX_PACKET_LEN]; + uint8_t plain[4u + NETNOX_SSH_MAX_PACKET_LEN]; + uint8_t mac_buf[NETNOX_SSH_MAC_LEN]; + uint8_t mac_input[4u + 4u + NETNOX_SSH_MAX_PACKET_LEN]; + uint8_t computed_mac[NETNOX_SSH_MAC_LEN]; + uint32_t plain_len; + uint32_t n_blocks; + noxtls_return_t aes_rc; + + if(netnox_ssh_recv_exact(s, enc, NETNOX_SSH_AES_BLOCK_LEN) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + aes_rc = aes_encrypt_ctr(s->c2s_key, enc, NETNOX_SSH_AES_BLOCK_LEN, s->c2s_counter, plain, AES_128_BIT); + if(aes_rc != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + netnox_ssh_ctr_increment_blocks(s->c2s_counter, 1u); + packet_len = netnox_ssh_read_u32_be(&plain[0]); + if(packet_len < (1u + NETNOX_SSH_MIN_PADDING_LEN) || packet_len > NETNOX_SSH_MAX_PACKET_LEN) { + return NETNOX_RETURN_FAILED; + } + plain_len = 4u + packet_len; + if(netnox_ssh_recv_exact(s, &enc[NETNOX_SSH_AES_BLOCK_LEN], plain_len - NETNOX_SSH_AES_BLOCK_LEN) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + aes_rc = aes_encrypt_ctr(s->c2s_key, &enc[NETNOX_SSH_AES_BLOCK_LEN], plain_len - NETNOX_SSH_AES_BLOCK_LEN, + s->c2s_counter, &plain[NETNOX_SSH_AES_BLOCK_LEN], AES_128_BIT); + if(aes_rc != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + n_blocks = (plain_len + NETNOX_SSH_AES_BLOCK_LEN - 1u) / NETNOX_SSH_AES_BLOCK_LEN; + netnox_ssh_ctr_increment_blocks(s->c2s_counter, n_blocks - 1u); + if(netnox_ssh_recv_exact(s, mac_buf, NETNOX_SSH_MAC_LEN) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + netnox_ssh_write_u32_be(mac_input, s->recv_seq); + memcpy(mac_input + 4u, plain, plain_len); + if(netnox_ssh_hmac_sha256(s->c2s_mac_key, (uint32_t)sizeof(s->c2s_mac_key), + mac_input, 4u + plain_len, computed_mac) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(memcmp(computed_mac, mac_buf, NETNOX_SSH_MAC_LEN) != 0) { + return NETNOX_RETURN_FAILED; + } + s->recv_seq++; + s->encrypted_bytes_recv += (uint64_t)(plain_len + NETNOX_SSH_MAC_LEN); + padding_len = plain[4]; + if(padding_len < NETNOX_SSH_MIN_PADDING_LEN || (uint32_t)padding_len >= packet_len) { + return NETNOX_RETURN_FAILED; + } + payload_len = packet_len - 1u - (uint32_t)padding_len; + if(payload_len > payload_max) { + return NETNOX_RETURN_FAILED; + } + if(payload_len > 0u) { + memcpy(payload_out, &plain[5], payload_len); + } + *payload_len_out = payload_len; + return NETNOX_RETURN_SUCCESS; + } + + if(netnox_ssh_recv_exact(s, len_buf, sizeof(len_buf)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + packet_len = netnox_ssh_read_u32_be(len_buf); + if(packet_len < (1u + NETNOX_SSH_MIN_PADDING_LEN) || packet_len > NETNOX_SSH_MAX_PACKET_LEN) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_recv_exact(s, packet, packet_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + padding_len = packet[0]; + if(padding_len < NETNOX_SSH_MIN_PADDING_LEN || (uint32_t)padding_len >= packet_len) { + return NETNOX_RETURN_FAILED; + } + payload_len = packet_len - 1u - (uint32_t)padding_len; + if(payload_len > payload_max) { + return NETNOX_RETURN_FAILED; + } + if(payload_len > 0u) { + memcpy(payload_out, &packet[1], payload_len); + } + *payload_len_out = payload_len; + s->recv_seq++; + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t srv_exchange_kexinit(netnox_ssh_server_t * s) +{ + uint8_t rx_payload[NETNOX_SSH_MAX_PACKET_LEN]; + uint32_t rx_payload_len = 0u; + uint8_t tx_payload[NETNOX_SSH_KEXINIT_PAYLOAD_MAX_LEN]; + uint32_t tx_payload_len = 0u; + + if(s == NULL || s->connected == 0u) { + return NETNOX_RETURN_BAD_PARAM; + } + if(srv_recv_packet(s, rx_payload, (uint32_t)sizeof(rx_payload), &rx_payload_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(rx_payload_len == 0u || rx_payload[0] != NETNOX_SSH_MSG_KEXINIT) { + return NETNOX_RETURN_FAILED; + } + if(rx_payload_len > NETNOX_SSH_MAX_KEXINIT_PAYLOAD_LEN) { + return NETNOX_RETURN_FAILED; + } + memcpy(s->kexinit_client_payload, rx_payload, rx_payload_len); + s->kexinit_client_payload_len = rx_payload_len; + if(netnox_ssh_validate_client_kexinit(rx_payload, rx_payload_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(srv_build_kexinit_payload(tx_payload, &tx_payload_len, (uint32_t)sizeof(tx_payload)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(tx_payload_len > NETNOX_SSH_MAX_KEXINIT_PAYLOAD_LEN) { + return NETNOX_RETURN_FAILED; + } + memcpy(s->kexinit_server_payload, tx_payload, tx_payload_len); + s->kexinit_server_payload_len = tx_payload_len; + if(srv_send_packet(s, tx_payload, tx_payload_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + s->kexinit_exchanged = 1u; + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t srv_perform_curve25519_kex(netnox_ssh_server_t * s) +{ + uint8_t priv_s[32]; + uint8_t pub_s[32]; + uint8_t shared_raw[32]; + uint8_t init_payload[2048]; + uint32_t init_payload_len = 0u; + uint8_t host_blob[256]; + uint32_t host_blob_len = 0u; + uint8_t H[32]; + uint8_t sig64[64]; + uint8_t sig_wire[128]; + uint32_t sig_wire_len = 0u; + uint8_t reply[2048]; + uint32_t reply_len = 0u; + const uint8_t * pub_c = NULL; + uint32_t pub_c_len = 0u; + uint32_t off = 1u; + uint8_t newkeys_payload[1]; + + if(s == NULL || s->kexinit_exchanged == 0u || s->host_key_ready == 0u) { + return NETNOX_RETURN_BAD_PARAM; + } + if(srv_recv_packet(s, init_payload, (uint32_t)sizeof(init_payload), &init_payload_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(init_payload_len == 0u || init_payload[0] != NETNOX_SSH_MSG_KEX_ECDH_INIT) { + return NETNOX_RETURN_FAILED; + } + off = 1u; + if(netnox_ssh_payload_read_string_view(init_payload, init_payload_len, &off, &pub_c, &pub_c_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(pub_c_len != 32u) { + return NETNOX_RETURN_FAILED; + } + if(noxtls_x25519_generate_key(priv_s, pub_s) != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(noxtls_x25519_shared_secret(priv_s, pub_c, shared_raw) != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(srv_build_host_key_blob(s, host_blob, (uint32_t)sizeof(host_blob), &host_blob_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(srv_compute_exchange_hash(s, host_blob, host_blob_len, + pub_c, pub_c_len, pub_s, 32u, + shared_raw, (uint32_t)sizeof(shared_raw), H) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + memcpy(s->session_id, H, 32u); + s->session_id_len = 32u; + memcpy(s->shared_secret_raw, shared_raw, sizeof(shared_raw)); + if(noxtls_ed25519_sign(s->host_ed25519_sk, H, 32u, sig64) != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + { + static const char sig_alg[] = "ssh-ed25519"; + sig_wire_len = 0u; + if(netnox_ssh_payload_append_string(sig_wire, &sig_wire_len, (uint32_t)sizeof(sig_wire), + (const uint8_t *)sig_alg, (uint32_t)strlen(sig_alg)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_string(sig_wire, &sig_wire_len, (uint32_t)sizeof(sig_wire), + sig64, 64u) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + } + reply_len = 0u; + if(netnox_ssh_payload_append_u8(reply, &reply_len, (uint32_t)sizeof(reply), NETNOX_SSH_MSG_KEX_ECDH_REPLY) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_string(reply, &reply_len, (uint32_t)sizeof(reply), host_blob, host_blob_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_string(reply, &reply_len, (uint32_t)sizeof(reply), pub_s, 32u) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_string(reply, &reply_len, (uint32_t)sizeof(reply), sig_wire, sig_wire_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(srv_send_packet(s, reply, reply_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + newkeys_payload[0] = NETNOX_SSH_MSG_NEWKEYS; + if(srv_send_packet(s, newkeys_payload, 1u) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + { + uint8_t rx[64]; + uint32_t rx_len = 0u; + if(srv_recv_packet(s, rx, (uint32_t)sizeof(rx), &rx_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(rx_len == 0u || rx[0] != NETNOX_SSH_MSG_NEWKEYS) { + return NETNOX_RETURN_FAILED; + } + } + if(srv_derive_transport_keys(s) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + s->key_exchange_complete = 1u; + s->send_seq = 0u; + s->recv_seq = 0u; + return NETNOX_RETURN_SUCCESS; +} + +static netnox_return_t srv_send_service_accept(netnox_ssh_server_t * s, const char * service) +{ + uint8_t payload[256]; + uint32_t payload_len = 0u; + if(s == NULL || service == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + if(netnox_ssh_payload_append_u8(payload, &payload_len, (uint32_t)sizeof(payload), NETNOX_SSH_MSG_SERVICE_ACCEPT) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_string(payload, &payload_len, (uint32_t)sizeof(payload), + (const uint8_t *)service, (uint32_t)strlen(service)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + return srv_send_packet(s, payload, payload_len); +} + +static netnox_return_t srv_send_userauth_failure(netnox_ssh_server_t * s, int partial) +{ + uint8_t payload[256]; + uint32_t payload_len = 0u; + const char * methods = "password,publickey"; + if(s == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + if(netnox_ssh_payload_append_u8(payload, &payload_len, (uint32_t)sizeof(payload), NETNOX_SSH_MSG_USERAUTH_FAILURE) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_string(payload, &payload_len, (uint32_t)sizeof(payload), + (const uint8_t *)methods, (uint32_t)strlen(methods)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_u8(payload, &payload_len, (uint32_t)sizeof(payload), (uint8_t)(partial ? 1u : 0u)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + return srv_send_packet(s, payload, payload_len); +} + +static netnox_return_t srv_send_userauth_success(netnox_ssh_server_t * s) +{ + uint8_t p[1] = { NETNOX_SSH_MSG_USERAUTH_SUCCESS }; + return srv_send_packet(s, p, 1u); +} + +static netnox_return_t srv_send_global_request_failure(netnox_ssh_server_t * s) +{ + uint8_t p[1] = { NETNOX_SSH_MSG_REQUEST_FAILURE }; + return srv_send_packet(s, p, 1u); +} + +static netnox_return_t srv_send_channel_open_confirmation(netnox_ssh_server_t * s, + uint32_t client_sender_channel) +{ + uint8_t payload[128]; + uint32_t payload_len = 0u; + s->local_channel_id = 0u; + s->remote_channel_id = client_sender_channel; + s->local_window_size = NETNOX_SSH_CHANNEL_WINDOW_SIZE; + /* remote_window_size / remote_max_packet_size set by caller from CHANNEL_OPEN */ + if(netnox_ssh_payload_append_u8(payload, &payload_len, (uint32_t)sizeof(payload), + NETNOX_SSH_MSG_CHANNEL_OPEN_CONFIRMATION) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_u32(payload, &payload_len, (uint32_t)sizeof(payload), client_sender_channel) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_u32(payload, &payload_len, (uint32_t)sizeof(payload), s->local_channel_id) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_u32(payload, &payload_len, (uint32_t)sizeof(payload), s->local_window_size) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_u32(payload, &payload_len, (uint32_t)sizeof(payload), NETNOX_SSH_CHANNEL_MAX_PACKET_SIZE) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + return srv_send_packet(s, payload, payload_len); +} + +static netnox_return_t srv_send_channel_failure(netnox_ssh_server_t * s) +{ + uint8_t payload[64]; + uint32_t payload_len = 0u; + if(netnox_ssh_payload_append_u8(payload, &payload_len, (uint32_t)sizeof(payload), NETNOX_SSH_MSG_CHANNEL_FAILURE) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_u32(payload, &payload_len, (uint32_t)sizeof(payload), s->remote_channel_id) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + return srv_send_packet(s, payload, payload_len); +} + +static netnox_return_t srv_send_channel_success(netnox_ssh_server_t * s) +{ + uint8_t payload[64]; + uint32_t payload_len = 0u; + if(netnox_ssh_payload_append_u8(payload, &payload_len, (uint32_t)sizeof(payload), NETNOX_SSH_MSG_CHANNEL_SUCCESS) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_u32(payload, &payload_len, (uint32_t)sizeof(payload), s->remote_channel_id) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + return srv_send_packet(s, payload, payload_len); +} + +static netnox_return_t srv_send_channel_data_chunk(netnox_ssh_server_t * s, + const uint8_t * data, + uint32_t len) +{ + uint8_t payload[4u + 1u + 4u + 4u + NETNOX_SSH_MAX_DATA_LEN]; + uint32_t payload_len = 0u; + uint32_t max_chunk; + if(s == NULL || data == NULL || len == 0u) { + return NETNOX_RETURN_BAD_PARAM; + } + max_chunk = NETNOX_SSH_MAX_DATA_LEN; + if(s->remote_max_packet_size > 64u && max_chunk > s->remote_max_packet_size - 64u) { + max_chunk = s->remote_max_packet_size - 64u; + } + if(len > max_chunk) { + len = max_chunk; + } + if(netnox_ssh_payload_append_u8(payload, &payload_len, (uint32_t)sizeof(payload), NETNOX_SSH_MSG_CHANNEL_DATA) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_u32(payload, &payload_len, (uint32_t)sizeof(payload), s->remote_channel_id) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_string(payload, &payload_len, (uint32_t)sizeof(payload), data, len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(s->remote_window_size < len) { + return NETNOX_RETURN_FAILED; + } + s->remote_window_size -= len; + return srv_send_packet(s, payload, payload_len); +} + +static netnox_return_t srv_send_channel_eof(netnox_ssh_server_t * s) +{ + uint8_t payload[32]; + uint32_t payload_len = 0u; + if(netnox_ssh_payload_append_u8(payload, &payload_len, (uint32_t)sizeof(payload), NETNOX_SSH_MSG_CHANNEL_EOF) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_u32(payload, &payload_len, (uint32_t)sizeof(payload), s->remote_channel_id) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + return srv_send_packet(s, payload, payload_len); +} + +static netnox_return_t srv_send_channel_close(netnox_ssh_server_t * s) +{ + uint8_t payload[32]; + uint32_t payload_len = 0u; + if(netnox_ssh_payload_append_u8(payload, &payload_len, (uint32_t)sizeof(payload), NETNOX_SSH_MSG_CHANNEL_CLOSE) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_u32(payload, &payload_len, (uint32_t)sizeof(payload), s->remote_channel_id) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + return srv_send_packet(s, payload, payload_len); +} + +static netnox_return_t srv_maybe_window_adjust(netnox_ssh_server_t * s) +{ + uint8_t payload[64]; + uint32_t payload_len = 0u; + const uint32_t bump = 262144u; + if(s->local_window_size > 65536u) { + return NETNOX_RETURN_SUCCESS; + } + if(netnox_ssh_payload_append_u8(payload, &payload_len, (uint32_t)sizeof(payload), NETNOX_SSH_MSG_CHANNEL_WINDOW_ADJUST) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_u32(payload, &payload_len, (uint32_t)sizeof(payload), s->local_channel_id) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_append_u32(payload, &payload_len, (uint32_t)sizeof(payload), bump) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + s->local_window_size += bump; + return srv_send_packet(s, payload, payload_len); +} + +#ifndef _WIN32 +static netnox_return_t srv_run_exec_unix(netnox_ssh_server_t * s, const char * cmd) +{ + int fd[2]; + pid_t pid; + ssize_t n; + uint8_t buf[4096]; + + if(pipe(fd) != 0) { + return NETNOX_RETURN_FAILED; + } + pid = fork(); + if(pid == (pid_t)0) { + char * argv[4]; + (void)close(fd[0]); + if(dup2(fd[1], STDOUT_FILENO) < 0) { + _exit(127); + } + if(dup2(fd[1], STDERR_FILENO) < 0) { + _exit(127); + } + (void)close(fd[1]); + argv[0] = (char *)"sh"; + argv[1] = (char *)"-c"; + argv[2] = (char *)cmd; + argv[3] = NULL; + (void)execve("/bin/sh", argv, environ); + _exit(127); + } + if(pid < (pid_t)0) { + (void)close(fd[0]); + (void)close(fd[1]); + return NETNOX_RETURN_FAILED; + } + (void)close(fd[1]); + while((n = read(fd[0], buf, sizeof(buf))) > 0) { + uint32_t rem = (uint32_t)n; + uint32_t pos = 0u; + while(rem > 0u) { + uint32_t chunk = rem > 32000u ? 32000u : rem; + if(srv_send_channel_data_chunk(s, buf + pos, chunk) != NETNOX_RETURN_SUCCESS) { + (void)close(fd[0]); + (void)waitpid(pid, NULL, 0); + return NETNOX_RETURN_FAILED; + } + pos += chunk; + rem -= chunk; + } + } + (void)close(fd[0]); + (void)waitpid(pid, NULL, 0); + (void)srv_send_channel_eof(s); + (void)srv_send_channel_close(s); + s->channel_open = 0u; + return NETNOX_RETURN_SUCCESS; +} +#else +static netnox_return_t srv_run_exec_win32(netnox_ssh_server_t * s, const char * cmd) +{ + char line[NETNOX_SSH_MAX_COMMAND_LEN + 32u]; + FILE * fp; + uint8_t buf[4096]; + size_t n; + + if(strlen(cmd) + 16u >= sizeof(line)) { + return NETNOX_RETURN_BAD_PARAM; + } + if(snprintf(line, sizeof(line), "cmd /c \"%s\"", cmd) <= 0) { + return NETNOX_RETURN_FAILED; + } + fp = _popen(line, "rb"); + if(fp == NULL) { + return NETNOX_RETURN_FAILED; + } + while((n = fread(buf, 1u, sizeof(buf), fp)) > 0u) { + uint32_t rem = (uint32_t)n; + uint32_t pos = 0u; + while(rem > 0u) { + uint32_t chunk = rem > 32000u ? 32000u : rem; + if(srv_send_channel_data_chunk(s, buf + pos, chunk) != NETNOX_RETURN_SUCCESS) { + (void)_pclose(fp); + return NETNOX_RETURN_FAILED; + } + pos += chunk; + rem -= chunk; + } + } + (void)_pclose(fp); + (void)srv_send_channel_eof(s); + (void)srv_send_channel_close(s); + s->channel_open = 0u; + return NETNOX_RETURN_SUCCESS; +} +#endif + +static netnox_return_t srv_handle_userauth_request(netnox_ssh_server_t * s, + const uint8_t * p, + uint32_t plen) +{ + uint32_t off = 1u; + const uint8_t * user = NULL; + uint32_t user_len = 0u; + const uint8_t * service = NULL; + uint32_t service_len = 0u; + const uint8_t * method = NULL; + uint32_t method_len = 0u; + if(plen < 2u || p[0] != NETNOX_SSH_MSG_USERAUTH_REQUEST) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_string_view(p, plen, &off, &user, &user_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_string_view(p, plen, &off, &service, &service_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_string_view(p, plen, &off, &method, &method_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + (void)user; + (void)user_len; + if(service_len != strlen(NETNOX_SSH_SERVICE_CONNECTION) || + memcmp(service, NETNOX_SSH_SERVICE_CONNECTION, service_len) != 0) { + return srv_send_userauth_failure(s, 0); + } + if(method_len == strlen(NETNOX_SSH_AUTH_METHOD_PASSWORD) && + memcmp(method, NETNOX_SSH_AUTH_METHOD_PASSWORD, method_len) == 0) { + uint8_t change = 0u; + const uint8_t * pw = NULL; + uint32_t pw_len = 0u; + if(off >= plen) { + return NETNOX_RETURN_FAILED; + } + change = p[off++]; + (void)change; + if(netnox_ssh_payload_read_string_view(p, plen, &off, &pw, &pw_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(s->allowed_password[0] == '\0') { + return srv_send_userauth_failure(s, 0); + } + if(pw_len == strlen(s->allowed_password) && memcmp(pw, s->allowed_password, pw_len) == 0) { + s->authenticated = 1u; + return srv_send_userauth_success(s); + } + return srv_send_userauth_failure(s, 0); + } + if(method_len == strlen("publickey") && memcmp(method, "publickey", method_len) == 0) { + /* Probe or verify: not implemented — RFC 4252 continuation. */ + return srv_send_userauth_failure(s, 1); + } + return srv_send_userauth_failure(s, 0); +} + +static netnox_return_t srv_handshake_to_encrypted(netnox_ssh_server_t * s) +{ + uint8_t bi; + char line[NETNOX_SSH_MAX_IDENT_LEN + 1u]; + char tx_ident[NETNOX_SSH_MAX_IDENT_LEN + 3u]; + + if(s->host_key_ready == 0u) { + return NETNOX_RETURN_BAD_PARAM; + } + for(bi = 0u; bi < NETNOX_SSH_MAX_BANNER_LINES; bi++) { + if(netnox_ssh_recv_line(s, line, (uint16_t)sizeof(line)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_is_ident_line(line) != 0) { + if(netnox_ssh_copy_string(s->client_ident, (uint16_t)sizeof(s->client_ident), line) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + break; + } + } + if(s->client_ident[0] == '\0') { + return NETNOX_RETURN_FAILED; + } + if(snprintf(tx_ident, sizeof(tx_ident), "%s\r\n", s->server_ident) <= 0) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_send_all(s, (const uint8_t *)tx_ident, (uint32_t)strlen(tx_ident)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + s->connected = 1u; + if(srv_exchange_kexinit(s) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(srv_perform_curve25519_kex(s) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + return NETNOX_RETURN_SUCCESS; +} + +netnox_return_t netnox_ssh_server_init(netnox_ssh_server_t * server) +{ + if(server == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + memset(server, 0, sizeof(*server)); + server->rekey_threshold_bytes = (uint64_t)256 * 1024u * 1024u; + return netnox_ssh_copy_string(server->server_ident, (uint16_t)sizeof(server->server_ident), NETNOX_SSH_DEFAULT_SERVER_IDENT); +} + +void netnox_ssh_server_set_io_callbacks(netnox_ssh_server_t * server, + netnox_ssh_transport_send_t send_cb, + netnox_ssh_transport_recv_t recv_cb, + void * io_user_data) +{ + if(server == NULL) { + return; + } + server->send_cb = send_cb; + server->recv_cb = recv_cb; + server->io_user_data = io_user_data; +} + +netnox_return_t netnox_ssh_server_set_identification(netnox_ssh_server_t * server, const char * server_ident) +{ + if(server == NULL || server_ident == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + return netnox_ssh_copy_string(server->server_ident, (uint16_t)sizeof(server->server_ident), server_ident); +} + +netnox_return_t netnox_ssh_server_set_host_ed25519_seed(netnox_ssh_server_t * server, + const uint8_t secret_seed[32]) +{ + if(server == NULL || secret_seed == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + memcpy(server->host_ed25519_sk, secret_seed, 32u); + if(noxtls_ed25519_public_key(server->host_ed25519_sk, server->host_ed25519_pk) != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + server->host_key_ready = 1u; + return NETNOX_RETURN_SUCCESS; +} + +netnox_return_t netnox_ssh_server_generate_host_ed25519_key(netnox_ssh_server_t * server) +{ + if(server == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + if(noxtls_ed25519_generate_key(server->host_ed25519_sk, server->host_ed25519_pk) != NOXTLS_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + server->host_key_ready = 1u; + return NETNOX_RETURN_SUCCESS; +} + +netnox_return_t netnox_ssh_server_set_allowed_password(netnox_ssh_server_t * server, const char * password) +{ + if(server == NULL || password == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + return netnox_ssh_copy_string(server->allowed_password, (uint16_t)sizeof(server->allowed_password), password); +} + +void netnox_ssh_server_reset(netnox_ssh_server_t * server) +{ + if(server == NULL) { + return; + } + memset(server->client_ident, 0, sizeof(server->client_ident)); + server->connected = 0u; + server->kexinit_exchanged = 0u; + server->userauth_service_ready = 0u; + server->connection_service_ready = 0u; + server->authenticated = 0u; + server->channel_open = 0u; + server->key_exchange_complete = 0u; + server->send_seq = 0u; + server->recv_seq = 0u; + server->pending_newkeys_len = 0u; + server->encrypted_bytes_sent = 0u; + server->encrypted_bytes_recv = 0u; +} + +netnox_return_t netnox_ssh_server_serve_one(netnox_ssh_server_t * s) +{ + uint8_t payload[NETNOX_SSH_MAX_PACKET_LEN]; + uint32_t payload_len = 0u; + + if(s == NULL || s->send_cb == NULL || s->recv_cb == NULL) { + return NETNOX_RETURN_BAD_PARAM; + } + if(srv_handshake_to_encrypted(s) != NETNOX_RETURN_SUCCESS) { + SRVDBG("handshake failed"); + return NETNOX_RETURN_FAILED; + } + + for(;;) { + if(srv_recv_packet(s, payload, (uint32_t)sizeof(payload), &payload_len) != NETNOX_RETURN_SUCCESS) { + break; + } + if(payload_len == 0u) { + continue; + } + if(s->rekey_threshold_bytes > 0u && + (s->encrypted_bytes_sent >= s->rekey_threshold_bytes || + s->encrypted_bytes_recv >= s->rekey_threshold_bytes)) { + /* Full mid-session rekey not implemented; counters reserved for RFC 4253 §9. */ + SRVDBG("rekey threshold reached (not implemented)"); + } + switch(payload[0]) { + case NETNOX_SSH_MSG_DISCONNECT: + return NETNOX_RETURN_SUCCESS; + case NETNOX_SSH_MSG_IGNORE: + case NETNOX_SSH_MSG_EXT_INFO: + continue; + case NETNOX_SSH_MSG_GLOBAL_REQUEST: { + uint32_t off = 1u; + const uint8_t * req = NULL; + uint32_t req_len = 0u; + uint8_t want = 0u; + if(payload_len < 3u) { + continue; + } + if(netnox_ssh_payload_read_string_view(payload, payload_len, &off, &req, &req_len) != NETNOX_RETURN_SUCCESS) { + continue; + } + if(off >= payload_len) { + continue; + } + want = payload[off]; + if(want != 0u) { + (void)srv_send_global_request_failure(s); + } + continue; + } + case NETNOX_SSH_MSG_SERVICE_REQUEST: { + uint32_t off = 1u; + const uint8_t * svc = NULL; + uint32_t svc_len = 0u; + if(netnox_ssh_payload_read_string_view(payload, payload_len, &off, &svc, &svc_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(svc_len == strlen(NETNOX_SSH_SERVICE_USERAUTH) && + memcmp(svc, NETNOX_SSH_SERVICE_USERAUTH, svc_len) == 0) { + s->userauth_service_ready = 1u; + if(srv_send_service_accept(s, NETNOX_SSH_SERVICE_USERAUTH) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + continue; + } + if(s->authenticated != 0u && svc_len == strlen(NETNOX_SSH_SERVICE_CONNECTION) && + memcmp(svc, NETNOX_SSH_SERVICE_CONNECTION, svc_len) == 0) { + s->connection_service_ready = 1u; + if(srv_send_service_accept(s, NETNOX_SSH_SERVICE_CONNECTION) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + continue; + } + return NETNOX_RETURN_FAILED; + } + case NETNOX_SSH_MSG_USERAUTH_REQUEST: + if(s->userauth_service_ready == 0u) { + return NETNOX_RETURN_FAILED; + } + if(srv_handle_userauth_request(s, payload, payload_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + continue; + case NETNOX_SSH_MSG_CHANNEL_OPEN: { + uint32_t off = 1u; + const uint8_t * typ = NULL; + uint32_t typ_len = 0u; + uint32_t sender = 0u; + uint32_t client_init_win = 0u; + uint32_t client_max_pkt = 0u; + if(s->authenticated == 0u || s->connection_service_ready == 0u) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_string_view(payload, payload_len, &off, &typ, &typ_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_u32(payload, payload_len, &off, &sender) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_u32(payload, payload_len, &off, &client_init_win) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(netnox_ssh_payload_read_u32(payload, payload_len, &off, &client_max_pkt) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(typ_len == strlen(NETNOX_SSH_CHANNEL_TYPE_SESSION) && + memcmp(typ, NETNOX_SSH_CHANNEL_TYPE_SESSION, typ_len) == 0) { + s->remote_window_size = client_init_win; + s->remote_max_packet_size = (client_max_pkt != 0u) ? client_max_pkt : NETNOX_SSH_CHANNEL_MAX_PACKET_SIZE; + if(srv_send_channel_open_confirmation(s, sender) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + s->channel_open = 1u; + continue; + } + return NETNOX_RETURN_FAILED; + } + case NETNOX_SSH_MSG_CHANNEL_REQUEST: { + uint32_t off = 1u; + uint32_t chan = 0u; + const uint8_t * req = NULL; + uint32_t req_len = 0u; + uint8_t want_reply = 0u; + if(s->channel_open == 0u) { + continue; + } + if(netnox_ssh_payload_read_u32(payload, payload_len, &off, &chan) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(chan != s->local_channel_id) { + continue; + } + if(netnox_ssh_payload_read_string_view(payload, payload_len, &off, &req, &req_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(off >= payload_len) { + return NETNOX_RETURN_FAILED; + } + want_reply = payload[off++]; + if(req_len == strlen(NETNOX_SSH_CHANNEL_REQ_EXEC) && memcmp(req, NETNOX_SSH_CHANNEL_REQ_EXEC, req_len) == 0) { + const uint8_t * cmd = NULL; + uint32_t cmd_len = 0u; + char cmdbuf[NETNOX_SSH_MAX_COMMAND_LEN + 1u]; + if(netnox_ssh_payload_read_string_view(payload, payload_len, &off, &cmd, &cmd_len) != NETNOX_RETURN_SUCCESS) { + if(want_reply != 0u) { + (void)srv_send_channel_failure(s); + } + continue; + } + if(cmd_len > NETNOX_SSH_MAX_COMMAND_LEN) { + if(want_reply != 0u) { + (void)srv_send_channel_failure(s); + } + continue; + } + memcpy(cmdbuf, cmd, cmd_len); + cmdbuf[cmd_len] = '\0'; + if(want_reply != 0u) { + if(srv_send_channel_success(s) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + } +#ifndef _WIN32 + if(srv_run_exec_unix(s, cmdbuf) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } +#else + if(srv_run_exec_win32(s, cmdbuf) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } +#endif + continue; + } + if(req_len == strlen(NETNOX_SSH_CHANNEL_REQ_SHELL) && memcmp(req, NETNOX_SSH_CHANNEL_REQ_SHELL, req_len) == 0) { + if(want_reply != 0u) { + (void)srv_send_channel_failure(s); + } + continue; + } + if(req_len == strlen(NETNOX_SSH_CHANNEL_REQ_SUBSYSTEM) && + memcmp(req, NETNOX_SSH_CHANNEL_REQ_SUBSYSTEM, req_len) == 0) { + const uint8_t * sub = NULL; + uint32_t sub_len = 0u; + if(netnox_ssh_payload_read_string_view(payload, payload_len, &off, &sub, &sub_len) != NETNOX_RETURN_SUCCESS) { + if(want_reply != 0u) { + (void)srv_send_channel_failure(s); + } + continue; + } + if(sub_len == strlen(NETNOX_SSH_SUBSYSTEM_SFTP) && + memcmp(sub, NETNOX_SSH_SUBSYSTEM_SFTP, sub_len) == 0) { + /* SFTP subsystem: scaffolding — reject until SFTP layer is implemented. */ + if(want_reply != 0u) { + (void)srv_send_channel_failure(s); + } + continue; + } + if(want_reply != 0u) { + (void)srv_send_channel_failure(s); + } + continue; + } + if(want_reply != 0u) { + (void)srv_send_channel_failure(s); + } + continue; + } + case NETNOX_SSH_MSG_CHANNEL_DATA: { + uint32_t off = 1u; + uint32_t recipient = 0u; + const uint8_t * dat = NULL; + uint32_t dat_len = 0u; + if(netnox_ssh_payload_read_u32(payload, payload_len, &off, &recipient) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(recipient != s->local_channel_id) { + continue; + } + if(netnox_ssh_payload_read_string_view(payload, payload_len, &off, &dat, &dat_len) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + if(s->local_window_size >= dat_len) { + s->local_window_size -= dat_len; + } + (void)srv_maybe_window_adjust(s); + continue; + } + case NETNOX_SSH_MSG_CHANNEL_EOF: + case NETNOX_SSH_MSG_CHANNEL_CLOSE: + s->channel_open = 0u; + return NETNOX_RETURN_SUCCESS; + default: + SRVDBG("unhandled msg %u", (unsigned)payload[0]); + continue; + } + } + return NETNOX_RETURN_SUCCESS; +} diff --git a/common/noxssh_server.h b/common/noxssh_server.h new file mode 100644 index 0000000..109cab2 --- /dev/null +++ b/common/noxssh_server.h @@ -0,0 +1,113 @@ +/***************************************************************************** +* Copyright (c) [2019] - [2026], Argenox Technologies LLC +* SPDX-License-Identifier: GPL-2.0-or-later OR NoxTLS-Commercial +* +* @file noxssh_server.h +* @brief NoxSSH SSH-2 server session API (transport callbacks; one connection). +*/ + +#ifndef _NOXSSH_SERVER_H_ +#define _NOXSSH_SERVER_H_ + +#include +#include "noxssh_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Server session context (one accepted TCP/stream connection). + * + * I/O is supplied via @ref netnox_ssh_server_set_io_callbacks. Cryptographic + * host identity is an Ed25519 key (RFC 8709 ssh-ed25519). + */ +typedef struct netnox_ssh_server +{ + netnox_ssh_transport_send_t send_cb; + netnox_ssh_transport_recv_t recv_cb; + void * io_user_data; + + char client_ident[NETNOX_SSH_MAX_IDENT_LEN + 1u]; + char server_ident[NETNOX_SSH_MAX_IDENT_LEN + 1u]; + + uint8_t host_ed25519_sk[32]; + uint8_t host_ed25519_pk[32]; + uint8_t host_key_ready; + + /** Plain password accepted for any username (demo / lab use). Empty = no password auth. */ + char allowed_password[NETNOX_SSH_MAX_PASSWORD_LEN + 1u]; + + uint8_t connected; + uint8_t kexinit_exchanged; + uint8_t userauth_service_ready; + uint8_t connection_service_ready; + uint8_t authenticated; + uint8_t channel_open; + uint8_t key_exchange_complete; + + uint32_t local_channel_id; + uint32_t remote_channel_id; + uint32_t local_window_size; + uint32_t remote_window_size; + uint32_t remote_max_packet_size; + + uint8_t kexinit_client_payload[NETNOX_SSH_MAX_KEXINIT_PAYLOAD_LEN]; + uint8_t kexinit_server_payload[NETNOX_SSH_MAX_KEXINIT_PAYLOAD_LEN]; + uint32_t kexinit_client_payload_len; + uint32_t kexinit_server_payload_len; + + uint8_t session_id[32]; + uint8_t session_id_len; + uint8_t shared_secret_raw[32]; + uint8_t c2s_iv[16]; + uint8_t s2c_iv[16]; + uint8_t c2s_key[16]; + uint8_t s2c_key[16]; + uint8_t c2s_mac_key[32]; + uint8_t s2c_mac_key[32]; + uint32_t send_seq; + uint32_t recv_seq; + uint8_t c2s_counter[16]; + uint8_t s2c_counter[16]; + uint8_t pending_newkeys[32]; + uint32_t pending_newkeys_len; + + /** Rekey scaffolding (RFC 4253 §9): incremented with encrypted payload bytes. */ + uint64_t encrypted_bytes_sent; + uint64_t encrypted_bytes_recv; + uint64_t rekey_threshold_bytes; +} netnox_ssh_server_t; + +extern netnox_return_t netnox_ssh_server_init(netnox_ssh_server_t * server); +extern void netnox_ssh_server_set_io_callbacks(netnox_ssh_server_t * server, + netnox_ssh_transport_send_t send_cb, + netnox_ssh_transport_recv_t recv_cb, + void * io_user_data); +extern netnox_return_t netnox_ssh_server_set_identification(netnox_ssh_server_t * server, + const char * server_ident); +/** + * @brief Load host key from 32-byte Ed25519 seed (RFC 8032 private key format used by NoxTLS). + */ +extern netnox_return_t netnox_ssh_server_set_host_ed25519_seed(netnox_ssh_server_t * server, + const uint8_t secret_seed[32]); +extern netnox_return_t netnox_ssh_server_generate_host_ed25519_key(netnox_ssh_server_t * server); +extern netnox_return_t netnox_ssh_server_set_allowed_password(netnox_ssh_server_t * server, + const char * password); + +/** + * @brief Run full handshake and one interactive session (auth, channel, exec/shell requests). + * + * Blocks until the peer disconnects or a fatal protocol error. Exec requests run a + * one-shot subprocess (stdout only bridged to the channel). Shell and SFTP subsystem + * requests are rejected unless extended by platform adapters. + */ +extern netnox_return_t netnox_ssh_server_serve_one(netnox_ssh_server_t * server); + +extern void netnox_ssh_server_reset(netnox_ssh_server_t * server); + +#ifdef __cplusplus +} +#endif + +#endif /* _NOXSSH_SERVER_H_ */ diff --git a/docs/SERVER_EMBEDDED.md b/docs/SERVER_EMBEDDED.md new file mode 100644 index 0000000..008abeb --- /dev/null +++ b/docs/SERVER_EMBEDDED.md @@ -0,0 +1,49 @@ +# NoxSSH server and embedded integration + +## Overview + +`netnox_ssh_server_t` (`common/noxssh_server.h`) implements one SSH-2 **server** +role over a byte stream. It uses the same negotiated algorithms as the stock +client (`curve25519-sha256`, `ssh-ed25519`, `aes128-ctr`, `hmac-sha2-256`). + +The reference daemon **`noxsshd`** (`server/main.c`) binds a TCP socket and +calls `netnox_ssh_server_serve_one()` per accepted connection. + +## Transport (required) + +Implement `netnox_ssh_transport_send_t` and `netnox_ssh_transport_recv_t` with +blocking semantics matching the TCP socket behavior of the CLI client. The +server core never opens sockets itself. + +## Host key + +Call `netnox_ssh_server_set_host_ed25519_seed()` with a 32-byte Ed25519 seed +(RFC 8032 private key format as used by NoxTLS), or +`netnox_ssh_server_generate_host_ed25519_key()` for ephemeral keys. + +## Authentication + +`netnox_ssh_server_set_allowed_password()` configures a single shared password +accepted for **any** username (intended for lab / firmware shells). Extend the +server sources for per-user stores or public-key verification if needed. + +## Subsystems and PTY + +- **exec**: one-shot `sh -c` on Unix (`fork`/`pipe`) and `cmd /c` on Windows + (`_popen`), stdout/stderr streamed as channel data. +- **shell** / **pty-req**: rejected with `CHANNEL_FAILURE` until PTY adapters + are wired. +- **subsystem sftp**: rejected (scaffolding only); SFTP framing can be added + later. + +## Optional hooks + +See `server/embedded_adapter.h` for typedefs describing optional monotonic +clock and CSPRNG hooks. The current implementation uses `rand()` for SSH +padding and KEXINIT cookies; hardened devices should replace these with NoxTLS +DRBG calls. + +## Build without the daemon + +The `noxsshd` target is in `server/`. The static library `netnox_ssh` always +includes `noxssh_server.c`; omit linking `noxsshd` if you only need the client. diff --git a/scripts/ci_smoke_sshd.ps1 b/scripts/ci_smoke_sshd.ps1 new file mode 100644 index 0000000..6cf090e --- /dev/null +++ b/scripts/ci_smoke_sshd.ps1 @@ -0,0 +1,28 @@ +# Smoke: noxsshd + noxssh to localhost (Windows). +$ErrorActionPreference = "Stop" +$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +$Noxssh = Join-Path $Root "bin/noxssh.exe" +$Noxsshd = Join-Path $Root "bin/noxsshd.exe" +if (-not (Test-Path $Noxssh) -or -not (Test-Path $Noxsshd)) { + Write-Error "Missing $Noxssh or $Noxsshd" +} +$keyPath = Join-Path $env:TEMP ("noxsshd-smoke-" + [Guid]::NewGuid().ToString() + ".key") +$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() +$bytes = New-Object byte[] 32 +$rng.GetBytes($bytes) +[IO.File]::WriteAllBytes($keyPath, $bytes) +$port = if ($env:NOXSSH_SMOKE_PORT) { [int]$env:NOXSSH_SMOKE_PORT } else { 4022 } +$args = @("-p", "$port", "--password", "smoketest", "--host-key", $keyPath) +$proc = Start-Process -FilePath $Noxsshd -ArgumentList $args -PassThru -WindowStyle Hidden +Start-Sleep -Seconds 2 +try { + $out = & $Noxssh -p $port -w smoketest user@127.0.0.1 "echo ci_ok" 2>&1 | Out-String + if ($out -notmatch "ci_ok") { + Write-Error "Expected ci_ok in output: $out" + } + Write-Host "ci_smoke_sshd: ok" +} +finally { + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue + Remove-Item -Force $keyPath -ErrorAction SilentlyContinue +} diff --git a/scripts/ci_smoke_sshd.sh b/scripts/ci_smoke_sshd.sh new file mode 100644 index 0000000..0981f32 --- /dev/null +++ b/scripts/ci_smoke_sshd.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Smoke: noxsshd + noxssh to localhost (Unix). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +NOXSSH="${ROOT}/bin/noxssh" +NOXSSHD="${ROOT}/bin/noxsshd" +if [[ ! -f "${NOXSSH}" ]] || [[ ! -f "${NOXSSHD}" ]]; then + echo "ci_smoke_sshd: expected ${NOXSSH} and ${NOXSSHD}" >&2 + exit 1 +fi +KEYFILE="$(mktemp)" +trap 'rm -f "${KEYFILE}"; kill "${NOXSSHD_PID:-}" 2>/dev/null || true' EXIT +if ! head -c 32 /dev/urandom > "${KEYFILE}" 2>/dev/null; then + openssl rand -out "${KEYFILE}" 32 +fi +PORT="${NOXSSH_SMOKE_PORT:-4022}" +"${NOXSSHD}" -p "${PORT}" --password smoketest --host-key "${KEYFILE}" & +NOXSSHD_PID=$! +sleep 2 +OUT="$("${NOXSSH}" -p "${PORT}" -w smoketest user@127.0.0.1 "echo ci_ok" || true)" +if [[ "${OUT}" != *"ci_ok"* ]]; then + echo "ci_smoke_sshd: expected ci_ok in output, got: ${OUT}" >&2 + exit 1 +fi +kill "${NOXSSHD_PID}" 2>/dev/null || true +wait "${NOXSSHD_PID}" 2>/dev/null || true +echo "ci_smoke_sshd: ok" diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt new file mode 100644 index 0000000..6242e8e --- /dev/null +++ b/server/CMakeLists.txt @@ -0,0 +1,19 @@ +add_executable(noxsshd + main.c + daemon_runtime.c +) + +target_include_directories(noxsshd + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/common +) + +target_link_libraries(noxsshd + PRIVATE + netnox_ssh +) + +if(WIN32) + target_link_libraries(noxsshd PRIVATE ws2_32) +endif() diff --git a/server/daemon_runtime.c b/server/daemon_runtime.c new file mode 100644 index 0000000..3e9fab8 --- /dev/null +++ b/server/daemon_runtime.c @@ -0,0 +1,235 @@ +/***************************************************************************** +* Copyright (c) [2019] - [2026], Argenox Technologies LLC +* SPDX-License-Identifier: GPL-2.0-or-later OR NoxTLS-Commercial +*/ + +#include +#include +#include + +#include "daemon_runtime.h" + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +typedef SOCKET noxsshd_sock_t; +#define NOXSSH_INVALID_SOCKET INVALID_SOCKET +#define NOXSSH_CLOSESOCK closesocket +#define NOXSSH_SOCKERR SOCKET_ERROR +#else +#include +#include +#include +#include +#include +#include +#include +typedef int noxsshd_sock_t; +#define NOXSSH_INVALID_SOCKET (-1) +#define NOXSSH_CLOSESOCK close +#define NOXSSH_SOCKERR (-1) +#endif + +typedef struct +{ + noxsshd_sock_t sock; +} noxsshd_conn_t; + +static int32_t noxsshd_send_cb(void * user_data, const uint8_t * data, uint32_t len) +{ + noxsshd_conn_t * c = (noxsshd_conn_t *)user_data; + if(c == NULL || data == NULL) { + return -1; + } +#ifdef _WIN32 + return send(c->sock, (const char *)data, (int)len, 0); +#else + return (int32_t)send(c->sock, data, (size_t)len, 0); +#endif +} + +static int32_t noxsshd_recv_cb(void * user_data, uint8_t * data, uint32_t len) +{ + noxsshd_conn_t * c = (noxsshd_conn_t *)user_data; + if(c == NULL || data == NULL) { + return -1; + } +#ifdef _WIN32 + return recv(c->sock, (char *)data, (int)len, 0); +#else + return (int32_t)recv(c->sock, data, (size_t)len, 0); +#endif +} + +static int noxsshd_serve_connection(noxsshd_sock_t client_sock, + const char * password, + const uint8_t host_ed25519_seed[32]) +{ + netnox_ssh_server_t srv; + noxsshd_conn_t conn; + netnox_return_t rc; + + conn.sock = client_sock; + if(netnox_ssh_server_init(&srv) != NETNOX_RETURN_SUCCESS) { + return 1; + } + netnox_ssh_server_set_io_callbacks(&srv, noxsshd_send_cb, noxsshd_recv_cb, &conn); + if(netnox_ssh_server_set_host_ed25519_seed(&srv, host_ed25519_seed) != NETNOX_RETURN_SUCCESS) { + return 1; + } + if(password != NULL && password[0] != '\0') { + if(netnox_ssh_server_set_allowed_password(&srv, password) != NETNOX_RETURN_SUCCESS) { + return 1; + } + } + rc = netnox_ssh_server_serve_one(&srv); + netnox_ssh_server_reset(&srv); + return (rc == NETNOX_RETURN_SUCCESS) ? 0 : 1; +} + +#ifndef _WIN32 +static int noxsshd_listen_posix(const char * bind_host, uint16_t port, + const char * password, + const uint8_t host_ed25519_seed[32]) +{ + struct addrinfo hints; + struct addrinfo * res = NULL; + struct addrinfo * rp = NULL; + char portbuf[16]; + noxsshd_sock_t listen_fd = NOXSSH_INVALID_SOCKET; + int yes = 1; + int gai_err; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + (void)snprintf(portbuf, sizeof(portbuf), "%u", (unsigned)port); + gai_err = getaddrinfo((bind_host != NULL && bind_host[0] != '\0') ? bind_host : NULL, portbuf, &hints, &res); + if(gai_err != 0 || res == NULL) { + fprintf(stderr, "noxsshd: getaddrinfo: %s\n", gai_strerror(gai_err)); + return 1; + } + for(rp = res; rp != NULL; rp = rp->ai_next) { + listen_fd = (noxsshd_sock_t)socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if(listen_fd == NOXSSH_INVALID_SOCKET) { + continue; + } + if(setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, (socklen_t)sizeof(yes)) == NOXSSH_SOCKERR) { + NOXSSH_CLOSESOCK(listen_fd); + listen_fd = NOXSSH_INVALID_SOCKET; + continue; + } + if(bind(listen_fd, rp->ai_addr, rp->ai_addrlen) == 0) { + break; + } + NOXSSH_CLOSESOCK(listen_fd); + listen_fd = NOXSSH_INVALID_SOCKET; + } + freeaddrinfo(res); + if(listen_fd == NOXSSH_INVALID_SOCKET) { + fprintf(stderr, "noxsshd: bind failed\n"); + return 1; + } + if(listen(listen_fd, 8) != 0) { + perror("noxsshd: listen"); + NOXSSH_CLOSESOCK(listen_fd); + return 1; + } + fprintf(stderr, "noxsshd: listening (sequential accept)\n"); + for(;;) { + noxsshd_sock_t client = accept(listen_fd, NULL, NULL); + if(client == NOXSSH_INVALID_SOCKET) { + perror("noxsshd: accept"); + continue; + } + (void)noxsshd_serve_connection(client, password, host_ed25519_seed); + NOXSSH_CLOSESOCK(client); + } +} +#else +static int noxsshd_listen_win32(const char * bind_host, uint16_t port, + const char * password, + const uint8_t host_ed25519_seed[32]) +{ + struct addrinfo hints; + struct addrinfo * res = NULL; + struct addrinfo * rp = NULL; + char portbuf[16]; + noxsshd_sock_t listen_fd = NOXSSH_INVALID_SOCKET; + WSADATA wsa; + int gai_err; + + if(WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { + fprintf(stderr, "noxsshd: WSAStartup failed\n"); + return 1; + } + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + (void)snprintf(portbuf, sizeof(portbuf), "%u", (unsigned)port); + gai_err = getaddrinfo((bind_host != NULL && bind_host[0] != '\0') ? bind_host : NULL, portbuf, &hints, &res); + if(gai_err != 0 || res == NULL) { + fprintf(stderr, "noxsshd: getaddrinfo failed %d\n", gai_err); + WSACleanup(); + return 1; + } + for(rp = res; rp != NULL; rp = rp->ai_next) { + listen_fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if(listen_fd == INVALID_SOCKET) { + continue; + } + { + int yes = 1; + if(setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes)) == SOCKET_ERROR) { + closesocket(listen_fd); + listen_fd = INVALID_SOCKET; + continue; + } + } + if(bind(listen_fd, rp->ai_addr, (int)rp->ai_addrlen) == 0) { + break; + } + closesocket(listen_fd); + listen_fd = INVALID_SOCKET; + } + freeaddrinfo(res); + if(listen_fd == INVALID_SOCKET) { + fprintf(stderr, "noxsshd: bind failed\n"); + WSACleanup(); + return 1; + } + if(listen(listen_fd, SOMAXCONN) == SOCKET_ERROR) { + fprintf(stderr, "noxsshd: listen failed\n"); + closesocket(listen_fd); + WSACleanup(); + return 1; + } + fprintf(stderr, "noxsshd: listening (sequential accept)\n"); + for(;;) { + noxsshd_sock_t client = accept(listen_fd, NULL, NULL); + if(client == INVALID_SOCKET) { + fprintf(stderr, "noxsshd: accept failed\n"); + continue; + } + (void)noxsshd_serve_connection(client, password, host_ed25519_seed); + closesocket(client); + } +} +#endif + +int noxsshd_listen_loop(const char * bind_host, + uint16_t port, + const char * password, + const uint8_t host_ed25519_seed[32]) +{ +#ifndef _WIN32 + return noxsshd_listen_posix(bind_host, port, password, host_ed25519_seed); +#else + return noxsshd_listen_win32(bind_host, port, password, host_ed25519_seed); +#endif +} diff --git a/server/daemon_runtime.h b/server/daemon_runtime.h new file mode 100644 index 0000000..183d544 --- /dev/null +++ b/server/daemon_runtime.h @@ -0,0 +1,31 @@ +/***************************************************************************** +* Copyright (c) [2019] - [2026], Argenox Technologies LLC +* SPDX-License-Identifier: GPL-2.0-or-later OR NoxTLS-Commercial +*/ + +#ifndef _NOXSSH_DAEMON_RUNTIME_H_ +#define _NOXSSH_DAEMON_RUNTIME_H_ + +#include +#include "noxssh_server.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Listen on @p bind_host : @p port and serve one client at a time (sequential). + * + * @p bind_host may be NULL for all interfaces. @p host_ed25519_seed is always + * 32 bytes (Ed25519 seed); generate or load before calling. + */ +int noxsshd_listen_loop(const char * bind_host, + uint16_t port, + const char * password, + const uint8_t host_ed25519_seed[32]); + +#ifdef __cplusplus +} +#endif + +#endif /* _NOXSSH_DAEMON_RUNTIME_H_ */ diff --git a/server/embedded_adapter.h b/server/embedded_adapter.h new file mode 100644 index 0000000..a3610af --- /dev/null +++ b/server/embedded_adapter.h @@ -0,0 +1,44 @@ +/***************************************************************************** +* Copyright (c) [2019] - [2026], Argenox Technologies LLC +* SPDX-License-Identifier: GPL-2.0-or-later OR NoxTLS-Commercial +* +* @file embedded_adapter.h +* @brief Contract for embedding noxsshd / netnox_ssh_server on RTOS or bare-metal. +* +* The portable server core lives in @c netnox_ssh_server_t (noxssh_server.h): +* it only needs blocking @c send_cb / @c recv_cb over any byte stream (TCP, +* UART framing, custom link). Desktop daemons use POSIX or Winsock sockets +* behind those callbacks; embedded ports supply their own. +* +* Optional hooks (future / platform-specific): +* - Monotonic milliseconds for rekey and timeouts +* - CSPRNG for KEXINIT cookies and padding (today the stack uses @c rand() for +* non-cryptographic SSH padding; replace with DRBG for hardened devices) +* - Subprocess / PTY: @c netnox_ssh_server_serve_one implements one-shot exec +* on POSIX (@c fork/@c pipe) and @c _popen on Windows. Embedded targets without +* a shell should wrap @c netnox_ssh_server_serve_one or add a compile-time +* stub that maps CHANNEL_REQUEST exec to an application-defined handler. +* +* See docs/SERVER_EMBEDDED.md for integration steps. +*/ + +#ifndef _NOXSSH_EMBEDDED_ADAPTER_H_ +#define _NOXSSH_EMBEDDED_ADAPTER_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Optional: monotonic time in milliseconds (for rekey / watchdogs). */ +typedef uint64_t (*noxssh_embedded_monotonic_ms_fn)(void * user_data); + +/** @brief Optional: fill @p buf with @p len bytes of cryptographic-quality random. */ +typedef int (*noxssh_embedded_csprng_fn)(void * user_data, uint8_t * buf, uint32_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* _NOXSSH_EMBEDDED_ADAPTER_H_ */ diff --git a/server/main.c b/server/main.c new file mode 100644 index 0000000..95baf9a --- /dev/null +++ b/server/main.c @@ -0,0 +1,88 @@ +/***************************************************************************** +* Copyright (c) [2019] - [2026], Argenox Technologies LLC +* SPDX-License-Identifier: GPL-2.0-or-later OR NoxTLS-Commercial +*/ + +#include +#include +#include + +#include "daemon_runtime.h" +#include "../noxtls/noxtls-lib/pkc/ed25519/noxtls_ed25519.h" +#include "../client/version.h" + +static void print_usage(const char * prog) +{ + fprintf(stderr, + "%s " NOXSSH_VERSION_STRING " — NoxSSH server (sequential accept)\n" + "Usage: %s [-b bind] [-p port] --password [--host-key ]\n" + " -b bind Address to bind (default: all interfaces)\n" + " -p port TCP port (default: 2222)\n" + " --password Password accepted for any username (required for password auth)\n" + " --host-key Path to 32-byte raw Ed25519 seed file (optional; ephemeral if omitted)\n", + prog, prog); +} + +int main(int argc, char ** argv) +{ + const char * bind_host = NULL; + const char * password = NULL; + const char * host_key_path = NULL; + uint16_t port = 2222u; + uint8_t host_seed[32]; + uint8_t host_pk[32]; + int i; + + for(i = 1; i < argc; i++) { + if(strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return 0; + } + if(strcmp(argv[i], "-b") == 0 && i + 1 < argc) { + bind_host = argv[++i]; + } else if(strcmp(argv[i], "-p") == 0 && i + 1 < argc) { + int p = atoi(argv[++i]); + if(p <= 0 || p > 65535) { + fprintf(stderr, "noxsshd: invalid port\n"); + return 1; + } + port = (uint16_t)p; + } else if(strcmp(argv[i], "--password") == 0 && i + 1 < argc) { + password = argv[++i]; + } else if(strcmp(argv[i], "--host-key") == 0 && i + 1 < argc) { + host_key_path = argv[++i]; + } else { + fprintf(stderr, "noxsshd: unknown argument: %s\n", argv[i]); + print_usage(argv[0]); + return 1; + } + } + + if(password == NULL || password[0] == '\0') { + fprintf(stderr, "noxsshd: --password is required\n"); + return 1; + } + + if(host_key_path != NULL) { + FILE * f = fopen(host_key_path, "rb"); + if(f == NULL) { + perror("noxsshd: fopen host key"); + return 1; + } + if(fread(host_seed, 1u, sizeof(host_seed), f) != sizeof(host_seed)) { + fprintf(stderr, "noxsshd: host key file must be exactly 32 bytes\n"); + fclose(f); + return 1; + } + fclose(f); + } else { + if(noxtls_ed25519_generate_key(host_seed, host_pk) != NOXTLS_RETURN_SUCCESS) { + fprintf(stderr, "noxsshd: failed to generate ephemeral host key\n"); + return 1; + } + (void)fprintf(stderr, "noxsshd: warning: ephemeral host key (not saved); clients will see host key warnings\n"); + (void)host_pk; + } + + return noxsshd_listen_loop(bind_host, port, password, host_seed); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..d3b542c --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(test_server_init + test_server_init.c +) +target_include_directories(test_server_init + PRIVATE + ${CMAKE_SOURCE_DIR}/common +) +target_link_libraries(test_server_init + PRIVATE + netnox_ssh +) +add_test(NAME server_init COMMAND test_server_init) diff --git a/tests/test_server_init.c b/tests/test_server_init.c new file mode 100644 index 0000000..af99cd8 --- /dev/null +++ b/tests/test_server_init.c @@ -0,0 +1,11 @@ +#include +#include "noxssh_server.h" + +int main(void) +{ + netnox_ssh_server_t s; + if(netnox_ssh_server_init(&s) != NETNOX_RETURN_SUCCESS) { + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} From e866d9d7d823de4fed2e982101872772f5542d4a Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 12 Oct 2026 22:48:36 -0500 Subject: [PATCH 02/13] Add Linux Ed25519 SSH daemon with interactive PTY sessions --- .github/workflows/build.yml | 26 +- .github/workflows/release-on-tag.yml | 4 +- .gitmodules | 2 +- CMakeLists.txt | 8 +- README.md | 2 + common/noxssh_server.c | 440 +++++++++++++++++++-------- common/noxssh_server.h | 26 +- docs/server.md | 33 ++ scripts/ci_smoke_sshd.ps1 | 28 -- scripts/ci_smoke_sshd.sh | 56 ++-- server/CMakeLists.txt | 5 + server/auth_posix.c | 85 ++++++ server/auth_posix.h | 5 + server/daemon_runtime.c | 59 ++-- server/daemon_runtime.h | 1 - server/main.c | 163 +++++----- tests/CMakeLists.txt | 2 + 17 files changed, 637 insertions(+), 308 deletions(-) create mode 100644 docs/server.md delete mode 100644 scripts/ci_smoke_sshd.ps1 create mode 100644 server/auth_posix.c create mode 100644 server/auth_posix.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 245dfa0..d0f115e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,12 +47,12 @@ jobs: mkdir -p dist/debian cp build/*.deb dist/debian/ - - name: Verify daemon binary + - name: Verify binaries shell: bash run: | set -euo pipefail test -f bin/noxssh - test -f bin/noxsshd || test -f bin/noxsshd.exe + if [[ "${{ runner.os }}" == "Linux" ]]; then test -f bin/noxsshd; fi - name: CTest if: runner.os != 'Windows' @@ -64,26 +64,6 @@ jobs: run: cd build; ctest -C Release --output-on-failure - name: Smoke noxsshd (Unix) - if: runner.os != 'Windows' + if: runner.os == 'Linux' shell: bash run: bash scripts/ci_smoke_sshd.sh - - - name: Smoke noxsshd (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: ./scripts/ci_smoke_sshd.ps1 - - - name: Upload binaries - uses: actions/upload-artifact@v4 - with: - name: noxssh-${{ matrix.artifact_suffix }} - path: bin/** - if-no-files-found: error - - - name: Upload Debian package - if: runner.os == 'Linux' - uses: actions/upload-artifact@v4 - with: - name: noxssh-${{ matrix.artifact_suffix }}-deb - path: dist/debian/*.deb - if-no-files-found: error diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml index efbf029..413a2c6 100644 --- a/.github/workflows/release-on-tag.yml +++ b/.github/workflows/release-on-tag.yml @@ -127,12 +127,12 @@ jobs: cp "${deb}" "release-assets/noxssh-${{ matrix.artifact_suffix }}-v${VERSION}.deb" done - - name: Verify client and daemon binaries + - name: Verify binaries shell: bash run: | set -euo pipefail test -f bin/noxssh - test -f bin/noxsshd || test -f bin/noxsshd.exe + if [[ "${{ runner.os }}" == "Linux" ]]; then test -f bin/noxsshd; fi - name: Create release archives shell: bash diff --git a/.gitmodules b/.gitmodules index 61e96b9..e773a24 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "noxtls"] path = noxtls - url = git@github.com:argenox/noxtls.git + url = https://github.com/argenox/noxtls.git branch = master diff --git a/CMakeLists.txt b/CMakeLists.txt index c3c8f67..72853d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,8 +47,10 @@ add_subdirectory(noxtls EXCLUDE_FROM_ALL) add_library(netnox_ssh common/noxssh_common.c - common/noxssh_server.c ) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_sources(netnox_ssh PRIVATE common/noxssh_server.c) +endif() target_include_directories(netnox_ssh PUBLIC ${CMAKE_SOURCE_DIR}/common @@ -75,7 +77,9 @@ target_compile_definitions(netnox_ssh NOXTLS_FEATURE_AES_CTR=1 ) add_subdirectory(client) -add_subdirectory(server) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_subdirectory(server) +endif() option(NOXSSH_BUILD_TESTS "Build noxssh unit tests" ON) if(NOXSSH_BUILD_TESTS) diff --git a/README.md b/README.md index fb76524..7b27d8d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # NoxSSH +The opt-in Linux SSH server is documented in [docs/server.md](docs/server.md). + [![Build](https://github.com/argenox/noxssh/actions/workflows/build.yml/badge.svg)](https://github.com/argenox/noxssh/actions/workflows/build.yml) [![Release on tag](https://github.com/argenox/noxssh/actions/workflows/release-on-tag.yml/badge.svg)](https://github.com/argenox/noxssh/actions/workflows/release-on-tag.yml) diff --git a/common/noxssh_server.c b/common/noxssh_server.c index bf828be..2180714 100644 --- a/common/noxssh_server.c +++ b/common/noxssh_server.c @@ -1,3 +1,4 @@ +#define _GNU_SOURCE /***************************************************************************** * Copyright (c) [2019] - [2026], Argenox Technologies LLC * SPDX-License-Identifier: GPL-2.0-or-later OR NoxTLS-Commercial @@ -10,11 +11,22 @@ #include #include #include +#include #ifdef _WIN32 /* Windows: one-shot exec uses _popen only. */ #else #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include @@ -37,7 +49,12 @@ extern char ** environ; #define NETNOX_SSH_AES_BLOCK_LEN (16u) #define NETNOX_SSH_MAC_LEN (32u) -static uint8_t g_srv_prng_seeded; +static uint64_t srv_now_ms(void) +{ + struct timespec ts; + if(clock_gettime(CLOCK_MONOTONIC,&ts)) return UINT64_MAX; + return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u; +} static int netnox_ssh_srv_debug(void) { @@ -79,19 +96,23 @@ static uint32_t netnox_ssh_read_u32_be(const uint8_t * in) (uint32_t)in[3]; } -static void netnox_ssh_fill_random(uint8_t * out, uint32_t len) +static netnox_return_t netnox_ssh_fill_random(uint8_t * out, uint32_t len) { - uint32_t i; - if(out == NULL || len == 0u) { - return; - } - if(g_srv_prng_seeded == 0u) { - srand((unsigned int)time(NULL)); - g_srv_prng_seeded = 1u; - } - for(i = 0u; i < len; i++) { - out[i] = (uint8_t)(rand() & 0xFF); + uint32_t pos = 0u; + if(out == NULL) return NETNOX_RETURN_BAD_PARAM; +#ifdef __linux__ + while(pos < len) { + ssize_t n = getrandom(out + pos, len - pos, 0); + if(n > 0) { pos += (uint32_t)n; continue; } + if(n < 0 && errno == EINTR) continue; + return NETNOX_RETURN_FAILED; } + return NETNOX_RETURN_SUCCESS; +#else + /* This server requires a blocking OS CSPRNG. Do not silently use rand(). */ + (void)pos; (void)len; + return NETNOX_RETURN_FAILED; +#endif } static netnox_return_t netnox_ssh_copy_string(char * dst, uint16_t dst_len, const char * src) @@ -168,6 +189,9 @@ static netnox_return_t netnox_ssh_send_all(netnox_ssh_server_t * s, const uint8_ return NETNOX_RETURN_BAD_PARAM; } while(sent_total < len) { + uint64_t now = srv_now_ms(); + if(now == UINT64_MAX || (s->send_deadline_ms && now > s->send_deadline_ms) || + (!s->session_started && s->handshake_deadline_ms && now > s->handshake_deadline_ms)) return NETNOX_RETURN_FAILED; int32_t sent = s->send_cb(s->io_user_data, data + sent_total, len - sent_total); if(sent <= 0) { return NETNOX_RETURN_FAILED; @@ -184,6 +208,9 @@ static netnox_return_t netnox_ssh_recv_exact(netnox_ssh_server_t * s, uint8_t * return NETNOX_RETURN_BAD_PARAM; } while(recv_total < len) { + uint64_t now = srv_now_ms(); + if(now == UINT64_MAX || (s->packet_deadline_ms && now > s->packet_deadline_ms) || + (!s->session_started && s->handshake_deadline_ms && now > s->handshake_deadline_ms)) return NETNOX_RETURN_FAILED; int32_t got = s->recv_cb(s->io_user_data, data + recv_total, len - recv_total); if(got <= 0) { return NETNOX_RETURN_FAILED; @@ -202,6 +229,7 @@ static netnox_return_t netnox_ssh_recv_line(netnox_ssh_server_t * s, char * out, while(used < (uint16_t)(out_len - 1u)) { int32_t rc; uint8_t ch = 0u; + if(srv_now_ms() > s->handshake_deadline_ms) return NETNOX_RETURN_FAILED; rc = s->recv_cb(s->io_user_data, &ch, 1u); if(rc <= 0) { return NETNOX_RETURN_FAILED; @@ -769,29 +797,29 @@ static netnox_return_t srv_build_kexinit_payload(uint8_t * payload, } payload[0] = NETNOX_SSH_MSG_KEXINIT; *payload_len = 1u; - netnox_ssh_fill_random(&payload[*payload_len], NETNOX_SSH_KEXINIT_COOKIE_LEN); + if(netnox_ssh_fill_random(&payload[*payload_len], NETNOX_SSH_KEXINIT_COOKIE_LEN) != NETNOX_RETURN_SUCCESS) return NETNOX_RETURN_FAILED; *payload_len += NETNOX_SSH_KEXINIT_COOKIE_LEN; - rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, NETNOX_SSH_KEX_ALG_LIST); + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, "curve25519-sha256"); if(rc != NETNOX_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } - rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, NETNOX_SSH_HOST_KEY_ALG_LIST); + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, "ssh-ed25519"); if(rc != NETNOX_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } - rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, NETNOX_SSH_CIPHER_ALG_LIST); + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, "aes128-ctr"); if(rc != NETNOX_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } - rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, NETNOX_SSH_CIPHER_ALG_LIST); + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, "aes128-ctr"); if(rc != NETNOX_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } - rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, NETNOX_SSH_MAC_ALG_LIST); + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, "hmac-sha2-256"); if(rc != NETNOX_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } - rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, NETNOX_SSH_MAC_ALG_LIST); + rc = netnox_ssh_payload_append_namelist(payload, payload_len, payload_max, "hmac-sha2-256"); if(rc != NETNOX_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } @@ -843,6 +871,7 @@ static netnox_return_t srv_send_packet(netnox_ssh_server_t * s, if(s == NULL || payload == NULL || payload_len == 0u) { return NETNOX_RETURN_BAD_PARAM; } + s->send_deadline_ms = srv_now_ms() + 10000u; block_size = (s->key_exchange_complete != 0u) ? NETNOX_SSH_AES_BLOCK_LEN : NETNOX_SSH_PACKET_BLOCK_SIZE; while(((1u + payload_len + padding_len + 4u) % block_size) != 0u) { padding_len++; @@ -855,7 +884,7 @@ static netnox_return_t srv_send_packet(netnox_ssh_server_t * s, netnox_ssh_write_u32_be(&plain[0], packet_len); plain[4] = (uint8_t)padding_len; memcpy(&plain[5], payload, payload_len); - netnox_ssh_fill_random(&plain[5u + payload_len], padding_len); + if(netnox_ssh_fill_random(&plain[5u + payload_len], padding_len) != NETNOX_RETURN_SUCCESS) return NETNOX_RETURN_FAILED; if(s->key_exchange_complete != 0u) { uint8_t mac_input[4u + 4u + NETNOX_SSH_MAX_PACKET_LEN]; @@ -867,40 +896,15 @@ static netnox_return_t srv_send_packet(netnox_ssh_server_t * s, mac_input, 4u + plain_len, mac_buf) != NETNOX_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } - aes_rc = aes_encrypt_ctr(s->s2c_key, plain, plain_len, s->s2c_counter, enc, AES_128_BIT); + aes_rc = noxtls_aes_encrypt_ctr(s->s2c_key, plain, plain_len, s->s2c_counter, enc, NOXTLS_AES_128_BIT); if(aes_rc != NOXTLS_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } n_blocks = (plain_len + NETNOX_SSH_AES_BLOCK_LEN - 1u) / NETNOX_SSH_AES_BLOCK_LEN; netnox_ssh_ctr_increment_blocks(s->s2c_counter, n_blocks); - { - uint8_t combined[128]; - uint32_t total = s->pending_newkeys_len + enc_mac_len; - if(total > 0u && total <= (uint32_t)sizeof(combined)) { - uint32_t off = 0u; - if(s->pending_newkeys_len > 0u) { - memcpy(combined, s->pending_newkeys, s->pending_newkeys_len); - off = s->pending_newkeys_len; - s->pending_newkeys_len = 0u; - } - memcpy(combined + off, enc, plain_len); - memcpy(combined + off + plain_len, mac_buf, NETNOX_SSH_MAC_LEN); - if(netnox_ssh_send_all(s, combined, total) != NETNOX_RETURN_SUCCESS) { - return NETNOX_RETURN_FAILED; - } - } else { - if(s->pending_newkeys_len > 0u) { - if(netnox_ssh_send_all(s, s->pending_newkeys, s->pending_newkeys_len) != NETNOX_RETURN_SUCCESS) { - return NETNOX_RETURN_FAILED; - } - s->pending_newkeys_len = 0u; - } - memcpy(combined, enc, plain_len); - memcpy(combined + plain_len, mac_buf, NETNOX_SSH_MAC_LEN); - if(netnox_ssh_send_all(s, combined, enc_mac_len) != NETNOX_RETURN_SUCCESS) { - return NETNOX_RETURN_FAILED; - } - } + if(netnox_ssh_send_all(s, enc, plain_len) != NETNOX_RETURN_SUCCESS || + netnox_ssh_send_all(s, mac_buf, NETNOX_SSH_MAC_LEN) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; } s->send_seq++; s->encrypted_bytes_sent += (uint64_t)enc_mac_len; @@ -934,6 +938,7 @@ static netnox_return_t srv_recv_packet(netnox_ssh_server_t * s, if(s == NULL || payload_out == NULL || payload_len_out == NULL) { return NETNOX_RETURN_BAD_PARAM; } + s->packet_deadline_ms = srv_now_ms() + 10000u; if(s->key_exchange_complete != 0u) { uint8_t enc[4u + NETNOX_SSH_MAX_PACKET_LEN]; @@ -948,21 +953,22 @@ static netnox_return_t srv_recv_packet(netnox_ssh_server_t * s, if(netnox_ssh_recv_exact(s, enc, NETNOX_SSH_AES_BLOCK_LEN) != NETNOX_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } - aes_rc = aes_encrypt_ctr(s->c2s_key, enc, NETNOX_SSH_AES_BLOCK_LEN, s->c2s_counter, plain, AES_128_BIT); + aes_rc = noxtls_aes_encrypt_ctr(s->c2s_key, enc, NETNOX_SSH_AES_BLOCK_LEN, s->c2s_counter, plain, NOXTLS_AES_128_BIT); if(aes_rc != NOXTLS_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } netnox_ssh_ctr_increment_blocks(s->c2s_counter, 1u); packet_len = netnox_ssh_read_u32_be(&plain[0]); if(packet_len < (1u + NETNOX_SSH_MIN_PADDING_LEN) || packet_len > NETNOX_SSH_MAX_PACKET_LEN) { + SRVDBG("bad encrypted packet length %u", (unsigned)packet_len); return NETNOX_RETURN_FAILED; } plain_len = 4u + packet_len; if(netnox_ssh_recv_exact(s, &enc[NETNOX_SSH_AES_BLOCK_LEN], plain_len - NETNOX_SSH_AES_BLOCK_LEN) != NETNOX_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } - aes_rc = aes_encrypt_ctr(s->c2s_key, &enc[NETNOX_SSH_AES_BLOCK_LEN], plain_len - NETNOX_SSH_AES_BLOCK_LEN, - s->c2s_counter, &plain[NETNOX_SSH_AES_BLOCK_LEN], AES_128_BIT); + aes_rc = noxtls_aes_encrypt_ctr(s->c2s_key, &enc[NETNOX_SSH_AES_BLOCK_LEN], plain_len - NETNOX_SSH_AES_BLOCK_LEN, + s->c2s_counter, &plain[NETNOX_SSH_AES_BLOCK_LEN], NOXTLS_AES_128_BIT); if(aes_rc != NOXTLS_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } @@ -978,6 +984,7 @@ static netnox_return_t srv_recv_packet(netnox_ssh_server_t * s, return NETNOX_RETURN_FAILED; } if(memcmp(computed_mac, mac_buf, NETNOX_SSH_MAC_LEN) != 0) { + SRVDBG("encrypted packet MAC mismatch"); return NETNOX_RETURN_FAILED; } s->recv_seq++; @@ -1098,7 +1105,11 @@ static netnox_return_t srv_perform_curve25519_kex(netnox_ssh_server_t * s) if(pub_c_len != 32u) { return NETNOX_RETURN_FAILED; } - if(noxtls_x25519_generate_key(priv_s, pub_s) != NOXTLS_RETURN_SUCCESS) { + if(netnox_ssh_fill_random(priv_s, sizeof(priv_s)) != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + noxtls_x25519_clamp_scalar(priv_s); + if(noxtls_x25519_public_key(priv_s, pub_s) != NOXTLS_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } if(noxtls_x25519_shared_secret(priv_s, pub_c, shared_raw) != NOXTLS_RETURN_SUCCESS) { @@ -1164,8 +1175,6 @@ static netnox_return_t srv_perform_curve25519_kex(netnox_ssh_server_t * s) return NETNOX_RETURN_FAILED; } s->key_exchange_complete = 1u; - s->send_seq = 0u; - s->recv_seq = 0u; return NETNOX_RETURN_SUCCESS; } @@ -1190,7 +1199,7 @@ static netnox_return_t srv_send_userauth_failure(netnox_ssh_server_t * s, int pa { uint8_t payload[256]; uint32_t payload_len = 0u; - const char * methods = "password,publickey"; + const char * methods = "publickey"; if(s == NULL) { return NETNOX_RETURN_BAD_PARAM; } @@ -1213,6 +1222,16 @@ static netnox_return_t srv_send_userauth_success(netnox_ssh_server_t * s) return srv_send_packet(s, p, 1u); } +static netnox_return_t srv_send_userauth_pk_ok(netnox_ssh_server_t *s, const uint8_t *alg, + uint32_t alg_len, const uint8_t *blob, uint32_t blob_len) +{ + uint8_t p[256]; uint32_t len = 0u; + if(netnox_ssh_payload_append_u8(p, &len, sizeof(p), 60u) != NETNOX_RETURN_SUCCESS || + netnox_ssh_payload_append_string(p, &len, sizeof(p), alg, alg_len) != NETNOX_RETURN_SUCCESS || + netnox_ssh_payload_append_string(p, &len, sizeof(p), blob, blob_len) != NETNOX_RETURN_SUCCESS) return NETNOX_RETURN_FAILED; + return srv_send_packet(s, p, len); +} + static netnox_return_t srv_send_global_request_failure(netnox_ssh_server_t * s) { uint8_t p[1] = { NETNOX_SSH_MSG_REQUEST_FAILURE }; @@ -1354,60 +1373,152 @@ static netnox_return_t srv_maybe_window_adjust(netnox_ssh_server_t * s) } #ifndef _WIN32 -static netnox_return_t srv_run_exec_unix(netnox_ssh_server_t * s, const char * cmd) +static netnox_return_t srv_send_exit_status(netnox_ssh_server_t *s, uint32_t status) { - int fd[2]; - pid_t pid; - ssize_t n; - uint8_t buf[4096]; + uint8_t p[64]; uint32_t len = 0u; + if(netnox_ssh_payload_append_u8(p, &len, sizeof(p), NETNOX_SSH_MSG_CHANNEL_REQUEST) != NETNOX_RETURN_SUCCESS || + netnox_ssh_payload_append_u32(p, &len, sizeof(p), s->remote_channel_id) != NETNOX_RETURN_SUCCESS || + netnox_ssh_payload_append_string(p, &len, sizeof(p), (const uint8_t *)"exit-status", 11u) != NETNOX_RETURN_SUCCESS || + netnox_ssh_payload_append_u8(p, &len, sizeof(p), 0u) != NETNOX_RETURN_SUCCESS || + netnox_ssh_payload_append_u32(p, &len, sizeof(p), status) != NETNOX_RETURN_SUCCESS) return NETNOX_RETURN_FAILED; + return srv_send_packet(s, p, len); +} - if(pipe(fd) != 0) { - return NETNOX_RETURN_FAILED; - } - pid = fork(); - if(pid == (pid_t)0) { - char * argv[4]; - (void)close(fd[0]); - if(dup2(fd[1], STDOUT_FILENO) < 0) { - _exit(127); +static int srv_write_child(int fd, const uint8_t *data, uint32_t len, int pty) +{ + uint32_t pos = 0u; + while(pos < len) { + ssize_t n = pty ? write(fd, data + pos, len - pos) : send(fd, data + pos, len - pos, MSG_NOSIGNAL); + if(n > 0) { pos += (uint32_t)n; continue; } + if(n < 0 && errno == EINTR) continue; + if(n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + struct pollfd p = { fd, POLLOUT, 0 }; + if(poll(&p, 1, 10000) > 0) continue; } - if(dup2(fd[1], STDERR_FILENO) < 0) { - _exit(127); + return -1; + } + return 0; +} + +static netnox_return_t srv_run_session_unix(netnox_ssh_server_t *s, const char *cmd, int shell) +{ + int pair[2] = {-1, -1}, child_fd = -1, status = 0, child_reaped = 0, output_eof = 0; + pid_t pid; + struct winsize ws; + uint8_t payload[NETNOX_SSH_MAX_PACKET_LEN], output[4096]; + uint32_t len; + if(s->transport_fd < 0 || s->authenticated_user[0] == '\0') return NETNOX_RETURN_FAILED; + s->session_started = 1u; + memset(&ws, 0, sizeof(ws)); + ws.ws_col = (unsigned short)s->pty_cols; ws.ws_row = (unsigned short)s->pty_rows; + ws.ws_xpixel = (unsigned short)s->pty_width; ws.ws_ypixel = (unsigned short)s->pty_height; + if(s->want_pty) pid = forkpty(&child_fd, NULL, NULL, &ws); + else { + if(socketpair(AF_UNIX, SOCK_STREAM, 0, pair)) return NETNOX_RETURN_FAILED; + pid = fork(); + } + if(pid == 0) { + struct passwd *pw = getpwnam(s->authenticated_user); + const char *sh; + if(!s->want_pty) { + close(pair[0]); + if(setsid() < 0 || dup2(pair[1], 0) < 0 || dup2(pair[1], 1) < 0 || dup2(pair[1], 2) < 0) _exit(127); + if(pair[1] > 2) close(pair[1]); } - (void)close(fd[1]); - argv[0] = (char *)"sh"; - argv[1] = (char *)"-c"; - argv[2] = (char *)cmd; - argv[3] = NULL; - (void)execve("/bin/sh", argv, environ); + close(s->transport_fd); + if(!pw || !pw->pw_dir || !pw->pw_shell || !pw->pw_shell[0]) _exit(127); + if(geteuid() == 0) { + if(initgroups(pw->pw_name, pw->pw_gid) || setgid(pw->pw_gid) || setuid(pw->pw_uid)) _exit(127); + } else if(geteuid() != pw->pw_uid || getegid() != pw->pw_gid) _exit(127); + if(chdir(pw->pw_dir)) _exit(127); + sh = pw->pw_shell; + if(clearenv() || setenv("HOME", pw->pw_dir, 1) || setenv("USER", pw->pw_name, 1) || + setenv("LOGNAME", pw->pw_name, 1) || setenv("SHELL", sh, 1) || + setenv("PATH", "/usr/local/bin:/usr/bin:/bin", 1) || + (s->want_pty && setenv("TERM", s->term[0] ? s->term : "vt100", 1))) _exit(127); + if(shell) execl(sh, sh, "-i", (char *)NULL); + else execl(sh, sh, "-c", cmd, (char *)NULL); _exit(127); } - if(pid < (pid_t)0) { - (void)close(fd[0]); - (void)close(fd[1]); - return NETNOX_RETURN_FAILED; - } - (void)close(fd[1]); - while((n = read(fd[0], buf, sizeof(buf))) > 0) { - uint32_t rem = (uint32_t)n; - uint32_t pos = 0u; - while(rem > 0u) { - uint32_t chunk = rem > 32000u ? 32000u : rem; - if(srv_send_channel_data_chunk(s, buf + pos, chunk) != NETNOX_RETURN_SUCCESS) { - (void)close(fd[0]); - (void)waitpid(pid, NULL, 0); - return NETNOX_RETURN_FAILED; + if(pid < 0) { if(pair[0] >= 0) { close(pair[0]); close(pair[1]); } return NETNOX_RETURN_FAILED; } + if(!s->want_pty) { close(pair[1]); child_fd = pair[0]; } + (void)fcntl(child_fd, F_SETFL, fcntl(child_fd, F_GETFL, 0) | O_NONBLOCK); + for(;;) { + struct pollfd pf[2]; int nfd = 1, rc; + if(s->rekey_threshold_bytes && + (s->encrypted_bytes_sent >= s->rekey_threshold_bytes || + s->encrypted_bytes_recv >= s->rekey_threshold_bytes)) break; + pf[0].fd = s->transport_fd; pf[0].events = POLLIN; pf[0].revents = 0; + if(!output_eof && s->remote_window_size) { + pf[1].fd = child_fd; pf[1].events = POLLIN | POLLHUP; pf[1].revents = 0; nfd = 2; + } + rc = poll(pf, nfd, 1800000); /* 30-minute idle session limit. */ + if(rc < 0 && errno == EINTR) continue; + if(rc <= 0) break; /* Bound idle client or stalled remote window. */ + if(pf[0].revents & (POLLERR | POLLHUP | POLLNVAL)) break; + if(pf[0].revents & POLLIN) { + uint32_t off = 1u, channel, amount; + const uint8_t *data; uint32_t data_len; + if(srv_recv_packet(s, payload, sizeof(payload), &len) != NETNOX_RETURN_SUCCESS || !len) break; + if(payload[0] == NETNOX_SSH_MSG_CHANNEL_DATA) { + if(netnox_ssh_payload_read_u32(payload,len,&off,&channel) != NETNOX_RETURN_SUCCESS || channel != s->local_channel_id || + netnox_ssh_payload_read_string_view(payload,len,&off,&data,&data_len) != NETNOX_RETURN_SUCCESS || + off != len || data_len > s->local_window_size || srv_write_child(child_fd,data,data_len,s->want_pty)) break; + s->local_window_size -= data_len; + if(srv_maybe_window_adjust(s) != NETNOX_RETURN_SUCCESS) break; + } else if(payload[0] == NETNOX_SSH_MSG_CHANNEL_WINDOW_ADJUST) { + if(netnox_ssh_payload_read_u32(payload,len,&off,&channel) != NETNOX_RETURN_SUCCESS || + netnox_ssh_payload_read_u32(payload,len,&off,&amount) != NETNOX_RETURN_SUCCESS || + channel != s->local_channel_id || amount > UINT32_MAX - s->remote_window_size) break; + s->remote_window_size += amount; + } else if(payload[0] == NETNOX_SSH_MSG_CHANNEL_EOF) { + if(!s->want_pty) shutdown(child_fd, SHUT_WR); + else { const uint8_t eot = 4u; (void)srv_write_child(child_fd,&eot,1u,1); } + } else if(payload[0] == NETNOX_SSH_MSG_CHANNEL_CLOSE || payload[0] == NETNOX_SSH_MSG_DISCONNECT) break; + else if(payload[0] == NETNOX_SSH_MSG_CHANNEL_REQUEST && s->want_pty) { + const uint8_t *req; uint32_t req_len; uint32_t cols, rows, width, height; + if(netnox_ssh_payload_read_u32(payload,len,&off,&channel) != NETNOX_RETURN_SUCCESS || + netnox_ssh_payload_read_string_view(payload,len,&off,&req,&req_len) != NETNOX_RETURN_SUCCESS || + channel != s->local_channel_id || off >= len) break; + off++; /* want-reply */ + if(req_len == 13u && memcmp(req,"window-change",13u) == 0 && + netnox_ssh_payload_read_u32(payload,len,&off,&cols) == NETNOX_RETURN_SUCCESS && + netnox_ssh_payload_read_u32(payload,len,&off,&rows) == NETNOX_RETURN_SUCCESS && + netnox_ssh_payload_read_u32(payload,len,&off,&width) == NETNOX_RETURN_SUCCESS && + netnox_ssh_payload_read_u32(payload,len,&off,&height) == NETNOX_RETURN_SUCCESS) { + ws.ws_col=(unsigned short)cols; ws.ws_row=(unsigned short)rows; + ws.ws_xpixel=(unsigned short)width; ws.ws_ypixel=(unsigned short)height; + (void)ioctl(child_fd,TIOCSWINSZ,&ws); + } } - pos += chunk; - rem -= chunk; + } + if(nfd == 2 && (pf[1].revents & (POLLIN | POLLHUP))) { + uint32_t cap = sizeof(output); + ssize_t n; + if(cap > s->remote_window_size) cap = s->remote_window_size; + if(s->remote_max_packet_size > 64u && cap > s->remote_max_packet_size - 64u) cap = s->remote_max_packet_size - 64u; + if(cap == 0u) continue; + n = read(child_fd, output, cap); + if(n > 0) { if(srv_send_channel_data_chunk(s,output,(uint32_t)n) != NETNOX_RETURN_SUCCESS) break; } + else if(n == 0 || (n < 0 && errno == EIO)) output_eof = 1; + else if(errno != EINTR && errno != EAGAIN) break; + } + if(!child_reaped && waitpid(pid,&status,WNOHANG) == pid) child_reaped = 1; + if(output_eof && child_reaped) { + uint32_t exit_code = WIFEXITED(status) ? (uint32_t)WEXITSTATUS(status) : 128u + (uint32_t)WTERMSIG(status); + (void)srv_send_exit_status(s, exit_code); + (void)srv_send_channel_eof(s); (void)srv_send_channel_close(s); + s->channel_open = 0u; close(child_fd); return NETNOX_RETURN_SUCCESS; } } - (void)close(fd[0]); - (void)waitpid(pid, NULL, 0); - (void)srv_send_channel_eof(s); - (void)srv_send_channel_close(s); + close(child_fd); + if(!child_reaped) { + kill(-pid,SIGTERM); + for(int tries=0; tries<20 && waitpid(pid,&status,WNOHANG)==0; tries++) usleep(50000); + if(waitpid(pid,&status,WNOHANG)==0) { kill(-pid,SIGKILL); waitpid(pid,&status,0); } + } s->channel_open = 0u; - return NETNOX_RETURN_SUCCESS; + return NETNOX_RETURN_FAILED; } #else static netnox_return_t srv_run_exec_win32(netnox_ssh_server_t * s, const char * cmd) @@ -1471,37 +1582,59 @@ static netnox_return_t srv_handle_userauth_request(netnox_ssh_server_t * s, if(netnox_ssh_payload_read_string_view(p, plen, &off, &method, &method_len) != NETNOX_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } - (void)user; - (void)user_len; if(service_len != strlen(NETNOX_SSH_SERVICE_CONNECTION) || memcmp(service, NETNOX_SSH_SERVICE_CONNECTION, service_len) != 0) { return srv_send_userauth_failure(s, 0); } - if(method_len == strlen(NETNOX_SSH_AUTH_METHOD_PASSWORD) && - memcmp(method, NETNOX_SSH_AUTH_METHOD_PASSWORD, method_len) == 0) { - uint8_t change = 0u; - const uint8_t * pw = NULL; - uint32_t pw_len = 0u; - if(off >= plen) { - return NETNOX_RETURN_FAILED; - } - change = p[off++]; - (void)change; - if(netnox_ssh_payload_read_string_view(p, plen, &off, &pw, &pw_len) != NETNOX_RETURN_SUCCESS) { - return NETNOX_RETURN_FAILED; + if(method_len == strlen("publickey") && memcmp(method, "publickey", method_len) == 0) { + const uint8_t * alg, * key_blob, * blob_alg, * key, * sig_blob, * sig_alg, * sig; + uint32_t alg_len, key_blob_len, blob_alg_len, key_len, sig_blob_len, sig_alg_len, sig_len; + uint32_t blob_off = 0u, sig_off = 0u, req_len, signed_len; + uint8_t * to_sign; + char username[65]; + int allowed; + uint8_t signed_flag; + if(user_len == 0u || user_len >= sizeof(username) || memchr(user, 0, user_len) != NULL || + s->publickey_allowed_cb == NULL || off >= plen) return srv_send_userauth_failure(s, 0); + signed_flag = p[off++]; + if(signed_flag > 1u || + netnox_ssh_payload_read_string_view(p, plen, &off, &alg, &alg_len) != NETNOX_RETURN_SUCCESS || + netnox_ssh_payload_read_string_view(p, plen, &off, &key_blob, &key_blob_len) != NETNOX_RETURN_SUCCESS) { + return srv_send_userauth_failure(s, 0); } - if(s->allowed_password[0] == '\0') { + if(alg_len != strlen(NETNOX_SSH_KEYALG_ED25519) || + memcmp(alg, NETNOX_SSH_KEYALG_ED25519, alg_len) != 0 || + netnox_ssh_payload_read_string_view(key_blob, key_blob_len, &blob_off, &blob_alg, &blob_alg_len) != NETNOX_RETURN_SUCCESS || + netnox_ssh_payload_read_string_view(key_blob, key_blob_len, &blob_off, &key, &key_len) != NETNOX_RETURN_SUCCESS || + blob_off != key_blob_len || blob_alg_len != alg_len || memcmp(blob_alg, alg, alg_len) != 0 || key_len != 32u) { return srv_send_userauth_failure(s, 0); } - if(pw_len == strlen(s->allowed_password) && memcmp(pw, s->allowed_password, pw_len) == 0) { - s->authenticated = 1u; - return srv_send_userauth_success(s); + req_len = off; + memcpy(username, user, user_len); username[user_len] = '\0'; + if(!s->publickey_allowed_cb(s->publickey_allowed_user_data, username, key)) return srv_send_userauth_failure(s, 0); + if(signed_flag == 0u) { + if(off != plen) return srv_send_userauth_failure(s, 0); + return srv_send_userauth_pk_ok(s, alg, alg_len, key_blob, key_blob_len); } - return srv_send_userauth_failure(s, 0); - } - if(method_len == strlen("publickey") && memcmp(method, "publickey", method_len) == 0) { - /* Probe or verify: not implemented — RFC 4252 continuation. */ - return srv_send_userauth_failure(s, 1); + if(netnox_ssh_payload_read_string_view(p, plen, &off, &sig_blob, &sig_blob_len) != NETNOX_RETURN_SUCCESS || off != plen || + netnox_ssh_payload_read_string_view(sig_blob, sig_blob_len, &sig_off, &sig_alg, &sig_alg_len) != NETNOX_RETURN_SUCCESS || + netnox_ssh_payload_read_string_view(sig_blob, sig_blob_len, &sig_off, &sig, &sig_len) != NETNOX_RETURN_SUCCESS || + sig_off != sig_blob_len || sig_alg_len != alg_len || memcmp(sig_alg, alg, alg_len) != 0 || sig_len != 64u) { + return srv_send_userauth_failure(s, 0); + } + signed_len = 4u + s->session_id_len + req_len; + to_sign = (uint8_t *)malloc(signed_len); + if(to_sign == NULL) return NETNOX_RETURN_FAILED; + netnox_ssh_write_u32_be(to_sign, s->session_id_len); + memcpy(to_sign + 4u, s->session_id, s->session_id_len); + memcpy(to_sign + 4u + s->session_id_len, p, req_len); + allowed = noxtls_ed25519_verify(key, to_sign, signed_len, sig) == NOXTLS_RETURN_SUCCESS; + free(to_sign); + if(!allowed) return srv_send_userauth_failure(s, 0); + memcpy(s->authenticated_user, username, user_len + 1u); + s->authenticated = 1u; + s->connection_service_ready = 1u; + return srv_send_userauth_success(s); } return srv_send_userauth_failure(s, 0); } @@ -1551,6 +1684,7 @@ netnox_return_t netnox_ssh_server_init(netnox_ssh_server_t * server) return NETNOX_RETURN_BAD_PARAM; } memset(server, 0, sizeof(*server)); + server->transport_fd = -1; server->rekey_threshold_bytes = (uint64_t)256 * 1024u * 1024u; return netnox_ssh_copy_string(server->server_ident, (uint16_t)sizeof(server->server_ident), NETNOX_SSH_DEFAULT_SERVER_IDENT); } @@ -1595,19 +1729,21 @@ netnox_return_t netnox_ssh_server_generate_host_ed25519_key(netnox_ssh_server_t if(server == NULL) { return NETNOX_RETURN_BAD_PARAM; } - if(noxtls_ed25519_generate_key(server->host_ed25519_sk, server->host_ed25519_pk) != NOXTLS_RETURN_SUCCESS) { + if(netnox_ssh_fill_random(server->host_ed25519_sk, sizeof(server->host_ed25519_sk)) != NETNOX_RETURN_SUCCESS || + noxtls_ed25519_public_key(server->host_ed25519_sk, server->host_ed25519_pk) != NOXTLS_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } server->host_key_ready = 1u; return NETNOX_RETURN_SUCCESS; } -netnox_return_t netnox_ssh_server_set_allowed_password(netnox_ssh_server_t * server, const char * password) +void netnox_ssh_server_set_publickey_callback(netnox_ssh_server_t * server, + int (*callback)(void *, const char *, const uint8_t [32]), + void * user_data) { - if(server == NULL || password == NULL) { - return NETNOX_RETURN_BAD_PARAM; - } - return netnox_ssh_copy_string(server->allowed_password, (uint16_t)sizeof(server->allowed_password), password); + if(server == NULL) return; + server->publickey_allowed_cb = callback; + server->publickey_allowed_user_data = user_data; } void netnox_ssh_server_reset(netnox_ssh_server_t * server) @@ -1621,6 +1757,12 @@ void netnox_ssh_server_reset(netnox_ssh_server_t * server) server->userauth_service_ready = 0u; server->connection_service_ready = 0u; server->authenticated = 0u; + server->authenticated_user[0] = '\0'; + server->auth_attempts = 0u; + server->session_started = 0u; + server->handshake_deadline_ms = 0u; + server->packet_deadline_ms = 0u; + server->send_deadline_ms = 0u; server->channel_open = 0u; server->key_exchange_complete = 0u; server->send_seq = 0u; @@ -1638,6 +1780,7 @@ netnox_return_t netnox_ssh_server_serve_one(netnox_ssh_server_t * s) if(s == NULL || s->send_cb == NULL || s->recv_cb == NULL) { return NETNOX_RETURN_BAD_PARAM; } + s->handshake_deadline_ms = srv_now_ms() + 30000u; if(srv_handshake_to_encrypted(s) != NETNOX_RETURN_SUCCESS) { SRVDBG("handshake failed"); return NETNOX_RETURN_FAILED; @@ -1645,8 +1788,10 @@ netnox_return_t netnox_ssh_server_serve_one(netnox_ssh_server_t * s) for(;;) { if(srv_recv_packet(s, payload, (uint32_t)sizeof(payload), &payload_len) != NETNOX_RETURN_SUCCESS) { + SRVDBG("receive packet failed"); break; } + SRVDBG("received packet %u", (unsigned)(payload_len ? payload[0] : 0u)); if(payload_len == 0u) { continue; } @@ -1654,7 +1799,8 @@ netnox_return_t netnox_ssh_server_serve_one(netnox_ssh_server_t * s) (s->encrypted_bytes_sent >= s->rekey_threshold_bytes || s->encrypted_bytes_recv >= s->rekey_threshold_bytes)) { /* Full mid-session rekey not implemented; counters reserved for RFC 4253 §9. */ - SRVDBG("rekey threshold reached (not implemented)"); + SRVDBG("rekey threshold reached; closing connection"); + return NETNOX_RETURN_FAILED; } switch(payload[0]) { case NETNOX_SSH_MSG_DISCONNECT: @@ -1711,6 +1857,7 @@ netnox_return_t netnox_ssh_server_serve_one(netnox_ssh_server_t * s) if(s->userauth_service_ready == 0u) { return NETNOX_RETURN_FAILED; } + if(s->authenticated || ++s->auth_attempts > 6u) return NETNOX_RETURN_FAILED; if(srv_handle_userauth_request(s, payload, payload_len) != NETNOX_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } @@ -1739,6 +1886,7 @@ netnox_return_t netnox_ssh_server_serve_one(netnox_ssh_server_t * s) } if(typ_len == strlen(NETNOX_SSH_CHANNEL_TYPE_SESSION) && memcmp(typ, NETNOX_SSH_CHANNEL_TYPE_SESSION, typ_len) == 0) { + if(s->channel_open || client_max_pkt < 128u) return NETNOX_RETURN_FAILED; s->remote_window_size = client_init_win; s->remote_max_packet_size = (client_max_pkt != 0u) ? client_max_pkt : NETNOX_SSH_CHANNEL_MAX_PACKET_SIZE; if(srv_send_channel_open_confirmation(s, sender) != NETNOX_RETURN_SUCCESS) { @@ -1771,6 +1919,24 @@ netnox_return_t netnox_ssh_server_serve_one(netnox_ssh_server_t * s) return NETNOX_RETURN_FAILED; } want_reply = payload[off++]; + if(req_len == 7u && memcmp(req, "pty-req", 7u) == 0) { + const uint8_t *term, *modes; uint32_t term_len, modes_len; + uint32_t cols,rows,width,height; + if(s->want_pty || + netnox_ssh_payload_read_string_view(payload,payload_len,&off,&term,&term_len) != NETNOX_RETURN_SUCCESS || + term_len >= sizeof(s->term) || memchr(term,0,term_len) || + netnox_ssh_payload_read_u32(payload,payload_len,&off,&cols) != NETNOX_RETURN_SUCCESS || + netnox_ssh_payload_read_u32(payload,payload_len,&off,&rows) != NETNOX_RETURN_SUCCESS || + netnox_ssh_payload_read_u32(payload,payload_len,&off,&width) != NETNOX_RETURN_SUCCESS || + netnox_ssh_payload_read_u32(payload,payload_len,&off,&height) != NETNOX_RETURN_SUCCESS || + netnox_ssh_payload_read_string_view(payload,payload_len,&off,&modes,&modes_len) != NETNOX_RETURN_SUCCESS || + off != payload_len) { if(want_reply) (void)srv_send_channel_failure(s); continue; } + memcpy(s->term,term,term_len); s->term[term_len]='\0'; + s->pty_cols=cols; s->pty_rows=rows; s->pty_width=width; s->pty_height=height; + s->want_pty=1u; + if(want_reply && srv_send_channel_success(s) != NETNOX_RETURN_SUCCESS) return NETNOX_RETURN_FAILED; + continue; + } if(req_len == strlen(NETNOX_SSH_CHANNEL_REQ_EXEC) && memcmp(req, NETNOX_SSH_CHANNEL_REQ_EXEC, req_len) == 0) { const uint8_t * cmd = NULL; uint32_t cmd_len = 0u; @@ -1781,7 +1947,7 @@ netnox_return_t netnox_ssh_server_serve_one(netnox_ssh_server_t * s) } continue; } - if(cmd_len > NETNOX_SSH_MAX_COMMAND_LEN) { + if(cmd_len > NETNOX_SSH_MAX_COMMAND_LEN || memchr(cmd,0,cmd_len) || off != payload_len) { if(want_reply != 0u) { (void)srv_send_channel_failure(s); } @@ -1795,7 +1961,7 @@ netnox_return_t netnox_ssh_server_serve_one(netnox_ssh_server_t * s) } } #ifndef _WIN32 - if(srv_run_exec_unix(s, cmdbuf) != NETNOX_RETURN_SUCCESS) { + if(srv_run_session_unix(s, cmdbuf, 0) != NETNOX_RETURN_SUCCESS) { return NETNOX_RETURN_FAILED; } #else @@ -1806,9 +1972,13 @@ netnox_return_t netnox_ssh_server_serve_one(netnox_ssh_server_t * s) continue; } if(req_len == strlen(NETNOX_SSH_CHANNEL_REQ_SHELL) && memcmp(req, NETNOX_SSH_CHANNEL_REQ_SHELL, req_len) == 0) { - if(want_reply != 0u) { - (void)srv_send_channel_failure(s); - } + if(off != payload_len) { if(want_reply) (void)srv_send_channel_failure(s); continue; } +#ifndef _WIN32 + if(want_reply && srv_send_channel_success(s) != NETNOX_RETURN_SUCCESS) return NETNOX_RETURN_FAILED; + if(srv_run_session_unix(s, NULL, 1) != NETNOX_RETURN_SUCCESS) return NETNOX_RETURN_FAILED; +#else + if(want_reply) (void)srv_send_channel_failure(s); +#endif continue; } if(req_len == strlen(NETNOX_SSH_CHANNEL_REQ_SUBSYSTEM) && diff --git a/common/noxssh_server.h b/common/noxssh_server.h index 109cab2..260a126 100644 --- a/common/noxssh_server.h +++ b/common/noxssh_server.h @@ -27,6 +27,7 @@ typedef struct netnox_ssh_server netnox_ssh_transport_send_t send_cb; netnox_ssh_transport_recv_t recv_cb; void * io_user_data; + int transport_fd; /* POSIX poll descriptor; -1 when unavailable. */ char client_ident[NETNOX_SSH_MAX_IDENT_LEN + 1u]; char server_ident[NETNOX_SSH_MAX_IDENT_LEN + 1u]; @@ -35,8 +36,18 @@ typedef struct netnox_ssh_server uint8_t host_ed25519_pk[32]; uint8_t host_key_ready; - /** Plain password accepted for any username (demo / lab use). Empty = no password auth. */ - char allowed_password[NETNOX_SSH_MAX_PASSWORD_LEN + 1u]; + /** The daemon checks the OS account and authorized_keys before signature verification. */ + int (*publickey_allowed_cb)(void *, const char *, const uint8_t [32]); + void * publickey_allowed_user_data; + char authenticated_user[65]; + uint8_t auth_attempts; + uint8_t session_started; + uint64_t handshake_deadline_ms; + uint64_t packet_deadline_ms; + uint64_t send_deadline_ms; + uint8_t want_pty; + char term[65]; + uint32_t pty_cols, pty_rows, pty_width, pty_height; uint8_t connected; uint8_t kexinit_exchanged; @@ -92,15 +103,16 @@ extern netnox_return_t netnox_ssh_server_set_identification(netnox_ssh_server_t extern netnox_return_t netnox_ssh_server_set_host_ed25519_seed(netnox_ssh_server_t * server, const uint8_t secret_seed[32]); extern netnox_return_t netnox_ssh_server_generate_host_ed25519_key(netnox_ssh_server_t * server); -extern netnox_return_t netnox_ssh_server_set_allowed_password(netnox_ssh_server_t * server, - const char * password); +extern void netnox_ssh_server_set_publickey_callback(netnox_ssh_server_t * server, + int (*callback)(void *, const char *, const uint8_t [32]), + void * user_data); /** * @brief Run full handshake and one interactive session (auth, channel, exec/shell requests). * - * Blocks until the peer disconnects or a fatal protocol error. Exec requests run a - * one-shot subprocess (stdout only bridged to the channel). Shell and SFTP subsystem - * requests are rejected unless extended by platform adapters. + * Blocks until the peer disconnects or a fatal protocol error. On Linux, + * session channels support exec and shell with optional PTY. SFTP and port + * forwarding are not implemented. */ extern netnox_return_t netnox_ssh_server_serve_one(netnox_ssh_server_t * server); diff --git a/docs/server.md b/docs/server.md new file mode 100644 index 0000000..e3802c7 --- /dev/null +++ b/docs/server.md @@ -0,0 +1,33 @@ +# Linux server + +`noxsshd` is an opt-in Linux SSH server. It does not start automatically. +It binds to `127.0.0.1:2222` unless `-b` and `-p` are supplied. The +MPUNoxV image can retain OpenSSH while this server is qualified. + +The server accepts Ed25519 public keys only. The requested username must +resolve to a local account with a usable shell. It reads that account's +`~/.ssh/authorized_keys`, accepting plain `ssh-ed25519` lines without key +options. Home, `.ssh`, and `authorized_keys` must be owned by the account +or root and not writable by group or other users. The key file must be +regular and at most 64 KiB. Successful sessions use that account's UID, +GID, groups, home directory, and shell. + +Create a protected host key directory before starting the server: + +```sh +install -d -m 0700 /etc/noxssh +noxsshd -b 0.0.0.0 -p 2222 +``` + +On its first start, the daemon creates `/etc/noxssh/host_ed25519_seed` with +mode `0600` using Linux `getrandom()`. The 32-byte seed is reused on future +starts; back it up as a secret if clients pin the host identity. A supplied +`--host-key` must be an absolute path under a directory owned by the daemon +and not group/other writable. The server refuses an unsafe or malformed key. + +The current server supports one session channel per connection, remote +commands, a shell, optional PTY, terminal resize, stdin/stdout/stderr, +and `exit-status`. It permits four concurrent connections. Idle sessions +close after 30 minutes, and connections close at the 256 MiB rekey limit +until in-session rekey is implemented. It does not offer SFTP, forwarding, +password authentication, or every SSH terminal-mode control. diff --git a/scripts/ci_smoke_sshd.ps1 b/scripts/ci_smoke_sshd.ps1 deleted file mode 100644 index 6cf090e..0000000 --- a/scripts/ci_smoke_sshd.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -# Smoke: noxsshd + noxssh to localhost (Windows). -$ErrorActionPreference = "Stop" -$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) -$Noxssh = Join-Path $Root "bin/noxssh.exe" -$Noxsshd = Join-Path $Root "bin/noxsshd.exe" -if (-not (Test-Path $Noxssh) -or -not (Test-Path $Noxsshd)) { - Write-Error "Missing $Noxssh or $Noxsshd" -} -$keyPath = Join-Path $env:TEMP ("noxsshd-smoke-" + [Guid]::NewGuid().ToString() + ".key") -$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() -$bytes = New-Object byte[] 32 -$rng.GetBytes($bytes) -[IO.File]::WriteAllBytes($keyPath, $bytes) -$port = if ($env:NOXSSH_SMOKE_PORT) { [int]$env:NOXSSH_SMOKE_PORT } else { 4022 } -$args = @("-p", "$port", "--password", "smoketest", "--host-key", $keyPath) -$proc = Start-Process -FilePath $Noxsshd -ArgumentList $args -PassThru -WindowStyle Hidden -Start-Sleep -Seconds 2 -try { - $out = & $Noxssh -p $port -w smoketest user@127.0.0.1 "echo ci_ok" 2>&1 | Out-String - if ($out -notmatch "ci_ok") { - Write-Error "Expected ci_ok in output: $out" - } - Write-Host "ci_smoke_sshd: ok" -} -finally { - Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue - Remove-Item -Force $keyPath -ErrorAction SilentlyContinue -} diff --git a/scripts/ci_smoke_sshd.sh b/scripts/ci_smoke_sshd.sh index 0981f32..afe4f27 100644 --- a/scripts/ci_smoke_sshd.sh +++ b/scripts/ci_smoke_sshd.sh @@ -1,27 +1,39 @@ #!/usr/bin/env bash -# Smoke: noxsshd + noxssh to localhost (Unix). +# Linux integration smoke. Creates a disposable OS account; never edits a real user's authorized_keys. set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" -NOXSSH="${ROOT}/bin/noxssh" -NOXSSHD="${ROOT}/bin/noxsshd" -if [[ ! -f "${NOXSSH}" ]] || [[ ! -f "${NOXSSHD}" ]]; then - echo "ci_smoke_sshd: expected ${NOXSSH} and ${NOXSSHD}" >&2 - exit 1 -fi -KEYFILE="$(mktemp)" -trap 'rm -f "${KEYFILE}"; kill "${NOXSSHD_PID:-}" 2>/dev/null || true' EXIT -if ! head -c 32 /dev/urandom > "${KEYFILE}" 2>/dev/null; then - openssl rand -out "${KEYFILE}" 32 -fi +NOXSSH="$ROOT/bin/noxssh" +NOXSSHD="$ROOT/bin/noxsshd" +command -v sudo >/dev/null || { echo 'sudo required for isolated account smoke' >&2; exit 1; } +sudo -n true || { echo 'passwordless sudo required for isolated account smoke' >&2; exit 1; } +[[ -x "$NOXSSH" && -x "$NOXSSHD" ]] +KEYDIR="$(mktemp -d)" +HOSTDIR="$(sudo mktemp -d)" +USER_NAME="noxsshci${RANDOM}" PORT="${NOXSSH_SMOKE_PORT:-4022}" -"${NOXSSHD}" -p "${PORT}" --password smoketest --host-key "${KEYFILE}" & -NOXSSHD_PID=$! -sleep 2 -OUT="$("${NOXSSH}" -p "${PORT}" -w smoketest user@127.0.0.1 "echo ci_ok" || true)" -if [[ "${OUT}" != *"ci_ok"* ]]; then - echo "ci_smoke_sshd: expected ci_ok in output, got: ${OUT}" >&2 - exit 1 +DAEMON_PID='' +cleanup() { + if [[ -n "$DAEMON_PID" ]]; then kill "$DAEMON_PID" 2>/dev/null || true; wait "$DAEMON_PID" 2>/dev/null || true; fi + sudo userdel -r "$USER_NAME" >/dev/null 2>&1 || true + sudo rm -rf -- "$HOSTDIR" + rm -rf -- "$KEYDIR" +} +trap cleanup EXIT +ssh-keygen -q -t ed25519 -N '' -f "$KEYDIR/id" +sudo useradd -m -U -s /bin/sh "$USER_NAME" +sudo install -d -m 700 -o "$USER_NAME" -g "$USER_NAME" "/home/$USER_NAME/.ssh" +sudo install -m 600 -o "$USER_NAME" -g "$USER_NAME" "$KEYDIR/id.pub" "/home/$USER_NAME/.ssh/authorized_keys" +sudo timeout 60s "$NOXSSHD" -p "$PORT" --host-key "$HOSTDIR/seed" >"$KEYDIR/server.log" 2>&1 & +DAEMON_PID=$! +sleep 1 +SSH_OPTS=(-o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i "$KEYDIR/id" -p "$PORT") +OUT="$(ssh "${SSH_OPTS[@]}" "$USER_NAME@127.0.0.1" 'printf ci_exec_ok')" +[[ "$OUT" == 'ci_exec_ok' ]] +OUT="$(printf 'printf ci_shell_ok; exit\n' | ssh -tt "${SSH_OPTS[@]}" "$USER_NAME@127.0.0.1")" +[[ "$OUT" == *ci_shell_ok* ]] +if ssh "${SSH_OPTS[@]}" "no_such_noxssh_user@127.0.0.1" 'printf bad' >/dev/null 2>&1; then + echo 'invalid account authenticated' >&2; exit 1 fi -kill "${NOXSSHD_PID}" 2>/dev/null || true -wait "${NOXSSHD_PID}" 2>/dev/null || true -echo "ci_smoke_sshd: ok" +OUT="$("$NOXSSH" --batch --strict-host-key-checking no -i "$KEYDIR/id" -p "$PORT" "$USER_NAME@127.0.0.1" 'printf ci_noxssh_ok')" +[[ "$OUT" == *ci_noxssh_ok* ]] +echo 'ci_smoke_sshd: exec, PTY shell, invalid account, NoxSSH client passed' diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 6242e8e..68d5b46 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -2,6 +2,9 @@ add_executable(noxsshd main.c daemon_runtime.c ) +if(UNIX) + target_sources(noxsshd PRIVATE auth_posix.c) +endif() target_include_directories(noxsshd PRIVATE @@ -17,3 +20,5 @@ target_link_libraries(noxsshd if(WIN32) target_link_libraries(noxsshd PRIVATE ws2_32) endif() + +install(TARGETS noxsshd RUNTIME DESTINATION bin) diff --git a/server/auth_posix.c b/server/auth_posix.c new file mode 100644 index 0000000..5e3dae7 --- /dev/null +++ b/server/auth_posix.c @@ -0,0 +1,85 @@ +#define _GNU_SOURCE +#include "auth_posix.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef O_NOFOLLOW +#define O_NOFOLLOW 0 +#endif + +static int b64val(unsigned char c) +{ + if(c >= 'A' && c <= 'Z') return c - 'A'; + if(c >= 'a' && c <= 'z') return c - 'a' + 26; + if(c >= '0' && c <= '9') return c - '0' + 52; + if(c == '+') return 62; + if(c == '/') return 63; + return -1; +} + +static int decode_blob(const char *src, size_t len, uint8_t out[51]) +{ + size_t i, n = 0; + unsigned x; + if(len != 68u) return 0; /* 51-byte ssh-ed25519 blob. */ + for(i = 0; i < len; i += 4u) { + int a = b64val((unsigned char)src[i]); + int b = b64val((unsigned char)src[i+1]); + int c = b64val((unsigned char)src[i+2]); + int d = b64val((unsigned char)src[i+3]); + if(a < 0 || b < 0 || c < 0 || d < 0) return 0; + x = ((unsigned)a << 18) | ((unsigned)b << 12) | ((unsigned)c << 6) | (unsigned)d; + out[n++] = (uint8_t)(x >> 16); + out[n++] = (uint8_t)(x >> 8); + out[n++] = (uint8_t)x; + } + return n == 51u && memcmp(out, "\0\0\0\13ssh-ed25519\0\0\0\040", 19u) == 0; +} + +int noxsshd_authorized_ed25519(void * unused, const char * username, const uint8_t key[32]) +{ + struct passwd pwd, *pw = NULL; + char pwbuf[16384], path[4096], line[8192]; + struct stat st; + FILE *f; + int fd, ok = 0; + size_t home_len; + (void)unused; + if(!username || !key || strchr(username, '/') || strchr(username, '\\') || + getpwnam_r(username, &pwd, pwbuf, sizeof(pwbuf), &pw) != 0 || !pw || + !pw->pw_dir || !pw->pw_shell || strstr(pw->pw_shell, "nologin") || strstr(pw->pw_shell, "false")) return 0; + home_len = strlen(pw->pw_dir); + if(home_len + sizeof("/.ssh/authorized_keys") > sizeof(path)) return 0; + if(stat(pw->pw_dir, &st) || !S_ISDIR(st.st_mode) || (st.st_mode & 022) || + (st.st_uid != pw->pw_uid && st.st_uid != 0)) return 0; + snprintf(path, sizeof(path), "%s/.ssh", pw->pw_dir); + if(lstat(path, &st) || !S_ISDIR(st.st_mode) || (st.st_mode & 022) || + (st.st_uid != pw->pw_uid && st.st_uid != 0)) return 0; + snprintf(path, sizeof(path), "%s/.ssh/authorized_keys", pw->pw_dir); + fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + if(fd < 0) return 0; + if(fstat(fd, &st) || !S_ISREG(st.st_mode) || (st.st_mode & 022) || + st.st_size > 65536 || (st.st_uid != pw->pw_uid && st.st_uid != 0)) { close(fd); return 0; } + f = fdopen(fd, "r"); + if(!f) { close(fd); return 0; } + while(fgets(line, sizeof(line), f)) { + char *alg = line, *blob; + size_t len; + uint8_t decoded[51]; + while(*alg == ' ' || *alg == '\t') alg++; + if(strncmp(alg, "ssh-ed25519", 11u) != 0 || (alg[11] != ' ' && alg[11] != '\t')) continue; + blob = alg + 11; + while(*blob == ' ' || *blob == '\t') blob++; + len = strcspn(blob, " \t\r\n"); + if(decode_blob(blob, len, decoded) && memcmp(decoded + 19, key, 32u) == 0) { ok = 1; break; } + } + fclose(f); + return ok; +} diff --git a/server/auth_posix.h b/server/auth_posix.h new file mode 100644 index 0000000..4672e7c --- /dev/null +++ b/server/auth_posix.h @@ -0,0 +1,5 @@ +#ifndef NOXSSHD_AUTH_POSIX_H +#define NOXSSHD_AUTH_POSIX_H +#include +int noxsshd_authorized_ed25519(void * unused, const char * username, const uint8_t key[32]); +#endif diff --git a/server/daemon_runtime.c b/server/daemon_runtime.c index 3e9fab8..a33ea96 100644 --- a/server/daemon_runtime.c +++ b/server/daemon_runtime.c @@ -8,6 +8,9 @@ #include #include "daemon_runtime.h" +#ifndef _WIN32 +#include "auth_posix.h" +#endif #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN @@ -27,6 +30,10 @@ typedef SOCKET noxsshd_sock_t; #include #include #include +#include +#include +#include +static void noxsshd_child_signal(int signo) { (void)signo; } typedef int noxsshd_sock_t; #define NOXSSH_INVALID_SOCKET (-1) #define NOXSSH_CLOSESOCK close @@ -47,7 +54,7 @@ static int32_t noxsshd_send_cb(void * user_data, const uint8_t * data, uint32_t #ifdef _WIN32 return send(c->sock, (const char *)data, (int)len, 0); #else - return (int32_t)send(c->sock, data, (size_t)len, 0); + return (int32_t)send(c->sock, data, (size_t)len, MSG_NOSIGNAL); #endif } @@ -65,7 +72,6 @@ static int32_t noxsshd_recv_cb(void * user_data, uint8_t * data, uint32_t len) } static int noxsshd_serve_connection(noxsshd_sock_t client_sock, - const char * password, const uint8_t host_ed25519_seed[32]) { netnox_ssh_server_t srv; @@ -80,11 +86,10 @@ static int noxsshd_serve_connection(noxsshd_sock_t client_sock, if(netnox_ssh_server_set_host_ed25519_seed(&srv, host_ed25519_seed) != NETNOX_RETURN_SUCCESS) { return 1; } - if(password != NULL && password[0] != '\0') { - if(netnox_ssh_server_set_allowed_password(&srv, password) != NETNOX_RETURN_SUCCESS) { - return 1; - } - } +#ifndef _WIN32 + netnox_ssh_server_set_publickey_callback(&srv, noxsshd_authorized_ed25519, NULL); + srv.transport_fd = client_sock; +#endif rc = netnox_ssh_server_serve_one(&srv); netnox_ssh_server_reset(&srv); return (rc == NETNOX_RETURN_SUCCESS) ? 0 : 1; @@ -92,7 +97,6 @@ static int noxsshd_serve_connection(noxsshd_sock_t client_sock, #ifndef _WIN32 static int noxsshd_listen_posix(const char * bind_host, uint16_t port, - const char * password, const uint8_t host_ed25519_seed[32]) { struct addrinfo hints; @@ -102,6 +106,8 @@ static int noxsshd_listen_posix(const char * bind_host, uint16_t port, noxsshd_sock_t listen_fd = NOXSSH_INVALID_SOCKET; int yes = 1; int gai_err; + unsigned active = 0u; + struct sigaction sa; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; @@ -139,20 +145,40 @@ static int noxsshd_listen_posix(const char * bind_host, uint16_t port, NOXSSH_CLOSESOCK(listen_fd); return 1; } - fprintf(stderr, "noxsshd: listening (sequential accept)\n"); + memset(&sa,0,sizeof(sa)); + sa.sa_handler = noxsshd_child_signal; + sigemptyset(&sa.sa_mask); + if(sigaction(SIGCHLD,&sa,NULL)) { NOXSSH_CLOSESOCK(listen_fd); return 1; } + fprintf(stderr, "noxsshd: listening (four concurrent sessions maximum)\n"); for(;;) { + pid_t done; + while((done = waitpid(-1,NULL,WNOHANG)) > 0) { if(active) active--; } noxsshd_sock_t client = accept(listen_fd, NULL, NULL); if(client == NOXSSH_INVALID_SOCKET) { - perror("noxsshd: accept"); + if(errno != EINTR) perror("noxsshd: accept"); continue; } - (void)noxsshd_serve_connection(client, password, host_ed25519_seed); - NOXSSH_CLOSESOCK(client); + if(active >= 4u) { NOXSSH_CLOSESOCK(client); continue; } + { + struct timeval tv = { 10, 0 }; + pid_t child; + (void)setsockopt(client,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)); + (void)setsockopt(client,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv)); + child = fork(); + if(child == 0) { + int rc; + NOXSSH_CLOSESOCK(listen_fd); + rc = noxsshd_serve_connection(client,host_ed25519_seed); + NOXSSH_CLOSESOCK(client); + _exit(rc); + } + if(child > 0) active++; + NOXSSH_CLOSESOCK(client); + } } } #else static int noxsshd_listen_win32(const char * bind_host, uint16_t port, - const char * password, const uint8_t host_ed25519_seed[32]) { struct addrinfo hints; @@ -216,7 +242,7 @@ static int noxsshd_listen_win32(const char * bind_host, uint16_t port, fprintf(stderr, "noxsshd: accept failed\n"); continue; } - (void)noxsshd_serve_connection(client, password, host_ed25519_seed); + (void)noxsshd_serve_connection(client, host_ed25519_seed); closesocket(client); } } @@ -224,12 +250,11 @@ static int noxsshd_listen_win32(const char * bind_host, uint16_t port, int noxsshd_listen_loop(const char * bind_host, uint16_t port, - const char * password, const uint8_t host_ed25519_seed[32]) { #ifndef _WIN32 - return noxsshd_listen_posix(bind_host, port, password, host_ed25519_seed); + return noxsshd_listen_posix(bind_host, port, host_ed25519_seed); #else - return noxsshd_listen_win32(bind_host, port, password, host_ed25519_seed); + return noxsshd_listen_win32(bind_host, port, host_ed25519_seed); #endif } diff --git a/server/daemon_runtime.h b/server/daemon_runtime.h index 183d544..983fc20 100644 --- a/server/daemon_runtime.h +++ b/server/daemon_runtime.h @@ -21,7 +21,6 @@ extern "C" { */ int noxsshd_listen_loop(const char * bind_host, uint16_t port, - const char * password, const uint8_t host_ed25519_seed[32]); #ifdef __cplusplus diff --git a/server/main.c b/server/main.c index 95baf9a..792cff4 100644 --- a/server/main.c +++ b/server/main.c @@ -1,88 +1,111 @@ -/***************************************************************************** -* Copyright (c) [2019] - [2026], Argenox Technologies LLC -* SPDX-License-Identifier: GPL-2.0-or-later OR NoxTLS-Commercial -*/ - +/* Copyright (c) [2019] - [2026], Argenox Technologies LLC + * SPDX-License-Identifier: GPL-2.0-or-later OR NoxTLS-Commercial */ +#define _GNU_SOURCE +#include +#include +#include #include #include #include - +#include +#include +#include +#ifdef __linux__ +#include +#endif #include "daemon_runtime.h" -#include "../noxtls/noxtls-lib/pkc/ed25519/noxtls_ed25519.h" #include "../client/version.h" -static void print_usage(const char * prog) +static int random_seed(uint8_t seed[32]) { - fprintf(stderr, - "%s " NOXSSH_VERSION_STRING " — NoxSSH server (sequential accept)\n" - "Usage: %s [-b bind] [-p port] --password [--host-key ]\n" - " -b bind Address to bind (default: all interfaces)\n" - " -p port TCP port (default: 2222)\n" - " --password Password accepted for any username (required for password auth)\n" - " --host-key Path to 32-byte raw Ed25519 seed file (optional; ephemeral if omitted)\n", - prog, prog); +#ifdef __linux__ + size_t pos = 0; + while(pos < 32u) { + ssize_t n = getrandom(seed + pos, 32u - pos, 0); + if(n > 0) { pos += (size_t)n; continue; } + if(n < 0 && errno == EINTR) continue; + return -1; + } + return 0; +#else + (void)seed; + return -1; +#endif } -int main(int argc, char ** argv) +static int host_seed(const char *path, uint8_t seed[32]) { - const char * bind_host = NULL; - const char * password = NULL; - const char * host_key_path = NULL; + int fd, created = 0; + struct stat st; + uint8_t extra; + ssize_t n; + size_t pos; + char parent[4096]; + const char *slash = strrchr(path, '/'); + if(!slash || (size_t)(slash - path) >= sizeof(parent) || slash == path) return -1; + memcpy(parent, path, (size_t)(slash - path)); + parent[slash - path] = 0; + if(lstat(parent, &st) || !S_ISDIR(st.st_mode) || (st.st_mode & 022) || st.st_uid != geteuid()) return -1; + fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + if(fd < 0 && errno == ENOENT) { + if(random_seed(seed)) return -1; + fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0600); + if(fd >= 0) { + created = 1; + pos = 0; + while(pos < 32u) { + n = write(fd, seed + pos, 32u - pos); + if(n > 0) { pos += (size_t)n; continue; } + if(n < 0 && errno == EINTR) continue; + close(fd); unlink(path); return -1; + } + { int sync_rc = fsync(fd); int close_rc = close(fd); + if(sync_rc || close_rc) { unlink(path); return -1; } } + fd = open(parent, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if(fd < 0 || fsync(fd)) { if(fd >= 0) close(fd); return -1; } + close(fd); + } else if(errno != EEXIST) return -1; + } + if(created) return 0; + if(fd < 0) fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + if(fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_uid != geteuid() || + (st.st_mode & 077) || st.st_size != 32) { if(fd >= 0) close(fd); return -1; } + pos = 0; + while(pos < 32u) { + n = read(fd, seed + pos, 32u - pos); + if(n > 0) { pos += (size_t)n; continue; } + if(n < 0 && errno == EINTR) continue; + close(fd); return -1; + } + n = read(fd, &extra, 1u); + close(fd); + return n == 0 ? 0 : -1; +} + +int main(int argc, char **argv) +{ + const char *bind_host = "127.0.0.1"; + const char *key_path = "/etc/noxssh/host_ed25519_seed"; + uint8_t seed[32]; uint16_t port = 2222u; - uint8_t host_seed[32]; - uint8_t host_pk[32]; int i; - for(i = 1; i < argc; i++) { - if(strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { - print_usage(argv[0]); + if(!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) { + fprintf(stderr, "noxsshd " NOXSSH_VERSION_STRING "\nUsage: %s [-b bind] [-p port] [--host-key absolute-path]\nPublic-key authentication only; binds 127.0.0.1 by default.\n", argv[0]); return 0; - } - if(strcmp(argv[i], "-b") == 0 && i + 1 < argc) { - bind_host = argv[++i]; - } else if(strcmp(argv[i], "-p") == 0 && i + 1 < argc) { - int p = atoi(argv[++i]); - if(p <= 0 || p > 65535) { - fprintf(stderr, "noxsshd: invalid port\n"); - return 1; - } - port = (uint16_t)p; - } else if(strcmp(argv[i], "--password") == 0 && i + 1 < argc) { - password = argv[++i]; - } else if(strcmp(argv[i], "--host-key") == 0 && i + 1 < argc) { - host_key_path = argv[++i]; - } else { - fprintf(stderr, "noxsshd: unknown argument: %s\n", argv[i]); - print_usage(argv[0]); - return 1; - } + } else if(!strcmp(argv[i], "-b") && i + 1 < argc) bind_host = argv[++i]; + else if(!strcmp(argv[i], "-p") && i + 1 < argc) { + char *end; unsigned long value = strtoul(argv[++i], &end, 10); + if(*end || value < 1u || value > 65535u) return 2; + port = (uint16_t)value; + } else if(!strcmp(argv[i], "--host-key") && i + 1 < argc) key_path = argv[++i]; + else { fprintf(stderr, "noxsshd: invalid option\n"); return 2; } } - - if(password == NULL || password[0] == '\0') { - fprintf(stderr, "noxsshd: --password is required\n"); + if(key_path[0] != '/' || host_seed(key_path, seed)) { + fprintf(stderr, "noxsshd: secure persistent host key unavailable at %s\n", key_path); return 1; } - - if(host_key_path != NULL) { - FILE * f = fopen(host_key_path, "rb"); - if(f == NULL) { - perror("noxsshd: fopen host key"); - return 1; - } - if(fread(host_seed, 1u, sizeof(host_seed), f) != sizeof(host_seed)) { - fprintf(stderr, "noxsshd: host key file must be exactly 32 bytes\n"); - fclose(f); - return 1; - } - fclose(f); - } else { - if(noxtls_ed25519_generate_key(host_seed, host_pk) != NOXTLS_RETURN_SUCCESS) { - fprintf(stderr, "noxsshd: failed to generate ephemeral host key\n"); - return 1; - } - (void)fprintf(stderr, "noxsshd: warning: ephemeral host key (not saved); clients will see host key warnings\n"); - (void)host_pk; - } - - return noxsshd_listen_loop(bind_host, port, password, host_seed); + i = noxsshd_listen_loop(bind_host, port, seed); + memset(seed, 0, sizeof(seed)); + return i; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d3b542c..2d91f75 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,4 @@ +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") add_executable(test_server_init test_server_init.c ) @@ -10,3 +11,4 @@ target_link_libraries(test_server_init netnox_ssh ) add_test(NAME server_init COMMAND test_server_init) +endif() From 109ec5b78fbc8b1af584a661951960f9b547b975 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 12 Oct 2026 22:51:23 -0500 Subject: [PATCH 03/13] Compare SSH packet MACs in constant time --- common/noxssh_server.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/common/noxssh_server.c b/common/noxssh_server.c index 2180714..f31978c 100644 --- a/common/noxssh_server.c +++ b/common/noxssh_server.c @@ -56,6 +56,13 @@ static uint64_t srv_now_ms(void) return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u; } +static int srv_equal_mac(const uint8_t *a, const uint8_t *b, uint32_t len) +{ + uint8_t diff = 0u; uint32_t i; + for(i=0u;i Date: Mon, 12 Oct 2026 22:52:58 -0500 Subject: [PATCH 04/13] Report SSH client smoke failure details --- scripts/ci_smoke_sshd.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/ci_smoke_sshd.sh b/scripts/ci_smoke_sshd.sh index afe4f27..8c874f3 100644 --- a/scripts/ci_smoke_sshd.sh +++ b/scripts/ci_smoke_sshd.sh @@ -23,7 +23,7 @@ ssh-keygen -q -t ed25519 -N '' -f "$KEYDIR/id" sudo useradd -m -U -s /bin/sh "$USER_NAME" sudo install -d -m 700 -o "$USER_NAME" -g "$USER_NAME" "/home/$USER_NAME/.ssh" sudo install -m 600 -o "$USER_NAME" -g "$USER_NAME" "$KEYDIR/id.pub" "/home/$USER_NAME/.ssh/authorized_keys" -sudo timeout 60s "$NOXSSHD" -p "$PORT" --host-key "$HOSTDIR/seed" >"$KEYDIR/server.log" 2>&1 & +sudo env NETNOX_SSH_DEBUG=1 timeout 60s "$NOXSSHD" -p "$PORT" --host-key "$HOSTDIR/seed" >"$KEYDIR/server.log" 2>&1 & DAEMON_PID=$! sleep 1 SSH_OPTS=(-o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i "$KEYDIR/id" -p "$PORT") @@ -34,6 +34,10 @@ OUT="$(printf 'printf ci_shell_ok; exit\n' | ssh -tt "${SSH_OPTS[@]}" "$USER_NAM if ssh "${SSH_OPTS[@]}" "no_such_noxssh_user@127.0.0.1" 'printf bad' >/dev/null 2>&1; then echo 'invalid account authenticated' >&2; exit 1 fi -OUT="$("$NOXSSH" --batch --strict-host-key-checking no -i "$KEYDIR/id" -p "$PORT" "$USER_NAME@127.0.0.1" 'printf ci_noxssh_ok')" +if ! OUT="$("$NOXSSH" -d --batch --strict-host-key-checking no -i "$KEYDIR/id" -p "$PORT" "$USER_NAME@127.0.0.1" 'printf ci_noxssh_ok' 2>&1)"; then + echo "NoxSSH client failed: $OUT" >&2 + cat "$KEYDIR/server.log" >&2 + exit 1 +fi [[ "$OUT" == *ci_noxssh_ok* ]] echo 'ci_smoke_sshd: exec, PTY shell, invalid account, NoxSSH client passed' From 66dead7f6307aa12dee48c6ffc652861c50ab736 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 12 Oct 2026 22:53:17 -0500 Subject: [PATCH 05/13] Limit build workflow token to repository read --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d0f115e..b3d6791 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,8 @@ name: Build noxssh +permissions: + contents: read + on: push: branches: [ master, main ] From fb39c8dd9c96547d9f7ebd4608bf279181c6f1ec Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 12 Oct 2026 22:55:43 -0500 Subject: [PATCH 06/13] Parse OpenSSH Ed25519 identity before hex seed --- client/main.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/main.c b/client/main.c index 07fbc4f..6755053 100644 --- a/client/main.c +++ b/client/main.c @@ -463,6 +463,13 @@ static int noxssh_load_ed25519_seed_file(const char * path, uint8_t out_seed[32] } raw[nread] = '\0'; + /* Base64 contains hex-looking characters; detect OpenSSH keys before + * scanning for a bare hex seed. Otherwise a normal key exceeds 64 hex + * characters and is rejected before the OpenSSH parser runs. */ + if(strstr(raw, "-----BEGIN OPENSSH PRIVATE KEY-----") != NULL) { + return noxssh_load_openssh_ed25519_key(raw, (uint32_t)nread, out_seed); + } + for(i = 0u; i < (uint32_t)nread; i++) { int nib = noxssh_hex_nibble(raw[i]); if(nib >= 0) { From 13a5743b2ed4a00692000ccf2731f97ced24843f Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 12 Oct 2026 22:57:36 -0500 Subject: [PATCH 07/13] Allow slow CI account setup and log SSH smoke stages --- scripts/ci_smoke_sshd.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/ci_smoke_sshd.sh b/scripts/ci_smoke_sshd.sh index 8c874f3..41cb590 100644 --- a/scripts/ci_smoke_sshd.sh +++ b/scripts/ci_smoke_sshd.sh @@ -13,6 +13,11 @@ USER_NAME="noxsshci${RANDOM}" PORT="${NOXSSH_SMOKE_PORT:-4022}" DAEMON_PID='' cleanup() { + local status=$? + if ((status != 0)); then + echo "ci_smoke_sshd failed at $(date -u +%FT%TZ), status=$status" >&2 + if [[ -f "$KEYDIR/server.log" ]]; then cat "$KEYDIR/server.log" >&2; fi + fi if [[ -n "$DAEMON_PID" ]]; then kill "$DAEMON_PID" 2>/dev/null || true; wait "$DAEMON_PID" 2>/dev/null || true; fi sudo userdel -r "$USER_NAME" >/dev/null 2>&1 || true sudo rm -rf -- "$HOSTDIR" @@ -20,20 +25,25 @@ cleanup() { } trap cleanup EXIT ssh-keygen -q -t ed25519 -N '' -f "$KEYDIR/id" +echo "ci_smoke_sshd: account setup $(date -u +%FT%TZ)" sudo useradd -m -U -s /bin/sh "$USER_NAME" sudo install -d -m 700 -o "$USER_NAME" -g "$USER_NAME" "/home/$USER_NAME/.ssh" sudo install -m 600 -o "$USER_NAME" -g "$USER_NAME" "$KEYDIR/id.pub" "/home/$USER_NAME/.ssh/authorized_keys" -sudo env NETNOX_SSH_DEBUG=1 timeout 60s "$NOXSSHD" -p "$PORT" --host-key "$HOSTDIR/seed" >"$KEYDIR/server.log" 2>&1 & +echo "ci_smoke_sshd: daemon start $(date -u +%FT%TZ)" +sudo env NETNOX_SSH_DEBUG=1 timeout 180s "$NOXSSHD" -p "$PORT" --host-key "$HOSTDIR/seed" >"$KEYDIR/server.log" 2>&1 & DAEMON_PID=$! sleep 1 SSH_OPTS=(-o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i "$KEYDIR/id" -p "$PORT") +echo "ci_smoke_sshd: OpenSSH exec $(date -u +%FT%TZ)" OUT="$(ssh "${SSH_OPTS[@]}" "$USER_NAME@127.0.0.1" 'printf ci_exec_ok')" [[ "$OUT" == 'ci_exec_ok' ]] +echo "ci_smoke_sshd: OpenSSH PTY $(date -u +%FT%TZ)" OUT="$(printf 'printf ci_shell_ok; exit\n' | ssh -tt "${SSH_OPTS[@]}" "$USER_NAME@127.0.0.1")" [[ "$OUT" == *ci_shell_ok* ]] if ssh "${SSH_OPTS[@]}" "no_such_noxssh_user@127.0.0.1" 'printf bad' >/dev/null 2>&1; then echo 'invalid account authenticated' >&2; exit 1 fi +echo "ci_smoke_sshd: NoxSSH exec $(date -u +%FT%TZ)" if ! OUT="$("$NOXSSH" -d --batch --strict-host-key-checking no -i "$KEYDIR/id" -p "$PORT" "$USER_NAME@127.0.0.1" 'printf ci_noxssh_ok' 2>&1)"; then echo "NoxSSH client failed: $OUT" >&2 cat "$KEYDIR/server.log" >&2 From 0f30ed035f84e4292533df8722e418f1aa6e02aa Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 12 Oct 2026 23:00:07 -0500 Subject: [PATCH 08/13] Reap completed SSH command without socket wait --- common/noxssh_server.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/common/noxssh_server.c b/common/noxssh_server.c index f31978c..3cf600d 100644 --- a/common/noxssh_server.c +++ b/common/noxssh_server.c @@ -1410,6 +1410,7 @@ static int srv_write_child(int fd, const uint8_t *data, uint32_t len, int pty) static netnox_return_t srv_run_session_unix(netnox_ssh_server_t *s, const char *cmd, int shell) { int pair[2] = {-1, -1}, child_fd = -1, status = 0, child_reaped = 0, output_eof = 0; + unsigned eof_waits = 0u; pid_t pid; struct winsize ws; uint8_t payload[NETNOX_SSH_MAX_PACKET_LEN], output[4096]; @@ -1459,9 +1460,11 @@ static netnox_return_t srv_run_session_unix(netnox_ssh_server_t *s, const char * if(!output_eof && s->remote_window_size) { pf[1].fd = child_fd; pf[1].events = POLLIN | POLLHUP; pf[1].revents = 0; nfd = 2; } - rc = poll(pf, nfd, 1800000); /* 30-minute idle session limit. */ + /* Once child output closes, the socket alone may never become readable. + * Recheck child exit promptly instead of waiting for a client keepalive. */ + rc = poll(pf, nfd, output_eof ? 50 : 1800000); /* 30-minute idle session limit. */ if(rc < 0 && errno == EINTR) continue; - if(rc <= 0) break; /* Bound idle client or stalled remote window. */ + if(rc <= 0 && !(rc == 0 && output_eof && ++eof_waits <= 200u)) break; if(pf[0].revents & (POLLERR | POLLHUP | POLLNVAL)) break; if(pf[0].revents & POLLIN) { uint32_t off = 1u, channel, amount; From 04d265bfe7929a53a21c39255bfa0cf650749b45 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 12 Oct 2026 23:00:54 -0500 Subject: [PATCH 09/13] Preserve quiet SSH command lifetime after output closes --- common/noxssh_server.c | 3 +-- scripts/ci_smoke_sshd.sh | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/common/noxssh_server.c b/common/noxssh_server.c index 3cf600d..10ac3a1 100644 --- a/common/noxssh_server.c +++ b/common/noxssh_server.c @@ -1410,7 +1410,6 @@ static int srv_write_child(int fd, const uint8_t *data, uint32_t len, int pty) static netnox_return_t srv_run_session_unix(netnox_ssh_server_t *s, const char *cmd, int shell) { int pair[2] = {-1, -1}, child_fd = -1, status = 0, child_reaped = 0, output_eof = 0; - unsigned eof_waits = 0u; pid_t pid; struct winsize ws; uint8_t payload[NETNOX_SSH_MAX_PACKET_LEN], output[4096]; @@ -1464,7 +1463,7 @@ static netnox_return_t srv_run_session_unix(netnox_ssh_server_t *s, const char * * Recheck child exit promptly instead of waiting for a client keepalive. */ rc = poll(pf, nfd, output_eof ? 50 : 1800000); /* 30-minute idle session limit. */ if(rc < 0 && errno == EINTR) continue; - if(rc <= 0 && !(rc == 0 && output_eof && ++eof_waits <= 200u)) break; + if(rc <= 0 && !(rc == 0 && output_eof)) break; if(pf[0].revents & (POLLERR | POLLHUP | POLLNVAL)) break; if(pf[0].revents & POLLIN) { uint32_t off = 1u, channel, amount; diff --git a/scripts/ci_smoke_sshd.sh b/scripts/ci_smoke_sshd.sh index 41cb590..fc9f130 100644 --- a/scripts/ci_smoke_sshd.sh +++ b/scripts/ci_smoke_sshd.sh @@ -37,6 +37,13 @@ SSH_OPTS=(-o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o echo "ci_smoke_sshd: OpenSSH exec $(date -u +%FT%TZ)" OUT="$(ssh "${SSH_OPTS[@]}" "$USER_NAME@127.0.0.1" 'printf ci_exec_ok')" [[ "$OUT" == 'ci_exec_ok' ]] +echo "ci_smoke_sshd: quiet child after output EOF $(date -u +%FT%TZ)" +if ssh "${SSH_OPTS[@]}" "$USER_NAME@127.0.0.1" 'exec 0<&- 1>&- 2>&-; sleep 11; exit 9' > /dev/null 2>"$KEYDIR/quiet.err"; then + echo 'quiet child returned success instead of status 9' >&2; exit 1 +else + rc=$? + if ((rc != 9)); then cat "$KEYDIR/quiet.err" >&2; echo "quiet child returned status $rc instead of 9" >&2; exit 1; fi +fi echo "ci_smoke_sshd: OpenSSH PTY $(date -u +%FT%TZ)" OUT="$(printf 'printf ci_shell_ok; exit\n' | ssh -tt "${SSH_OPTS[@]}" "$USER_NAME@127.0.0.1")" [[ "$OUT" == *ci_shell_ok* ]] From 1ef8774585f5a7daa5d603ff6b6c2608d8e4a7fd Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 12 Oct 2026 23:12:24 -0500 Subject: [PATCH 10/13] Add safe SSH account preflight and host public key output --- docs/server.md | 10 +++++++++- scripts/ci_smoke_sshd.sh | 16 ++++++++++++++++ server/auth_posix.c | 21 ++++++++++++++++----- server/auth_posix.h | 2 ++ server/main.c | 37 +++++++++++++++++++++++++++++++++++-- 5 files changed, 78 insertions(+), 8 deletions(-) diff --git a/docs/server.md b/docs/server.md index e3802c7..f051a2c 100644 --- a/docs/server.md +++ b/docs/server.md @@ -1,6 +1,6 @@ # Linux server -`noxsshd` is an opt-in Linux SSH server. It does not start automatically. +`noxsshd` is an opt-in Linux SSH server. The packaged service is disabled by default. It binds to `127.0.0.1:2222` unless `-b` and `-p` are supplied. The MPUNoxV image can retain OpenSSH while this server is qualified. @@ -25,6 +25,14 @@ starts; back it up as a secret if clients pin the host identity. A supplied `--host-key` must be an absolute path under a directory owned by the daemon and not group/other writable. The server refuses an unsafe or malformed key. +`noxsshd --check-user root` returns success only if root's account and +`authorized_keys` currently permit an Ed25519 key. It does not create a host +key or bind a socket. On the trusted UART console, run +`noxsshd --print-host-public-key` to create or read the persistent host seed +and print only its OpenSSH public key. `ssh-keygen -lf -` can display its +fingerprint, and the public key can be pinned in the client's `known_hosts` +before the first network connection. Never transfer the host seed itself. + The current server supports one session channel per connection, remote commands, a shell, optional PTY, terminal resize, stdin/stdout/stderr, and `exit-status`. It permits four concurrent connections. Idle sessions diff --git a/scripts/ci_smoke_sshd.sh b/scripts/ci_smoke_sshd.sh index fc9f130..b6aaf7b 100644 --- a/scripts/ci_smoke_sshd.sh +++ b/scripts/ci_smoke_sshd.sh @@ -29,6 +29,22 @@ echo "ci_smoke_sshd: account setup $(date -u +%FT%TZ)" sudo useradd -m -U -s /bin/sh "$USER_NAME" sudo install -d -m 700 -o "$USER_NAME" -g "$USER_NAME" "/home/$USER_NAME/.ssh" sudo install -m 600 -o "$USER_NAME" -g "$USER_NAME" "$KEYDIR/id.pub" "/home/$USER_NAME/.ssh/authorized_keys" +echo "ci_smoke_sshd: read-only user preflight $(date -u +%FT%TZ)" +sudo "$NOXSSHD" --check-user "$USER_NAME" +if sudo "$NOXSSHD" --check-user no_such_noxssh_user; then echo 'missing account passed preflight' >&2; exit 1; fi +sudo chmod 666 "/home/$USER_NAME/.ssh/authorized_keys" +if sudo "$NOXSSHD" --check-user "$USER_NAME"; then echo 'writable key file passed preflight' >&2; exit 1; fi +sudo chmod 600 "/home/$USER_NAME/.ssh/authorized_keys" +printf 'ssh-ed25519 AAAA invalid\n' | sudo tee "/home/$USER_NAME/.ssh/authorized_keys" >/dev/null +if sudo "$NOXSSHD" --check-user "$USER_NAME"; then echo 'malformed key passed preflight' >&2; exit 1; fi +sudo install -m 600 -o "$USER_NAME" -g "$USER_NAME" "$KEYDIR/id.pub" "/home/$USER_NAME/.ssh/authorized_keys" +sudo "$NOXSSHD" --check-user "$USER_NAME" +echo "ci_smoke_sshd: persistent public host key $(date -u +%FT%TZ)" +HOST_PUB1="$(sudo "$NOXSSHD" --print-host-public-key --host-key "$HOSTDIR/seed")" +HOST_PUB2="$(sudo "$NOXSSHD" --print-host-public-key --host-key "$HOSTDIR/seed")" +[[ "$HOST_PUB1" == "$HOST_PUB2" && "$HOST_PUB1" == ssh-ed25519\ * ]] +printf '%s\n' "$HOST_PUB1" | ssh-keygen -lf - >/dev/null +[[ "$(sudo stat -c '%a' "$HOSTDIR/seed")" == 600 ]] echo "ci_smoke_sshd: daemon start $(date -u +%FT%TZ)" sudo env NETNOX_SSH_DEBUG=1 timeout 180s "$NOXSSHD" -p "$PORT" --host-key "$HOSTDIR/seed" >"$KEYDIR/server.log" 2>&1 & DAEMON_PID=$! diff --git a/server/auth_posix.c b/server/auth_posix.c index 5e3dae7..b979e2a 100644 --- a/server/auth_posix.c +++ b/server/auth_posix.c @@ -43,7 +43,7 @@ static int decode_blob(const char *src, size_t len, uint8_t out[51]) return n == 51u && memcmp(out, "\0\0\0\13ssh-ed25519\0\0\0\040", 19u) == 0; } -int noxsshd_authorized_ed25519(void * unused, const char * username, const uint8_t key[32]) +static int authorized_user_key(const char * username, const uint8_t * key) { struct passwd pwd, *pw = NULL; char pwbuf[16384], path[4096], line[8192]; @@ -51,10 +51,10 @@ int noxsshd_authorized_ed25519(void * unused, const char * username, const uint8 FILE *f; int fd, ok = 0; size_t home_len; - (void)unused; - if(!username || !key || strchr(username, '/') || strchr(username, '\\') || + if(!username || !username[0] || strchr(username, '/') || strchr(username, '\\') || getpwnam_r(username, &pwd, pwbuf, sizeof(pwbuf), &pw) != 0 || !pw || - !pw->pw_dir || !pw->pw_shell || strstr(pw->pw_shell, "nologin") || strstr(pw->pw_shell, "false")) return 0; + !pw->pw_dir || !pw->pw_dir[0] || !pw->pw_shell || !pw->pw_shell[0] || + strstr(pw->pw_shell, "nologin") || strstr(pw->pw_shell, "false")) return 0; home_len = strlen(pw->pw_dir); if(home_len + sizeof("/.ssh/authorized_keys") > sizeof(path)) return 0; if(stat(pw->pw_dir, &st) || !S_ISDIR(st.st_mode) || (st.st_mode & 022) || @@ -78,8 +78,19 @@ int noxsshd_authorized_ed25519(void * unused, const char * username, const uint8 blob = alg + 11; while(*blob == ' ' || *blob == '\t') blob++; len = strcspn(blob, " \t\r\n"); - if(decode_blob(blob, len, decoded) && memcmp(decoded + 19, key, 32u) == 0) { ok = 1; break; } + if(decode_blob(blob, len, decoded) && (!key || memcmp(decoded + 19, key, 32u) == 0)) { ok = 1; break; } } fclose(f); return ok; } + +int noxsshd_authorized_ed25519(void * unused, const char * username, const uint8_t key[32]) +{ + (void)unused; + return key ? authorized_user_key(username, key) : 0; +} + +int noxsshd_authorized_user_ready(const char * username) +{ + return authorized_user_key(username, NULL); +} diff --git a/server/auth_posix.h b/server/auth_posix.h index 4672e7c..b8548e5 100644 --- a/server/auth_posix.h +++ b/server/auth_posix.h @@ -2,4 +2,6 @@ #define NOXSSHD_AUTH_POSIX_H #include int noxsshd_authorized_ed25519(void * unused, const char * username, const uint8_t key[32]); +/* Return true only if this account can authenticate with a valid plain Ed25519 key. */ +int noxsshd_authorized_user_ready(const char * username); #endif diff --git a/server/main.c b/server/main.c index 792cff4..a19f24d 100644 --- a/server/main.c +++ b/server/main.c @@ -14,8 +14,31 @@ #include #endif #include "daemon_runtime.h" +#include "auth_posix.h" +#include "../noxtls/noxtls-lib/pkc/ed25519/noxtls_ed25519.h" #include "../client/version.h" +/* 4 + 11 + 4 + 32 bytes, encoded as 68 base64 characters. */ +static int print_host_public_key(const uint8_t seed[32]) +{ + static const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + static const uint8_t prefix[19] = {0,0,0,11,'s','s','h','-','e','d','2','5','5','1','9',0,0,0,32}; + uint8_t blob[51]; + char encoded[69]; + size_t i, o = 0u; + memcpy(blob, prefix, sizeof(prefix)); + if(noxtls_ed25519_public_key(seed, blob + 19u) != NOXTLS_RETURN_SUCCESS) return -1; + for(i = 0u; i < sizeof(blob); i += 3u) { + uint32_t x = ((uint32_t)blob[i] << 16) | ((uint32_t)blob[i+1u] << 8) | blob[i+2u]; + encoded[o++] = alphabet[(x >> 18) & 63u]; + encoded[o++] = alphabet[(x >> 12) & 63u]; + encoded[o++] = alphabet[(x >> 6) & 63u]; + encoded[o++] = alphabet[x & 63u]; + } + encoded[o] = '\0'; + return printf("ssh-ed25519 %s noxsshd-host\n", encoded) < 0 ? -1 : 0; +} + static int random_seed(uint8_t seed[32]) { #ifdef __linux__ @@ -86,12 +109,13 @@ int main(int argc, char **argv) { const char *bind_host = "127.0.0.1"; const char *key_path = "/etc/noxssh/host_ed25519_seed"; + const char *check_user = NULL; uint8_t seed[32]; uint16_t port = 2222u; - int i; + int i, print_public = 0; for(i = 1; i < argc; i++) { if(!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) { - fprintf(stderr, "noxsshd " NOXSSH_VERSION_STRING "\nUsage: %s [-b bind] [-p port] [--host-key absolute-path]\nPublic-key authentication only; binds 127.0.0.1 by default.\n", argv[0]); + fprintf(stderr, "noxsshd " NOXSSH_VERSION_STRING "\nUsage: %s [-b bind] [-p port] [--host-key absolute-path] [--check-user account | --print-host-public-key]\nPublic-key authentication only; binds 127.0.0.1 by default.\n", argv[0]); return 0; } else if(!strcmp(argv[i], "-b") && i + 1 < argc) bind_host = argv[++i]; else if(!strcmp(argv[i], "-p") && i + 1 < argc) { @@ -99,12 +123,21 @@ int main(int argc, char **argv) if(*end || value < 1u || value > 65535u) return 2; port = (uint16_t)value; } else if(!strcmp(argv[i], "--host-key") && i + 1 < argc) key_path = argv[++i]; + else if(!strcmp(argv[i], "--check-user") && i + 1 < argc) check_user = argv[++i]; + else if(!strcmp(argv[i], "--print-host-public-key")) print_public = 1; else { fprintf(stderr, "noxsshd: invalid option\n"); return 2; } } + if(check_user && print_public) return 2; + if(check_user) return noxsshd_authorized_user_ready(check_user) ? 0 : 1; if(key_path[0] != '/' || host_seed(key_path, seed)) { fprintf(stderr, "noxsshd: secure persistent host key unavailable at %s\n", key_path); return 1; } + if(print_public) { + i = print_host_public_key(seed); + memset(seed, 0, sizeof(seed)); + return i == 0 ? 0 : 1; + } i = noxsshd_listen_loop(bind_host, port, seed); memset(seed, 0, sizeof(seed)); return i; From a5ebda65b587f1938733003eda36c3e44e0f0fda Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Mon, 12 Oct 2026 23:21:52 -0500 Subject: [PATCH 11/13] Record KC705 loopback SSH qualification provenance --- docs/server.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/server.md b/docs/server.md index f051a2c..dab8615 100644 --- a/docs/server.md +++ b/docs/server.md @@ -39,3 +39,20 @@ and `exit-status`. It permits four concurrent connections. Idle sessions close after 30 minutes, and connections close at the 256 MiB rekey limit until in-session rekey is implemented. It does not offer SFTP, forwarding, password authentication, or every SSH terminal-mode control. + +## KC705 qualification status + +The `04d265b` server binary ran on the KC705 RV64 Linux system over UART +controlled **loopback** `127.0.0.1:2222`. Remote exec (`id`, `uname`), PTY, +an 11-second quiet command returning status 9, invalid-user rejection, and +wrong-key rejection passed. The host seed was stored on SD with owner root +and mode `0600`; its public-key fingerprint was +`SHA256:1Cb2ywW9q2npFNJLYnYhWWgCRALJVdY5Pj1WiD33SbM`. The operator +reported cleanup status 0, removal of the temporary client keys and PC +authorization line, and daemon shutdown. The UART evidence is at +`E:/MPUNoxV_build_artifacts/ethernet_20261012/noxssh-loopback-04d265b.log`. + +Source commit `1ef8774` adds `--check-user` and +`--print-host-public-key` above that tested server version. Those commands +passed Linux x86/ARM CI; they have not yet been exercised on the KC705. +An RJ45 network connection to the server has not yet been tested. From a397eb91019cd1df9f667c94b758e770c23f5f2b Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Tue, 13 Oct 2026 08:41:31 -0500 Subject: [PATCH 12/13] Document physical KC705 Ethernet SSH and current server integration --- docs/SERVER_EMBEDDED.md | 25 ++++++++++++++----------- docs/server.md | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/docs/SERVER_EMBEDDED.md b/docs/SERVER_EMBEDDED.md index 008abeb..74d5595 100644 --- a/docs/SERVER_EMBEDDED.md +++ b/docs/SERVER_EMBEDDED.md @@ -23,25 +23,28 @@ Call `netnox_ssh_server_set_host_ed25519_seed()` with a 32-byte Ed25519 seed ## Authentication -`netnox_ssh_server_set_allowed_password()` configures a single shared password -accepted for **any** username (intended for lab / firmware shells). Extend the -server sources for per-user stores or public-key verification if needed. +Use `netnox_ssh_server_set_publickey_callback()` to decide whether the +requested username and Ed25519 public key are authorized. The Linux daemon +checks the local account and protected `authorized_keys` before signature +verification. Password authentication is not implemented. See +[Linux server integration](server.md) for its account and host-key rules. ## Subsystems and PTY -- **exec**: one-shot `sh -c` on Unix (`fork`/`pipe`) and `cmd /c` on Windows - (`_popen`), stdout/stderr streamed as channel data. -- **shell** / **pty-req**: rejected with `CHANNEL_FAILURE` until PTY adapters - are wired. +- On Linux, **exec**, **shell**, optional **PTY**, terminal resize and + exit-status forwarding are implemented using OS account/session support. + An individual exec command is limited to 512 bytes. - **subsystem sftp**: rejected (scaffolding only); SFTP framing can be added later. ## Optional hooks -See `server/embedded_adapter.h` for typedefs describing optional monotonic -clock and CSPRNG hooks. The current implementation uses `rand()` for SSH -padding and KEXINIT cookies; hardened devices should replace these with NoxTLS -DRBG calls. +`server/embedded_adapter.h` contains future/platform-specific hook typedefs; +they are not an implemented RTOS or bare-metal session backend. The current +server uses a blocking OS CSPRNG (Linux `getrandom`) and fails instead of +falling back to `rand()`. A non-Linux embedded port also needs monotonic time, +authorization and application/session adapters. Stream callbacks alone do +not establish a complete portable server implementation. ## Build without the daemon diff --git a/docs/server.md b/docs/server.md index dab8615..ef5550b 100644 --- a/docs/server.md +++ b/docs/server.md @@ -40,6 +40,10 @@ close after 30 minutes, and connections close at the 256 MiB rekey limit until in-session rekey is implemented. It does not offer SFTP, forwarding, password authentication, or every SSH terminal-mode control. +An individual exec command is limited to 512 bytes. Split longer diagnostics +into separate requests. A non-login exec shell may need an explicit PATH +including `/usr/sbin` and `/sbin` for tools such as `ip` and `ethtool`. + ## KC705 qualification status The `04d265b` server binary ran on the KC705 RV64 Linux system over UART @@ -52,7 +56,30 @@ reported cleanup status 0, removal of the temporary client keys and PC authorization line, and daemon shutdown. The UART evidence is at `E:/MPUNoxV_build_artifacts/ethernet_20261012/noxssh-loopback-04d265b.log`. -Source commit `1ef8774` adds `--check-user` and -`--print-host-public-key` above that tested server version. Those commands -passed Linux x86/ARM CI; they have not yet been exercised on the KC705. -An RJ45 network connection to the server has not yet been tested. +### Physical Ethernet trials, 2026-10-13 + +The final Yocto-pinned source `1ef8774` ran on the actual KC705, Linux +6.18.39 with both harts at 200 MHz, through the board's RJ45 connector and +our GMII MAC/PHY. Both the coherent-DMA and corrected PIO configurations +negotiated 1000 Mb/s full duplex with the Marvell 88E1111 at MDIO address 7. + +The physical helper passed trusted-host Ed25519 authentication, exec, PTY, +an 11-second quiet command returning 9, invalid-account/key rejection, and +262144-byte encrypted upload/download with exact checksum agreement. +`--check-user root` was exercised on the real board; the public host key +was obtained over the trusted UART path. Its fingerprint remained +`SHA256:1Cb2ywW9q2npFNJLYnYhWWgCRALJVdY5Pj1WiD33SbM` after the next +JTAG Linux reboot. NoxSSH was enabled in persistent `/etc/default/noxsshd` +on port 2222 and started automatically after that reboot; the physical PIO +helper passed without launching a manual daemon. + +Evidence under `E:/MPUNoxV_build_artifacts/ethernet_qualification_20261013`: + +- `dma-hpm-7e390d9-noxssh-physical-supplier/result.json`: + transfer SHA-256 `4a26d10d0747718f9b3d0788eeab640357c2e06d79e08747ac787662764a99b7`. +- `pio-v2-hpm-7e390d9-noxssh-persistent/result.json`: + transfer SHA-256 `9e2675e51d78132dfe1d4da1d8875dcb13e1b5534bedd8f45f176e46eb1cff85`. + +RX busy/buffer drops were recorded. These tests prove working SSH over +physical Ethernet, not line-rate, zero-drop operation, complete link-loss +recovery, power-cycle persistence or autonomous FPGA flash configuration. From 2e294de4b933249964b2ae5c6358e4db9472c6b9 Mon Sep 17 00:00:00 2001 From: Gustavo Litovsky Date: Tue, 13 Oct 2026 11:45:34 -0500 Subject: [PATCH 13/13] Anchor persistent host seed operations to validated directory fd --- docs/server.md | 18 ++ scripts/test_host_seed_dirfd.py | 313 ++++++++++++++++++++++++++++++++ server/main.c | 58 ++++-- 3 files changed, 371 insertions(+), 18 deletions(-) create mode 100755 scripts/test_host_seed_dirfd.py diff --git a/docs/server.md b/docs/server.md index ef5550b..b8942d8 100644 --- a/docs/server.md +++ b/docs/server.md @@ -25,6 +25,24 @@ starts; back it up as a secret if clients pin the host identity. A supplied `--host-key` must be an absolute path under a directory owned by the daemon and not group/other writable. The server refuses an unsafe or malformed key. +The server opens and validates the parent directory descriptor once, then uses +descriptor-relative seed reads, exclusive creation and failure cleanup. Directory +fsync uses that same descriptor, so replacing the parent pathname after validation +cannot redirect these operations. Parent and seed final-component symlinks are +refused. Seed ownership, private permissions and the existing 32-byte format are +preserved. A directory-fsync failure returns failure while retaining the complete +seed; a later invocation reuses it rather than silently changing host identity. + +Focused synthetic fixtures exercise directory replacement, existing-key reuse, +creation, cleanup and injected I/O failures without using real keys or a daemon: + +```sh +python3 scripts/test_host_seed_dirfd.py +``` + +This compiles only the actual seed functions with `-Wall -Wextra -Werror` in a +temporary fixture directory. It does not replace full server, CI or target testing. + `noxsshd --check-user root` returns success only if root's account and `authorized_keys` currently permit an Ed25519 key. It does not create a host key or bind a socket. On the trusted UART console, run diff --git a/scripts/test_host_seed_dirfd.py b/scripts/test_host_seed_dirfd.py new file mode 100755 index 0000000..000c2f8 --- /dev/null +++ b/scripts/test_host_seed_dirfd.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +"""Compile only the production seed functions; test synthetic private temp paths.""" + +import argparse +from pathlib import Path +import subprocess +import tempfile +import unittest + +HARNESS = r''' +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *scenario, *parent_path, *held_path, *attacker_path; +static int parent_fd_seen = -1, swapped, dir_syncs, relative_opens, relative_unlinks; +static int random_interrupt, write_interrupt, read_interrupt; + +static void swap_parent(void) +{ + if(swapped) return; + if(rename(parent_path, held_path) || symlink(attacker_path, parent_path)) exit(99); + swapped = 1; +} + +static int fixture_open(const char *path, int flags, ...) +{ + mode_t mode = 0; + if(flags & O_CREAT) { + va_list ap; va_start(ap, flags); mode = (mode_t)va_arg(ap, int); va_end(ap); + } + int fd = open(path, flags, mode); + if(fd >= 0 && (flags & O_DIRECTORY)) parent_fd_seen = fd; + return fd; +} + +static int fixture_fstat(int fd, struct stat *st) +{ + int rc = fstat(fd, st); + if(rc) return rc; + if(fd == parent_fd_seen) { + if(!strcmp(scenario, "parent-owner")) st->st_uid = geteuid() + 1; + if(!strcmp(scenario, "parent-fstat-fail")) { errno = EIO; return -1; } + if(!strcmp(scenario, "swap-reuse") || !strcmp(scenario, "swap-create")) swap_parent(); + } else if(!strcmp(scenario, "seed-owner")) st->st_uid = geteuid() + 1; + return 0; +} + +static int fixture_openat(int dirfd, const char *path, int flags, ...) +{ + mode_t mode = 0; + relative_opens++; + if(flags & O_CREAT) { + va_list ap; va_start(ap, flags); mode = (mode_t)va_arg(ap, int); va_end(ap); + if(!strcmp(scenario, "create-error")) { errno = EACCES; return -1; } + if(!strcmp(scenario, "create-race")) { + uint8_t other[32]; memset(other, 0x33, sizeof(other)); + int fd = openat(dirfd, path, flags, mode); + if(fd < 0 || write(fd, other, sizeof(other)) != 32 || close(fd)) exit(99); + errno = EEXIST; return -1; + } + } + return openat(dirfd, path, flags, mode); +} + +static ssize_t fixture_getrandom(void *data, size_t count, unsigned int flags) +{ + (void)flags; + if(!strcmp(scenario, "entropy-error")) { errno = EIO; return -1; } + if(!strcmp(scenario, "interrupts") && !random_interrupt++) { errno = EINTR; return -1; } + if(count > 7) count = 7; + memset(data, 0x5a, count); + return (ssize_t)count; +} + +static ssize_t fixture_write(int fd, const void *data, size_t count) +{ + if(!strcmp(scenario, "swap-write-error")) { swap_parent(); errno = EIO; return -1; } + if(!strcmp(scenario, "write-error")) { errno = EIO; return -1; } + if(!strcmp(scenario, "zero-write")) return 0; + if(!strcmp(scenario, "interrupts") && !write_interrupt++) { errno = EINTR; return -1; } + if(count > 5) count = 5; + return write(fd, data, count); +} + +static ssize_t fixture_read(int fd, void *data, size_t count) +{ + if(!strcmp(scenario, "read-error")) { errno = EIO; return -1; } + if(!strcmp(scenario, "interrupt-read") && !read_interrupt++) { errno = EINTR; return -1; } + if(count > 6) count = 6; + return read(fd, data, count); +} + +static int fixture_fsync(int fd) +{ + if(fd == parent_fd_seen) { + dir_syncs++; + if(!strcmp(scenario, "directory-sync-error")) { errno = EIO; return -1; } + } else if(!strcmp(scenario, "file-sync-error")) { errno = EIO; return -1; } + return fsync(fd); +} + +static int fixture_close(int fd) +{ + int rc = close(fd); + if(fd != parent_fd_seen && !strcmp(scenario, "file-close-error")) { errno = EIO; return -1; } + return rc; +} + +static int fixture_unlinkat(int dirfd, const char *path, int flags) +{ + relative_unlinks++; + return unlinkat(dirfd, path, flags); +} + +#define open fixture_open +#define openat fixture_openat +#define fstat fixture_fstat +#define getrandom fixture_getrandom +#define write fixture_write +#define read fixture_read +#define fsync fixture_fsync +#define close fixture_close +#define unlinkat fixture_unlinkat +PRODUCTION_FUNCTIONS +#undef open +#undef openat +#undef fstat +#undef getrandom +#undef write +#undef read +#undef fsync +#undef close +#undef unlinkat + +int main(int argc, char **argv) +{ + uint8_t seed[32], expected[32]; + if(argc != 6) return 99; + scenario = argv[1]; parent_path = argv[3]; held_path = argv[4]; attacker_path = argv[5]; + memset(seed, 0x7c, sizeof(seed)); + int result = host_seed(argv[2], seed); + memset(expected, !strcmp(scenario, "create-race") ? 0x33 : 0x5a, sizeof(expected)); + if(result == 0 && memcmp(seed, expected, sizeof(seed))) return 98; + printf("result=%d relative_opens=%d relative_unlinks=%d dir_syncs=%d swapped=%d\n", + result, relative_opens, relative_unlinks, dir_syncs, swapped); + return result == 0 ? 0 : 1; +} +''' + + +class SeedFixtures(unittest.TestCase): + @classmethod + def setUpClass(cls): + source = SOURCE.read_text() + functions = source[source.index("static int random_seed("):source.index("\nint main(")] + cls.build = tempfile.TemporaryDirectory(prefix="noxssh-seed-functions-") + cls.addClassCleanup(cls.build.cleanup) + path = Path(cls.build.name) + (path / "fixture.c").write_text(HARNESS.replace("PRODUCTION_FUNCTIONS", functions)) + cls.binary = path / "fixture" + subprocess.run([CC, "-std=c99", "-Wall", "-Wextra", "-Werror", "-O2", + str(path / "fixture.c"), "-o", str(cls.binary)], check=True) + + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix="noxssh-seed-case-") + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.parent = self.root / "private" + self.held = self.root / "held" + self.attack = self.root / "attacker" + self.parent.mkdir(mode=0o700) + self.attack.mkdir(mode=0o777) + self.seed = self.parent / "seed" + self.other = self.attack / "seed" + self.other.write_bytes(b"A" * 32) + self.other.chmod(0o600) + + def existing(self, size=32, mode=0o600): + self.seed.write_bytes(b"\x5a" * size) + self.seed.chmod(mode) + + def run_case(self, scenario="normal", success=True, path=None): + result = subprocess.run([str(self.binary), scenario, str(path or self.seed), + str(self.parent), str(self.held), str(self.attack)], + capture_output=True, text=True, timeout=5) + self.assertEqual(result.returncode, 0 if success else 1, result.stdout + result.stderr) + self.assertEqual(self.other.read_bytes(), b"A" * 32) + return result.stdout + + def test_create_and_reuse_exact_private_seed(self): + first = self.run_case() + self.assertIn("dir_syncs=1", first) + self.assertEqual(self.seed.read_bytes(), b"\x5a" * 32) + self.assertEqual(self.seed.stat().st_mode & 0o777, 0o600) + self.assertIn("dir_syncs=0", self.run_case()) + + def test_existing_read_interrupted_and_short(self): + self.existing() + self.run_case("interrupt-read") + + def test_entropy_and_write_interrupted_and_short(self): + self.run_case("interrupts") + + def test_concurrent_creator_reused(self): + self.run_case("create-race") + self.assertEqual(self.seed.read_bytes(), b"\x33" * 32) + + def test_existing_parent_path_swap_uses_validated_directory(self): + self.existing() + self.assertIn("swapped=1", self.run_case("swap-reuse")) + self.assertTrue(self.parent.is_symlink()) + self.assertEqual((self.held / "seed").read_bytes(), b"\x5a" * 32) + + def test_creation_parent_path_swap_uses_validated_directory_and_fsync(self): + self.assertIn("dir_syncs=1 swapped=1", self.run_case("swap-create")) + self.assertEqual((self.held / "seed").read_bytes(), b"\x5a" * 32) + + def test_write_failure_cleanup_not_redirected_by_parent_swap(self): + self.assertIn("relative_unlinks=1", self.run_case("swap-write-error", False)) + self.assertFalse((self.held / "seed").exists()) + + def test_symlink_parent_rejected(self): + self.parent.rmdir() + self.parent.symlink_to(self.attack, target_is_directory=True) + self.run_case(success=False) + + def test_writable_parent_rejected(self): + self.parent.chmod(0o777) + self.run_case(success=False) + + def test_symlink_parent_with_repeated_separator_rejected(self): + self.parent.rmdir() + self.parent.symlink_to(self.attack, target_is_directory=True) + self.run_case(success=False, path=str(self.parent) + "//seed") + + def test_wrong_parent_owner_rejected(self): + self.run_case("parent-owner", False) + + def test_parent_fstat_failure_rejected(self): + self.run_case("parent-fstat-fail", False) + + def test_symlink_seed_rejected(self): + self.seed.symlink_to(self.other) + self.run_case(success=False) + + def test_fifo_seed_rejected_without_blocking(self): + import os + os.mkfifo(self.seed, 0o600) + self.run_case(success=False) + + def test_directory_seed_rejected(self): + self.seed.mkdir(mode=0o700) + self.run_case(success=False) + + def test_wrong_seed_owner_rejected(self): + self.existing() + self.run_case("seed-owner", False) + + def test_exposed_seed_permissions_rejected(self): + self.existing(mode=0o644) + self.run_case(success=False) + + def test_wrong_seed_length_rejected(self): + for size in [0, 31, 33]: + self.existing(size=size) + self.run_case(success=False) + self.assertEqual(self.seed.stat().st_size, size) + + def test_read_failure_preserves_existing_seed(self): + self.existing() + self.run_case("read-error", False) + self.assertEqual(self.seed.read_bytes(), b"\x5a" * 32) + + def test_entropy_failure_does_not_create_seed(self): + self.run_case("entropy-error", False) + self.assertFalse(self.seed.exists()) + + def test_create_error_does_not_create_seed(self): + self.run_case("create-error", False) + self.assertFalse(self.seed.exists()) + + def test_failed_write_sync_close_cleanup(self): + for scenario in ["write-error", "zero-write", "file-sync-error", "file-close-error"]: + self.run_case(scenario, False) + self.assertFalse(self.seed.exists()) + + def test_directory_sync_failure_preserves_complete_identity(self): + self.run_case("directory-sync-error", False) + self.assertEqual(self.seed.read_bytes(), b"\x5a" * 32) + self.run_case() + + def test_invalid_final_components(self): + for path in [str(self.parent) + "/", str(self.parent) + "/.", str(self.parent) + "/.."]: + self.run_case(success=False, path=path) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, default=Path(__file__).resolve().parents[1] / "server/main.c") + parser.add_argument("--cc", default="cc") + args, remainder = parser.parse_known_args() + SOURCE, CC = args.source, args.cc + unittest.main(argv=[__file__, *remainder], verbosity=2) diff --git a/server/main.c b/server/main.c index a19f24d..54e1c15 100644 --- a/server/main.c +++ b/server/main.c @@ -58,7 +58,7 @@ static int random_seed(uint8_t seed[32]) static int host_seed(const char *path, uint8_t seed[32]) { - int fd, created = 0; + int fd = -1, parent_fd = -1, result = -1; struct stat st; uint8_t extra; ssize_t n; @@ -66,43 +66,65 @@ static int host_seed(const char *path, uint8_t seed[32]) char parent[4096]; const char *slash = strrchr(path, '/'); if(!slash || (size_t)(slash - path) >= sizeof(parent) || slash == path) return -1; + if(!slash[1] || !strcmp(slash + 1, ".") || !strcmp(slash + 1, "..")) return -1; memcpy(parent, path, (size_t)(slash - path)); parent[slash - path] = 0; - if(lstat(parent, &st) || !S_ISDIR(st.st_mode) || (st.st_mode & 022) || st.st_uid != geteuid()) return -1; - fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + /* A trailing slash would make the kernel follow a final directory symlink + * even with O_NOFOLLOW. Keep only the root directory's required slash. */ + pos = (size_t)(slash - path); + while(pos > 1u && parent[pos - 1u] == '/') parent[--pos] = 0; + /* Validate and retain the directory we actually opened. A later pathname + * rename/symlink replacement must not redirect seed I/O, cleanup or fsync. */ + parent_fd = open(parent, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW); + if(parent_fd < 0 || fstat(parent_fd, &st) || !S_ISDIR(st.st_mode) || + (st.st_mode & 022) || st.st_uid != geteuid()) goto out; + fd = openat(parent_fd, slash + 1, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK); if(fd < 0 && errno == ENOENT) { - if(random_seed(seed)) return -1; - fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0600); + if(random_seed(seed)) goto out; + fd = openat(parent_fd, slash + 1, + O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0600); if(fd >= 0) { - created = 1; + if(fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_uid != geteuid() || + (st.st_mode & 077) || st.st_size != 0) goto remove_created; pos = 0; while(pos < 32u) { n = write(fd, seed + pos, 32u - pos); if(n > 0) { pos += (size_t)n; continue; } if(n < 0 && errno == EINTR) continue; - close(fd); unlink(path); return -1; + goto remove_created; } { int sync_rc = fsync(fd); int close_rc = close(fd); - if(sync_rc || close_rc) { unlink(path); return -1; } } - fd = open(parent, O_RDONLY | O_DIRECTORY | O_CLOEXEC); - if(fd < 0 || fsync(fd)) { if(fd >= 0) close(fd); return -1; } - close(fd); - } else if(errno != EEXIST) return -1; + fd = -1; + if(sync_rc || close_rc) goto remove_created; } + /* Keep a complete seed if directory durability cannot be confirmed; + * fail this invocation without silently generating another identity. */ + if(fsync(parent_fd)) goto out; + result = 0; + goto out; + } else if(errno != EEXIST) goto out; } - if(created) return 0; - if(fd < 0) fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + if(fd < 0) fd = openat(parent_fd, slash + 1, + O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK); if(fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_uid != geteuid() || - (st.st_mode & 077) || st.st_size != 32) { if(fd >= 0) close(fd); return -1; } + (st.st_mode & 077) || st.st_size != 32) goto out; pos = 0; while(pos < 32u) { n = read(fd, seed + pos, 32u - pos); if(n > 0) { pos += (size_t)n; continue; } if(n < 0 && errno == EINTR) continue; - close(fd); return -1; + goto out; } n = read(fd, &extra, 1u); - close(fd); - return n == 0 ? 0 : -1; + result = n == 0 ? 0 : -1; + goto out; +remove_created: + if(fd >= 0) { close(fd); fd = -1; } + (void)unlinkat(parent_fd, slash + 1, 0); +out: + if(fd >= 0 && close(fd)) result = -1; + if(parent_fd >= 0 && close(parent_fd)) result = -1; + if(result) memset(seed, 0, 32u); + return result; } int main(int argc, char **argv)