C · 30826 bytes Raw Blame History
1 /* SSH key and agent management with comprehensive isolation and security
2 * Implements per-account SSH agents to prevent key leakage between accounts
3 */
4
5 #define _POSIX_C_SOURCE 200809L
6 #define _DEFAULT_SOURCE
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <string.h>
10 #include <unistd.h>
11 #include <sys/wait.h>
12 #include <sys/stat.h>
13 #include <signal.h>
14 #include <errno.h>
15 #include <fcntl.h>
16
17 #include "ssh_manager.h"
18 #include "error.h"
19 #include "utils.h"
20 #include "display.h"
21
22 /* Internal helper functions */
23 static int execute_ssh_command(const char *command, char *output, size_t output_size);
24 static int setup_ssh_environment(ssh_config_t *ssh_config);
25 static int create_isolated_agent_socket_dir(char *socket_dir, size_t socket_dir_size);
26 static bool is_ssh_agent_running(pid_t pid);
27 static int kill_ssh_agent_gracefully(pid_t pid);
28 static int validate_ssh_agent_socket(const char *socket_path);
29 static int parse_ssh_agent_output(const char *output, ssh_config_t *ssh_config);
30 static void kill_orphaned_gitswitch_agents(void);
31
32 /* Global SSH configuration for cleanup */
33 static ssh_config_t *g_active_ssh_config = NULL;
34
35 /* Initialize SSH manager */
36 int ssh_manager_init(ssh_config_t *ssh_config, ssh_agent_mode_t mode) {
37 if (!ssh_config) {
38 set_error(ERR_INVALID_ARGS, "NULL ssh_config to ssh_manager_init");
39 return -1;
40 }
41
42 log_debug("Initializing SSH manager with mode: %d", mode);
43
44 /* Initialize structure */
45 memset(ssh_config, 0, sizeof(ssh_config_t));
46 ssh_config->mode = mode;
47 ssh_config->agent_pid = -1;
48 ssh_config->agent_owned = false;
49
50 /* Validate SSH is available */
51 if (!command_exists("ssh")) {
52 set_error(ERR_SSH_NOT_FOUND, "SSH command not found in PATH");
53 return -1;
54 }
55
56 if (!command_exists("ssh-agent")) {
57 set_error(ERR_SSH_AGENT_NOT_FOUND, "ssh-agent command not found in PATH");
58 return -1;
59 }
60
61 if (!command_exists("ssh-add")) {
62 set_error(ERR_SSH_AGENT_NOT_FOUND, "ssh-add command not found in PATH");
63 return -1;
64 }
65
66 /* Set up based on mode */
67 switch (mode) {
68 case SSH_AGENT_SYSTEM:
69 /* Use existing SSH_AUTH_SOCK */
70 if (getenv("SSH_AUTH_SOCK")) {
71 safe_strncpy(ssh_config->agent_socket_path, getenv("SSH_AUTH_SOCK"),
72 sizeof(ssh_config->agent_socket_path));
73 log_info("Using system SSH agent at: %s", ssh_config->agent_socket_path);
74 } else {
75 log_warning("No system SSH agent found (SSH_AUTH_SOCK not set)");
76 }
77 break;
78
79 case SSH_AGENT_ISOLATED:
80 /* Will create isolated agents on demand */
81 log_info("Initialized for isolated SSH agent mode");
82 break;
83
84 case SSH_AGENT_NONE:
85 log_info("SSH agent management disabled");
86 break;
87
88 default:
89 set_error(ERR_INVALID_ARGS, "Invalid SSH agent mode: %d", mode);
90 return -1;
91 }
92
93 /* Register for cleanup */
94 g_active_ssh_config = ssh_config;
95
96 log_info("SSH manager initialized successfully");
97 return 0;
98 }
99
100 /* Cleanup SSH manager */
101 void ssh_manager_cleanup(ssh_config_t *ssh_config) {
102 if (!ssh_config) {
103 return;
104 }
105
106 log_debug("Cleaning up SSH manager");
107
108 /* Stop agent if we own it */
109 if (ssh_config->agent_owned && ssh_config->agent_pid > 0) {
110 log_info("Stopping owned SSH agent (PID: %d)", ssh_config->agent_pid);
111 ssh_stop_agent(ssh_config);
112 }
113
114 /* Clear global reference */
115 if (g_active_ssh_config == ssh_config) {
116 g_active_ssh_config = NULL;
117 }
118
119 /* Clear sensitive data */
120 secure_zero_memory(ssh_config, sizeof(ssh_config_t));
121
122 log_debug("SSH manager cleanup complete");
123 }
124
125 /* Switch to account's SSH configuration */
126 int ssh_switch_account(ssh_config_t *ssh_config, const account_t *account) {
127 char expanded_key_path[MAX_PATH_LEN];
128
129 if (!ssh_config || !account) {
130 set_error(ERR_INVALID_ARGS, "Invalid arguments to ssh_switch_account");
131 return -1;
132 }
133
134 /* Skip if SSH not enabled for this account */
135 if (!account->ssh_enabled || strlen(account->ssh_key_path) == 0) {
136 log_debug("SSH not enabled for account: %s", account->name);
137 return 0;
138 }
139
140 log_info("Switching SSH configuration for account: %s", account->name);
141
142 /* Validate and expand key path */
143 if (expand_path(account->ssh_key_path, expanded_key_path, sizeof(expanded_key_path)) != 0) {
144 set_error(ERR_INVALID_PATH, "Failed to expand SSH key path: %s", account->ssh_key_path);
145 return -1;
146 }
147
148 /* Validate key file */
149 if (ssh_validate_key_file(expanded_key_path) != 0) {
150 return -1; /* Error already set */
151 }
152
153 /* Handle based on mode */
154 switch (ssh_config->mode) {
155 case SSH_AGENT_SYSTEM:
156 /* Clear existing keys and add new one to system agent */
157 if (strlen(ssh_config->agent_socket_path) > 0) {
158 log_debug("Clearing system SSH agent keys");
159 ssh_clear_agent_keys(ssh_config);
160
161 log_debug("Adding key to system SSH agent: %s", expanded_key_path);
162 if (ssh_add_key(ssh_config, expanded_key_path) != 0) {
163 set_error(ERR_SSH_KEY_LOAD_FAILED, "Failed to load key into system SSH agent");
164 return -1;
165 }
166 } else {
167 log_warning("No system SSH agent available");
168 }
169 break;
170
171 case SSH_AGENT_ISOLATED:
172 /* Start isolated agent for this account */
173 if (ssh_start_isolated_agent(ssh_config, account) != 0) {
174 return -1; /* Error already set */
175 }
176
177 /* Add key to isolated agent */
178 if (ssh_add_key(ssh_config, expanded_key_path) != 0) {
179 set_error(ERR_SSH_KEY_LOAD_FAILED, "Failed to load key into isolated SSH agent");
180 return -1;
181 }
182 break;
183
184 case SSH_AGENT_NONE:
185 /* No agent management - just validate key */
186 log_info("SSH agent management disabled - key validated but not loaded");
187 break;
188
189 default:
190 set_error(ERR_INVALID_ARGS, "Invalid SSH agent mode");
191 return -1;
192 }
193
194 /* Configure host alias if specified */
195 if (strlen(account->ssh_host_alias) > 0) {
196 if (ssh_configure_host_alias(account) != 0) {
197 log_warning("Failed to configure SSH host alias: %s", account->ssh_host_alias);
198 /* Don't fail completely for host alias issues */
199 }
200 }
201
202 log_info("SSH configuration switched successfully for account: %s", account->name);
203 return 0;
204 }
205
206 /* Start isolated SSH agent */
207 int ssh_start_isolated_agent(ssh_config_t *ssh_config, const account_t *account) {
208 char command[512];
209 char output[1024];
210 char socket_dir[MAX_PATH_LEN];
211 char socket_path[MAX_PATH_LEN];
212
213 if (!ssh_config || !account) {
214 set_error(ERR_INVALID_ARGS, "Invalid arguments to ssh_start_isolated_agent");
215 return -1;
216 }
217
218 log_info("Starting isolated SSH agent for account: %s", account->name);
219
220 /* Kill any orphaned gitswitch agents from previous runs */
221 kill_orphaned_gitswitch_agents();
222
223 /* Stop any existing agent we own */
224 if (ssh_config->agent_owned && ssh_config->agent_pid > 0) {
225 log_debug("Stopping existing SSH agent");
226 ssh_stop_agent(ssh_config);
227 }
228
229 /* Create secure socket directory */
230 if (create_isolated_agent_socket_dir(socket_dir, sizeof(socket_dir)) != 0) {
231 return -1;
232 }
233
234 /* Build socket path and check for stale sockets */
235 if ((size_t)snprintf(socket_path, sizeof(socket_path),
236 "%s/ssh-agent.%s.sock",
237 socket_dir, account->name) >= sizeof(socket_path)) {
238 set_error(ERR_INVALID_ARGS, "SSH socket path too long");
239 return -1;
240 }
241
242 /* Remove stale socket if it exists */
243 if (path_exists(socket_path)) {
244 log_debug("Removing stale SSH agent socket: %s", socket_path);
245 if (unlink(socket_path) != 0) {
246 set_system_error(ERR_FILE_IO, "Failed to remove stale SSH socket");
247 return -1;
248 }
249 }
250
251 /* Build ssh-agent command with socket path */
252 if ((size_t)snprintf(command, sizeof(command),
253 "ssh-agent -a '%s'", socket_path) >= sizeof(command)) {
254 set_error(ERR_INVALID_ARGS, "SSH agent command too long");
255 return -1;
256 }
257
258 log_debug("Starting SSH agent: %s", command);
259
260 /* Execute ssh-agent */
261 if (execute_ssh_command(command, output, sizeof(output)) != 0) {
262 set_error(ERR_SSH_AGENT_START_FAILED, "Failed to start SSH agent");
263 return -1;
264 }
265
266 /* Parse ssh-agent output to get socket and PID */
267 if (parse_ssh_agent_output(output, ssh_config) != 0) {
268 set_error(ERR_SSH_AGENT_START_FAILED, "Failed to parse ssh-agent output");
269 return -1;
270 }
271
272 /* Validate the agent is working */
273 if (validate_ssh_agent_socket(ssh_config->agent_socket_path) != 0) {
274 set_error(ERR_SSH_AGENT_START_FAILED, "SSH agent socket validation failed");
275 return -1;
276 }
277
278 /* Mark as owned */
279 ssh_config->agent_owned = true;
280
281 /* Set up environment */
282 if (setup_ssh_environment(ssh_config) != 0) {
283 set_error(ERR_SSH_AGENT_START_FAILED, "Failed to set up SSH environment");
284 return -1;
285 }
286
287 log_info("Isolated SSH agent started successfully (PID: %d, Socket: %s)",
288 ssh_config->agent_pid, ssh_config->agent_socket_path);
289
290 /* Create/update symlink for easy shell integration */
291 char symlink_path[MAX_PATH_LEN];
292 if ((size_t)snprintf(symlink_path, sizeof(symlink_path),
293 "%s/current.sock", socket_dir) < sizeof(symlink_path)) {
294 /* Remove old symlink if it exists */
295 unlink(symlink_path);
296 /* Create new symlink to current socket */
297 if (symlink(socket_path, symlink_path) == 0) {
298 log_debug("Created symlink: %s -> %s", symlink_path, socket_path);
299 } else {
300 log_warning("Failed to create socket symlink: %s", strerror(errno));
301 }
302 }
303
304 return 0;
305 }
306
307 /* Stop SSH agent */
308 int ssh_stop_agent(ssh_config_t *ssh_config) {
309 if (!ssh_config || ssh_config->agent_pid <= 0) {
310 return 0; /* Nothing to stop */
311 }
312
313 if (!ssh_config->agent_owned) {
314 log_debug("Not stopping SSH agent - we don't own it");
315 return 0;
316 }
317
318 log_info("Stopping SSH agent (PID: %d)", ssh_config->agent_pid);
319
320 /* Try graceful shutdown first */
321 if (kill_ssh_agent_gracefully(ssh_config->agent_pid) == 0) {
322 log_debug("SSH agent stopped gracefully");
323 } else {
324 log_warning("Failed to stop SSH agent gracefully");
325 }
326
327 /* Clean up socket file */
328 if (strlen(ssh_config->agent_socket_path) > 0) {
329 if (unlink(ssh_config->agent_socket_path) == 0) {
330 log_debug("Removed SSH agent socket: %s", ssh_config->agent_socket_path);
331 } else {
332 log_debug("Could not remove SSH agent socket (may already be gone)");
333 }
334 }
335
336 /* Reset state */
337 ssh_config->agent_pid = -1;
338 ssh_config->agent_owned = false;
339 ssh_config->agent_socket_path[0] = '\0';
340
341 /* Clear environment */
342 unsetenv("SSH_AUTH_SOCK");
343 unsetenv("SSH_AGENT_PID");
344
345 return 0;
346 }
347
348 /* Clear all keys from SSH agent */
349 int ssh_clear_agent_keys(ssh_config_t *ssh_config) {
350 char output[512];
351
352 if (!ssh_config || strlen(ssh_config->agent_socket_path) == 0) {
353 log_debug("No SSH agent available to clear keys");
354 return 0;
355 }
356
357 log_debug("Clearing all keys from SSH agent");
358
359 /* Set up environment for ssh-add */
360 if (setup_ssh_environment(ssh_config) != 0) {
361 return -1;
362 }
363
364 /* Execute ssh-add -D to delete all keys */
365 if (execute_ssh_command("ssh-add -D", output, sizeof(output)) != 0) {
366 log_warning("Failed to clear SSH agent keys (agent may be empty)");
367 /* This is not necessarily an error - agent might be empty */
368 } else {
369 log_debug("SSH agent keys cleared successfully");
370 }
371
372 return 0;
373 }
374
375 /* Add key to SSH agent */
376 int ssh_add_key(ssh_config_t *ssh_config, const char *key_path) {
377 char command[MAX_PATH_LEN + 32];
378 char output[512];
379
380 if (!ssh_config || !key_path) {
381 set_error(ERR_INVALID_ARGS, "Invalid arguments to ssh_add_key");
382 return -1;
383 }
384
385 if (strlen(ssh_config->agent_socket_path) == 0) {
386 set_error(ERR_SSH_AGENT_NOT_FOUND, "No SSH agent available");
387 return -1;
388 }
389
390 log_debug("Adding SSH key to agent: %s", key_path);
391
392 /* Set up environment */
393 if (setup_ssh_environment(ssh_config) != 0) {
394 return -1;
395 }
396
397 /* Build ssh-add command */
398 if ((size_t)snprintf(command, sizeof(command), "ssh-add '%s'", key_path) >= sizeof(command)) {
399 set_error(ERR_INVALID_ARGS, "SSH add command too long");
400 return -1;
401 }
402
403 /* Execute ssh-add */
404 if (execute_ssh_command(command, output, sizeof(output)) != 0) {
405 set_error(ERR_SSH_KEY_LOAD_FAILED, "Failed to add SSH key: %s", output);
406 return -1;
407 }
408
409 log_info("SSH key added successfully: %s", key_path);
410 return 0;
411 }
412
413 /* List loaded SSH keys */
414 int ssh_list_keys(ssh_config_t *ssh_config, char *output, size_t output_size) {
415 if (!ssh_config || !output || output_size == 0) {
416 set_error(ERR_INVALID_ARGS, "Invalid arguments to ssh_list_keys");
417 return -1;
418 }
419
420 if (strlen(ssh_config->agent_socket_path) == 0) {
421 safe_strncpy(output, "No SSH agent available", output_size);
422 return -1;
423 }
424
425 /* Set up environment */
426 if (setup_ssh_environment(ssh_config) != 0) {
427 return -1;
428 }
429
430 /* Execute ssh-add -l */
431 if (execute_ssh_command("ssh-add -l", output, output_size) != 0) {
432 safe_strncpy(output, "No keys loaded in SSH agent", output_size);
433 return -1;
434 }
435
436 return 0;
437 }
438
439 /* Validate SSH key file */
440 int ssh_validate_key_file(const char *key_path) {
441 struct stat key_stat;
442 mode_t key_mode;
443
444 if (!key_path) {
445 set_error(ERR_INVALID_ARGS, "NULL key_path to ssh_validate_key_file");
446 return -1;
447 }
448
449 /* Check if file exists */
450 if (stat(key_path, &key_stat) != 0) {
451 set_system_error(ERR_SSH_KEY_NOT_FOUND, "SSH key file not found: %s", key_path);
452 return -1;
453 }
454
455 /* Check if it's a regular file */
456 if (!S_ISREG(key_stat.st_mode)) {
457 set_error(ERR_SSH_KEY_INVALID, "SSH key path is not a regular file: %s", key_path);
458 return -1;
459 }
460
461 /* Check permissions - should be 600 (readable only by owner) */
462 key_mode = key_stat.st_mode & 0777;
463 if (key_mode != 0600) {
464 set_error(ERR_SSH_KEY_PERMISSIONS,
465 "SSH key file has unsafe permissions: %o (should be 600): %s",
466 key_mode, key_path);
467 return -1;
468 }
469
470 /* Check ownership - should be owned by current user */
471 if (key_stat.st_uid != getuid()) {
472 set_error(ERR_SSH_KEY_OWNERSHIP, "SSH key file not owned by current user: %s", key_path);
473 return -1;
474 }
475
476 /* Basic content validation - check it looks like a private key */
477 FILE *key_file = fopen(key_path, "r");
478 if (!key_file) {
479 set_system_error(ERR_SSH_KEY_INVALID, "Cannot read SSH key file: %s", key_path);
480 return -1;
481 }
482
483 char first_line[256];
484 bool valid_key = false;
485
486 if (fgets(first_line, sizeof(first_line), key_file)) {
487 /* Check for common private key headers */
488 if (strstr(first_line, "-----BEGIN") &&
489 (strstr(first_line, "PRIVATE KEY") ||
490 strstr(first_line, "RSA PRIVATE KEY") ||
491 strstr(first_line, "OPENSSH PRIVATE KEY") ||
492 strstr(first_line, "EC PRIVATE KEY"))) {
493 valid_key = true;
494 }
495 }
496
497 fclose(key_file);
498
499 if (!valid_key) {
500 set_error(ERR_SSH_KEY_INVALID, "File does not appear to be a valid SSH private key: %s", key_path);
501 return -1;
502 }
503
504 log_debug("SSH key validation passed: %s", key_path);
505 return 0;
506 }
507
508 /* Configure SSH host alias */
509 int ssh_configure_host_alias(const account_t *account) {
510 char ssh_config_path[MAX_PATH_LEN];
511 char ssh_config_dir[MAX_PATH_LEN];
512 FILE *ssh_config_file;
513 char expanded_key_path[MAX_PATH_LEN];
514
515 if (!account || strlen(account->ssh_host_alias) == 0) {
516 return 0; /* Nothing to configure */
517 }
518
519 log_debug("Configuring SSH host alias: %s", account->ssh_host_alias);
520
521 /* Get SSH config directory */
522 if ((size_t)snprintf(ssh_config_dir, sizeof(ssh_config_dir), "%s/.ssh", getenv("HOME")) >= sizeof(ssh_config_dir)) {
523 set_error(ERR_INVALID_PATH, "SSH config directory path too long");
524 return -1;
525 }
526
527 /* Create .ssh directory if it doesn't exist */
528 if (!path_exists(ssh_config_dir)) {
529 if (create_directory_recursive(ssh_config_dir, 0700) != 0) {
530 return -1;
531 }
532 }
533
534 /* SSH config file path */
535 if ((size_t)snprintf(ssh_config_path, sizeof(ssh_config_path), "%s/config", ssh_config_dir) >= sizeof(ssh_config_path)) {
536 set_error(ERR_INVALID_PATH, "SSH config file path too long");
537 return -1;
538 }
539
540 /* Expand key path */
541 if (expand_path(account->ssh_key_path, expanded_key_path, sizeof(expanded_key_path)) != 0) {
542 return -1;
543 }
544
545 /* Append to SSH config file */
546 ssh_config_file = fopen(ssh_config_path, "a");
547 if (!ssh_config_file) {
548 set_system_error(ERR_FILE_IO, "Failed to open SSH config file: %s", ssh_config_path);
549 return -1;
550 }
551
552 /* Write host configuration */
553 fprintf(ssh_config_file, "\n# gitswitch-c configuration for account: %s\n", account->name);
554 fprintf(ssh_config_file, "Host %s\n", account->ssh_host_alias);
555 fprintf(ssh_config_file, " IdentityFile %s\n", expanded_key_path);
556 fprintf(ssh_config_file, " IdentitiesOnly yes\n");
557 fprintf(ssh_config_file, " StrictHostKeyChecking no\n");
558 fprintf(ssh_config_file, " UserKnownHostsFile /dev/null\n");
559
560 fclose(ssh_config_file);
561
562 /* Set proper permissions on SSH config file */
563 if (chmod(ssh_config_path, 0600) != 0) {
564 log_warning("Failed to set permissions on SSH config file");
565 }
566
567 log_info("SSH host alias configured: %s -> %s", account->ssh_host_alias, expanded_key_path);
568 return 0;
569 }
570
571 /* Test SSH connection */
572 int ssh_test_connection(const account_t *account, const char *host) {
573 char command[512];
574 char output[1024];
575
576 if (!account || !host) {
577 set_error(ERR_INVALID_ARGS, "Invalid arguments to ssh_test_connection");
578 return -1;
579 }
580
581 log_debug("Testing SSH connection to: %s", host);
582
583 /* Build SSH test command using -T (no TTY) for git hosting services
584 * GitHub/GitLab/Bitbucket don't allow shell commands, they return a
585 * greeting message on successful auth (exit code 1 but with success message) */
586 if (strlen(account->ssh_host_alias) > 0) {
587 /* Use host alias */
588 if ((size_t)snprintf(command, sizeof(command),
589 "ssh -T -o ConnectTimeout=5 -o BatchMode=yes %s 2>&1",
590 account->ssh_host_alias) >= sizeof(command)) {
591 set_error(ERR_INVALID_ARGS, "SSH test command too long");
592 return -1;
593 }
594 } else {
595 /* Use direct host with identity file */
596 char expanded_key_path[MAX_PATH_LEN];
597 if (expand_path(account->ssh_key_path, expanded_key_path, sizeof(expanded_key_path)) != 0) {
598 return -1;
599 }
600
601 if ((size_t)snprintf(command, sizeof(command),
602 "ssh -T -o ConnectTimeout=5 -o BatchMode=yes -i '%s' %s 2>&1",
603 expanded_key_path, host) >= sizeof(command)) {
604 set_error(ERR_INVALID_ARGS, "SSH test command too long");
605 return -1;
606 }
607 }
608
609 /* Execute SSH test - for git hosts, check output for success indicators
610 * Note: GitHub returns exit code 1 even on success (no shell access) */
611 (void)execute_ssh_command(command, output, sizeof(output));
612
613 /* Check for authentication success messages from common git hosting services */
614 if (strstr(output, "successfully authenticated") || /* GitHub */
615 strstr(output, "Welcome to GitLab") || /* GitLab */
616 strstr(output, "logged in as") || /* Bitbucket */
617 strstr(output, "Hi ") || /* GitHub greeting */
618 strstr(output, "authentication successful")) { /* Generic */
619 log_debug("SSH authentication successful to %s", host);
620 return 0;
621 }
622
623 log_debug("SSH connection test failed to %s: %s", host, output);
624 return -1;
625 }
626
627 /* Internal helper functions */
628
629 /* Execute SSH command */
630 static int execute_ssh_command(const char *command, char *output, size_t output_size) {
631 FILE *pipe;
632
633 if (!command) {
634 return -1;
635 }
636
637 log_debug("Executing SSH command: %s", command);
638
639 pipe = popen(command, "r");
640 if (!pipe) {
641 set_system_error(ERR_SYSTEM_COMMAND_FAILED, "Failed to execute SSH command");
642 return -1;
643 }
644
645 /* Read output if buffer provided */
646 if (output && output_size > 0) {
647 size_t total_read = 0;
648 char *pos = output;
649
650 while (total_read < output_size - 1 &&
651 fgets(pos, output_size - total_read, pipe)) {
652 size_t line_len = strlen(pos);
653 total_read += line_len;
654 pos += line_len;
655 }
656 output[total_read] = '\0';
657
658 /* Remove trailing newline */
659 if (total_read > 0 && output[total_read - 1] == '\n') {
660 output[total_read - 1] = '\0';
661 }
662 }
663
664 int exit_code = pclose(pipe);
665 if (exit_code != 0) {
666 log_debug("SSH command failed with exit code: %d", exit_code);
667 return -1;
668 }
669
670 return 0;
671 }
672
673 /* Set up SSH environment variables */
674 static int setup_ssh_environment(ssh_config_t *ssh_config) {
675 if (!ssh_config || strlen(ssh_config->agent_socket_path) == 0) {
676 return -1;
677 }
678
679 /* Set SSH_AUTH_SOCK */
680 if (setenv("SSH_AUTH_SOCK", ssh_config->agent_socket_path, 1) != 0) {
681 set_system_error(ERR_SYSTEM_CALL, "Failed to set SSH_AUTH_SOCK");
682 return -1;
683 }
684
685 /* Set SSH_AGENT_PID if we have it */
686 if (ssh_config->agent_pid > 0) {
687 char pid_str[32];
688 snprintf(pid_str, sizeof(pid_str), "%d", ssh_config->agent_pid);
689 if (setenv("SSH_AGENT_PID", pid_str, 1) != 0) {
690 set_system_error(ERR_SYSTEM_CALL, "Failed to set SSH_AGENT_PID");
691 return -1;
692 }
693 }
694
695 log_debug("SSH environment configured: SSH_AUTH_SOCK=%s, SSH_AGENT_PID=%d",
696 ssh_config->agent_socket_path, ssh_config->agent_pid);
697 return 0;
698 }
699
700 /* Public: compute the stable SSH_AUTH_SOCK symlink path.
701 * Mirrors the selection done by create_isolated_agent_socket_dir() so shell
702 * integration emitted by `gitswitch init` points at the same socket the
703 * runtime maintains. */
704 int ssh_manager_get_auth_sock_path(char *buf, size_t buf_size) {
705 if (!buf || buf_size == 0) {
706 set_error(ERR_INVALID_ARGS, "NULL/empty buffer to ssh_manager_get_auth_sock_path");
707 return -1;
708 }
709
710 const char *runtime_dir = getenv("XDG_RUNTIME_DIR");
711 int written;
712 if (runtime_dir && *runtime_dir && path_exists(runtime_dir)) {
713 written = snprintf(buf, buf_size, "%s/gitswitch-ssh/current.sock", runtime_dir);
714 } else {
715 written = snprintf(buf, buf_size, "/tmp/gitswitch-ssh-%d/current.sock", getuid());
716 }
717
718 if (written < 0 || (size_t)written >= buf_size) {
719 set_error(ERR_INVALID_PATH, "SSH auth sock path too long");
720 return -1;
721 }
722 return 0;
723 }
724
725 /* Create isolated agent socket directory */
726 static int create_isolated_agent_socket_dir(char *socket_dir, size_t socket_dir_size) {
727 const char *runtime_dir = getenv("XDG_RUNTIME_DIR");
728 const char *tmp_dir = "/tmp";
729
730 /* Prefer XDG_RUNTIME_DIR if available */
731 if (runtime_dir && path_exists(runtime_dir)) {
732 if ((size_t)snprintf(socket_dir, socket_dir_size, "%s/gitswitch-ssh", runtime_dir) >= socket_dir_size) {
733 set_error(ERR_INVALID_PATH, "Socket directory path too long");
734 return -1;
735 }
736 } else {
737 if ((size_t)snprintf(socket_dir, socket_dir_size, "%s/gitswitch-ssh-%d", tmp_dir, getuid()) >= socket_dir_size) {
738 set_error(ERR_INVALID_PATH, "Socket directory path too long");
739 return -1;
740 }
741 }
742
743 /* Create directory with secure permissions */
744 if (!path_exists(socket_dir)) {
745 if (create_directory_recursive(socket_dir, 0700) != 0) {
746 return -1;
747 }
748 log_debug("Created SSH socket directory: %s", socket_dir);
749 }
750
751 /* Verify permissions */
752 struct stat dir_stat;
753 if (stat(socket_dir, &dir_stat) != 0) {
754 set_system_error(ERR_FILE_IO, "Failed to stat socket directory");
755 return -1;
756 }
757
758 if ((dir_stat.st_mode & 0777) != 0700) {
759 set_error(ERR_PERMISSION_DENIED, "Socket directory has insecure permissions");
760 return -1;
761 }
762
763 return 0;
764 }
765
766 /* Check if SSH agent is running */
767 static bool is_ssh_agent_running(pid_t pid) {
768 if (pid <= 0) {
769 return false;
770 }
771
772 /* Use kill(pid, 0) to test if process exists */
773 return (kill(pid, 0) == 0);
774 }
775
776 /* Kill SSH agent gracefully */
777 static int kill_ssh_agent_gracefully(pid_t pid) {
778 if (pid <= 0) {
779 return -1;
780 }
781
782 if (!is_ssh_agent_running(pid)) {
783 log_debug("SSH agent (PID: %d) not running", pid);
784 return 0;
785 }
786
787 /* Send SIGTERM first */
788 if (kill(pid, SIGTERM) != 0) {
789 set_system_error(ERR_SYSTEM_CALL, "Failed to send SIGTERM to SSH agent");
790 return -1;
791 }
792
793 /* Wait a bit for graceful shutdown */
794 for (int i = 0; i < 10; i++) {
795 if (!is_ssh_agent_running(pid)) {
796 return 0;
797 }
798 usleep(100000); /* 100ms */
799 }
800
801 /* Force kill if still running */
802 if (is_ssh_agent_running(pid)) {
803 log_warning("SSH agent did not respond to SIGTERM, sending SIGKILL");
804 if (kill(pid, SIGKILL) != 0) {
805 set_system_error(ERR_SYSTEM_CALL, "Failed to send SIGKILL to SSH agent");
806 return -1;
807 }
808 }
809
810 return 0;
811 }
812
813 /* Validate SSH agent socket */
814 static int validate_ssh_agent_socket(const char *socket_path) {
815 struct stat socket_stat;
816
817 if (!socket_path) {
818 return -1;
819 }
820
821 /* Check if socket exists */
822 if (stat(socket_path, &socket_stat) != 0) {
823 set_system_error(ERR_SSH_AGENT_SOCKET_INVALID, "SSH agent socket not found: %s", socket_path);
824 return -1;
825 }
826
827 /* Check if it's a socket */
828 if (!S_ISSOCK(socket_stat.st_mode)) {
829 set_error(ERR_SSH_AGENT_SOCKET_INVALID, "Path is not a socket: %s", socket_path);
830 return -1;
831 }
832
833 /* Check permissions */
834 if ((socket_stat.st_mode & 0777) != 0600) {
835 set_error(ERR_SSH_AGENT_SOCKET_INVALID, "SSH agent socket has wrong permissions: %s", socket_path);
836 return -1;
837 }
838
839 return 0;
840 }
841
842 /* Parse ssh-agent output */
843 static int parse_ssh_agent_output(const char *output, ssh_config_t *ssh_config) {
844 char *line;
845 char *output_copy;
846 char *saveptr;
847
848 if (!output || !ssh_config) {
849 return -1;
850 }
851
852 /* Make a copy of output for parsing */
853 output_copy = strdup(output);
854 if (!output_copy) {
855 set_error(ERR_MEMORY_ALLOCATION, "Failed to allocate memory for parsing");
856 return -1;
857 }
858
859 /* Parse line by line */
860 line = strtok_r(output_copy, "\n", &saveptr);
861 while (line) {
862 /* Look for SSH_AUTH_SOCK */
863 if (strstr(line, "SSH_AUTH_SOCK=")) {
864 char *socket_start = strchr(line, '=') + 1;
865 char *socket_end = strchr(socket_start, ';');
866 if (socket_end) {
867 *socket_end = '\0';
868 }
869 safe_strncpy(ssh_config->agent_socket_path, socket_start,
870 sizeof(ssh_config->agent_socket_path));
871 }
872
873 /* Look for SSH_AGENT_PID */
874 if (strstr(line, "SSH_AGENT_PID=")) {
875 char *pid_start = strchr(line, '=') + 1;
876 char *pid_end = strchr(pid_start, ';');
877 if (pid_end) {
878 *pid_end = '\0';
879 }
880 ssh_config->agent_pid = (pid_t)strtol(pid_start, NULL, 10);
881 }
882
883 line = strtok_r(NULL, "\n", &saveptr);
884 }
885
886 free(output_copy);
887
888 /* Validate we got the required information */
889 if (strlen(ssh_config->agent_socket_path) == 0 || ssh_config->agent_pid <= 0) {
890 set_error(ERR_SSH_AGENT_START_FAILED, "Failed to parse ssh-agent output");
891 return -1;
892 }
893
894 return 0;
895 }
896
897 /* Kill orphaned gitswitch ssh-agents from previous runs */
898 static void kill_orphaned_gitswitch_agents(void) {
899 char socket_dir[256];
900 const char *runtime_dir = getenv("XDG_RUNTIME_DIR");
901
902 /* Determine socket directory - use short buffer since runtime_dir is typically short */
903 if (runtime_dir && strlen(runtime_dir) < 200 && path_exists(runtime_dir)) {
904 snprintf(socket_dir, sizeof(socket_dir), "%s/gitswitch-ssh", runtime_dir);
905 } else {
906 snprintf(socket_dir, sizeof(socket_dir), "/tmp/gitswitch-ssh-%d", getuid());
907 }
908
909 /* Use pkill to kill any existing gitswitch ssh-agents */
910 char command[512];
911 snprintf(command, sizeof(command),
912 "pkill -f 'ssh-agent -a %.200s/' 2>/dev/null", socket_dir);
913
914 int result = system(command);
915 if (result == 0) {
916 log_debug("Killed orphaned gitswitch ssh-agents");
917 }
918 /* result != 0 is normal if no agents were running */
919
920 /* Also clean up any stale socket files */
921 char cleanup_cmd[512];
922 snprintf(cleanup_cmd, sizeof(cleanup_cmd),
923 "rm -f %.200s/ssh-agent.*.sock 2>/dev/null", socket_dir);
924 if (system(cleanup_cmd) != 0) {
925 /* Ignore cleanup failures - files may not exist */
926 }
927 }