Client: Process server's public key and send our public key.
Client processes server's KEY_EXCHANGE_INIT packet and responds with KEY_EXCHANGE_RESP. Supports both simple and authenticated formats. Verifies server signature if present.
29 {
32 }
33
34
37 size_t payload_len = 0;
38 int result =
receive_packet(client_socket, &packet_type, (
void **)&payload, &payload_len);
41 }
42
43
45 if (payload) {
47 }
49 }
50
51 log_debug(
"CLIENT_KEY_EXCHANGE: Received packet with payload_len=%zu, kex_size=%u, auth_size=%u, sig_size=%u",
52 payload_len, ctx->crypto_ctx.public_key_size, ctx->crypto_ctx.auth_public_key_size,
53 ctx->crypto_ctx.signature_size);
54
55
56
57 size_t expected_auth_size =
58 ctx->crypto_ctx.public_key_size + ctx->crypto_ctx.auth_public_key_size + ctx->crypto_ctx.signature_size;
59
61
62 size_t key_size = sizeof(ctx->crypto_ctx.public_key);
64 if (!server_ephemeral_key) {
66 }
71
72 if (!server_identity_key || !server_signature) {
74 if (server_identity_key)
76 if (server_signature)
79 }
80
81
85 if (payload) {
87 }
91 return validation_result;
92 }
93
94
95
96 if (ctx->crypto_ctx.auth_public_key_size > 0 && payload_len == expected_auth_size) {
97
98
99 log_info(
"Received authenticated KEY_EXCHANGE_INIT (%zu bytes)", expected_auth_size);
100 memcpy(server_ephemeral_key, payload, ctx->crypto_ctx.public_key_size);
101 memcpy(server_identity_key, payload + ctx->crypto_ctx.public_key_size, ctx->crypto_ctx.auth_public_key_size);
102 memcpy(server_signature, payload + ctx->crypto_ctx.public_key_size + ctx->crypto_ctx.auth_public_key_size,
103 ctx->crypto_ctx.signature_size);
104
105
106 ctx->server_uses_client_auth = true;
107
108
111 safe_snprintf(hex_id + i * 2, 3,
"%02x", server_identity_key[i]);
112 }
114 log_debug(
"Received identity key: %s", hex_id);
115
116
119 safe_snprintf(hex_eph + i * 2, 3,
"%02x", server_ephemeral_key[i]);
120 }
122 log_debug(
"Received ephemeral key: %s", hex_eph);
123
126 safe_snprintf(hex_sig + i * 2, 3,
"%02x", server_signature[i]);
127 }
129 log_debug(
"Received signature: %s", hex_sig);
130
131
132 log_debug(
"Verifying server's signature over ephemeral key (already logged above)");
133
134
135
136 if (!ctx->verify_server_key) {
137 log_info(
"Skipping server signature verification (no --server-key specified)");
138 log_warn(
"Connection is encrypted but server identity is NOT verified (vulnerable to MITM)");
139 } else {
140
141 const char *gpg_key_id = NULL;
142 if (ctx->expected_server_key[0] != '\0' && strncmp(ctx->expected_server_key, "gpg:", 4) == 0) {
143 const char *key_id_start = ctx->expected_server_key + 4;
144 size_t key_id_len = strlen(key_id_start);
145
146 if (key_id_len == 8 || key_id_len == 16 || key_id_len == 40) {
147 gpg_key_id = key_id_start;
148 log_debug(
"Using GPG key ID from --server-key for verification: %s", gpg_key_id);
149 }
150 }
151
153 server_signature, gpg_key_id) != 0) {
154 if (payload) {
156 }
161 "This indicates: Server's identity key does not "
162 "match its ephemeral key, Potential man-in-the-middle attack, "
163 "Corrupted or malicious server");
164 }
165 log_info(
"Server signature verified successfully");
166 }
167
168
169 if (ctx->verify_server_key && strlen(ctx->expected_server_key) > 0) {
170
172 size_t num_expected_keys = 0;
174 num_expected_keys == 0) {
175 if (payload) {
177 }
182 "Failed to parse expected server key: %s. Check that "
183 "--server-key value is valid (ssh-ed25519 "
184 "format, github:username, or hex)",
185 ctx->expected_server_key);
186 }
187
188
189
190 bool key_matched = false;
191 for (size_t i = 0; i < num_expected_keys; i++) {
193 key_matched = true;
194 log_info(
"Server identity key matched expected key %zu/%zu", i + 1, num_expected_keys);
195 break;
196 }
197 }
198
199 if (!key_matched) {
200 if (payload) {
202 }
207 "Server identity key mismatch - potential MITM attack! "
208 "Expected key(s) from: %s (checked %zu keys), Server presented a different key "
209 "than specified with --server-key, DO NOT CONNECT to this "
210 "server - likely man-in-the-middle attack!",
211 ctx->expected_server_key, num_expected_keys);
212 }
213 log_info(
"Server identity key verified against --server-key (%zu key(s) checked)", num_expected_keys);
214 }
215
216
217 if (ctx->server_ip[0] == '\0') {
218
219 struct sockaddr_storage server_addr;
220 socklen_t addr_len = sizeof(server_addr);
221 if (getpeername(client_socket, (struct sockaddr *)&server_addr, &addr_len) == 0) {
222 char ip_str[INET6_ADDRSTRLEN];
223 if (
format_ip_address(server_addr.ss_family, (
struct sockaddr *)&server_addr, ip_str,
sizeof(ip_str)) ==
225 SAFE_STRNCPY(ctx->server_ip, ip_str,
sizeof(ctx->server_ip) - 1);
226 if (server_addr.ss_family == AF_INET) {
227 struct sockaddr_in *addr_in = (struct sockaddr_in *)&server_addr;
229 } else if (server_addr.ss_family == AF_INET6) {
230 struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *)&server_addr;
232 }
233 }
234 } else {
235 log_warn(
"Failed to get server address from socket");
236 }
237 } else {
238 log_warn(
"Server IP already set: %s", ctx->server_ip);
239 }
240
241
242
243 bool skip_known_hosts = false;
244 const char *env_skip =
platform_getenv(
"ASCII_CHAT_INSECURE_NO_HOST_IDENTITY_CHECK");
245 if (env_skip && strcmp(env_skip,
STR_ONE) == 0) {
247 "Skipping known_hosts checking for authenticated connection (ASCII_CHAT_INSECURE_NO_HOST_IDENTITY_CHECK=1)");
248 skip_known_hosts = true;
249 }
250#ifndef NDEBUG
251
253 log_warn(
"Skipping known_hosts checking (CLAUDECODE set in debug build)");
254 skip_known_hosts = true;
255 }
256#endif
257
258 if (!skip_known_hosts && ctx->server_ip[0] != '\0' && ctx->server_port > 0) {
261
262 log_error(
"SECURITY: Server key does NOT match known_hosts entry!\n"
263 "This indicates a possible man-in-the-middle attack!");
265
267
268 if (payload) {
270 }
275 "SECURITY: Connection aborted - server key mismatch (possible MITM attack)");
276 }
277
278 log_warn(
"SECURITY WARNING: User accepted MITM risk - continuing with connection");
280
282
283 if (payload) {
285 }
290 }
291
292
294 if (payload) {
296 }
301 "CRITICAL SECURITY ERROR: Failed to create known_hosts "
302 "file! This is a security vulnerability - the "
303 "program cannot track known hosts. Please check file "
304 "permissions and ensure the program can write to: %s",
306 }
307 log_info(
"Server host added to known_hosts successfully");
308 } else if (known_host_result == 1) {
309
310 log_info(
"Server host key verified from known_hosts - connection secure");
311 } else {
312
313 if (payload) {
315 }
319 return SET_ERRNO(known_host_result,
"SECURITY: known_hosts verification failed with error code %d",
320 known_host_result);
321 }
322 }
323 } else if (payload_len == ctx->crypto_ctx.public_key_size) {
324
325 log_info(
"Received simple KEY_EXCHANGE_INIT (%zu bytes) - server has no "
326 "identity key",
327 payload_len);
328 memcpy(server_ephemeral_key, payload, ctx->crypto_ctx.public_key_size);
329
330
331 memset(server_identity_key, 0, ctx->crypto_ctx.auth_public_key_size);
332 memset(server_signature, 0, ctx->crypto_ctx.signature_size);
333
334
335 ctx->server_uses_client_auth = false;
336
337 log_debug(
"Received ephemeral key (simple format)");
338
339
340
341
342
343
344 if (ctx->server_ip[0] == '\0' || ctx->server_port <= 0) {
349 }
350
351
352 bool skip_known_hosts = false;
354 const char *env_skip_known_hosts_checking =
platform_getenv(
"ASCII_CHAT_INSECURE_NO_HOST_IDENTITY_CHECK");
355 if (env_skip_known_hosts_checking && strcmp(env_skip_known_hosts_checking,
STR_ONE) == 0) {
356 log_warn(
"Skipping known_hosts checking. This is a security vulnerability.");
357 skip_known_hosts = true;
358 }
359#ifndef NDEBUG
360
362 log_warn(
"Skipping known_hosts checking (CLAUDECODE set in debug build).");
363 skip_known_hosts = true;
364 }
365#endif
366 else {
368 }
369
370 if (skip_known_hosts || known_host_result == 1) {
371
372 log_info(
"SECURITY: Server IP %s:%u is known (no-identity entry found) - connection verified", ctx->server_ip,
373 ctx->server_port);
375
376 log_warn(
"SECURITY: Unknown server IP %s:%u with no identity key\n"
377 "This connection is vulnerable to man-in-the-middle attacks\n"
378 "Anyone can intercept your connection and read your data",
379 ctx->server_ip, ctx->server_port);
380
382 if (payload) {
384 }
389 }
390
391
392
395 if (payload) {
397 }
402 "CRITICAL SECURITY ERROR: Failed to create known_hosts "
403 "file! This is a security vulnerability - the "
404 "program cannot track known hosts. Please check file "
405 "permissions and ensure the program can write to: %s",
407 }
408 log_info(
"Server host added to known_hosts successfully");
410
411 log_warn(
"SECURITY: Server previously had identity key but now has none - potential security issue");
412 if (payload) {
414 }
419 } else {
420
421 if (payload) {
423 }
428 }
429 } else {
430 if (payload) {
432 }
437 "Invalid KEY_EXCHANGE_INIT size: %zu bytes (expected %zu or "
438 "%zu). This indicates: Protocol violation "
439 "or incompatible server version, Potential man-in-the-middle "
440 "attack, Network corruption",
441 payload_len, expected_auth_size, ctx->crypto_ctx.public_key_size);
442
443 }
444
445
447 if (payload) {
449 }
456 }
457
458
459 bool client_has_identity_key = (ctx->client_private_key.type ==
KEY_TYPE_ED25519);
460
461
462
463
464
465 bool server_has_identity = (ctx->crypto_ctx.auth_public_key_size > 0 && ctx->crypto_ctx.signature_size > 0);
466 bool server_requires_auth = server_has_identity || ctx->require_client_auth;
467
468 if (server_requires_auth) {
469
470
471
474 size_t response_size = ctx->crypto_ctx.public_key_size + ed25519_pubkey_size + ed25519_sig_size;
475
476
478 if (ctx->client_gpg_key_id[0] != '\0') {
479 gpg_key_id_len = (
uint8_t)strlen(ctx->client_gpg_key_id);
480 if (gpg_key_id_len > 40) {
481 gpg_key_id_len = 40;
482 }
483 response_size += 1 + gpg_key_id_len;
484 } else {
485 response_size += 1;
486 }
487
489 size_t offset = 0;
490
491
492 memcpy(key_response + offset, ctx->crypto_ctx.public_key,
493 ctx->crypto_ctx.public_key_size);
494 offset += ctx->crypto_ctx.public_key_size;
495
496 if (client_has_identity_key) {
497
498 memcpy(key_response + offset, ctx->client_private_key.public_key, ed25519_pubkey_size);
499 offset += ed25519_pubkey_size;
500
501
502 if (
ed25519_sign_message(&ctx->client_private_key, ctx->crypto_ctx.public_key, ctx->crypto_ctx.public_key_size,
503 key_response + offset) != 0) {
509 }
510 offset += ed25519_sig_size;
511 } else {
512
513 memset(key_response + offset, 0, ed25519_pubkey_size);
514 offset += ed25519_pubkey_size;
515 memset(key_response + offset, 0, ed25519_sig_size);
516 offset += ed25519_sig_size;
517 }
518
519
520 key_response[offset] = gpg_key_id_len;
521 offset += 1;
522
523
524 if (gpg_key_id_len > 0) {
525 memcpy(key_response + offset, ctx->client_gpg_key_id, gpg_key_id_len);
526 offset += gpg_key_id_len;
527 log_debug(
"Including client GPG key ID in KEY_EXCHANGE_RESPONSE: %.*s", gpg_key_id_len, ctx->client_gpg_key_id);
528 }
529
531 if (result != 0) {
537 }
538
539
540 sodium_memzero(key_response, response_size);
542 } else {
543
544
546 ctx->crypto_ctx.public_key_size);
547 if (result != 0) {
552 }
553 }
554
556
557
561
563}
#define NET_TO_HOST_U16(val)
#define SAFE_STRNCPY(dst, src, size)
#define SAFE_MALLOC(size, cast)
bool prompt_unknown_host(const char *server_ip, uint16_t port, const uint8_t server_key[32])
Interactive prompt for unknown host - returns true if user wants to add, false to abort.
const char * get_known_hosts_path(void)
Get the path to the known_hosts file.
crypto_result_t crypto_set_peer_public_key(crypto_context_t *ctx, const uint8_t *peer_public_key)
Set peer's public key and compute shared secret (step 2 of handshake)
#define ED25519_SIGNATURE_SIZE
Ed25519 signature size in bytes.
bool prompt_unknown_host_no_identity(const char *server_ip, uint16_t port)
Interactive prompt for unknown host without identity key - returns true if user wants to continue,...
#define ED25519_PUBLIC_KEY_SIZE
Ed25519 public key size in bytes.
asciichat_error_t check_known_host_no_identity(const char *server_ip, uint16_t port)
Check known_hosts for servers without identity key (no-identity entries)
#define HEX_STRING_SIZE_32
Hex string size for 32-byte values (64 hex chars + null terminator)
asciichat_error_t add_known_host(const char *server_ip, uint16_t port, const uint8_t server_key[32])
Add server to known_hosts.
#define ZERO_KEY_SIZE
Zero key array size (32 bytes, used for no-identity entries)
bool display_mitm_warning(const char *server_ip, uint16_t port, const uint8_t expected_key[32], const uint8_t received_key[32])
Display MITM warning with key comparison and prompt user for confirmation.
asciichat_error_t check_known_host(const char *server_ip, uint16_t port, const uint8_t server_key[32])
Check if server key is in known_hosts.
#define HEX_STRING_SIZE_64
Hex string size for 64-byte values (128 hex chars + null terminator)
@ ERROR_CRYPTO_VERIFICATION
asciichat_error_t ed25519_verify_signature(const uint8_t public_key[32], const uint8_t *message, size_t message_len, const uint8_t signature[64], const char *gpg_key_id)
Verify an Ed25519 signature.
asciichat_error_t parse_public_keys(const char *input, public_key_t *keys_out, size_t *num_keys, size_t max_keys)
Parse all SSH/GPG public keys from any format (returns all keys)
asciichat_error_t ed25519_sign_message(const private_key_t *key, const uint8_t *message, size_t message_len, uint8_t signature[64])
Sign a message with Ed25519 (uses SSH agent if available, otherwise in-memory key)
#define MAX_CLIENTS
Maximum possible clients (static array size) - actual runtime limit set by –max-clients (1-32)
#define log_warn(...)
Log a WARN message.
#define log_error(...)
Log an ERROR message.
int send_packet(socket_t sockfd, packet_type_t type, const void *data, size_t len)
Send a basic packet without encryption.
@ PACKET_TYPE_CRYPTO_KEY_EXCHANGE_INIT
Server -> Client: {server_pubkey[32]} (UNENCRYPTED)
@ PACKET_TYPE_CRYPTO_KEY_EXCHANGE_RESP
Client -> Server: {client_pubkey[32]} (UNENCRYPTED)
#define STR_ONE
String literal: "1" (one)
asciichat_error_t format_ip_address(int family, const struct sockaddr *addr, char *output, size_t output_size)
Format IP address from socket address structure.