ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
known_hosts.c File Reference

📜 SSH known_hosts file parser for host key verification and trust management More...

Go to the source code of this file.

Functions

const char * get_known_hosts_path (void)
 Get the path to the known_hosts file.
 
asciichat_error_t check_known_host (const char *server_ip, uint16_t port, const uint8_t server_key[32])
 
asciichat_error_t check_known_host_no_identity (const char *server_ip, uint16_t port)
 
asciichat_error_t add_known_host (const char *server_ip, uint16_t port, const uint8_t server_key[32])
 
asciichat_error_t remove_known_host (const char *server_ip, uint16_t port)
 
void compute_key_fingerprint (const uint8_t key[ED25519_PUBLIC_KEY_SIZE], char fingerprint[CRYPTO_HEX_KEY_SIZE_NULL])
 
bool prompt_unknown_host (const char *server_ip, uint16_t port, const uint8_t server_key[32])
 
bool display_mitm_warning (const char *server_ip, uint16_t port, const uint8_t expected_key[32], const uint8_t received_key[32])
 
bool prompt_unknown_host_no_identity (const char *server_ip, uint16_t port)
 
void known_hosts_destroy (void)
 

Detailed Description

📜 SSH known_hosts file parser for host key verification and trust management

Definition in file known_hosts.c.

Function Documentation

◆ add_known_host()

asciichat_error_t add_known_host ( const char *  server_ip,
uint16_t  port,
const uint8_t  server_key[32] 
)

Definition at line 381 of file known_hosts.c.

381 {
382 // Validate parameters first
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);
385 }
386
387 // Check for empty string (must be after NULL check)
388 size_t ip_len = strlen(server_ip);
389 if (ip_len == 0) {
390 return SET_ERRNO(ERROR_INVALID_PARAM, "Empty hostname/IP");
391 }
392
393 const char *path = get_known_hosts_path();
394 if (!path || path[0] == '\0') {
395 SET_ERRNO(ERROR_CONFIG, "Failed to get known hosts file path");
396 return ERROR_CONFIG;
397 }
398
399 // Create parent directories recursively (like mkdir -p)
400 size_t path_len = strlen(path);
401 if (path_len == 0) {
402 SET_ERRNO(ERROR_CONFIG, "Empty known hosts file path");
403 return ERROR_CONFIG;
404 }
405 char *dir = SAFE_MALLOC(path_len + 1, char *);
406 defer(SAFE_FREE(dir));
407 if (!dir) {
408 SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for directory path");
409 return ERROR_MEMORY;
410 }
411 memcpy(dir, path, path_len + 1);
412
413 // Find the last path separator
414 char *last_sep = strrchr(dir, PATH_DELIM);
415
416 if (last_sep) {
417 *last_sep = '\0'; // Truncate to get directory path
418 asciichat_error_t result = mkdir_recursive(dir);
419 if (result != ASCIICHAT_OK) {
420 return result; // Error already set by mkdir_recursive
421 }
422 }
423
424 // Create the file if it doesn't exist, then append to it
425 // Note: Temporarily removed log_debug to avoid potential crashes during debugging
426 // log_debug("KNOWN_HOSTS: Attempting to create/open file: %s", path);
427 // Use "a" mode for append-only (simpler and works better with chmod)
428 FILE *f = platform_fopen(path, "a");
429 defer(SAFE_FCLOSE(f));
430 if (!f) {
431 // log_debug("KNOWN_HOSTS: platform_fopen failed: %s (errno=%d)", SAFE_STRERROR(errno), errno);
432 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to create/open known hosts file: %s", path);
433 }
434 // Set secure permissions (0600) - only owner can read/write
435 // Note: chmod may fail if file was just created by fopen, but that's okay
436 (void)platform_chmod(path, FILE_PERM_PRIVATE);
437 // log_debug("KNOWN_HOSTS: Successfully opened file: %s", path);
438
439 // Format IP:port with proper bracket notation for IPv6
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);
443 }
444
445 // Convert key to hex for storage
446 char hex[CRYPTO_HEX_KEY_SIZE_NULL] = {0}; // Initialize to zeros for safety
447 bool is_placeholder = true;
448 // Build hex string byte by byte to avoid buffer overflow issues
449 for (int i = 0; i < ED25519_PUBLIC_KEY_SIZE; i++) {
450 // Convert each byte to 2 hex digits directly
451 uint8_t byte = server_key[i];
452 hex[i * 2] = "0123456789abcdef"[byte >> 4]; // High nibble
453 hex[i * 2 + 1] = "0123456789abcdef"[byte & 0xf]; // Low nibble
454 if (byte != 0) {
455 is_placeholder = false;
456 }
457 }
458 hex[CRYPTO_HEX_KEY_SIZE] = '\0'; // Ensure null termination (64 hex digits + null terminator)
459
460 // Write to file and check for errors
461 int fprintf_result;
462 if (is_placeholder) {
463 // Server has no identity key - store as placeholder
464 fprintf_result = safe_fprintf(f, "%s %s 0000000000000000000000000000000000000000000000000000000000000000 %s\n",
465 ip_with_port, NO_IDENTITY_MARKER, ASCII_CHAT_APP_NAME);
466 } else {
467 // Server has identity key - store normally
468 fprintf_result = safe_fprintf(f, "%s %s %s %s\n", ip_with_port, X25519_KEY_TYPE, hex, ASCII_CHAT_APP_NAME);
469 }
470
471 // Check if fprintf failed
472 if (fprintf_result < 0) {
473 return SET_ERRNO_SYS(ERROR_CONFIG, "CRITICAL SECURITY ERROR: Failed to write to known_hosts file: %s", path);
474 }
475
476 // Flush to ensure data is written
477 if (fflush(f) != 0) {
478 return SET_ERRNO_SYS(ERROR_CONFIG, "CRITICAL SECURITY ERROR: Failed to flush known_hosts file: %s", path);
479 }
480
481 log_debug("KNOWN_HOSTS: Successfully added host to known_hosts file: %s", path);
482
483 return ASCIICHAT_OK;
484}
asciichat_error_t format_ip_with_port(const char *ip, uint16_t port, char *output, size_t output_size)
Definition ip.c:221
const char * get_known_hosts_path(void)
Get the path to the known_hosts file.
Definition known_hosts.c:47
int safe_fprintf(FILE *stream, const char *format,...)
Safe formatted output to file stream.
Definition system.c:480
FILE * platform_fopen(const char *filename, const char *mode)

