78asciichat_error_t
check_known_host(
const char *server_ip, uint16_t port,
const uint8_t server_key[32]) {
80 if (!server_ip || !server_key) {
81 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters: server_ip=%p, server_key=%p", server_ip, server_key);
85 char ip_with_port[BUFFER_SIZE_MEDIUM];
86 if (
format_ip_with_port(server_ip, port, ip_with_port,
sizeof(ip_with_port)) != ASCIICHAT_OK) {
87 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid IP format: %s", server_ip);
91 char expected_prefix[BUFFER_SIZE_MEDIUM];
92 safe_snprintf(expected_prefix,
sizeof(expected_prefix),
"%s ", ip_with_port);
96 config_file_list_t known_hosts_files = {0};
99 if (search_result != ASCIICHAT_OK) {
101 log_debug(
"KNOWN_HOSTS: Failed to search for known_hosts files: %d", search_result);
106 if (known_hosts_files.count == 0) {
111 bool found_entries =
false;
112 for (
size_t file_idx = 0; file_idx < known_hosts_files.count; file_idx++) {
113 const char *path = known_hosts_files.files[file_idx].path;
115 int fd =
platform_open(path, PLATFORM_O_RDONLY, FILE_PERM_PRIVATE);
117 log_debug(
"KNOWN_HOSTS: Cannot open file: %s", path);
121 FILE *f = platform_fdopen(fd,
"r");
124 log_debug(
"KNOWN_HOSTS: Failed to fdopen: %s", path);
128 char line[BUFFER_SIZE_XLARGE];
131 while (fgets(line,
sizeof(line), f)) {
136 char *parsed_ip_port = NULL, *parsed_key_type = NULL, *parsed_hex_key = NULL, *parsed_comment = NULL;
143 SAFE_FREE(parsed_ip_port);
144 SAFE_FREE(parsed_key_type);
145 SAFE_FREE(parsed_hex_key);
146 SAFE_FREE(parsed_comment);
150 found_entries =
true;
152 if (strcmp(parsed_key_type, NO_IDENTITY_MARKER) == 0) {
154 log_debug(
"SECURITY_DEBUG: Found no-identity entry, but server has identity key");
155 SAFE_FREE(parsed_ip_port);
156 SAFE_FREE(parsed_key_type);
157 SAFE_FREE(parsed_hex_key);
158 SAFE_FREE(parsed_comment);
163 if (!parsed_hex_key) {
165 SAFE_FREE(parsed_ip_port);
166 SAFE_FREE(parsed_key_type);
167 SAFE_FREE(parsed_comment);
171 public_key_t stored_key;
173 SAFE_FREE(parsed_ip_port);
174 SAFE_FREE(parsed_key_type);
175 SAFE_FREE(parsed_hex_key);
176 SAFE_FREE(parsed_comment);
181 bool server_key_is_zero =
true;
182 for (
int i = 0; i < ED25519_PUBLIC_KEY_SIZE; i++) {
183 if (server_key[i] != 0) {
184 server_key_is_zero =
false;
190 bool stored_key_is_zero =
true;
191 for (
int i = 0; i < ED25519_PUBLIC_KEY_SIZE; i++) {
192 if (stored_key.key[i] != 0) {
193 stored_key_is_zero =
false;
199 if (server_key_is_zero && stored_key_is_zero) {
200 log_warn(
"SECURITY: Connecting to no-identity server at known IP:port");
201 SAFE_FREE(parsed_ip_port);
202 SAFE_FREE(parsed_key_type);
203 SAFE_FREE(parsed_hex_key);
204 SAFE_FREE(parsed_comment);
211 if (sodium_memcmp(server_key, stored_key.key, ED25519_PUBLIC_KEY_SIZE) == 0) {
212 log_info(
"SECURITY: Server key matches known_hosts - connection verified");
213 SAFE_FREE(parsed_ip_port);
214 SAFE_FREE(parsed_key_type);
215 SAFE_FREE(parsed_hex_key);
216 SAFE_FREE(parsed_comment);
223 SAFE_FREE(parsed_ip_port);
224 SAFE_FREE(parsed_key_type);
225 SAFE_FREE(parsed_hex_key);
226 SAFE_FREE(parsed_comment);
238 log_error(
"SECURITY: Server key does NOT match any known_hosts entries!");
239 log_error(
"SECURITY: This indicates a possible man-in-the-middle attack!");
240 return ERROR_CRYPTO_VERIFICATION;
253 int fd =
platform_open(path, PLATFORM_O_RDONLY, FILE_PERM_PRIVATE);
256 log_warn(
"Known hosts file does not exist: %s", path);
260 FILE *f = platform_fdopen(fd,
"r");
261 defer(SAFE_FCLOSE(f));
265 log_warn(
"Failed to open known hosts file: %s", path);
269 char line[BUFFER_SIZE_XLARGE];
270 char expected_prefix[BUFFER_SIZE_MEDIUM];
273 char ip_with_port[BUFFER_SIZE_MEDIUM];
274 if (
format_ip_with_port(server_ip, port, ip_with_port,
sizeof(ip_with_port)) != ASCIICHAT_OK) {
276 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid IP format: %s", server_ip);
280 safe_snprintf(expected_prefix,
sizeof(expected_prefix),
"%s ", ip_with_port);
282 while (fgets(line,
sizeof(line), f)) {
287 char *parsed_ip_port = NULL, *parsed_key_type = NULL, *parsed_hex_key = NULL, *parsed_comment = NULL;
294 SAFE_FREE(parsed_ip_port);
295 SAFE_FREE(parsed_key_type);
296 SAFE_FREE(parsed_hex_key);
297 SAFE_FREE(parsed_comment);
302 if (strcmp(parsed_key_type,
"no-identity") == 0) {
305 SAFE_FREE(parsed_ip_port);
306 SAFE_FREE(parsed_key_type);
307 SAFE_FREE(parsed_hex_key);
308 SAFE_FREE(parsed_comment);
314 log_warn(
"Server previously had identity key but now has none - potential security issue");
315 SAFE_FREE(parsed_ip_port);
316 SAFE_FREE(parsed_key_type);
317 SAFE_FREE(parsed_hex_key);
318 SAFE_FREE(parsed_comment);
319 return ERROR_CRYPTO_VERIFICATION;
381asciichat_error_t
add_known_host(
const char *server_ip, uint16_t port,
const uint8_t server_key[32]) {
383 if (!server_ip || !server_key) {
384 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters: server_ip=%p, server_key=%p", server_ip, server_key);
388 size_t ip_len = strlen(server_ip);
390 return SET_ERRNO(ERROR_INVALID_PARAM,
"Empty hostname/IP");
394 if (!path || path[0] ==
'\0') {
395 SET_ERRNO(ERROR_CONFIG,
"Failed to get known hosts file path");
400 size_t path_len = strlen(path);
402 SET_ERRNO(ERROR_CONFIG,
"Empty known hosts file path");
405 char *dir = SAFE_MALLOC(path_len + 1,
char *);
406 defer(SAFE_FREE(dir));
408 SET_ERRNO(ERROR_MEMORY,
"Failed to allocate memory for directory path");
411 memcpy(dir, path, path_len + 1);
414 char *last_sep = strrchr(dir, PATH_DELIM);
418 asciichat_error_t result = mkdir_recursive(dir);
419 if (result != ASCIICHAT_OK) {
429 defer(SAFE_FCLOSE(f));
432 return SET_ERRNO_SYS(ERROR_CONFIG,
"Failed to create/open known hosts file: %s", path);
436 (void)platform_chmod(path, FILE_PERM_PRIVATE);
440 char ip_with_port[BUFFER_SIZE_MEDIUM];
441 if (
format_ip_with_port(server_ip, port, ip_with_port,
sizeof(ip_with_port)) != ASCIICHAT_OK) {
442 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid IP format: %s", server_ip);
446 char hex[CRYPTO_HEX_KEY_SIZE_NULL] = {0};
447 bool is_placeholder =
true;
449 for (
int i = 0; i < ED25519_PUBLIC_KEY_SIZE; i++) {
451 uint8_t
byte = server_key[i];
452 hex[i * 2] =
"0123456789abcdef"[
byte >> 4];
453 hex[i * 2 + 1] =
"0123456789abcdef"[
byte & 0xf];
455 is_placeholder =
false;
458 hex[CRYPTO_HEX_KEY_SIZE] =
'\0';
462 if (is_placeholder) {
464 fprintf_result =
safe_fprintf(f,
"%s %s 0000000000000000000000000000000000000000000000000000000000000000 %s\n",
465 ip_with_port, NO_IDENTITY_MARKER, ASCII_CHAT_APP_NAME);
468 fprintf_result =
safe_fprintf(f,
"%s %s %s %s\n", ip_with_port, X25519_KEY_TYPE, hex, ASCII_CHAT_APP_NAME);
472 if (fprintf_result < 0) {
473 return SET_ERRNO_SYS(ERROR_CONFIG,
"CRITICAL SECURITY ERROR: Failed to write to known_hosts file: %s", path);
477 if (fflush(f) != 0) {
478 return SET_ERRNO_SYS(ERROR_CONFIG,
"CRITICAL SECURITY ERROR: Failed to flush known_hosts file: %s", path);
481 log_debug(
"KNOWN_HOSTS: Successfully added host to known_hosts file: %s", path);
489 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameter: server_ip=%p", server_ip);
493 int fd =
platform_open(path, PLATFORM_O_RDONLY, FILE_PERM_PRIVATE);
498 FILE *f = platform_fdopen(fd,
"r");
499 defer(SAFE_FCLOSE(f));
502 SET_ERRNO_SYS(ERROR_CONFIG,
"Failed to open known hosts file: %s", path);
507 char ip_with_port[BUFFER_SIZE_MEDIUM];
508 if (
format_ip_with_port(server_ip, port, ip_with_port,
sizeof(ip_with_port)) != ASCIICHAT_OK) {
510 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid IP format: %s", server_ip);
515 size_t num_lines = 0;
518 for (
size_t i = 0; i < num_lines; i++) {
524 char line[BUFFER_SIZE_XLARGE];
526 char expected_prefix[BUFFER_SIZE_MEDIUM];
527 safe_snprintf(expected_prefix,
sizeof(expected_prefix),
"%s ", ip_with_port);
529 while (fgets(line,
sizeof(line), f)) {
531 if (strncmp(line, expected_prefix, strlen(expected_prefix)) != 0) {
533 char **new_lines = SAFE_REALLOC((
void *)lines, (num_lines + 1) *
sizeof(
char *),
char **);
537 if (lines[num_lines] == NULL) {
538 return SET_ERRNO(ERROR_MEMORY,
"Failed to duplicate line from known_hosts file");
551 fd =
platform_open(path, PLATFORM_O_WRONLY | PLATFORM_O_CREAT | PLATFORM_O_TRUNC, FILE_PERM_PRIVATE);
552 f = platform_fdopen(fd,
"w");
557 return SET_ERRNO_SYS(ERROR_CONFIG,
"Failed to open known hosts file: %s", path);
560 for (
size_t i = 0; i < num_lines; i++) {
561 (void)fputs(lines[i], f);
565 log_debug(
"KNOWN_HOSTS: Successfully removed host from known_hosts file: %s", path);
585 char fingerprint[CRYPTO_HEX_KEY_SIZE_NULL];
589 char ip_with_port[BUFFER_SIZE_MEDIUM];
590 if (
format_ip_with_port(server_ip, port, ip_with_port,
sizeof(ip_with_port)) != ASCIICHAT_OK) {
592 safe_snprintf(ip_with_port,
sizeof(ip_with_port),
"%s:%u", server_ip, port);
596 const char *env_skip_known_hosts_checking =
platform_getenv(
"ASCII_CHAT_INSECURE_NO_HOST_IDENTITY_CHECK");
597 if (env_skip_known_hosts_checking && strcmp(env_skip_known_hosts_checking, STR_ONE) == 0) {
598 log_warn(
"Skipping known_hosts checking. This is a security vulnerability.");
604 if (env_claudecode && strlen(env_claudecode) > 0) {
605 log_warn(
"Skipping known_hosts checking (CLAUDECODE set in debug build).");
611 SET_ERRNO(ERROR_CRYPTO,
"SECURITY: Cannot verify unknown host in non-interactive mode");
612 log_error(
"ERROR: Cannot verify unknown host in non-interactive mode without environment variable bypass.\n"
613 "This connection may be a man-in-the-middle attack!\n"
615 "To connect to this host:\n"
616 " 1. Run the client interactively (from a terminal with TTY)\n"
617 " 2. Verify the fingerprint: SHA256:%s\n"
618 " 3. Accept the host when prompted\n"
619 " 4. The host will be added to: %s\n"
621 "Connection aborted for security.\n"
622 "To bypass this check, set the environment variable ASCII_CHAT_INSECURE_NO_HOST_IDENTITY_CHECK to 1",
632 log_plain(
"@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
633 "@ WARNING: REMOTE HOST IDENTIFICATION NOT KNOWN! @\n"
634 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
636 "The authenticity of host '%s' can't be established.\n"
637 "Ed25519 key fingerprint is SHA256:%s\n",
638 ip_with_port, fingerprint);
645 log_warn(
"Warning: Permanently added '%s' to the list of known hosts.", ip_with_port);
649 log_warn(
"Connection aborted by user.");
656 const uint8_t received_key[32]) {
657 char expected_fp[CRYPTO_HEX_KEY_SIZE_NULL], received_fp[CRYPTO_HEX_KEY_SIZE_NULL];
664 char ip_with_port[BUFFER_SIZE_MEDIUM];
665 if (
format_ip_with_port(server_ip, port, ip_with_port,
sizeof(ip_with_port)) != ASCIICHAT_OK) {
667 safe_snprintf(ip_with_port,
sizeof(ip_with_port),
"%s:%u", server_ip, port);
670 char escaped_ip_with_port[128];
671 escape_ascii(ip_with_port,
"[]", escaped_ip_with_port, 128);
673 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
674 "@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @\n"
675 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
677 "IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!\n"
678 "Someone could be eavesdropping on you right now (man-in-the-middle attack)!\n"
679 "It is also possible that the host key has just been changed.\n"
681 "The fingerprint for the Ed25519 key sent by the remote host is:\n"
684 "Expected fingerprint:\n"
687 "Please contact your system administrator.\n"
689 "Add correct host key in %s to get rid of this message.\n"
690 "Offending key for IP address %s was found at:\n"
693 "To update the key, run:\n"
695 " sed -i '' '/%s /d' ~/.ascii-chat/known_hosts\n"
696 " # or run this instead:\n"
697 " cat ~/.ascii-chat/known_hosts | grep -v '%s ' > /tmp/x; cp /tmp/x ~/.ascii-chat/known_hosts\n"
698 " # Windows PowerShell:\n"
699 " (Get-Content ~/.ascii-chat/known_hosts) | Where-Object { $_ -notmatch '^%s ' } | Set-Content "
700 "~/.ascii-chat/known_hosts\n"
701 " # Or manually edit ~/.ascii-chat/known_hosts to remove lines starting with '%s '\n"
703 "Host key verification failed.\n"
705 received_fp, expected_fp, known_hosts_path, ip_with_port, known_hosts_path, ip_with_port,
706 escaped_ip_with_port, ip_with_port, ip_with_port);