From 5fc877dfbdfd6b0961882798f20e7d89a52e8492 Mon Sep 17 00:00:00 2001 From: Kai Engert Date: Sun, 29 Mar 2026 22:41:18 +0200 Subject: [PATCH] Add --export-autocrypt-key command to rnpkeys CLI and a test. Co-Authored-By: Claude Sonnet 4.6 --- src/lib/rnp.cpp | 3 ++ src/rnp/fficli.cpp | 68 +++++++++++++++++++++++++++++++++++++++++ src/rnp/fficli.h | 1 + src/rnpkeys/rnpkeys.cpp | 11 +++++++ src/rnpkeys/rnpkeys.h | 1 + src/tests/exportkey.cpp | 52 +++++++++++++++++++++++++++++++ 6 files changed, 136 insertions(+) diff --git a/src/lib/rnp.cpp b/src/lib/rnp.cpp index 6a8d6223ce..1afc45c200 100644 --- a/src/lib/rnp.cpp +++ b/src/lib/rnp.cpp @@ -3982,6 +3982,9 @@ try { } else { res = primary->write_autocrypt(output->dst, *sub, uididx); } + if (res) { + output->keep = true; + } return res ? RNP_SUCCESS : RNP_ERROR_BAD_PARAMETERS; } FFI_GUARD diff --git a/src/rnp/fficli.cpp b/src/rnp/fficli.cpp index 01e61b194a..f39a0f2ced 100644 --- a/src/rnp/fficli.cpp +++ b/src/rnp/fficli.cpp @@ -2295,6 +2295,74 @@ cli_rnp_export_keys(cli_rnp_t *rnp, const char *filter) return result; } +bool +cli_rnp_export_autocrypt_key(cli_rnp_t *rnp, const char *filter) +{ + std::vector keys; + if (!rnp->keys_matching(keys, filter ? filter : std::string(), 0)) { + ERR_MSG("Key(s) matching '%s' not found.", filter); + return false; + } + + /* Explicit --userid takes priority for UID selection. */ + std::string explicit_uid; + if (rnp->cfg().get_count(CFG_USERID) > 0) { + explicit_uid = rnp->cfg().get_str(CFG_USERID, 0); + } + + rnp_output_t output = NULL; + bool result = false; + + output = cli_rnp_output_to_specifier(*rnp, rnp->cfg().get_str(CFG_OUTFILE)); + if (!output) { + goto done; + } + + for (auto key : keys) { + bool primary = false; + if (rnp_key_is_primary(key, &primary)) { + goto done; + } + if (!primary) { + continue; + } + + /* Determine which UID to embed. If --userid was given, pass it directly. + * Otherwise search the key's UIDs for one containing the filter string; + * fall back to the first UID if none match. */ + const char *uid = NULL; + std::string auto_uid; + if (!explicit_uid.empty()) { + uid = explicit_uid.c_str(); + } else { + size_t uid_count = 0; + rnp_key_get_uid_count(key, &uid_count); + for (size_t i = 0; i < uid_count && auto_uid.empty(); i++) { + char *uid_str = NULL; + if (rnp_key_get_uid_at(key, i, &uid_str)) { + continue; + } + if (!filter || strstr(uid_str, filter)) { + auto_uid = uid_str; + } + rnp_buffer_destroy(uid_str); + } + if (!auto_uid.empty()) { + uid = auto_uid.c_str(); + } + } + + if (rnp_key_export_autocrypt(key, NULL, uid, output, 0)) { + goto done; + } + } + result = true; +done: + rnp_output_destroy(output); + clear_key_handles(keys); + return result; +} + bool cli_rnp_export_revocation(cli_rnp_t *rnp, const char *key) { diff --git a/src/rnp/fficli.h b/src/rnp/fficli.h index 42c340d22b..ea78a43ddd 100644 --- a/src/rnp/fficli.h +++ b/src/rnp/fficli.h @@ -225,6 +225,7 @@ void cli_rnp_print_key_info( bool cli_rnp_set_generate_params(rnp_cfg &cfg, bool subkey = false); bool cli_rnp_generate_key(cli_rnp_t *rnp, const char *username); bool cli_rnp_export_keys(cli_rnp_t *rnp, const char *filter); +bool cli_rnp_export_autocrypt_key(cli_rnp_t *rnp, const char *filter); bool cli_rnp_export_revocation(cli_rnp_t *rnp, const char *key); bool cli_rnp_revoke_key(cli_rnp_t *rnp, const char *key); bool cli_rnp_remove_key(cli_rnp_t *rnp, const char *key); diff --git a/src/rnpkeys/rnpkeys.cpp b/src/rnpkeys/rnpkeys.cpp index 6263bd68af..935efd56b2 100644 --- a/src/rnpkeys/rnpkeys.cpp +++ b/src/rnpkeys/rnpkeys.cpp @@ -65,6 +65,8 @@ const char *usage = " --permissive Skip erroring keys/sigs instead of failing.\n" " --export-key Export a key.\n" " --secret Export a secret key instead of a public.\n" + " --export-autocrypt-key Export a key in Autocrypt format.\n" + " --userid Override which UID to embed in the Autocrypt export.\n" " --export-rev Export a key's revocation.\n" " --rev-type Set revocation type.\n" " --rev-reason Human-readable reason for revocation.\n" @@ -96,6 +98,7 @@ struct option options[] = { {"list-keys", no_argument, NULL, CMD_LIST_KEYS}, {"export", no_argument, NULL, CMD_EXPORT_KEY}, {"export-key", optional_argument, NULL, CMD_EXPORT_KEY}, + {"export-autocrypt-key", optional_argument, NULL, CMD_EXPORT_AUTOCRYPT_KEY}, {"import", no_argument, NULL, CMD_IMPORT}, {"import-key", no_argument, NULL, CMD_IMPORT_KEYS}, {"import-keys", no_argument, NULL, CMD_IMPORT_KEYS}, @@ -413,6 +416,13 @@ rnp_cmd(cli_rnp_t *rnp, optdefs_t cmd, const char *f) } return cli_rnp_export_keys(rnp, f); } + case CMD_EXPORT_AUTOCRYPT_KEY: { + if (!f && rnp->cfg().get_count(CFG_USERID)) { + fs = rnp->cfg().get_str(CFG_USERID, 0); + f = fs.c_str(); + } + return cli_rnp_export_autocrypt_key(rnp, f); + } case CMD_IMPORT: case CMD_IMPORT_KEYS: case CMD_IMPORT_SIGS: @@ -488,6 +498,7 @@ setoption(rnp_cfg &cfg, optdefs_t *cmd, int val, const char *arg) return true; case CMD_LIST_KEYS: case CMD_EXPORT_KEY: + case CMD_EXPORT_AUTOCRYPT_KEY: case CMD_EXPORT_REV: case CMD_REVOKE_KEY: case CMD_REMOVE_KEY: diff --git a/src/rnpkeys/rnpkeys.h b/src/rnpkeys/rnpkeys.h index f1206f9d03..8935ac07f9 100644 --- a/src/rnpkeys/rnpkeys.h +++ b/src/rnpkeys/rnpkeys.h @@ -17,6 +17,7 @@ typedef enum { CMD_NONE = 0, CMD_LIST_KEYS = 260, CMD_EXPORT_KEY, + CMD_EXPORT_AUTOCRYPT_KEY, CMD_IMPORT, CMD_IMPORT_KEYS, CMD_IMPORT_SIGS, diff --git a/src/tests/exportkey.cpp b/src/tests/exportkey.cpp index 40faa30356..c3cd3edff8 100644 --- a/src/tests/exportkey.cpp +++ b/src/tests/exportkey.cpp @@ -29,6 +29,7 @@ #include #include "rnp_tests.h" #include "support.h" +#include "../rnp/fficli.h" TEST_F(rnp_tests, rnpkeys_exportkey_verifyUserId) { @@ -66,3 +67,54 @@ TEST_F(rnp_tests, rnpkeys_exportkey_verifyUserId) } rnp.end(); // Free memory and other allocated resources. } + +TEST_F(rnp_tests, rnpkeys_exportkey_autocrypt) +{ + /* Set up CLI rnp pointing at keyring 1 (has key 7bc6709b15c23a4a with multiple UIDs + * and an encryption subkey 8a05b89fad5aded1). */ + cli_rnp_t rnp = {}; + int pipefd[2] = {-1, -1}; + assert_true( + setup_cli_rnp_common(&rnp, RNP_KEYSTORE_GPG, "data/keyrings/1", pipefd)); + assert_true(rnp.load_keyrings(false)); + + /* Export with explicit keyid filter and userid, verify output is valid. */ + rnp.cfg().set_str(CFG_OUTFILE, "ac-out.pgp"); + rnp.cfg().add_str(CFG_USERID, "key0-uid0"); + assert_true(cli_rnp_export_autocrypt_key(&rnp, "7bc6709b15c23a4a")); + + /* Re-import the exported data into a fresh FFI and verify structure. */ + rnp_ffi_t ffi = NULL; + assert_rnp_success(rnp_ffi_create(&ffi, "GPG", "GPG")); + assert_true(import_all_keys(ffi, "ac-out.pgp")); + size_t count = 0; + assert_rnp_success(rnp_get_public_key_count(ffi, &count)); + assert_int_equal(count, 2); + assert_rnp_success(rnp_get_secret_key_count(ffi, &count)); + assert_int_equal(count, 0); + rnp_key_handle_t key = NULL; + assert_rnp_success(rnp_locate_key(ffi, "keyid", "7bc6709b15c23a4a", &key)); + assert_non_null(key); + rnp_key_handle_t sub = NULL; + assert_rnp_success(rnp_locate_key(ffi, "keyid", "8a05b89fad5aded1", &sub)); + assert_non_null(sub); + /* Only one UID should be present in the autocrypt export. */ + assert_rnp_success(rnp_key_get_uid_count(key, &count)); + assert_int_equal(count, 1); + char *uid_str = NULL; + assert_rnp_success(rnp_key_get_uid_at(key, 0, &uid_str)); + assert_string_equal(uid_str, "key0-uid0"); + rnp_buffer_destroy(uid_str); + rnp_key_handle_destroy(sub); + rnp_key_handle_destroy(key); + rnp_ffi_destroy(ffi); + + /* A non-matching filter should return false. */ + rnp.cfg().set_str(CFG_OUTFILE, "ac-out2.pgp"); + assert_false(cli_rnp_export_autocrypt_key(&rnp, "nosuchkey")); + + if (pipefd[0] != -1) { + close(pipefd[0]); + } + rnp.end(); +}