ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
ssh_keys.c
Go to the documentation of this file.
1
7#include <ascii-chat/crypto/crypto.h> // Includes <sodium.h>
8#include <ascii-chat/crypto/ssh/ssh_keys.h>
9#include <ascii-chat/crypto/regex.h>
10#include <ascii-chat/common.h>
11#include <ascii-chat/asciichat_errno.h>
12#include <ascii-chat/util/password.h>
13#include <ascii-chat/platform/util.h>
14#include <ascii-chat/util/string.h>
15#include <ascii-chat/util/path.h>
16#include <ascii-chat/util/bytes.h>
17#include <ascii-chat/crypto/ssh/ssh_agent.h>
18#include <ascii-chat/crypto/gpg/gpg.h> // For GPG agent signing support
19#include <ascii-chat-deps/bearssl/inc/bearssl.h>
20#include <ascii-chat-deps/libsodium-bcrypt-pbkdf/include/sodium_bcrypt_pbkdf.h>
21#include <string.h>
22#include <stdio.h>
23#include <stdlib.h>
24#include <time.h>
25
26#ifndef _WIN32
27#include <sys/wait.h>
28#include <unistd.h>
29#endif
30#include <sys/stat.h>
31
32// =============================================================================
33// Helper Functions
34// =============================================================================
35
36// Forward declarations
37static asciichat_error_t base64_decode_ssh_key(const char *base64, size_t base64_len, uint8_t **blob_out,
38 size_t *blob_len);
39static asciichat_error_t decrypt_openssh_private_key(const uint8_t *encrypted_blob, size_t blob_len,
40 const char *passphrase, const uint8_t *salt, size_t salt_len,
41 uint32_t rounds, const char *cipher_name, uint8_t **decrypted_out,
42 size_t *decrypted_len);
43
61static asciichat_error_t decrypt_openssh_private_key(const uint8_t *encrypted_blob, size_t blob_len,
62 const char *passphrase, const uint8_t *salt, size_t salt_len,
63 uint32_t rounds, const char *cipher_name, uint8_t **decrypted_out,
64 size_t *decrypted_len) {
65 // OpenSSH uses: key_len + iv_len derived from bcrypt_pbkdf
66 // For AES-256: key=32 bytes, iv=16 bytes = 48 bytes total
67 const size_t key_size = 32;
68 const size_t iv_size = 16;
69 const size_t derived_size = key_size + iv_size;
70
71 uint8_t *derived = SAFE_MALLOC(derived_size, uint8_t *);
72 if (!derived) {
73 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for key derivation");
74 }
75
76 // Use libsodium-bcrypt-pbkdf (OpenBSD implementation)
77 if (sodium_bcrypt_pbkdf(passphrase, strlen(passphrase), salt, salt_len, derived, derived_size, rounds) != 0) {
78 SAFE_FREE(derived);
79 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to derive decryption key with sodium_bcrypt_pbkdf");
80 }
81
82 const uint8_t *key = derived;
83 const uint8_t *derived_iv = derived + key_size;
84
85 // Decrypt using BearSSL
86 uint8_t *decrypted = SAFE_MALLOC(blob_len, uint8_t *);
87 if (!decrypted) {
88 sodium_memzero(derived, derived_size);
89 SAFE_FREE(derived);
90 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for decryption");
91 }
92
93 if (strcmp(cipher_name, "aes256-ctr") == 0) {
94 // AES-256-CTR decryption using BearSSL
95 // OpenSSH derives a full 16-byte IV from bcrypt_pbkdf
96 // BearSSL expects: 12-byte nonce + 4-byte counter (big-endian uint32)
97 // We split the 16-byte IV: first 12 bytes as nonce, last 4 bytes as initial counter
98 br_aes_ct_ctr_keys aes_ctx;
99 br_aes_ct_ctr_init(&aes_ctx, key, key_size);
100
101 // Extract initial counter value from bytes 12-15 of derived IV (big-endian)
102 uint32_t initial_counter = ((uint32_t)derived_iv[12] << 24) | ((uint32_t)derived_iv[13] << 16) |
103 ((uint32_t)derived_iv[14] << 8) | ((uint32_t)derived_iv[15]);
104
105 // Decrypt in-place
106 memcpy(decrypted, encrypted_blob, blob_len);
107 br_aes_ct_ctr_run(&aes_ctx, derived_iv, initial_counter, decrypted, blob_len);
108 } else if (strcmp(cipher_name, "aes256-cbc") == 0) {
109 // AES-256-CBC decryption using BearSSL
110 br_aes_ct_cbcdec_keys aes_ctx;
111 br_aes_ct_cbcdec_init(&aes_ctx, key, key_size);
112
113 // Copy IV to working buffer
114 uint8_t working_iv[16];
115 memcpy(working_iv, derived_iv, 16);
116
117 // Decrypt in-place
118 memcpy(decrypted, encrypted_blob, blob_len);
119 br_aes_ct_cbcdec_run(&aes_ctx, working_iv, decrypted, blob_len);
120 } else {
121 sodium_memzero(derived, derived_size);
122 SAFE_FREE(derived);
123 SAFE_FREE(decrypted);
124 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported cipher: %s", cipher_name);
125 }
126
127 // Clean up sensitive data
128 sodium_memzero(derived, derived_size);
129 SAFE_FREE(derived);
130
131 *decrypted_out = decrypted;
132 *decrypted_len = blob_len;
133
134 return ASCIICHAT_OK;
135}
136
137// Base64 decode SSH key blob
138static asciichat_error_t base64_decode_ssh_key(const char *base64, size_t base64_len, uint8_t **blob_out,
139 size_t *blob_len) {
140 if (!base64 || !blob_out || !blob_len) {
141 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for base64 decode");
142 }
143
144 // Allocate max possible size
145 *blob_out = SAFE_MALLOC(base64_len, uint8_t *);
146
147 const char *end;
148 int result = sodium_base642bin(*blob_out, base64_len, base64, base64_len,
149 NULL, // ignore chars
150 blob_len, &end, sodium_base64_VARIANT_ORIGINAL);
151
152 if (result != 0) {
153 SAFE_FREE(*blob_out);
154 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to decode base64 SSH key data");
155 }
156
157 return ASCIICHAT_OK;
158}
159
160// =============================================================================
161// SSH Key Parsing Implementation
162// =============================================================================
163
164asciichat_error_t parse_ssh_ed25519_line(const char *line, uint8_t ed25519_pk[32]) {
165 if (!line || !ed25519_pk) {
166 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: line=%p, ed25519_pk=%p", line, ed25519_pk);
167 }
168
169 // Use regex to parse and extract SSH public key components
170 char *base64_key = NULL, *comment = NULL;
171 if (!crypto_regex_match_public_key(line, &base64_key, &comment)) {
172 return SET_ERRNO(ERROR_CRYPTO_KEY, "SSH key line does not match expected format");
173 }
174
175 // Base64 decode
176 uint8_t *blob;
177 size_t blob_len;
178 if (base64_decode_ssh_key(base64_key, strlen(base64_key), &blob, &blob_len) != 0) {
179 SAFE_FREE(base64_key);
180 SAFE_FREE(comment);
181 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to decode SSH key base64 data");
182 }
183
184 // Clean up regex-extracted strings
185 SAFE_FREE(base64_key);
186 SAFE_FREE(comment);
187
188 // Parse SSH key blob structure:
189 // [4 bytes: length of "ssh-ed25519"]
190 // [11 bytes: "ssh-ed25519"]
191 // [4 bytes: length of public key (32)]
192 // [32 bytes: Ed25519 public key]
193
194 if (blob_len < SSH_KEY_HEADER_SIZE) {
195 SAFE_FREE(blob);
196 return SET_ERRNO(ERROR_CRYPTO_KEY, "SSH key blob too small: %zu bytes (expected at least %d)", blob_len,
197 SSH_KEY_HEADER_SIZE);
198 }
199
200 // Extract Ed25519 public key (last 32 bytes)
201 memcpy(ed25519_pk, blob + blob_len - ED25519_PUBLIC_KEY_SIZE, ED25519_PUBLIC_KEY_SIZE);
202 SAFE_FREE(blob);
203
204 return ASCIICHAT_OK;
205}
206
207asciichat_error_t parse_ssh_private_key(const char *key_path, private_key_t *key_out) {
208 if (!key_path || !key_out) {
209 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key_path=%p, key_out=%p", key_path, key_out);
210 }
211
212 // First, check if we can get the key from ssh-agent (password-free)
213 // This requires reading the public key from the .pub file
214 char pub_key_path[BUFFER_SIZE_LARGE];
215 safe_snprintf(pub_key_path, sizeof(pub_key_path), "%s.pub", key_path);
216
217 FILE *pub_f = platform_fopen(pub_key_path, "r");
218 if (pub_f) {
219 char pub_line[BUFFER_SIZE_LARGE];
220 if (fgets(pub_line, sizeof(pub_line), pub_f)) {
221 public_key_t pub_key = {0};
222 pub_key.type = KEY_TYPE_ED25519;
223
224 if (parse_ssh_ed25519_line(pub_line, pub_key.key) == ASCIICHAT_OK) {
225 // Check if this key is in ssh-agent
226 if (ssh_agent_has_key(&pub_key)) {
227 fclose(pub_f);
228 log_dev("Key found in ssh-agent - using cached key (no password required)");
229 // Key is in agent, we can use it
230 key_out->type = KEY_TYPE_ED25519;
231 key_out->use_ssh_agent = true;
232 memcpy(key_out->key.ed25519 + 32, pub_key.key, 32); // Copy public key to second half
233 memcpy(key_out->public_key, pub_key.key, 32); // Also store in public_key field
234 // Note: We don't have the private key seed, but ssh-agent will sign for us
235 return ASCIICHAT_OK;
236 } else {
237 log_debug("Key not found in ssh-agent - will decrypt from file");
238 }
239 }
240 }
241 fclose(pub_f);
242 }
243
244 // Validate the SSH key file first
245 asciichat_error_t validation_result = validate_ssh_key_file(key_path);
246 if (validation_result != ASCIICHAT_OK) {
247 return validation_result;
248 }
249
250 // Read the private key file
251 FILE *f = platform_fopen(key_path, "r");
252 if (!f) {
253 return SET_ERRNO(ERROR_CRYPTO_KEY, "Cannot read private key file: %s", key_path);
254 }
255
256 // Read the entire file
257 char *file_content = NULL;
258 size_t file_size = 0;
259 char buffer[BUFFER_SIZE_XXLARGE];
260 size_t bytes_read;
261
262 while ((bytes_read = fread(buffer, 1, sizeof(buffer), f)) > 0) {
263 file_content = SAFE_REALLOC(file_content, file_size + bytes_read + 1, char *);
264 if (!file_content) {
265 (void)fclose(f);
266 return SET_ERRNO(ERROR_CRYPTO_KEY, "Out of memory reading private key file");
267 }
268 memcpy(file_content + file_size, buffer, bytes_read);
269 file_size += bytes_read;
270 }
271 (void)fclose(f);
272
273 if (file_size == 0) {
274 SAFE_FREE(file_content);
275 return SET_ERRNO(ERROR_CRYPTO_KEY, "Private key file is empty: %s", key_path);
276 }
277
278 file_content[file_size] = '\0';
279
280 // Use regex to extract base64 data from OpenSSH PEM format
281 char *pem_base64_data = NULL;
282 if (!crypto_regex_extract_pem_base64(file_content, &pem_base64_data)) {
283 SAFE_FREE(file_content);
284 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid OpenSSH private key format: %s", key_path);
285 }
286
287 // Remove any whitespace/newlines from the base64 data
288 char *clean_base64 = SAFE_MALLOC(strlen(pem_base64_data) + 1, char *);
289 char *clean_ptr = clean_base64;
290 for (const char *p = pem_base64_data; *p; p++) {
291 if (*p != '\n' && *p != '\r' && *p != ' ' && *p != '\t') {
292 *clean_ptr++ = *p;
293 }
294 }
295 *clean_ptr = '\0';
296 SAFE_FREE(pem_base64_data);
297
298 // Decode the base64 data
299 uint8_t *key_blob;
300 size_t key_blob_len;
301 asciichat_error_t decode_result = base64_decode_ssh_key(clean_base64, strlen(clean_base64), &key_blob, &key_blob_len);
302 SAFE_FREE(clean_base64);
303
304 if (decode_result != ASCIICHAT_OK) {
305 SAFE_FREE(file_content);
306 return decode_result;
307 }
308
309 // Parse the OpenSSH private key structure
310 // Format: [4 bytes: magic] [4 bytes: ciphername length] [ciphername] [4 bytes: kdfname length] [kdfname]
311 // [4 bytes: kdfoptions length] [kdfoptions] [4 bytes: num keys] [4 bytes: pubkey length] [pubkey]
312 // [4 bytes: privkey length] [privkey]
313
314 if (key_blob_len < 4) {
315 SAFE_FREE(key_blob);
316 SAFE_FREE(file_content);
317 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key blob too small: %s", key_path);
318 }
319
320 // Check magic number (should be "openssh-key-v1\0")
321 if (memcmp(key_blob, "openssh-key-v1\0", 15) != 0) {
322 SAFE_FREE(key_blob);
323 SAFE_FREE(file_content);
324 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid OpenSSH private key magic: %s", key_path);
325 }
326
327 size_t offset = 15; // Skip magic
328
329 // Read ciphername
330 if (offset + 4 > key_blob_len) {
331 SAFE_FREE(key_blob);
332 SAFE_FREE(file_content);
333 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at ciphername: %s", key_path);
334 }
335
336 uint32_t ciphername_len = read_u32_be(&key_blob[offset]);
337 offset += 4;
338
339 if (offset + ciphername_len > key_blob_len) {
340 SAFE_FREE(key_blob);
341 SAFE_FREE(file_content);
342 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at ciphername data: %s", key_path);
343 }
344
345 // Store the position of ciphername for later use
346 size_t ciphername_pos = offset;
347
348 // Check if key is encrypted
349 bool is_encrypted = (ciphername_len > 0 && memcmp(key_blob + offset, "none", 4) != 0);
350
351 offset += ciphername_len;
352
353 // Skip kdfname and kdfoptions (we don't support encryption)
354 if (offset + 4 > key_blob_len) {
355 SAFE_FREE(key_blob);
356 SAFE_FREE(file_content);
357 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at kdfname: %s", key_path);
358 }
359
360 uint32_t kdfname_len = read_u32_be(&key_blob[offset]);
361 offset += 4;
362
363 // Store the position of kdfname for later use
364 size_t kdfname_pos = offset;
365
366 offset += kdfname_len;
367
368 if (offset + 4 > key_blob_len) {
369 SAFE_FREE(key_blob);
370 SAFE_FREE(file_content);
371 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at kdfoptions: %s", key_path);
372 }
373
374 uint32_t kdfoptions_len = read_u32_be(&key_blob[offset]);
375 offset += 4 + kdfoptions_len;
376
377 // Handle encrypted keys
378 if (is_encrypted) {
379 // Parse the cipher name from the stored position
380 char ciphername[32] = {0};
381 if (ciphername_len > 0 && ciphername_len < sizeof(ciphername)) {
382 memcpy(ciphername, key_blob + ciphername_pos, ciphername_len);
383 }
384
385 // Parse the KDF name from the stored position
386 char kdfname[32] = {0};
387 if (kdfname_len > 0 && kdfname_len < sizeof(kdfname)) {
388 memcpy(kdfname, key_blob + kdfname_pos, kdfname_len);
389 }
390
391 log_debug("Cipher: %s, KDF: %s", ciphername, kdfname);
392
393 // Check if we support this encryption method
394 if (strcmp(ciphername, "aes256-ctr") != 0 && strcmp(ciphername, "aes256-cbc") != 0) {
395 SAFE_FREE(key_blob);
396 SAFE_FREE(file_content);
397 return SET_ERRNO(ERROR_CRYPTO_KEY,
398 "Unsupported cipher '%s' for encrypted SSH key: %s\n"
399 "Supported ciphers: aes256-ctr, aes256-cbc",
400 ciphername, key_path);
401 }
402
403 if (strcmp(kdfname, "bcrypt") != 0) {
404 SAFE_FREE(key_blob);
405 SAFE_FREE(file_content);
406 return SET_ERRNO(ERROR_CRYPTO_KEY,
407 "Unsupported KDF '%s' for encrypted SSH key: %s\n"
408 "Only bcrypt KDF is supported",
409 kdfname, key_path);
410 }
411
412 // Parse KDF options (bcrypt salt and rounds)
413 if (kdfoptions_len < 8) {
414 SAFE_FREE(key_blob);
415 SAFE_FREE(file_content);
416 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid KDF options length: %s", key_path);
417 }
418
419 // Parse KDF options: [salt_length:4][salt:N][rounds:4]
420 size_t kdf_opt_offset = offset - kdfoptions_len;
421
422 // Read salt length
423 if (kdf_opt_offset + 4 > key_blob_len) {
424 SAFE_FREE(key_blob);
425 SAFE_FREE(file_content);
426 return SET_ERRNO(ERROR_CRYPTO_KEY, "KDF options truncated at salt length: %s", key_path);
427 }
428 uint32_t salt_len = read_u32_be(&key_blob[kdf_opt_offset]);
429 kdf_opt_offset += 4;
430
431 // Read salt
432 if (salt_len != 16) {
433 SAFE_FREE(key_blob);
434 SAFE_FREE(file_content);
435 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unexpected bcrypt salt length %u (expected 16): %s", salt_len, key_path);
436 }
437 if (kdf_opt_offset + salt_len > key_blob_len) {
438 SAFE_FREE(key_blob);
439 SAFE_FREE(file_content);
440 return SET_ERRNO(ERROR_CRYPTO_KEY, "KDF options truncated at salt data: %s", key_path);
441 }
442 uint8_t bcrypt_salt[16];
443 memcpy(bcrypt_salt, key_blob + kdf_opt_offset, salt_len);
444 kdf_opt_offset += salt_len;
445
446 // Read rounds
447 if (kdf_opt_offset + 4 > key_blob_len) {
448 SAFE_FREE(key_blob);
449 SAFE_FREE(file_content);
450 return SET_ERRNO(ERROR_CRYPTO_KEY, "KDF options truncated at rounds: %s", key_path);
451 }
452 uint32_t bcrypt_rounds = read_u32_be(&key_blob[kdf_opt_offset]);
453
454 // Check for password in environment variable first
455 const char *env_password = platform_getenv("ASCII_CHAT_KEY_PASSWORD");
456 char *password = NULL;
457 if (env_password && strlen(env_password) > 0) {
458 // Use password from environment variable
459 password = platform_strdup(env_password);
460 if (!password) {
461 SAFE_FREE(key_blob);
462 SAFE_FREE(file_content);
463 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for password");
464 }
465 } else {
466 // Prompt for password interactively - allocate buffer for input
467 password = SAFE_MALLOC(1024, char *);
468 if (!password) {
469 SAFE_FREE(key_blob);
470 SAFE_FREE(file_content);
471 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for password");
472 }
473 if (prompt_password_simple("Encrypted SSH key - enter passphrase", password, 1024) != 0) {
474 SAFE_FREE(key_blob);
475 SAFE_FREE(file_content);
476 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to read passphrase for encrypted key: %s", key_path);
477 }
478 }
479
480 // Native OpenSSH key decryption using bcrypt_pbkdf + BearSSL AES
481
482 // Skip past the unencrypted public key section to get to encrypted private keys
483 // Format: [num_keys:4][pubkey_len:4][pubkey:N]...[encrypted_len:4][encrypted:N]
484
485 // Read num_keys
486 if (offset + 4 > key_blob_len) {
487 sodium_memzero(password, strlen(password));
488 SAFE_FREE(password);
489 SAFE_FREE(key_blob);
490 SAFE_FREE(file_content);
491 return SET_ERRNO(ERROR_CRYPTO_KEY, "Encrypted key truncated at num_keys: %s", key_path);
492 }
493 uint32_t num_keys = read_u32_be(&key_blob[offset]);
494 offset += 4;
495 log_debug("num_keys=%u", num_keys);
496
497 // Skip all public keys
498 for (uint32_t i = 0; i < num_keys; i++) {
499 if (offset + 4 > key_blob_len) {
500 sodium_memzero(password, strlen(password));
501 SAFE_FREE(password);
502 SAFE_FREE(key_blob);
503 SAFE_FREE(file_content);
504 return SET_ERRNO(ERROR_CRYPTO_KEY, "Encrypted key truncated at pubkey %u length: %s", i, key_path);
505 }
506 uint32_t pubkey_len = read_u32_be(&key_blob[offset]);
507 offset += 4;
508 log_debug("Skipping public key %u: %u bytes", i, pubkey_len);
509
510 if (offset + pubkey_len > key_blob_len) {
511 sodium_memzero(password, strlen(password));
512 SAFE_FREE(password);
513 SAFE_FREE(key_blob);
514 SAFE_FREE(file_content);
515 return SET_ERRNO(ERROR_CRYPTO_KEY, "Encrypted key truncated at pubkey %u data: %s", i, key_path);
516 }
517 offset += pubkey_len;
518 }
519
520 // Read encrypted private keys length
521 if (offset + 4 > key_blob_len) {
522 sodium_memzero(password, strlen(password));
523 SAFE_FREE(password);
524 SAFE_FREE(key_blob);
525 SAFE_FREE(file_content);
526 return SET_ERRNO(ERROR_CRYPTO_KEY, "Encrypted key truncated at encrypted_len: %s", key_path);
527 }
528 uint32_t encrypted_len = read_u32_be(&key_blob[offset]);
529 offset += 4;
530
531 // Now offset points to the actual encrypted data
532 size_t encrypted_data_start = offset;
533 size_t encrypted_data_len = encrypted_len;
534
535 if (encrypted_data_len < 16) {
536 sodium_memzero(password, strlen(password));
537 SAFE_FREE(password);
538 SAFE_FREE(key_blob);
539 SAFE_FREE(file_content);
540 return SET_ERRNO(ERROR_CRYPTO_KEY, "Encrypted data too small: %s", key_path);
541 }
542
543 // Extract encrypted blob (includes everything from offset onwards)
544 const uint8_t *encrypted_blob = key_blob + encrypted_data_start;
545
546 // Call native decryption function
547 uint8_t *decrypted_blob = NULL;
548 size_t decrypted_blob_len = 0;
549
550 // Note: decrypt_openssh_private_key derives IV from bcrypt_pbkdf, not from encrypted data
551 asciichat_error_t decrypt_result =
552 decrypt_openssh_private_key(encrypted_blob, encrypted_data_len, password, bcrypt_salt, salt_len, bcrypt_rounds,
553 ciphername, &decrypted_blob, &decrypted_blob_len);
554
555 // Clean up password immediately after use
556 sodium_memzero(password, strlen(password));
557 SAFE_FREE(password);
558
559 if (decrypt_result != ASCIICHAT_OK) {
560 SAFE_FREE(key_blob);
561 SAFE_FREE(file_content);
562 return decrypt_result;
563 }
564
565 // Parse the decrypted private key structure
566 // OpenSSH format (decrypted):
567 // [checkint1:4][checkint2:4][keytype:string][pubkey:string][privkey:string][comment:string][padding:N]
568
569 if (decrypted_blob_len < 8) {
570 SAFE_FREE(decrypted_blob);
571 SAFE_FREE(key_blob);
572 SAFE_FREE(file_content);
573 return SET_ERRNO(ERROR_CRYPTO_KEY, "Decrypted data too small (no checkints): %s", key_path);
574 }
575
576 // Verify checkints (first 8 bytes should be two identical 32-bit values)
577 uint32_t checkint1 = read_u32_be(&decrypted_blob[0]);
578 uint32_t checkint2 = read_u32_be(&decrypted_blob[4]);
579 if (checkint1 != checkint2) {
580 SAFE_FREE(decrypted_blob);
581 SAFE_FREE(key_blob);
582 SAFE_FREE(file_content);
583 return SET_ERRNO(ERROR_CRYPTO_KEY,
584 "Incorrect passphrase or corrupted key (checkint mismatch): %s\n"
585 "Expected matching checkints, got 0x%08x != 0x%08x",
586 key_path, checkint1, checkint2);
587 }
588
589 // Parse the decrypted private key structure manually
590 // Format after checkints: [keytype:string][pubkey:string][privkey:string][comment:string][padding:N]
591 size_t dec_offset = 8; // Skip checkints
592
593 // Read keytype length
594 if (dec_offset + 4 > decrypted_blob_len) {
595 SAFE_FREE(decrypted_blob);
596 SAFE_FREE(key_blob);
597 SAFE_FREE(file_content);
598 return SET_ERRNO(ERROR_CRYPTO_KEY, "Decrypted key truncated at keytype length: %s", key_path);
599 }
600 uint32_t keytype_len = read_u32_be(&decrypted_blob[dec_offset]);
601 dec_offset += 4;
602
603 // Read keytype
604 if (dec_offset + keytype_len > decrypted_blob_len) {
605 SAFE_FREE(decrypted_blob);
606 SAFE_FREE(key_blob);
607 SAFE_FREE(file_content);
608 return SET_ERRNO(ERROR_CRYPTO_KEY, "Decrypted key truncated at keytype data: %s", key_path);
609 }
610 char keytype[BUFFER_SIZE_SMALL / 4] = {0}; // SSH key type strings are short
611 if (keytype_len > 0 && keytype_len < sizeof(keytype)) {
612 memcpy(keytype, decrypted_blob + dec_offset, keytype_len);
613 }
614 dec_offset += keytype_len;
615
616 // Check if it's Ed25519
617 if (strcmp(keytype, "ssh-ed25519") != 0) {
618 SAFE_FREE(decrypted_blob);
619 SAFE_FREE(key_blob);
620 SAFE_FREE(file_content);
621 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported key type after decryption: '%s'", keytype);
622 }
623
624 // Read public key length
625 if (dec_offset + 4 > decrypted_blob_len) {
626 SAFE_FREE(decrypted_blob);
627 SAFE_FREE(key_blob);
628 SAFE_FREE(file_content);
629 return SET_ERRNO(ERROR_CRYPTO_KEY, "Decrypted key truncated at pubkey length: %s", key_path);
630 }
631 uint32_t pubkey_data_len = read_u32_be(&decrypted_blob[dec_offset]);
632 dec_offset += 4;
633
634 // Read public key (32 bytes for Ed25519)
635 if (pubkey_data_len != 32) {
636 SAFE_FREE(decrypted_blob);
637 SAFE_FREE(key_blob);
638 SAFE_FREE(file_content);
639 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid Ed25519 public key length: %u (expected 32)", pubkey_data_len);
640 }
641 if (dec_offset + pubkey_data_len > decrypted_blob_len) {
642 SAFE_FREE(decrypted_blob);
643 SAFE_FREE(key_blob);
644 SAFE_FREE(file_content);
645 return SET_ERRNO(ERROR_CRYPTO_KEY, "Decrypted key truncated at pubkey data: %s", key_path);
646 }
647 uint8_t ed25519_pk[32];
648 memcpy(ed25519_pk, decrypted_blob + dec_offset, 32);
649 dec_offset += 32;
650
651 // Read private key length
652 if (dec_offset + 4 > decrypted_blob_len) {
653 SAFE_FREE(decrypted_blob);
654 SAFE_FREE(key_blob);
655 SAFE_FREE(file_content);
656 return SET_ERRNO(ERROR_CRYPTO_KEY, "Decrypted key truncated at privkey length: %s", key_path);
657 }
658 uint32_t privkey_data_len = read_u32_be(&decrypted_blob[dec_offset]);
659 dec_offset += 4;
660
661 // Read private key (64 bytes for Ed25519: 32-byte seed + 32-byte public key)
662 if (privkey_data_len != 64) {
663 SAFE_FREE(decrypted_blob);
664 SAFE_FREE(key_blob);
665 SAFE_FREE(file_content);
666 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid Ed25519 private key length: %u (expected 64)", privkey_data_len);
667 }
668 if (dec_offset + privkey_data_len > decrypted_blob_len) {
669 SAFE_FREE(decrypted_blob);
670 SAFE_FREE(key_blob);
671 SAFE_FREE(file_content);
672 return SET_ERRNO(ERROR_CRYPTO_KEY, "Decrypted key truncated at privkey data: %s", key_path);
673 }
674 uint8_t ed25519_sk[64];
675 memcpy(ed25519_sk, decrypted_blob + dec_offset, 64);
676 dec_offset += 64;
677
678 // Populate key_out (ed25519_sk contains: 32-byte seed + 32-byte public key)
679 key_out->type = KEY_TYPE_ED25519;
680 memcpy(key_out->key.ed25519, ed25519_sk, 32); // Seed (first 32 bytes)
681 memcpy(key_out->key.ed25519 + 32, ed25519_sk + 32, 32); // Public key (next 32 bytes)
682
683 // Clean up decrypted data (sensitive!)
684 sodium_memzero(decrypted_blob, decrypted_blob_len);
685 SAFE_FREE(decrypted_blob);
686 SAFE_FREE(key_blob);
687 SAFE_FREE(file_content);
688
689 log_debug("Successfully parsed decrypted Ed25519 key");
690
691 // Attempt to add the decrypted key to ssh-agent for future password-free use
692 log_debug("Attempting to add decrypted key to ssh-agent");
693 asciichat_error_t agent_result = ssh_agent_add_key(key_out, key_path);
694 if (agent_result == ASCIICHAT_OK) {
695 log_debug("Successfully added key to ssh-agent - password will not be required on next run");
696 } else {
697 // Non-fatal: key is already decrypted and loaded, just won't be cached in agent
698 log_warn("Failed to add key to ssh-agent (non-fatal): %s", asciichat_error_string(agent_result));
699 log_warn("You can manually add it with: ssh-add %s", key_path);
700 }
701
702 return ASCIICHAT_OK;
703 }
704
705 // Read number of keys
706 if (offset + 4 > key_blob_len) {
707 SAFE_FREE(key_blob);
708 SAFE_FREE(file_content);
709 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at num keys: %s", key_path);
710 }
711
712 uint32_t num_keys = read_u32_be(&key_blob[offset]);
713 offset += 4;
714
715 log_debug("num_keys=%u, offset=%zu, key_blob_len=%zu", num_keys, offset, key_blob_len);
716 log_debug("Raw bytes at offset %zu: %02x %02x %02x %02x", offset, key_blob[offset], key_blob[offset + 1],
717 key_blob[offset + 2], key_blob[offset + 3]);
718 log_debug("After num_keys, offset=%zu", offset);
719
720 if (num_keys != 1) {
721 SAFE_FREE(key_blob);
722 SAFE_FREE(file_content);
723 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key contains %u keys (expected 1): %s", num_keys, key_path);
724 }
725
726 // Read public key
727 if (offset + 4 > key_blob_len) {
728 SAFE_FREE(key_blob);
729 SAFE_FREE(file_content);
730 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at pubkey length: %s", key_path);
731 }
732
733 log_debug("About to read pubkey_len at offset=%zu, bytes: %02x %02x %02x %02x", offset, key_blob[offset],
734 key_blob[offset + 1], key_blob[offset + 2], key_blob[offset + 3]);
735
736 uint32_t pubkey_len = read_u32_be(&key_blob[offset]);
737 offset += 4;
738
739 log_debug("pubkey_len=%u, offset=%zu", pubkey_len, offset);
740
741 if (offset + pubkey_len > key_blob_len) {
742 SAFE_FREE(key_blob);
743 SAFE_FREE(file_content);
744 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at pubkey data: %s", key_path);
745 }
746
747 // Parse the public key to extract the Ed25519 public key for validation
748 if (pubkey_len < 4) {
749 SAFE_FREE(key_blob);
750 SAFE_FREE(file_content);
751 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH public key too small: %s", key_path);
752 }
753
754 uint32_t key_type_len = read_u32_be(&key_blob[offset]);
755 offset += 4;
756
757 // Check if it's an Ed25519 key
758 if (key_type_len != 11 || memcmp(key_blob + offset, "ssh-ed25519", 11) != 0) {
759 SAFE_FREE(key_blob);
760 SAFE_FREE(file_content);
761 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key is not Ed25519: %s", key_path);
762 }
763
764 offset += key_type_len; // Skip the key type string
765 log_debug("After skipping key type, offset=%zu", offset);
766
767 // Read the public key length
768 if (offset + 4 > key_blob_len) {
769 SAFE_FREE(key_blob);
770 SAFE_FREE(file_content);
771 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at public key length: %s", key_path);
772 }
773
774 uint32_t pubkey_data_len = read_u32_be(&key_blob[offset]);
775 offset += 4;
776 log_debug("Public key data length: %u, offset=%zu", pubkey_data_len, offset);
777
778 log_debug("Public key data length: %u (expected 32 for Ed25519)", pubkey_data_len);
779
780 // For Ed25519, the public key should be 32 bytes, but let's be more flexible
781 if (pubkey_data_len < 32) {
782 SAFE_FREE(key_blob);
783 SAFE_FREE(file_content);
784 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH public key data too small (%u bytes, expected at least 32): %s",
785 pubkey_data_len, key_path);
786 }
787
788 // Read Ed25519 public key (first 32 bytes) for validation
789 if (offset + 32 > key_blob_len) {
790 SAFE_FREE(key_blob);
791 SAFE_FREE(file_content);
792 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at public key: %s", key_path);
793 }
794
795 uint8_t ed25519_pubkey[32];
796 memcpy(ed25519_pubkey, key_blob + offset, 32);
797 offset += pubkey_data_len; // Skip the entire public key data
798
799 // Skip the rest of the public key data to get to the private key
800 // We've already parsed: 4 (key_type_len) + 11 (ssh-ed25519) + 4 (pubkey_data_len) + 32 (key) = 51 bytes
801 // So we need to skip the remaining pubkey_len - 51 bytes
802 // Check for underflow before subtraction
803 if (pubkey_len >= 51) {
804 size_t remaining_pubkey = pubkey_len - 51;
805 if (remaining_pubkey > 0) {
806 offset += remaining_pubkey;
807 }
808 }
809
810 // Read private key
811 if (offset + 4 > key_blob_len) {
812 SAFE_FREE(key_blob);
813 SAFE_FREE(file_content);
814 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at privkey length: %s", key_path);
815 }
816
817 uint32_t privkey_len = read_u32_be(&key_blob[offset]);
818 offset += 4;
819
820 if (offset + privkey_len > key_blob_len) {
821 SAFE_FREE(key_blob);
822 SAFE_FREE(file_content);
823 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at privkey data: %s", key_path);
824 }
825
826 // Parse the private key structure
827 // Format: [4 bytes: checkint1] [4 bytes: checkint2] [4 bytes: key type length] [key type]
828 // [4 bytes: public key length] [public key] [4 bytes: private key length] [private key]
829 // [4 bytes: comment length] [comment]
830
831 if (privkey_len < 8) {
832 SAFE_FREE(key_blob);
833 SAFE_FREE(file_content);
834 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key data too small: %s", key_path);
835 }
836
837 // Verify checkints (should be equal)
838 uint32_t checkint1 = read_u32_be(&key_blob[offset]);
839 uint32_t checkint2 = read_u32_be(&key_blob[offset + 4]);
840 offset += 8;
841
842 if (checkint1 != checkint2) {
843 SAFE_FREE(key_blob);
844 SAFE_FREE(file_content);
845 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key checkints don't match: %s", key_path);
846 }
847
848 // Skip key type
849 if (offset + 4 > key_blob_len) {
850 SAFE_FREE(key_blob);
851 SAFE_FREE(file_content);
852 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at key type length: %s", key_path);
853 }
854
855 uint32_t key_type_len_priv = read_u32_be(&key_blob[offset]);
856 // Check for integer overflow before adding to offset
857 if (key_type_len_priv > key_blob_len || offset + 4 + key_type_len_priv > key_blob_len) {
858 SAFE_FREE(key_blob);
859 SAFE_FREE(file_content);
860 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key type length overflow: %u", key_type_len_priv);
861 }
862 offset += 4 + key_type_len_priv;
863
864 // Skip public key
865 if (offset + 4 > key_blob_len) {
866 SAFE_FREE(key_blob);
867 SAFE_FREE(file_content);
868 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at pubkey length: %s", key_path);
869 }
870
871 uint32_t pubkey_len_priv = read_u32_be(&key_blob[offset]);
872 // Check for integer overflow before adding to offset
873 if (pubkey_len_priv > key_blob_len || offset + 4 + pubkey_len_priv > key_blob_len) {
874 SAFE_FREE(key_blob);
875 SAFE_FREE(file_content);
876 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key public key length overflow: %u", pubkey_len_priv);
877 }
878 offset += 4 + pubkey_len_priv;
879
880 // Read private key
881 if (offset + 4 > key_blob_len) {
882 SAFE_FREE(key_blob);
883 SAFE_FREE(file_content);
884 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at privkey length: %s", key_path);
885 }
886
887 uint32_t privkey_data_len = read_u32_be(&key_blob[offset]);
888 offset += 4;
889
890 if (offset + privkey_data_len > key_blob_len) {
891 SAFE_FREE(key_blob);
892 SAFE_FREE(file_content);
893 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at privkey data: %s", key_path);
894 }
895
896 // The private key data should be at least 64 bytes (32 bytes private key + 32 bytes public key)
897 // But OpenSSH format may have additional data
898 if (privkey_data_len < 64) {
899 SAFE_FREE(key_blob);
900 SAFE_FREE(file_content);
901 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key data length is %u (expected at least 64): %s",
902 privkey_data_len, key_path);
903 }
904
905 // Extract the Ed25519 private key (first 32 bytes)
906 uint8_t ed25519_privkey[32];
907 memcpy(ed25519_privkey, key_blob + offset, 32);
908
909 // Verify the public key matches
910 // The public key in the privkey section is raw Ed25519, while the one in pubkey section is SSH format
911 // We need to compare the raw public key from privkey with the raw public key extracted from pubkey
912 // Use constant-time comparison for cryptographic material
913 if (sodium_memcmp(key_blob + offset + 32, ed25519_pubkey, 32) != 0) {
914 SAFE_FREE(key_blob);
915 SAFE_FREE(file_content);
916 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key public key mismatch: %s", key_path);
917 }
918
919 // Initialize the private key structure
920 memset(key_out, 0, sizeof(private_key_t));
921 key_out->type = KEY_TYPE_ED25519;
922
923 // Store the actual private key (seed + public key = 64 bytes)
924 // Ed25519 private key format: [32 bytes seed][32 bytes public key]
925 memcpy(key_out->key.ed25519, ed25519_privkey, 32); // Store the seed (first 32 bytes)
926 memcpy(key_out->key.ed25519 + 32, ed25519_pubkey, 32); // Store the public key (next 32 bytes)
927
928 // Also store the public key in the public_key field for easy access
929 memcpy(key_out->public_key, ed25519_pubkey, 32);
930
931 // Set a comment
932 SAFE_STRNCPY(key_out->key_comment, "ssh-ed25519", sizeof(key_out->key_comment) - 1);
933
934 SAFE_FREE(key_blob);
935 SAFE_FREE(file_content);
936
937 return ASCIICHAT_OK;
938}
939
940asciichat_error_t validate_ssh_key_file(const char *key_path) {
941 if (!key_path) {
942 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key_path=%p", key_path);
943 }
944
945 if (!path_looks_like_path(key_path)) {
946 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid SSH key path: %s", key_path);
947 }
948
949 char *normalized_path = NULL;
950 asciichat_error_t path_result = path_validate_user_path(key_path, PATH_ROLE_KEY_PRIVATE, &normalized_path);
951 if (path_result != ASCIICHAT_OK) {
952 SAFE_FREE(normalized_path);
953 return path_result;
954 }
955
956 // Check if file exists and is readable
957 FILE *test_file = platform_fopen(normalized_path, "r");
958 if (test_file == NULL) {
959 SAFE_FREE(normalized_path);
960 return SET_ERRNO(ERROR_CRYPTO_KEY, "Cannot read key file: %s", key_path);
961 }
962
963 // Check if this is an SSH key file or GPG armored key by looking for the header
964 char header[BUFFER_SIZE_SMALL];
965 bool is_valid_key_file = false;
966 if (fgets(header, sizeof(header), test_file) != NULL) {
967 if (strstr(header, "BEGIN OPENSSH PRIVATE KEY") != NULL || strstr(header, "BEGIN RSA PRIVATE KEY") != NULL ||
968 strstr(header, "BEGIN EC PRIVATE KEY") != NULL || strstr(header, "BEGIN PGP PRIVATE KEY") != NULL ||
969 strstr(header, "BEGIN PGP SECRET KEY") != NULL) {
970 is_valid_key_file = true;
971 }
972 }
973 (void)fclose(test_file);
974
975 if (!is_valid_key_file) {
976 SAFE_FREE(normalized_path);
977 return SET_ERRNO(ERROR_CRYPTO_KEY, "File is not a valid SSH or GPG key: %s", key_path);
978 }
979
980 // Check permissions for SSH key files (should be 600 or 400)
981#ifndef _WIN32
982 struct stat st;
983 if (stat(normalized_path, &st) == 0) {
984 if ((st.st_mode & SSH_KEY_PERMISSIONS_MASK) != 0) {
985 log_error("SSH key file %s has overly permissive permissions: %o", key_path, st.st_mode & 0777);
986 log_error("Run 'chmod 600 %s' to fix this", key_path);
987 SAFE_FREE(normalized_path);
988 return SET_ERRNO(ERROR_CRYPTO_KEY, "SSH key file has overly permissive permissions: %s", key_path);
989 }
990 }
991#endif
992
993 SAFE_FREE(normalized_path);
994 return ASCIICHAT_OK;
995}
996
997// =============================================================================
998// Key Conversion Functions
999// =============================================================================
1000
1001asciichat_error_t ed25519_to_x25519_public(const uint8_t ed25519_pk[32], uint8_t x25519_pk[32]) {
1002 if (!ed25519_pk || !x25519_pk) {
1003 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ed25519_pk=%p, x25519_pk=%p", ed25519_pk, x25519_pk);
1004 }
1005
1006 // Convert Ed25519 public key to X25519 public key
1007 if (crypto_sign_ed25519_pk_to_curve25519(x25519_pk, ed25519_pk) != 0) {
1008 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to convert Ed25519 public key to X25519");
1009 }
1010
1011 return ASCIICHAT_OK;
1012}
1013
1014asciichat_error_t ed25519_to_x25519_private(const uint8_t ed25519_sk[64], uint8_t x25519_sk[32]) {
1015 if (!ed25519_sk || !x25519_sk) {
1016 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ed25519_sk=%p, x25519_sk=%p", ed25519_sk, x25519_sk);
1017 }
1018
1019 // Convert Ed25519 private key to X25519 private key
1020 if (crypto_sign_ed25519_sk_to_curve25519(x25519_sk, ed25519_sk) != 0) {
1021 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to convert Ed25519 private key to X25519");
1022 }
1023
1024 return ASCIICHAT_OK;
1025}
1026
1027// =============================================================================
1028// SSH Key Operations
1029// =============================================================================
1030
1031asciichat_error_t ed25519_sign_message(const private_key_t *key, const uint8_t *message, size_t message_len,
1032 uint8_t signature[64]) {
1033 if (!key || !message || !signature) {
1034 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key=%p, message=%p, signature=%p", key, message,
1035 signature);
1036 }
1037
1038 if (key->type != KEY_TYPE_ED25519) {
1039 return SET_ERRNO(ERROR_CRYPTO_KEY, "Key is not an Ed25519 key");
1040 }
1041
1042 // If using GPG agent, delegate signing to GPG agent
1043 if (key->use_gpg_agent) {
1044 log_debug("Using GPG agent for Ed25519 signing (keygrip: %.40s)", key->gpg_keygrip);
1045
1046 // Connect to GPG agent
1047 int agent_sock = gpg_agent_connect();
1048 log_debug("GPG agent connection result: %d", agent_sock);
1049
1050 if (agent_sock < 0) {
1051 log_debug("GPG agent not available, falling back to gpg --detach-sign for signing");
1052
1053 // Extract GPG key ID from key comment ("GPG key <key_id>")
1054 const char *key_id_prefix = "GPG key ";
1055 const char *key_id = NULL;
1056 if (strncmp(key->key_comment, key_id_prefix, strlen(key_id_prefix)) == 0) {
1057 key_id = key->key_comment + strlen(key_id_prefix);
1058 }
1059
1060 log_debug("Extracted GPG key ID: %s (len=%zu)", key_id ? key_id : "NULL", key_id ? strlen(key_id) : 0);
1061
1062 if (!key_id || strlen(key_id) != 16) {
1063 return SET_ERRNO(ERROR_CRYPTO, "Cannot extract GPG key ID from comment for fallback signing");
1064 }
1065
1066 // Use gpg --detach-sign fallback
1067 log_debug("Calling gpg_sign_detached_ed25519 with key %s", key_id);
1068 int result = gpg_sign_detached_ed25519(key_id, message, message_len, signature);
1069 log_debug("gpg_sign_detached_ed25519 result: %d", result);
1070
1071 if (result != 0) {
1072 return SET_ERRNO(ERROR_CRYPTO, "GPG fallback signing failed (both agent and gpg command failed)");
1073 }
1074
1075 log_debug("Successfully signed message with gpg --detach-sign fallback (64 bytes)");
1076 return ASCIICHAT_OK;
1077 }
1078
1079 log_debug("Using GPG agent for signing");
1080
1081 // Sign the message using GPG agent
1082 size_t sig_len = 0;
1083 int result = gpg_agent_sign(agent_sock, key->gpg_keygrip, message, message_len, signature, &sig_len);
1084
1085 // Disconnect from agent
1086 gpg_agent_disconnect(agent_sock);
1087
1088 if (result != 0) {
1089 return SET_ERRNO(ERROR_CRYPTO, "GPG agent signing failed");
1090 }
1091
1092 if (sig_len != 64) {
1093 return SET_ERRNO(ERROR_CRYPTO, "GPG agent returned invalid signature length: %zu (expected 64)", sig_len);
1094 }
1095
1096 log_debug("Successfully signed message with GPG agent (64 bytes)");
1097 return ASCIICHAT_OK;
1098 }
1099
1100 // If using SSH agent, delegate signing to SSH agent
1101 if (key->use_ssh_agent) {
1102 log_debug("Using SSH agent for Ed25519 signing");
1103
1104 // Create a public_key_t from the stored public key
1105 public_key_t pub_key = {0};
1106 pub_key.type = KEY_TYPE_ED25519;
1107 memcpy(pub_key.key, key->public_key, 32);
1108
1109 // Sign using SSH agent
1110 asciichat_error_t result = ssh_agent_sign(&pub_key, message, message_len, signature);
1111 if (result != ASCIICHAT_OK) {
1112 return result; // Error already set by ssh_agent_sign
1113 }
1114
1115 log_debug("Successfully signed message with SSH agent (64 bytes)");
1116 return ASCIICHAT_OK;
1117 }
1118
1119 // Sign the message with Ed25519 (in-memory key)
1120 if (crypto_sign_detached(signature, NULL, message, message_len, key->key.ed25519) != 0) {
1121 return SET_ERRNO(ERROR_CRYPTO, "Failed to sign message with Ed25519");
1122 }
1123
1124 return ASCIICHAT_OK;
1125}
1126
1127asciichat_error_t ed25519_verify_signature(const uint8_t public_key[32], const uint8_t *message, size_t message_len,
1128 const uint8_t signature[64], const char *gpg_key_id) {
1129 if (!public_key || !message || !signature) {
1130 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: public_key=%p, message=%p, signature=%p", public_key,
1131 message, signature);
1132 }
1133
1134 // Try standard Ed25519 verification first
1135 if (crypto_sign_verify_detached(signature, message, message_len, public_key) == 0) {
1136 return ASCIICHAT_OK;
1137 }
1138
1139 // If standard verification fails, try GPG fallback (for GPG-signed messages)
1140 // Use provided gpg_key_id if available, otherwise check environment variable (for tests)
1141 const char *key_id_to_use = gpg_key_id;
1142 size_t key_id_len = key_id_to_use ? strlen(key_id_to_use) : 0;
1143 // Accept 8, 16, or 40 character GPG key IDs (short, long, or full fingerprint)
1144 bool valid_key_id = (key_id_len == 8 || key_id_len == 16 || key_id_len == 40);
1145
1146 if (!valid_key_id) {
1147 key_id_to_use = platform_getenv("TEST_GPG_KEY_ID");
1148 key_id_len = key_id_to_use ? strlen(key_id_to_use) : 0;
1149 valid_key_id = (key_id_len == 8 || key_id_len == 16 || key_id_len == 40);
1150 }
1151
1152 if (valid_key_id) {
1153 log_debug("Standard Ed25519 verification failed, trying GPG fallback with key %s", key_id_to_use);
1154 int gpg_result = gpg_verify_detached_ed25519(key_id_to_use, message, message_len, signature);
1155 if (gpg_result == 0) {
1156 log_debug("GPG verification succeeded");
1157 return ASCIICHAT_OK;
1158 }
1159 log_debug("GPG verification also failed");
1160 }
1161
1162 return SET_ERRNO(ERROR_CRYPTO, "Ed25519 signature verification failed (tried both libsodium and GPG)");
1163}
int gpg_agent_sign(int handle_as_int, const char *keygrip, const uint8_t *message, size_t message_len, uint8_t *signature_out, size_t *signature_len_out)
Definition agent.c:154
int gpg_agent_connect(void)
Definition agent.c:96
void gpg_agent_disconnect(int handle_as_int)
Definition agent.c:146
int prompt_password_simple(const char *prompt, char *password, size_t max_len)
Definition password.c:70
bool path_looks_like_path(const char *value)
Definition path.c:766
asciichat_error_t path_validate_user_path(const char *input, path_role_t role, char **normalized_out)
Definition path.c:974
char * platform_strdup(const char *s)
bool crypto_regex_match_public_key(const char *line, char **base64_key_out, char **comment_out)
Definition regex.c:184
bool crypto_regex_extract_pem_base64(const char *file_content, char **base64_data_out)
Definition regex.c:227
int gpg_sign_detached_ed25519(const char *key_id, const uint8_t *message, size_t message_len, uint8_t signature_out[64])
Definition signing.c:140
bool ssh_agent_has_key(const public_key_t *public_key)
Definition ssh_agent.c:50
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
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)
Definition ssh_keys.c:1127
asciichat_error_t ed25519_to_x25519_private(const uint8_t ed25519_sk[64], uint8_t x25519_sk[32])
Definition ssh_keys.c:1014
asciichat_error_t ed25519_to_x25519_public(const uint8_t ed25519_pk[32], uint8_t x25519_pk[32])
Definition ssh_keys.c:1001
asciichat_error_t parse_ssh_ed25519_line(const char *line, uint8_t ed25519_pk[32])
Definition ssh_keys.c:164
asciichat_error_t ed25519_sign_message(const private_key_t *key, const uint8_t *message, size_t message_len, uint8_t signature[64])
Definition ssh_keys.c:1031
asciichat_error_t parse_ssh_private_key(const char *key_path, private_key_t *key_out)
Definition ssh_keys.c:207
asciichat_error_t validate_ssh_key_file(const char *key_path)
Definition ssh_keys.c:940
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
int gpg_verify_detached_ed25519(const char *key_id, const uint8_t *message, size_t message_len, const uint8_t signature[64])
FILE * platform_fopen(const char *filename, const char *mode)
const char * platform_getenv(const char *name)
Definition wasm/system.c:13