References format_ip_with_port(), get_known_hosts_path(), platform_fopen(), and safe_fprintf().

Referenced by crypto_handshake_client_key_exchange().

◆ check_known_host()

asciichat_error_t check_known_host ( const char *  server_ip,
uint16_t  port,
const uint8_t  server_key[32] 
)

Definition at line 78 of file known_hosts.c.

78 {
79 // Validate parameters first
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);
82 }
83
84 // Format IP:port with proper bracket notation for IPv6
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);
88 }
89
90 // Add space after IP:port for prefix matching
91 char expected_prefix[BUFFER_SIZE_MEDIUM];
92 safe_snprintf(expected_prefix, sizeof(expected_prefix), "%s ", ip_with_port);
93
94 // Use platform abstraction to find all known_hosts files across standard locations
95 // Append semantics: search ALL files for matching entries (user + system)
96 config_file_list_t known_hosts_files = {0};
97 asciichat_error_t search_result = platform_find_config_file("known_hosts", &known_hosts_files);
98
99 if (search_result != ASCIICHAT_OK) {
100 // Platform search failed - non-fatal, treat as unknown host
101 log_debug("KNOWN_HOSTS: Failed to search for known_hosts files: %d", search_result);
102 return ASCIICHAT_OK;
103 }
104
105 // If no files found, this is an unknown host (first connection)
106 if (known_hosts_files.count == 0) {
107 return ASCIICHAT_OK;
108 }
109
110 // Search through ALL known_hosts files for matching entry (append semantics)
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;
114
115 int fd = platform_open(path, PLATFORM_O_RDONLY, FILE_PERM_PRIVATE);
116 if (fd < 0) {
117 log_debug("KNOWN_HOSTS: Cannot open file: %s", path);
118 continue; // Skip files that can't be opened
119 }
120
121 FILE *f = platform_fdopen(fd, "r");
122 if (!f) {
123 platform_close(fd);
124 log_debug("KNOWN_HOSTS: Failed to fdopen: %s", path);
125 continue;
126 }
127
128 char line[BUFFER_SIZE_XLARGE];
129
130 // Search this file for matching IP:port entries
131 while (fgets(line, sizeof(line), f)) {
132 if (line[0] == '#')
133 continue; // Comment
134
135 // Use regex to parse and extract known_hosts line components
136 char *parsed_ip_port = NULL, *parsed_key_type = NULL, *parsed_hex_key = NULL, *parsed_comment = NULL;
137 if (!crypto_regex_match_known_hosts(line, &parsed_ip_port, &parsed_key_type, &parsed_hex_key, &parsed_comment)) {
138 continue; // Line doesn't match known_hosts format
139 }
140
141 // Check if IP:port matches what we're looking for (with IPv6 normalization)
142 if (!compare_ip_port_strings(parsed_ip_port, ip_with_port)) {
143 SAFE_FREE(parsed_ip_port);
144 SAFE_FREE(parsed_key_type);
145 SAFE_FREE(parsed_hex_key);
146 SAFE_FREE(parsed_comment);
147 continue; // Different IP:port
148 }
149
150 found_entries = true;
151
152 if (strcmp(parsed_key_type, NO_IDENTITY_MARKER) == 0) {
153 // No-identity entry but server has identity - continue searching
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);
159 continue;
160 }
161
162 // Parse identity key from hex string
163 if (!parsed_hex_key) {
164 // No hex key found - invalid entry
165 SAFE_FREE(parsed_ip_port);
166 SAFE_FREE(parsed_key_type);
167 SAFE_FREE(parsed_comment);
168 continue;
169 }
170
171 public_key_t stored_key;
172 if (parse_public_key(parsed_hex_key, &stored_key) != 0) {
173 SAFE_FREE(parsed_ip_port);
174 SAFE_FREE(parsed_key_type);
175 SAFE_FREE(parsed_hex_key);
176 SAFE_FREE(parsed_comment);
177 continue; // Invalid key format
178 }
179
180 // Check if server key is all zeros (no-identity server)
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;
185 break;
186 }
187 }
188
189 // Check if stored key is all zeros
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;
194 break;
195 }
196 }
197
198 // Both zero = no-identity connection (weaker security)
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);
205 fclose(f);
206 config_file_list_destroy(&known_hosts_files);
207 return 1; // Match found
208 }
209
210 // Compare keys (constant-time)
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);
217 fclose(f);
218 config_file_list_destroy(&known_hosts_files);
219 return 1; // Match found!
220 }
221
222 // Key mismatch - free and continue searching
223 SAFE_FREE(parsed_ip_port);
224 SAFE_FREE(parsed_key_type);
225 SAFE_FREE(parsed_hex_key);
226 SAFE_FREE(parsed_comment);
227 }
228
229 fclose(f);
230 }
231
232 config_file_list_destroy(&known_hosts_files);
233
234 // Check if we found any entries at all
235 if (found_entries) {
236 // We found entries for this IP:port but none matched the server key
237 // This is a MITM warning!
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;
241 }
242
243 // No entries found for this IP:port - first connection
244 return ASCIICHAT_OK;
245}
int compare_ip_port_strings(const char *ip_port1, const char *ip_port2)
Definition ip.c:1133
asciichat_error_t parse_public_key(const char *input, public_key_t *key_out)
Definition keys.c:29
bool crypto_regex_match_known_hosts(const char *line, char **ip_port_out, char **key_type_out, char **hex_key_out, char **comment_out)
Definition regex.c:136
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
asciichat_error_t platform_find_config_file(const char *filename, config_file_list_t *list_out)
int platform_close(int fd)
int platform_open(const char *pathname, int flags,...)
void config_file_list_destroy(config_file_list_t *list)

