ascii-chat 0.8.38
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 <ascii-chat/crypto/ssh/ssh_agent.h>
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>
14#include <ascii-chat/platform/pipe.h>
15#include <ascii-chat/platform/agent.h>
16#include <ascii-chat/log/logging.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) {
42 platform_pipe_close(pipe);
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) {
73 platform_pipe_close(pipe);
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) {
81 platform_pipe_close(pipe);
82 return false;
83 }
84
85 platform_pipe_close(pipe);
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 memcpy(buf + pos, "ssh-ed25519", 11);
182 pos += 11;
183
184 // Public key (32 bytes) - last 32 bytes of the 64-byte ed25519 key
185 len = 32;
186 write_u32_be(buf + pos, len);
187 pos += 4;
188 memcpy(buf + pos, private_key->key.ed25519 + 32, 32); // Public key is second half
189 pos += 32;
190
191 // Private key (64 bytes - full ed25519 key: 32-byte seed + 32-byte public)
192 len = 64;
193 write_u32_be(buf + pos, len);
194 pos += 4;
195 memcpy(buf + pos, private_key->key.ed25519, 64);
196 pos += 64;
197
198 // Comment (key path)
199 len = key_path ? strlen(key_path) : 0;
200
201 // SECURITY: Validate key path length to prevent buffer overflow
202 // Buffer is BUFFER_SIZE_XXLARGE (4096), pos is ~128 at this point, need 4 bytes for length prefix
203 size_t max_key_path_len = sizeof(buf) - pos - 4;
204 if (len > max_key_path_len) {
205 platform_pipe_close(pipe);
206 sodium_memzero(buf, sizeof(buf));
207 return SET_ERRNO(ERROR_BUFFER_OVERFLOW, "SSH key path too long: %u bytes (max %zu)", len, max_key_path_len);
208 }
209
210 write_u32_be(buf + pos, len);
211 pos += 4;
212 if (len > 0) {
213 memcpy(buf + pos, key_path, len);
214 pos += len;
215 }
216
217 // Write message length at start (excluding the 4-byte length field itself)
218 uint32_t msg_len = pos - 4;
219 write_u32_be(buf, msg_len);
220
221 // Send message to agent
222 ssize_t bytes_written = platform_pipe_write(pipe, buf, pos);
223 if (bytes_written != (ssize_t)pos) {
224 platform_pipe_close(pipe);
225 sodium_memzero(buf, sizeof(buf));
226 return SET_ERRNO_SYS(ERROR_CRYPTO, "Failed to write to ssh-agent pipe");
227 }
228
229 // Read response
230 unsigned char response[BUFFER_SIZE_SMALL];
231 ssize_t bytes_read = platform_pipe_read(pipe, response, sizeof(response));
232 if (bytes_read < 5) {
233 platform_pipe_close(pipe);
234 sodium_memzero(buf, sizeof(buf));
235 return SET_ERRNO_SYS(ERROR_CRYPTO, "Failed to read from ssh-agent pipe");
236 }
237
238 // Done with the pipe - close it
239 platform_pipe_close(pipe);
240 sodium_memzero(buf, sizeof(buf));
241
242 // Check response: should be SSH_AGENT_SUCCESS (6)
243 // Response format: uint32 length, byte message_type
244 uint8_t response_type = response[4];
245 if (response_type == 6) {
246 log_debug("Successfully added key to ssh-agent");
247 return ASCIICHAT_OK;
248 } else if (response_type == 5) {
249 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent rejected key (SSH_AGENT_FAILURE)");
250 } else {
251 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent returned unexpected response: %d", response_type);
252 }
253}
254
255asciichat_error_t ssh_agent_sign(const public_key_t *public_key, const uint8_t *message, size_t message_len,
256 uint8_t signature[64]) {
257 if (!public_key || !message || !signature) {
258 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: public_key=%p, message=%p, signature=%p", public_key,
259 message, signature);
260 }
261
262 if (public_key->type != KEY_TYPE_ED25519) {
263 return SET_ERRNO(ERROR_CRYPTO_KEY, "Only Ed25519 keys are supported for SSH agent signing");
264 }
265
266 // SSH agent protocol limits message size (typical OpenSSH agent limit is around 1MB)
267 // Large messages can cause agent memory exhaustion or timeout
268 if (message_len > 1024 * 1024) {
269 return SET_ERRNO(ERROR_CRYPTO, "Message too large for SSH agent (max 1MB)");
270 }
271
272 // Connect to SSH agent
273 pipe_t pipe = ssh_agent_open_pipe();
274 if (pipe == INVALID_PIPE_VALUE) {
275 return SET_ERRNO(ERROR_CRYPTO, "Cannot connect to ssh-agent");
276 }
277
278 // Build SSH2_AGENTC_SIGN_REQUEST message (type 13)
279 // Format: uint32 length, byte type, string key_blob, string data, uint32 flags
280 // For Ed25519, key_blob is: string "ssh-ed25519", string public_key(32 bytes)
281
282 const char *key_type = "ssh-ed25519";
283 uint32_t key_type_len = (uint32_t)strlen(key_type);
284
285 // Calculate total message length
286 // 1 (type) + 4 (key_blob_len) + key_blob_size + 4 (data_len) + data_size + 4 (flags)
287 uint32_t key_blob_size = 4 + key_type_len + 4 + 32; // string(key_type) + string(pubkey)
288 uint32_t total_len = 1 + 4 + key_blob_size + 4 + message_len + 4;
289
290 uint8_t *buf = SAFE_MALLOC(total_len + 4, uint8_t *); // +4 for length prefix
291 if (!buf) {
292 platform_pipe_close(pipe);
293 return SET_ERRNO(ERROR_CRYPTO, "Out of memory for SSH agent sign request");
294 }
295
296 uint32_t offset = 0;
297
298 // Write total message length (excluding this 4-byte length field)
299 write_u32_be(buf + offset, total_len);
300 offset += 4;
301
302 // Write message type (13 = SSH2_AGENTC_SIGN_REQUEST)
303 buf[offset++] = 13;
304
305 // Write key_blob length
306 write_u32_be(buf + offset, key_blob_size);
307 offset += 4;
308
309 // Write key_blob: string(key_type)
310 write_u32_be(buf + offset, key_type_len);
311 offset += 4;
312 memcpy(buf + offset, key_type, key_type_len);
313 offset += key_type_len;
314
315 // Write key_blob: string(public_key)
316 write_u32_be(buf + offset, 32);
317 offset += 4;
318 memcpy(buf + offset, public_key->key, 32);
319 offset += 32;
320
321 // Write data to sign
322 write_u32_be(buf + offset, (uint32_t)message_len);
323 offset += 4;
324 memcpy(buf + offset, message, message_len);
325 offset += (uint32_t)message_len;
326
327 // Write flags (0 = default)
328 write_u32_be(buf + offset, 0);
329 offset += 4;
330
331 // Send request
332 ssize_t written = platform_pipe_write(pipe, buf, total_len + 4);
333 sodium_memzero(buf, total_len + 4);
334 SAFE_FREE(buf);
335
336 if (written < 0 || (size_t)written != total_len + 4) {
337 platform_pipe_close(pipe);
338 return SET_ERRNO(ERROR_CRYPTO, "Failed to write SSH agent sign request");
339 }
340
341 // Read response
342 uint8_t response[BUFFER_SIZE_XXLARGE];
343 ssize_t read_bytes = platform_pipe_read(pipe, response, sizeof(response));
344 platform_pipe_close(pipe);
345
346 if (read_bytes < 5) {
347 return SET_ERRNO(ERROR_CRYPTO, "Failed to read SSH agent sign response (read %zd bytes)", read_bytes);
348 }
349
350 // Response format: uint32 length, byte type, data...
351 // We validate length implicitly by checking read_bytes and parsing the full response
352 (void)read_u32_be(response); // Read but don't need explicit length check
353 uint8_t response_type = response[4];
354
355 // Check for SSH2_AGENT_SIGN_RESPONSE (14)
356 if (response_type != 14) {
357 if (response_type == 5) {
358 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent refused to sign (SSH_AGENT_FAILURE)");
359 }
360 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent returned unexpected response type: %d (expected 14)", response_type);
361 }
362
363 // Parse signature blob
364 // Response format: uint32 len, byte type(14), string signature_blob
365 if (read_bytes < 9) {
366 return SET_ERRNO(ERROR_CRYPTO, "SSH agent response too short (no signature blob length)");
367 }
368
369 uint32_t sig_blob_len = read_u32_be(response + 5);
370 uint32_t expected_total = 4 + 1 + 4 + sig_blob_len;
371
372 if ((size_t)read_bytes < expected_total) {
373 return SET_ERRNO(ERROR_CRYPTO, "SSH agent response truncated (expected %u bytes, got %zd)", expected_total,
374 read_bytes);
375 }
376
377 // Signature blob format for Ed25519: string "ssh-ed25519", string signature(64 bytes)
378 uint32_t offset_sig = 9;
379 uint32_t sig_type_len = read_u32_be(response + offset_sig);
380 offset_sig += 4;
381
382 if (offset_sig + sig_type_len + 4 > (uint32_t)read_bytes) {
383 return SET_ERRNO(ERROR_CRYPTO, "SSH agent signature blob truncated at signature type");
384 }
385
386 // Verify signature type is "ssh-ed25519"
387 if (sig_type_len != 11 || memcmp(response + offset_sig, "ssh-ed25519", 11) != 0) {
388 return SET_ERRNO(ERROR_CRYPTO, "SSH agent returned non-Ed25519 signature");
389 }
390 offset_sig += sig_type_len;
391
392 // Read signature bytes
393 uint32_t sig_len = read_u32_be(response + offset_sig);
394 offset_sig += 4;
395
396 if (sig_len != 64) {
397 return SET_ERRNO(ERROR_CRYPTO, "SSH agent returned invalid Ed25519 signature length: %u (expected 64)", sig_len);
398 }
399
400 if (offset_sig + 64 > (uint32_t)read_bytes) {
401 return SET_ERRNO(ERROR_CRYPTO, "SSH agent signature blob truncated at signature bytes");
402 }
403
404 // Copy signature to output
405 memcpy(signature, response + offset_sig, 64);
406
407 log_debug("SSH agent successfully signed %zu bytes with Ed25519 key", message_len);
408 return ASCIICHAT_OK;
409}
_Atomic uint64_t bytes_written
Definition mmap.c:42
bool ssh_agent_has_key(const public_key_t *public_key)
Definition ssh_agent.c:50
bool ssh_agent_is_available(void)
Definition ssh_agent.c:38
asciichat_error_t ssh_agent_add_key(const private_key_t *private_key, const char *key_path)
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])
Definition ssh_agent.c:255