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