References compare_ip_port_strings(), config_file_list_destroy(), crypto_regex_match_known_hosts(), format_ip_with_port(), parse_public_key(), platform_close(), platform_find_config_file(), platform_open(), and safe_snprintf().

Referenced by crypto_handshake_client_key_exchange().

◆ check_known_host_no_identity()

asciichat_error_t check_known_host_no_identity ( const char *  server_ip,
uint16_t  port 
)

Definition at line 251 of file known_hosts.c.

251 {
252 const char *path = get_known_hosts_path();
253 int fd = platform_open(path, PLATFORM_O_RDONLY, FILE_PERM_PRIVATE);
254 if (fd < 0) {
255 // File doesn't exist - this is an unknown host that needs verification
256 log_warn("Known hosts file does not exist: %s", path);
257 return ASCIICHAT_OK; // Return 0 to indicate unknown host (first connection)
258 }
259
260 FILE *f = platform_fdopen(fd, "r");
261 defer(SAFE_FCLOSE(f));
262 if (!f) {
263 // Failed to open file descriptor as FILE*
264 platform_close(fd);
265 log_warn("Failed to open known hosts file: %s", path);
266 return ASCIICHAT_OK; // Return 0 to indicate unknown host (first connection)
267 }
268
269 char line[BUFFER_SIZE_XLARGE];
270 char expected_prefix[BUFFER_SIZE_MEDIUM];
271
272 // Format IP:port with proper bracket notation for IPv6
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) {
275
276 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", server_ip);
277 }
278
279 // Add space after IP:port for prefix matching
280 safe_snprintf(expected_prefix, sizeof(expected_prefix), "%s ", ip_with_port);
281
282 while (fgets(line, sizeof(line), f)) {
283 if (line[0] == '#')
284 continue; // Comment
285
286 // Use regex to parse and extract known_hosts line components
287 char *parsed_ip_port = NULL, *parsed_key_type = NULL, *parsed_hex_key = NULL, *parsed_comment = NULL;
288 if (!crypto_regex_match_known_hosts(line, &parsed_ip_port, &parsed_key_type, &parsed_hex_key, &parsed_comment)) {
289 continue; // Line doesn't match known_hosts format
290 }
291
292 // Check if IP:port matches what we're looking for (with IPv6 normalization)
293 if (!compare_ip_port_strings(parsed_ip_port, ip_with_port)) {
294 SAFE_FREE(parsed_ip_port);
295 SAFE_FREE(parsed_key_type);
296 SAFE_FREE(parsed_hex_key);
297 SAFE_FREE(parsed_comment);
298 continue; // Different IP:port
299 }
300
301 // Found matching IP:port - check key type
302 if (strcmp(parsed_key_type, "no-identity") == 0) {
303 // This is a server without identity key that was previously accepted by the user
304 // No warnings or user confirmation needed - user already accepted this server
305 SAFE_FREE(parsed_ip_port);
306 SAFE_FREE(parsed_key_type);
307 SAFE_FREE(parsed_hex_key);
308 SAFE_FREE(parsed_comment);
309 return 1; // Known host (no-identity entry) - secure connection
310 }
311
312 // If we found a normal identity key entry, this is a mismatch
313 // Server previously had identity key but now has none
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; // Mismatch - server changed from identity to no-identity
320 }
321
322 return ASCIICHAT_OK; // Not found = first connection
323}

