ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
ssh_agent.c
Go to the documentation of this file.
1
7#include "ssh_agent.h"
8#include "common.h"
9#include "util/bytes.h" // For write_u32_be, read_u32_be
10#include <stdio.h>
11#include <stdlib.h>
12#include <string.h>
13#include <sodium.h>
14#include "platform/pipe.h"
15
16#ifdef _WIN32
17#include <io.h>
18#include <sys/stat.h>
19#include <fcntl.h>
20#define SAFE_CLOSE _close
21#define SAFE_UNLINK _unlink
22#define SAFE_POPEN _popen
23#define SAFE_PCLOSE _pclose
24#else
25#include <unistd.h>
26#include <sys/stat.h>
27#include <fcntl.h>
28#define SAFE_CLOSE close
29#define SAFE_UNLINK unlink
30#define SAFE_POPEN popen
31#define SAFE_PCLOSE pclose
32#endif
33
34// Open the SSH agent pipe/socket
35static pipe_t ssh_agent_open_pipe(void) {
36 const char *auth_sock = SAFE_GETENV("SSH_AUTH_SOCK");
37
38#ifdef _WIN32
39 // On Windows, use named pipe path (default or from SSH_AUTH_SOCK)
40 const char *pipe_path = (auth_sock && strlen(auth_sock) > 0) ? auth_sock : "\\\\.\\pipe\\openssh-ssh-agent";
41 return platform_pipe_connect(pipe_path);
42#else
43 // On Unix, use Unix domain socket path from SSH_AUTH_SOCK
44 if (!auth_sock || strlen(auth_sock) == 0) {
45 log_debug("SSH_AUTH_SOCK not set, cannot connect to ssh-agent");
46 return INVALID_PIPE_VALUE;
47 }
48 return platform_pipe_connect(auth_sock);
49#endif
50}
51
53 // Check if SSH_AUTH_SOCK environment variable is set
54 const char *auth_sock = SAFE_GETENV("SSH_AUTH_SOCK");
55
56#ifdef _WIN32
57 // On Windows, if SSH_AUTH_SOCK is not set, try to open the Windows named pipe to check availability
58 if (!auth_sock || strlen(auth_sock) == 0) {
59 pipe_t pipe = ssh_agent_open_pipe();
60 if (pipe != INVALID_PIPE_VALUE) {
61 platform_pipe_close(pipe); // Close immediately after checking
62 log_debug("ssh-agent is available via Windows named pipe (SSH_AUTH_SOCK not set)");
63 return true;
64 } else {
65 log_debug("ssh-agent not available: SSH_AUTH_SOCK not set and Windows named pipe not accessible");
66 return false;
67 }
68 }
69
70 // SSH_AUTH_SOCK is set on Windows
71 log_debug("ssh-agent appears available (SSH_AUTH_SOCK=%s)", auth_sock);
72 return true;
73#else
74 // Unix: SSH_AUTH_SOCK is required
75 if (!auth_sock || strlen(auth_sock) == 0) {
76 log_debug("ssh-agent not available: SSH_AUTH_SOCK not set");
77 return false;
78 }
79
80 // Check if Unix socket exists and is accessible
81 if (access(auth_sock, W_OK) != 0) {
82 log_debug("ssh-agent not available: cannot access socket at %s", auth_sock);
83 return false;
84 }
85 log_debug("ssh-agent is available at %s", auth_sock);
86 return true;
87#endif
88}
89
90bool ssh_agent_has_key(const public_key_t *public_key) {
91 if (public_key == NULL) {
92 log_warn("NULL is not a valid public key");
93 return false;
94 }
95
96 // Use SSH agent protocol to list keys (works on both Windows and Unix)
97 pipe_t pipe = ssh_agent_open_pipe();
98 if (pipe == INVALID_PIPE_VALUE) {
99 return false;
100 }
101
102 // Build SSH2_AGENTC_REQUEST_IDENTITIES message (type 11)
103 unsigned char request[5];
104 request[0] = 0; // length: 1 (4-byte big-endian)
105 request[1] = 0;
106 request[2] = 0;
107 request[3] = 1;
108 request[4] = 11; // SSH2_AGENTC_REQUEST_IDENTITIES
109
110 // Send request
111 ssize_t bytes_written = platform_pipe_write(pipe, request, 5);
112 if (bytes_written != 5) {
114 return false;
115 }
116
117 // Read response
118 unsigned char response[BUFFER_SIZE_XXXLARGE];
119 ssize_t bytes_read = platform_pipe_read(pipe, response, sizeof(response));
120 if (bytes_read < 9) {
122 return false;
123 }
124
126
127 // Parse response: type should be SSH2_AGENT_IDENTITIES_ANSWER (12)
128 uint8_t resp_type = response[4];
129 if (resp_type != 12) {
130 return false;
131 }
132
133 // Number of keys at bytes 5-8
134 uint32_t num_keys = read_u32_be(response + 5);
135
136 // Parse keys and check if our public key matches
137 size_t pos = 9;
138 for (uint32_t i = 0; i < num_keys && pos + 4 < (size_t)bytes_read; i++) {
139 // Read key blob length
140 uint32_t blob_len = read_u32_be(response + pos);
141 pos += 4;
142
143 if (pos + blob_len > (size_t)bytes_read)
144 break;
145
146 // Parse the blob to extract the Ed25519 public key
147 size_t blob_pos = pos;
148 // Skip key type string
149 if (blob_pos + 4 > pos + blob_len) {
150 pos += blob_len;
151 continue;
152 }
153 uint32_t type_len = read_u32_be(response + blob_pos);
154 blob_pos += 4 + type_len;
155
156 // Read public key data
157 if (blob_pos + 4 > pos + blob_len) {
158 pos += blob_len;
159 continue;
160 }
161 uint32_t pubkey_len = read_u32_be(response + blob_pos);
162 blob_pos += 4;
163
164 // Compare public key (should be 32 bytes for Ed25519)
165 // Use constant-time comparison to prevent timing side channels
166 if (pubkey_len == 32 && blob_pos + 32 <= pos + blob_len) {
167 if (sodium_memcmp(response + blob_pos, public_key->key, 32) == 0) {
168 log_debug("Found matching key in ssh-agent");
169 return true;
170 }
171 }
172
173 pos += blob_len;
174
175 // Skip comment string length + comment
176 if (pos + 4 > (size_t)bytes_read)
177 break;
178 uint32_t comment_len = read_u32_be(response + pos);
179 pos += 4 + comment_len;
180 }
181
182 return false;
183}
184
185asciichat_error_t ssh_agent_add_key(const private_key_t *private_key, const char *key_path) {
186 if (private_key == NULL) {
187 return SET_ERRNO(ERROR_INVALID_PARAM, "Cannot add key to ssh-agent: private_key is NULL");
188 }
189 if (private_key->type != KEY_TYPE_ED25519) {
190 return SET_ERRNO(ERROR_INVALID_PARAM, "Cannot add key to ssh-agent: only Ed25519 keys supported");
191 }
192
193 log_info("Adding key to ssh-agent: %s", key_path ? key_path : "(memory)");
194
195 // Open the pipe/socket for this operation (works on both Windows and Unix)
196 pipe_t pipe = ssh_agent_open_pipe();
197 if (pipe == INVALID_PIPE_VALUE) {
198 return SET_ERRNO(ERROR_CRYPTO, "Failed to connect to ssh-agent");
199 }
200
201 // Build SSH agent protocol message: SSH2_AGENTC_ADD_IDENTITY (17)
202 // Message format:
203 // uint32: message length
204 // byte: SSH2_AGENTC_ADD_IDENTITY (17)
205 // string: key type ("ssh-ed25519")
206 // string: public key (32 bytes)
207 // string: private key (64 bytes)
208 // string: comment (key path or empty)
209
210 unsigned char buf[BUFFER_SIZE_XXLARGE];
211 size_t pos = 4; // Reserve space for length prefix
212
213 // Message type: SSH2_AGENTC_ADD_IDENTITY
214 buf[pos++] = 17;
215
216 // Key type: "ssh-ed25519" (11 bytes)
217 uint32_t len = 11;
218 write_u32_be(buf + pos, len);
219 pos += 4;
220 // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - Binary protocol, intentionally not null-terminated
221 memcpy(buf + pos, "ssh-ed25519", 11);
222 pos += 11;
223
224 // Public key (32 bytes) - last 32 bytes of the 64-byte ed25519 key
225 len = 32;
226 write_u32_be(buf + pos, len);
227 pos += 4;
228 memcpy(buf + pos, private_key->key.ed25519 + 32, 32); // Public key is second half
229 pos += 32;
230
231 // Private key (64 bytes - full ed25519 key: 32-byte seed + 32-byte public)
232 len = 64;
233 write_u32_be(buf + pos, len);
234 pos += 4;
235 memcpy(buf + pos, private_key->key.ed25519, 64);
236 pos += 64;
237
238 // Comment (key path)
239 len = key_path ? strlen(key_path) : 0;
240
241 // SECURITY: Validate key path length to prevent buffer overflow
242 // Buffer is BUFFER_SIZE_XXLARGE (4096), pos is ~128 at this point, need 4 bytes for length prefix
243 size_t max_key_path_len = sizeof(buf) - pos - 4;
244 if (len > max_key_path_len) {
246 sodium_memzero(buf, sizeof(buf));
247 return SET_ERRNO(ERROR_BUFFER_OVERFLOW, "SSH key path too long: %u bytes (max %zu)", len, max_key_path_len);
248 }
249
250 write_u32_be(buf + pos, len);
251 pos += 4;
252 if (len > 0) {
253 memcpy(buf + pos, key_path, len);
254 pos += len;
255 }
256
257 // Write message length at start (excluding the 4-byte length field itself)
258 uint32_t msg_len = pos - 4;
259 write_u32_be(buf, msg_len);
260
261 // Send message to agent
262 ssize_t bytes_written = platform_pipe_write(pipe, buf, pos);
263 if (bytes_written != (ssize_t)pos) {
265 sodium_memzero(buf, sizeof(buf));
266 return SET_ERRNO_SYS(ERROR_CRYPTO, "Failed to write to ssh-agent pipe");
267 }
268
269 // Read response
270 unsigned char response[BUFFER_SIZE_SMALL];
271 ssize_t bytes_read = platform_pipe_read(pipe, response, sizeof(response));
272 if (bytes_read < 5) {
274 sodium_memzero(buf, sizeof(buf));
275 return SET_ERRNO_SYS(ERROR_CRYPTO, "Failed to read from ssh-agent pipe");
276 }
277
278 // Done with the pipe - close it
280 sodium_memzero(buf, sizeof(buf));
281
282 // Check response: should be SSH_AGENT_SUCCESS (6)
283 // Response format: uint32 length, byte message_type
284 uint8_t response_type = response[4];
285 if (response_type == 6) {
286 log_info("Successfully added key to ssh-agent");
287 return ASCIICHAT_OK;
288 } else if (response_type == 5) {
289 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent rejected key (SSH_AGENT_FAILURE)");
290 } else {
291 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent returned unexpected response: %d", response_type);
292 }
293}
294
295asciichat_error_t ssh_agent_sign(const public_key_t *public_key, const uint8_t *message, size_t message_len,
296 uint8_t signature[64]) {
297 if (!public_key || !message || !signature) {
298 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: public_key=%p, message=%p, signature=%p", public_key,
299 message, signature);
300 }
301
302 if (public_key->type != KEY_TYPE_ED25519) {
303 return SET_ERRNO(ERROR_CRYPTO_KEY, "Only Ed25519 keys are supported for SSH agent signing");
304 }
305
306 // Connect to SSH agent
307 pipe_t pipe = ssh_agent_open_pipe();
308 if (pipe == INVALID_PIPE_VALUE) {
309 return SET_ERRNO(ERROR_CRYPTO, "Cannot connect to ssh-agent");
310 }
311
312 // Build SSH2_AGENTC_SIGN_REQUEST message (type 13)
313 // Format: uint32 length, byte type, string key_blob, string data, uint32 flags
314 // For Ed25519, key_blob is: string "ssh-ed25519", string public_key(32 bytes)
315
316 const char *key_type = "ssh-ed25519";
317 uint32_t key_type_len = (uint32_t)strlen(key_type);
318
319 // Calculate total message length
320 // 1 (type) + 4 (key_blob_len) + key_blob_size + 4 (data_len) + data_size + 4 (flags)
321 uint32_t key_blob_size = 4 + key_type_len + 4 + 32; // string(key_type) + string(pubkey)
322 uint32_t total_len = 1 + 4 + key_blob_size + 4 + message_len + 4;
323
324 uint8_t *buf = SAFE_MALLOC(total_len + 4, uint8_t *); // +4 for length prefix
325 if (!buf) {
327 return SET_ERRNO(ERROR_CRYPTO, "Out of memory for SSH agent sign request");
328 }
329
330 uint32_t offset = 0;
331
332 // Write total message length (excluding this 4-byte length field)
333 write_u32_be(buf + offset, total_len);
334 offset += 4;
335
336 // Write message type (13 = SSH2_AGENTC_SIGN_REQUEST)
337 buf[offset++] = 13;
338
339 // Write key_blob length
340 write_u32_be(buf + offset, key_blob_size);
341 offset += 4;
342
343 // Write key_blob: string(key_type)
344 write_u32_be(buf + offset, key_type_len);
345 offset += 4;
346 memcpy(buf + offset, key_type, key_type_len);
347 offset += key_type_len;
348
349 // Write key_blob: string(public_key)
350 write_u32_be(buf + offset, 32);
351 offset += 4;
352 memcpy(buf + offset, public_key->key, 32);
353 offset += 32;
354
355 // Write data to sign
356 write_u32_be(buf + offset, (uint32_t)message_len);
357 offset += 4;
358 memcpy(buf + offset, message, message_len);
359 offset += (uint32_t)message_len;
360
361 // Write flags (0 = default)
362 write_u32_be(buf + offset, 0);
363 offset += 4;
364
365 // Send request
366 ssize_t written = platform_pipe_write(pipe, buf, total_len + 4);
367 sodium_memzero(buf, total_len + 4);
368 SAFE_FREE(buf);
369
370 if (written < 0 || (size_t)written != total_len + 4) {
372 return SET_ERRNO(ERROR_CRYPTO, "Failed to write SSH agent sign request");
373 }
374
375 // Read response
377 ssize_t read_bytes = platform_pipe_read(pipe, response, sizeof(response));
379
380 if (read_bytes < 5) {
381 return SET_ERRNO(ERROR_CRYPTO, "Failed to read SSH agent sign response (read %zd bytes)", read_bytes);
382 }
383
384 // Response format: uint32 length, byte type, data...
385 // We validate length implicitly by checking read_bytes and parsing the full response
386 (void)read_u32_be(response); // Read but don't need explicit length check
387 uint8_t response_type = response[4];
388
389 // Check for SSH2_AGENT_SIGN_RESPONSE (14)
390 if (response_type != 14) {
391 if (response_type == 5) {
392 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent refused to sign (SSH_AGENT_FAILURE)");
393 }
394 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent returned unexpected response type: %d (expected 14)", response_type);
395 }
396
397 // Parse signature blob
398 // Response format: uint32 len, byte type(14), string signature_blob
399 if (read_bytes < 9) {
400 return SET_ERRNO(ERROR_CRYPTO, "SSH agent response too short (no signature blob length)");
401 }
402
403 uint32_t sig_blob_len = read_u32_be(response + 5);
404 uint32_t expected_total = 4 + 1 + 4 + sig_blob_len;
405
406 if ((size_t)read_bytes < expected_total) {
407 return SET_ERRNO(ERROR_CRYPTO, "SSH agent response truncated (expected %u bytes, got %zd)", expected_total,
408 read_bytes);
409 }
410
411 // Signature blob format for Ed25519: string "ssh-ed25519", string signature(64 bytes)
412 uint32_t offset_sig = 9;
413 uint32_t sig_type_len = read_u32_be(response + offset_sig);
414 offset_sig += 4;
415
416 if (offset_sig + sig_type_len + 4 > (uint32_t)read_bytes) {
417 return SET_ERRNO(ERROR_CRYPTO, "SSH agent signature blob truncated at signature type");
418 }
419
420 // Verify signature type is "ssh-ed25519"
421 if (sig_type_len != 11 || memcmp(response + offset_sig, "ssh-ed25519", 11) != 0) {
422 return SET_ERRNO(ERROR_CRYPTO, "SSH agent returned non-Ed25519 signature");
423 }
424 offset_sig += sig_type_len;
425
426 // Read signature bytes
427 uint32_t sig_len = read_u32_be(response + offset_sig);
428 offset_sig += 4;
429
430 if (sig_len != 64) {
431 return SET_ERRNO(ERROR_CRYPTO, "SSH agent returned invalid Ed25519 signature length: %u (expected 64)", sig_len);
432 }
433
434 if (offset_sig + 64 > (uint32_t)read_bytes) {
435 return SET_ERRNO(ERROR_CRYPTO, "SSH agent signature blob truncated at signature bytes");
436 }
437
438 // Copy signature to output
439 memcpy(signature, response + offset_sig, 64);
440
441 log_debug("SSH agent successfully signed %zu bytes with Ed25519 key", message_len);
442 return ASCIICHAT_OK;
443}
🔢 Byte-Level Access and Arithmetic Utilities
#define BUFFER_SIZE_XXXLARGE
Extra extra extra large buffer size (8192 bytes)
#define BUFFER_SIZE_XXLARGE
Extra extra large buffer size (4096 bytes)
#define BUFFER_SIZE_SMALL
Small buffer size (256 bytes)
unsigned int uint32_t
Definition common.h:58
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_MALLOC(size, cast)
Definition common.h:208
#define SAFE_GETENV(name)
Definition common.h:378
unsigned char uint8_t
Definition common.h:56
bool ssh_agent_has_key(const public_key_t *public_key)
Check if a public key is already in ssh-agent.
Definition ssh_agent.c:90
bool ssh_agent_is_available(void)
Check if ssh-agent is running and available.
Definition ssh_agent.c:52
asciichat_error_t ssh_agent_add_key(const private_key_t *private_key, const char *key_path)
Add a private key to ssh-agent.
Definition ssh_agent.c:185
asciichat_error_t ssh_agent_sign(const public_key_t *public_key, const uint8_t *message, size_t message_len, uint8_t signature[64])
Sign data using SSH agent with the specified public key.
Definition ssh_agent.c:295
#define SET_ERRNO_SYS(code, context_msg,...)
Set error code with custom message and system error context.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ERROR_CRYPTO_KEY
Definition error_codes.h:89
@ ASCIICHAT_OK
Definition error_codes.h:48
@ ERROR_CRYPTO
Definition error_codes.h:88
@ ERROR_INVALID_PARAM
@ ERROR_BUFFER_OVERFLOW
Definition error_codes.h:98
uint8_t key[32]
Definition key_types.h:71
key_type_t type
Definition key_types.h:92
uint8_t ed25519[64]
Definition key_types.h:94
union private_key_t::@1 key
key_type_t type
Definition key_types.h:70
@ KEY_TYPE_ED25519
Definition key_types.h:52
#define log_warn(...)
Log a WARN message.
#define log_info(...)
Log an INFO message.
#define log_debug(...)
Log a DEBUG message.
ssize_t platform_pipe_read(pipe_t pipe, void *buf, size_t len)
Read data from a pipe.
pipe_t platform_pipe_connect(const char *path)
Connect to an agent via named pipe (Windows) or Unix socket (POSIX)
int pipe_t
Pipe handle type (POSIX: int file descriptor)
Definition pipe.h:40
ssize_t platform_pipe_write(pipe_t pipe, const void *buf, size_t len)
Write data to a pipe.
int platform_pipe_close(pipe_t pipe)
Close a pipe connection.
#define INVALID_PIPE_VALUE
Invalid pipe value (POSIX: -1)
Definition pipe.h:42
_Atomic uint64_t bytes_written
Definition mmap.c:40
Cross-platform pipe/agent socket interface for ascii-chat.
Private key structure (for server –ssh-key)
Definition key_types.h:91
Public key structure.
Definition key_types.h:69