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