References compare_ip_port_strings(), crypto_regex_match_known_hosts(), format_ip_with_port(), get_known_hosts_path(), platform_close(), platform_open(), and safe_snprintf().

Referenced by crypto_handshake_client_key_exchange().

◆ compute_key_fingerprint()

void compute_key_fingerprint ( const uint8_t  key[ED25519_PUBLIC_KEY_SIZE],
char  fingerprint[CRYPTO_HEX_KEY_SIZE_NULL] 
)

Definition at line 570 of file known_hosts.c.

570 {
571 uint8_t hash[HMAC_SHA256_SIZE];
572 crypto_hash_sha256(hash, key, ED25519_PUBLIC_KEY_SIZE);
573
574 // Build hex string byte by byte to avoid buffer overflow issues
575 for (int i = 0; i < HMAC_SHA256_SIZE; i++) {
576 uint8_t byte = hash[i];
577 fingerprint[i * 2] = "0123456789abcdef"[byte >> 4]; // High nibble
578 fingerprint[i * 2 + 1] = "0123456789abcdef"[byte & 0xf]; // Low nibble
579 }
580 fingerprint[CRYPTO_HEX_KEY_SIZE] = '\0';
581}

Referenced by discovery_keys_verify_change(), display_mitm_warning(), and prompt_unknown_host().

◆ display_mitm_warning()

bool display_mitm_warning ( const char *  server_ip,
uint16_t  port,
const uint8_t  expected_key[32],
const uint8_t  received_key[32] 
)

Definition at line 655 of file known_hosts.c.

