251 {
252 if (!key_id || !public_key_out) {
253 log_error("Invalid arguments to gpg_get_public_key");
254 return -1;
255 }
256
257
258
260 log_error("Invalid GPG key ID format - contains unsafe characters: %s", key_id);
261 return -1;
262 }
263
264
265 for (size_t i = 0; key_id[i] != '\0'; i++) {
266 if (!isxdigit((unsigned char)key_id[i])) {
267 log_error("Invalid GPG key ID format - must be hexadecimal: %s", key_id);
268 return -1;
269 }
270 }
271
272
273 char escaped_key_id[BUFFER_SIZE_MEDIUM];
275 log_error("Failed to escape GPG key ID for shell command");
276 return -1;
277 }
278
279
280 char cmd[BUFFER_SIZE_LARGE];
281 safe_snprintf(cmd,
sizeof(cmd),
"gpg --list-keys --with-keygrip --with-colons 0x%s " PLATFORM_SHELL_NULL_REDIRECT,
282 escaped_key_id);
283
284 FILE *fp = NULL;
285 if (platform_popen(cmd, "r", &fp) != ASCIICHAT_OK || !fp) {
286 log_error("Failed to run gpg command - GPG may not be installed");
287#ifdef _WIN32
288 log_error("To install GPG on Windows, download Gpg4win from:");
289 log_error(" https://www.gpg4win.org/download.html");
290#elif defined(__APPLE__)
291 log_error("To install GPG on macOS, use Homebrew:");
292 log_error(" brew install gnupg");
293#else
294 log_error("To install GPG on Linux:");
295 log_error(" Debian/Ubuntu: sudo apt-get install gnupg");
296 log_error(" Fedora/RHEL: sudo dnf install gnupg2");
297 log_error(" Arch Linux: sudo pacman -S gnupg");
298 log_error(" Alpine Linux: sudo apk add gnupg");
299#endif
300 return -1;
301 }
302
303 char line[BUFFER_SIZE_XLARGE];
304 char found_keygrip[128] = {0};
305 bool found_key = false;
306
307
308
309 while (fgets(line, sizeof(line), fp)) {
310 if (strncmp(line, "pub:", 4) == 0) {
311
312 found_key = true;
313 } else if (found_key && strncmp(line, "grp:", 4) == 0) {
314
315
316 char *keygrip_extracted = NULL;
317
319
320 SAFE_STRNCPY(found_keygrip, keygrip_extracted, sizeof(found_keygrip));
321 if (keygrip_out) {
322 SAFE_STRNCPY(keygrip_out, found_keygrip, 41);
323 }
324 SAFE_FREE(keygrip_extracted);
325 } else {
326
327 const char *grp_start = line + 4;
328 int colon_count = 0;
329 while (*grp_start && colon_count < 8) {
330 if (*grp_start == ':') {
331 colon_count++;
332 }
333 grp_start++;
334 }
335
336 if (colon_count == 8) {
337 const char *grp_end = strchr(grp_start, ':');
338 if (grp_end) {
339 size_t grp_len = grp_end - grp_start;
340 if (grp_len < sizeof(found_keygrip)) {
341 memcpy(found_keygrip, grp_start, grp_len);
342 found_keygrip[grp_len] = '\0';
343
344 if (keygrip_out) {
345 SAFE_STRNCPY(keygrip_out, found_keygrip, 41);
346 }
347 }
348 }
349 }
350 }
351 break;
352 }
353 }
354
355 platform_pclose(&fp);
356
357 if (!found_key || strlen(found_keygrip) == 0) {
358 log_error("Could not find GPG key with ID: %s", key_id);
359 return -1;
360 }
361
362 log_debug("Found keygrip for key %s: %s", key_id, found_keygrip);
363
364
366 if (agent_sock < 0) {
367 log_debug("GPG agent not available, falling back to gpg --export for public key extraction");
368
369 int export_result = gpg_export_public_key(key_id, public_key_out);
370 if (export_result == 0) {
371 log_debug("Successfully extracted public key using fallback method");
372 } else {
373 log_error("Fallback public key extraction failed for key ID: %s", key_id);
374 }
375 return export_result;
376 }
377
378
379 char readkey_cmd[BUFFER_SIZE_SMALL];
380 safe_snprintf(readkey_cmd,
sizeof(readkey_cmd),
"READKEY %s\n", found_keygrip);
381
382
383 pipe_t agent_pipe = (pipe_t)(intptr_t)agent_sock;
384 ssize_t
bytes_written = platform_pipe_write(agent_pipe, (
const unsigned char *)readkey_cmd, strlen(readkey_cmd));
386 log_error("Failed to send READKEY command to GPG agent");
388 return -1;
389 }
390
391
392 char response[BUFFER_SIZE_XXXLARGE];
393 memset(response, 0, sizeof(response));
394 ssize_t bytes_read = platform_pipe_read(agent_pipe, (unsigned char *)response, sizeof(response) - 1);
395
397
398 if (bytes_read <= 0) {
399 log_error("Failed to read READKEY response from GPG agent");
400 return -1;
401 }
402
403
404
405
406 const char *q_marker = strstr(response, "(1:q");
407 if (!q_marker) {
408 log_warn("Failed to find public key (1:q) in GPG agent READKEY response, trying gpg --export fallback");
409 log_debug("Response was: %.*s", (int)(bytes_read < 200 ? bytes_read : 200), response);
411
412
413 int export_result = gpg_export_public_key(key_id, public_key_out);
414 if (export_result == 0) {
415 log_debug("Successfully extracted public key using gpg --export fallback");
416 } else {
417 log_error("Fallback public key extraction failed for key ID: %s", key_id);
418 }
419 return export_result;
420 }
421
422
423 const char *len_start = q_marker + 4;
424
425
426 char *colon = strchr(len_start, ':');
427 if (!colon) {
428 log_error("Malformed S-expression: missing colon after length");
429 return -1;
430 }
431
432 size_t key_len = strtoul(len_start, NULL, 10);
433 if (key_len != 33) {
434 log_error("Unexpected Ed25519 public key length: %zu bytes (expected 33)", key_len);
435 return -1;
436 }
437
438
439 const unsigned char *binary_start = (const unsigned char *)(colon + 1);
440
441
442 if (binary_start[0] != 0x40) {
443 log_error("Invalid Ed25519 public key prefix: 0x%02x (expected 0x40)", binary_start[0]);
444 return -1;
445 }
446
447
448 memcpy(public_key_out, binary_start + 1, 32);
449
450 log_debug("Extracted Ed25519 public key from GPG agent via READKEY command");
451 return 0;
452}
int gpg_agent_connect(void)
void gpg_agent_disconnect(int handle_as_int)
_Atomic uint64_t bytes_written
bool crypto_regex_extract_gpg_keygrip(const char *line, char **keygrip_out)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
bool escape_shell_single_quotes(const char *str, char *out_buffer, size_t out_buffer_size)
bool validate_shell_safe(const char *str, const char *allowed_chars)