diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f29325e..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 ] @@ -47,17 +50,23 @@ jobs: mkdir -p dist/debian cp build/*.deb dist/debian/ - - name: Upload binaries - uses: actions/upload-artifact@v4 - with: - name: noxssh-${{ matrix.artifact_suffix }} - path: bin/** - if-no-files-found: error + - name: Verify binaries + shell: bash + run: | + set -euo pipefail + test -f bin/noxssh + if [[ "${{ runner.os }}" == "Linux" ]]; then test -f bin/noxsshd; fi - - name: Upload Debian package + - 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 == 'Linux' - uses: actions/upload-artifact@v4 - with: - name: noxssh-${{ matrix.artifact_suffix }}-deb - path: dist/debian/*.deb - if-no-files-found: error + shell: bash + run: bash scripts/ci_smoke_sshd.sh diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml index f299c06..413a2c6 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 binaries + shell: bash + run: | + set -euo pipefail + test -f bin/noxssh + if [[ "${{ runner.os }}" == "Linux" ]]; then test -f bin/noxsshd; fi + - name: Create release archives shell: bash run: | 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 4fb92b6..72853d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,9 @@ add_subdirectory(noxtls EXCLUDE_FROM_ALL) add_library(netnox_ssh common/noxssh_common.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 @@ -74,6 +77,15 @@ target_compile_definitions(netnox_ssh NOXTLS_FEATURE_AES_CTR=1 ) add_subdirectory(client) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_subdirectory(server) +endif() + +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/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/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/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) { 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..10ac3a1 --- /dev/null +++ b/common/noxssh_server.c @@ -0,0 +1,2051 @@ +#define _GNU_SOURCE +/***************************************************************************** +* 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 +#include + +#ifdef _WIN32 +/* Windows: one-shot exec uses _popen only. */ +#else +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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 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 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= '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 netnox_return_t netnox_ssh_fill_random(uint8_t * out, uint32_t len) +{ + 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) +{ + 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) { + 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; + } + 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) { + 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; + } + 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; + 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; + } + 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; + 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, "curve25519-sha256"); + if(rc != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + 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, "aes128-ctr"); + if(rc != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + 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, "hmac-sha2-256"); + if(rc != NETNOX_RETURN_SUCCESS) { + return NETNOX_RETURN_FAILED; + } + 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_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; + } + 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++; + } + 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); + 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]; + 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 = 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); + 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; + 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; + } + s->packet_deadline_ms = srv_now_ms() + 10000u; + + 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 = 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 = 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; + } + 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(!srv_equal_mac(computed_mac, mac_buf, NETNOX_SSH_MAC_LEN)) { + SRVDBG("encrypted packet MAC mismatch"); + 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(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) { + 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; + 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 = "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_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 }; + 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_send_exit_status(netnox_ssh_server_t *s, uint32_t status) +{ + 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); +} + +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; + } + 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]); + } + 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 < 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; + } + /* 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 && !(rc == 0 && output_eof)) break; + 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); + } + } + } + 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; + } + } + 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_FAILED; +} +#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; + } + 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("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(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); + } + 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); + } + 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); +} + +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->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); +} + +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(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; +} + +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) return; + server->publickey_allowed_cb = callback; + server->publickey_allowed_user_data = user_data; +} + +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->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; + 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; + } + s->handshake_deadline_ms = srv_now_ms() + 30000u; + 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) { + SRVDBG("receive packet failed"); + break; + } + SRVDBG("received packet %u", (unsigned)(payload_len ? payload[0] : 0u)); + 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; closing connection"); + return NETNOX_RETURN_FAILED; + } + 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(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; + } + 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) { + 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) { + 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 == 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; + 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 || memchr(cmd,0,cmd_len) || off != payload_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_session_unix(s, cmdbuf, 0) != 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(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) && + 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..260a126 --- /dev/null +++ b/common/noxssh_server.h @@ -0,0 +1,125 @@ +/***************************************************************************** +* 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; + 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]; + + uint8_t host_ed25519_sk[32]; + uint8_t host_ed25519_pk[32]; + uint8_t host_key_ready; + + /** 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; + 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 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. 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); + +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..74d5595 --- /dev/null +++ b/docs/SERVER_EMBEDDED.md @@ -0,0 +1,52 @@ +# 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 + +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 + +- 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 + +`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 + +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/docs/server.md b/docs/server.md new file mode 100644 index 0000000..b8942d8 --- /dev/null +++ b/docs/server.md @@ -0,0 +1,103 @@ +# Linux server + +`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. + +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 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 +`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 +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 +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`. + +### 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. diff --git a/scripts/ci_smoke_sshd.sh b/scripts/ci_smoke_sshd.sh new file mode 100644 index 0000000..b6aaf7b --- /dev/null +++ b/scripts/ci_smoke_sshd.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# 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" +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}" +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" + rm -rf -- "$KEYDIR" +} +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" +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=$! +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: 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* ]] +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 + exit 1 +fi +[[ "$OUT" == *ci_noxssh_ok* ]] +echo 'ci_smoke_sshd: exec, PTY shell, invalid account, NoxSSH client passed' 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/CMakeLists.txt b/server/CMakeLists.txt new file mode 100644 index 0000000..68d5b46 --- /dev/null +++ b/server/CMakeLists.txt @@ -0,0 +1,24 @@ +add_executable(noxsshd + main.c + daemon_runtime.c +) +if(UNIX) + target_sources(noxsshd PRIVATE auth_posix.c) +endif() + +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() + +install(TARGETS noxsshd RUNTIME DESTINATION bin) diff --git a/server/auth_posix.c b/server/auth_posix.c new file mode 100644 index 0000000..b979e2a --- /dev/null +++ b/server/auth_posix.c @@ -0,0 +1,96 @@ +#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; +} + +static int authorized_user_key(const char * username, const uint8_t * key) +{ + 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; + if(!username || !username[0] || strchr(username, '/') || strchr(username, '\\') || + getpwnam_r(username, &pwd, pwbuf, sizeof(pwbuf), &pw) != 0 || !pw || + !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) || + (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) && (!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 new file mode 100644 index 0000000..b8548e5 --- /dev/null +++ b/server/auth_posix.h @@ -0,0 +1,7 @@ +#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]); +/* 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/daemon_runtime.c b/server/daemon_runtime.c new file mode 100644 index 0000000..a33ea96 --- /dev/null +++ b/server/daemon_runtime.c @@ -0,0 +1,260 @@ +/***************************************************************************** +* 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" +#ifndef _WIN32 +#include "auth_posix.h" +#endif + +#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 +#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 +#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, MSG_NOSIGNAL); +#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 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; + } +#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; +} + +#ifndef _WIN32 +static int noxsshd_listen_posix(const char * bind_host, uint16_t port, + 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; + unsigned active = 0u; + struct sigaction sa; + + 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; + } + 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) { + if(errno != EINTR) perror("noxsshd: accept"); + continue; + } + 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 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, host_ed25519_seed); + closesocket(client); + } +} +#endif + +int noxsshd_listen_loop(const char * bind_host, + uint16_t port, + const uint8_t host_ed25519_seed[32]) +{ +#ifndef _WIN32 + return noxsshd_listen_posix(bind_host, port, host_ed25519_seed); +#else + return noxsshd_listen_win32(bind_host, port, host_ed25519_seed); +#endif +} diff --git a/server/daemon_runtime.h b/server/daemon_runtime.h new file mode 100644 index 0000000..983fc20 --- /dev/null +++ b/server/daemon_runtime.h @@ -0,0 +1,30 @@ +/***************************************************************************** +* 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 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..54e1c15 --- /dev/null +++ b/server/main.c @@ -0,0 +1,166 @@ +/* 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 "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__ + 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 +} + +static int host_seed(const char *path, uint8_t seed[32]) +{ + int fd = -1, parent_fd = -1, result = -1; + 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; + if(!slash[1] || !strcmp(slash + 1, ".") || !strcmp(slash + 1, "..")) return -1; + memcpy(parent, path, (size_t)(slash - path)); + parent[slash - path] = 0; + /* 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)) goto out; + fd = openat(parent_fd, slash + 1, + O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0600); + if(fd >= 0) { + 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; + goto remove_created; + } + { int sync_rc = fsync(fd); int close_rc = close(fd); + 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(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) 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; + goto out; + } + n = read(fd, &extra, 1u); + 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) +{ + 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, 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] [--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) { + 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 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; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..2d91f75 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,14 @@ +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") +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) +endif() 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; +}