656 {
657 char expected_fp[CRYPTO_HEX_KEY_SIZE_NULL], received_fp[CRYPTO_HEX_KEY_SIZE_NULL];
658 compute_key_fingerprint(expected_key, expected_fp);
659 compute_key_fingerprint(received_key, received_fp);
660
661 const char *known_hosts_path = get_known_hosts_path();
662
663 // Format IP:port with proper bracket notation for IPv6
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) {
666 // Fallback to basic format if error
667 safe_snprintf(ip_with_port, sizeof(ip_with_port), "%s:%u", server_ip, port);
668 }
669
670 char escaped_ip_with_port[128];
671 escape_ascii(ip_with_port, "[]", escaped_ip_with_port, 128);
672 log_warn("\n"
673 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
674 "@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @\n"
675 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
676 "\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"
680 "\n"
681 "The fingerprint for the Ed25519 key sent by the remote host is:\n"
682 "SHA256:%s\n"
683 "\n"
684 "Expected fingerprint:\n"
685 "SHA256:%s\n"
686 "\n"
687 "Please contact your system administrator.\n"
688 "\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"
691 "%s\n"
692 "\n"
693 "To update the key, run:\n"
694 " # Linux/macOS:\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"
702 "\n"
703 "Host key verification failed.\n"
704 "\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);
707
708 return false;
709}
void compute_key_fingerprint(const uint8_t key[ED25519_PUBLIC_KEY_SIZE], char fingerprint[CRYPTO_HEX_KEY_SIZE_NULL])
void escape_ascii(const char *str, const char *escape_char, char *out_buffer, size_t out_buffer_size)
Definition util/string.c:17

References compute_key_fingerprint(), escape_ascii(), format_ip_with_port(), get_known_hosts_path(), and safe_snprintf().

Referenced by crypto_handshake_client_key_exchange().

◆ get_known_hosts_path()

const char * get_known_hosts_path ( void  )

Get the path to the known_hosts file.

Returns the path to the known_hosts file, using the same directory as all other ascii-chat configuration files (get_config_dir()).

PATH RESOLUTION:

  • Unix: $XDG_CONFIG_HOME/ascii-chat/known_hosts or ~/.config/ascii-chat/known_hosts
  • Windows: APPDATA%\ascii-chat\known_hosts
Returns
Pointer to cached known_hosts path (do not free), or NULL on failure
Note
The returned pointer is valid for the lifetime of the program

Definition at line 47 of file known_hosts.c.

47 {
48 // Return cached path if already determined
49 if (!g_known_hosts_path_cache) {
50 char *config_dir = get_config_dir();
51 if (!config_dir) {
52 log_error("Failed to determine configuration directory for known_hosts");
53 return NULL;
54 }
55
56 // Build path: config_dir + "known_hosts"
57 size_t config_len = strlen(config_dir);
58 size_t total_len = config_len + strlen("known_hosts") + 1;
59 char *path = SAFE_MALLOC(total_len, char *);
60 if (!path) {
61 SAFE_FREE(config_dir);
62 log_error("Failed to allocate memory for known_hosts path");
63 return NULL;
64 }
65
66 safe_snprintf(path, total_len, "%sknown_hosts", config_dir);
67 SAFE_FREE(config_dir);
68
69 g_known_hosts_path_cache = path;
70 log_debug("KNOWN_HOSTS: Using path %s", g_known_hosts_path_cache);
71 }
72 return g_known_hosts_path_cache;
73}
char * get_config_dir(void)
Definition path.c:493

References get_config_dir(), and safe_snprintf().

Referenced by add_known_host(), check_known_host_no_identity(), crypto_handshake_client_key_exchange(), display_mitm_warning(), prompt_unknown_host(), and remove_known_host().

◆ known_hosts_destroy()

void known_hosts_destroy ( void  )

Definition at line 828 of file known_hosts.c.

828 {
829 if (g_known_hosts_path_cache) {
830 SAFE_FREE(g_known_hosts_path_cache);
831 g_known_hosts_path_cache = NULL;
832 }
833}

Referenced by asciichat_shared_destroy().

◆ prompt_unknown_host()

bool prompt_unknown_host ( const char *  server_ip,
uint16_t  port,
const uint8_t  server_key[32] 
)

Definition at line 584 of file known_hosts.c.

584 {
585 char fingerprint[CRYPTO_HEX_KEY_SIZE_NULL];
586 compute_key_fingerprint(server_key, fingerprint);
587
588 // Format IP:port with proper bracket notation for IPv6
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) {
591 // Fallback to basic format if error
592 safe_snprintf(ip_with_port, sizeof(ip_with_port), "%s:%u", server_ip, port);
593 }
594
595 // Check if we're running interactively (stdin is a terminal and not in snapshot mode)
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.");
599 return true;
600 }
601#ifndef NDEBUG
602 // In debug builds, also skip for Claude Code (LLM automation can't do interactive prompts)
603 const char *env_claudecode = platform_getenv("CLAUDECODE");
604 if (env_claudecode && strlen(env_claudecode) > 0) {
605 log_warn("Skipping known_hosts checking (CLAUDECODE set in debug build).");
606 return true;
607 }
608#endif
610 // SECURITY: Non-interactive mode - REJECT unknown hosts to prevent MITM attacks
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"
614 "\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"
620 "\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",
623 fingerprint, get_known_hosts_path());
624 return false; // REJECT unknown hosts in non-interactive mode
625 }
626
627 // Interactive mode - prompt user
628 // Lock terminal so only this thread can output to terminal
629 // Other threads' logs are buffered until we unlock
630 bool previous_terminal_state = log_lock_terminal();
631
632 log_plain("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
633 "@ WARNING: REMOTE HOST IDENTIFICATION NOT KNOWN! @\n"
634 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
635 "\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);
639
640 // Unlock before prompt (prompt_yes_no handles its own terminal locking)
641 log_unlock_terminal(previous_terminal_state);
642
643 // Prompt user - default is No for security
644 if (platform_prompt_yes_no("Are you sure you want to continue connecting", false)) {
645 log_warn("Warning: Permanently added '%s' to the list of known hosts.", ip_with_port);
646 return true;
647 }
648
649 log_warn("Connection aborted by user.");
650 return false;
651}
bool log_lock_terminal(void)
void log_unlock_terminal(bool previous_state)
bool terminal_can_prompt_user(void)
bool platform_prompt_yes_no(const char *question, bool default_yes)
Definition util.c:81
const char * platform_getenv(const char *name)
Definition wasm/system.c:13

References compute_key_fingerprint(), format_ip_with_port(), get_known_hosts_path(), log_lock_terminal(), log_unlock_terminal(), platform_getenv(), platform_prompt_yes_no(), safe_snprintf(), and terminal_can_prompt_user().

Referenced by crypto_handshake_client_key_exchange().

◆ prompt_unknown_host_no_identity()

bool prompt_unknown_host_no_identity ( const char *  server_ip,
uint16_t  port 
)

Definition at line 766 of file known_hosts.c.

766 {
767 // Format IP:port with proper bracket notation for IPv6
768 char ip_with_port[BUFFER_SIZE_MEDIUM];
769 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
770 // Fallback to basic format if error
771 safe_snprintf(ip_with_port, sizeof(ip_with_port), "%s:%u", server_ip, port);
772 }
773
774 // LOCALHOST OPTIMIZATION: Skip authentication prompt for localhost addresses
775 // Localhost is inherently trusted (same machine, no network exposure)
776 // This matches SSH behavior (ssh-keyscan doesn't prompt for localhost)
777 if (is_localhost_address(server_ip)) {
778 log_debug(
779 "SECURITY: Localhost address %s detected - skipping unknown host prompt (localhost is inherently trusted)",
780 ip_with_port);
781 return true; // Silently trust localhost
782 }
783
784 // Check if running in automated/non-interactive environment
785 // In debug builds, Claude Code automation can't interact with prompts
786 if (is_automated_mode()) {
787 SET_ERRNO(ERROR_CRYPTO, "SECURITY: Cannot verify server without identity key in non-interactive/automated mode");
788 log_error("ERROR: Cannot verify server without identity key in non-interactive/automated mode.\n"
789 "ERROR: This connection is vulnerable to man-in-the-middle attacks!\n"
790 "\n"
791 "To connect to this host:\n"
792 " 1. Run the client interactively (from a terminal with TTY)\n"
793 " 2. Verify you trust this server despite no identity key\n"
794 " 3. Accept the risk when prompted\n"
795 " OR better: Ask server admin to use --key for proper authentication\n"
796 "\n"
797 "Connection aborted for security.\n"
798 "\n");
799 return false;
800 }
801
802 log_warn("\n"
803 "The authenticity of host '%s' can't be established.\n"
804 "The server has no identity key to verify its authenticity.\n"
805 "\n"
806 "WARNING: This connection is vulnerable to man-in-the-middle attacks!\n"
807 "Anyone can intercept your connection and read your data.\n"
808 "\n"
809 "To secure this connection:\n"
810 " 1. Server should use --key to provide an identity key\n"
811 " 2. Client should use --server-key to verify the server\n"
812 "\n",
813 ip_with_port);
814
815 // Interactive mode - prompt user (default is No for security)
816 if (platform_prompt_yes_no("Are you sure you want to continue connecting", false)) {
817 log_warn("Warning: Proceeding with unverified connection.\n"
818 "Your data may be intercepted by attackers!\n"
819 "\n");
820 return true;
821 }
822
823 log_plain("Connection aborted by user.");
824 return false;
825}

References format_ip_with_port(), platform_prompt_yes_no(), and safe_snprintf().

Referenced by crypto_handshake_client_key_exchange().

◆ remove_known_host()

asciichat_error_t remove_known_host ( const char *  server_ip,
uint16_t  port 
)

Definition at line 486 of file known_hosts.c.

486 {
487 // Validate parameters first
488 if (!server_ip) {
489 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameter: server_ip=%p", server_ip);
490 }
491
492 const char *path = get_known_hosts_path();
493 int fd = platform_open(path, PLATFORM_O_RDONLY, FILE_PERM_PRIVATE);
494 if (fd < 0) {
495 // File doesn't exist - nothing to remove, return success
496 return ASCIICHAT_OK;
497 }
498 FILE *f = platform_fdopen(fd, "r");
499 defer(SAFE_FCLOSE(f));
500 if (!f) {
501 platform_close(fd);
502 SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open known hosts file: %s", path);
503 return ERROR_CONFIG;
504 }
505
506 // Format IP:port with proper bracket notation for IPv6
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) {
509
510 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", server_ip);
511 }
512
513 // Read all lines into memory
514 char **lines = NULL;
515 size_t num_lines = 0;
516 defer({
517 if (lines) {
518 for (size_t i = 0; i < num_lines; i++) {
519 SAFE_FREE(lines[i]);
520 }
521 }
522 SAFE_FREE(lines);
523 });
524 char line[BUFFER_SIZE_XLARGE];
525
526 char expected_prefix[BUFFER_SIZE_MEDIUM];
527 safe_snprintf(expected_prefix, sizeof(expected_prefix), "%s ", ip_with_port);
528
529 while (fgets(line, sizeof(line), f)) {
530 // Skip lines that match this IP:port
531 if (strncmp(line, expected_prefix, strlen(expected_prefix)) != 0) {
532 // Keep this line
533 char **new_lines = SAFE_REALLOC((void *)lines, (num_lines + 1) * sizeof(char *), char **);
534 if (new_lines) {
535 lines = new_lines;
536 lines[num_lines] = platform_strdup(line);
537 if (lines[num_lines] == NULL) {
538 return SET_ERRNO(ERROR_MEMORY, "Failed to duplicate line from known_hosts file");
539 }
540 num_lines++;
541 }
542 }
543 }
544 // Close first file before opening for write
545 if (f) {
546 fclose(f);
547 f = NULL; // Prevent double-close by defer
548 }
549
550 // Write back the filtered lines
551 fd = platform_open(path, PLATFORM_O_WRONLY | PLATFORM_O_CREAT | PLATFORM_O_TRUNC, FILE_PERM_PRIVATE);
552 f = platform_fdopen(fd, "w");
553 if (!f) {
554 // Cleanup on error - fdopen failed, so fd is still open but f is NULL
555 // Individual line strings will be freed by defer cleanup
556 platform_close(fd); // Close fd directly since fdopen failed
557 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open known hosts file: %s", path);
558 }
559
560 for (size_t i = 0; i < num_lines; i++) {
561 (void)fputs(lines[i], f);
562 }
563 // All cleanup (lines, individual strings, file handle) handled by defer statements
564
565 log_debug("KNOWN_HOSTS: Successfully removed host from known_hosts file: %s", path);
566 return ASCIICHAT_OK;
567}
char * platform_strdup(const char *s)

References format_ip_with_port(), get_known_hosts_path(), platform_close(), platform_open(), platform_strdup(), and safe_snprintf().