/* * Copyright (c) 2017-2021, [Ribose Inc](https://www.ribose.com). * Copyright (c) 2009 The NetBSD Foundation, Inc. * All rights reserved. * * This code is originally derived from software contributed to * The NetBSD Foundation by Alistair Crooks (agc@netbsd.org), and * carried further by Ribose Inc (https://www.ribose.com). * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* Command line program to perform rnp operations */ #ifdef _MSC_VER #include "uniwin.h" #else #include #endif #include #include #include "rnpkeys.h" #include "str-utils.h" #include const char *usage = "Manipulate OpenPGP keys and keyrings.\n" "Usage: rnpkeys --command [options] [files]\n" "Commands:\n" " -h, --help This help message.\n" " -V, --version Print RNP version information.\n" " -g, --generate-key Generate a new keypair (default is RSA).\n" " --userid Specify key's userid.\n" " --expert Select key type, size, and additional parameters.\n" " --numbits Override default key size (2048).\n" " --expiration Set key and subkey expiration time.\n" " --cipher Set cipher used to encrypt a secret key.\n" " --hash Set hash which is used for key derivation.\n" " --allow-weak-hash Allow usage of a weak hash algorithm.\n" " -l, --list-keys List keys in the keyrings.\n" " --secret List secret keys instead of public ones.\n" " --with-sigs List signatures as well.\n" " --import Import keys or signatures.\n" " --import-keys Import keys.\n" " --import-sigs Import signatures.\n" " --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-rev Export a key's revocation.\n" " --rev-type Set revocation type.\n" " --rev-reason Human-readable reason for revocation.\n" " --revoke-key Revoke a key specified.\n" " --remove-key Remove a key specified.\n" " --edit-key Edit key properties.\n" " --add-subkey Add new subkey.\n" " --check-cv25519-bits Check whether Cv25519 subkey bits are correct.\n" " --fix-cv25519-bits Fix Cv25519 subkey bits.\n" " --set-expire Set key expiration time.\n" "\n" "Other options:\n" " --homedir Override home directory (default is ~/.rnp/).\n" " --password Password, which should be used during operation.\n" " --pass-fd Read password(s) from the file descriptor.\n" " --force Force operation (like secret key removal).\n" " --output [file, -] Write data to the specified file or stdout.\n" " --overwrite Overwrite output file without a prompt.\n" " --notty Do not write anything to the TTY.\n" " --current-time Override system's time.\n" "\n" "See man page for a detailed listing and explanation.\n" "\n"; struct option options[] = { /* key-management commands */ {"list-keys", no_argument, NULL, CMD_LIST_KEYS}, {"export", no_argument, NULL, CMD_EXPORT_KEY}, {"export-key", optional_argument, NULL, CMD_EXPORT_KEY}, {"import", no_argument, NULL, CMD_IMPORT}, {"import-key", no_argument, NULL, CMD_IMPORT_KEYS}, {"import-keys", no_argument, NULL, CMD_IMPORT_KEYS}, {"import-sigs", no_argument, NULL, CMD_IMPORT_SIGS}, {"gen", optional_argument, NULL, CMD_GENERATE_KEY}, {"gen-key", optional_argument, NULL, CMD_GENERATE_KEY}, {"generate", optional_argument, NULL, CMD_GENERATE_KEY}, {"generate-key", optional_argument, NULL, CMD_GENERATE_KEY}, {"export-rev", no_argument, NULL, CMD_EXPORT_REV}, {"export-revocation", no_argument, NULL, CMD_EXPORT_REV}, {"revoke-key", no_argument, NULL, CMD_REVOKE_KEY}, {"remove-key", no_argument, NULL, CMD_REMOVE_KEY}, {"edit-key", no_argument, NULL, CMD_EDIT_KEY}, /* debugging commands */ {"help", no_argument, NULL, CMD_HELP}, {"version", no_argument, NULL, CMD_VERSION}, {"debug", required_argument, NULL, OPT_DEBUG}, /* options */ {"coredumps", no_argument, NULL, OPT_COREDUMPS}, {"keystore-format", required_argument, NULL, OPT_KEY_STORE_FORMAT}, {"userid", required_argument, NULL, OPT_USERID}, {"with-sigs", no_argument, NULL, OPT_WITH_SIGS}, {"hash", required_argument, NULL, OPT_HASH_ALG}, {"home", required_argument, NULL, OPT_HOMEDIR}, {"homedir", required_argument, NULL, OPT_HOMEDIR}, {"numbits", required_argument, NULL, OPT_NUMBITS}, {"s2k-iterations", required_argument, NULL, OPT_S2K_ITER}, {"s2k-msec", required_argument, NULL, OPT_S2K_MSEC}, {"expiration", required_argument, NULL, OPT_EXPIRATION}, {"pass-fd", required_argument, NULL, OPT_PASSWDFD}, {"password", required_argument, NULL, OPT_PASSWD}, {"results", required_argument, NULL, OPT_RESULTS}, {"cipher", required_argument, NULL, OPT_CIPHER}, {"expert", no_argument, NULL, OPT_EXPERT}, {"output", required_argument, NULL, OPT_OUTPUT}, {"overwrite", no_argument, NULL, OPT_OVERWRITE}, {"force", no_argument, NULL, OPT_FORCE}, {"secret", no_argument, NULL, OPT_SECRET}, {"rev-type", required_argument, NULL, OPT_REV_TYPE}, {"rev-reason", required_argument, NULL, OPT_REV_REASON}, {"permissive", no_argument, NULL, OPT_PERMISSIVE}, {"notty", no_argument, NULL, OPT_NOTTY}, {"fix-cv25519-bits", no_argument, NULL, OPT_FIX_25519_BITS}, {"check-cv25519-bits", no_argument, NULL, OPT_CHK_25519_BITS}, {"add-subkey", no_argument, NULL, OPT_ADD_SUBKEY}, {"set-expire", required_argument, NULL, OPT_SET_EXPIRE}, {"current-time", required_argument, NULL, OPT_CURTIME}, {"allow-weak-hash", no_argument, NULL, OPT_ALLOW_WEAK_HASH}, {NULL, 0, NULL, 0}, }; /* list keys */ static bool print_keys_info(cli_rnp_t *rnp, FILE *fp, const char *filter) { bool psecret = rnp->cfg().get_bool(CFG_SECRET); bool psigs = rnp->cfg().get_bool(CFG_WITH_SIGS); int flags = CLI_SEARCH_SUBKEYS_AFTER | (psecret ? CLI_SEARCH_SECRET : 0); std::vector keys; if (!cli_rnp_keys_matching_string(rnp, keys, filter ? filter : "", flags)) { fprintf(fp, "Key(s) not found.\n"); return false; } fprintf(fp, "%d key%s found\n", (int) keys.size(), (keys.size() == 1) ? "" : "s"); for (auto key : keys) { cli_rnp_print_key_info(fp, rnp->ffi, key, psecret, psigs); } fprintf(fp, "\n"); /* clean up */ clear_key_handles(keys); return true; } static bool import_keys(cli_rnp_t *rnp, rnp_input_t input, const std::string &inname) { std::set new_pub_keys; std::set new_sec_keys; std::set updated_keys; bool res = false; bool updated = false; size_t unchanged_keys = 0; size_t processed_keys = 0; uint32_t flags = RNP_LOAD_SAVE_PUBLIC_KEYS | RNP_LOAD_SAVE_SECRET_KEYS | RNP_LOAD_SAVE_SINGLE | RNP_LOAD_SAVE_BASE64; bool permissive = rnp->cfg().get_bool(CFG_PERMISSIVE); if (permissive) { flags |= RNP_LOAD_SAVE_PERMISSIVE; } do { /* load keys one-by-one */ char * results = NULL; rnp_result_t ret = rnp_import_keys(rnp->ffi, input, flags, &results); if (ret == RNP_ERROR_EOF) { res = true; break; } if (ret && updated) { /* some keys were imported, but then error occurred */ ERR_MSG("warning: not all data was processed."); res = true; break; } if (ret) { ERR_MSG("failed to import key(s) from %s, stopping.", inname.c_str()); break; } // print information about imported key(s) json_object *jso = json_tokener_parse(results); rnp_buffer_destroy(results); if (!jso) { ERR_MSG("invalid key import resulting JSON"); break; } json_object *keys = NULL; if (!json_object_object_get_ex(jso, "keys", &keys)) { ERR_MSG("invalid key import JSON contents"); json_object_put(jso); break; } processed_keys += json_object_array_length(keys); for (size_t idx = 0; idx < (size_t) json_object_array_length(keys); idx++) { json_object * keyinfo = json_object_array_get_idx(keys, idx); rnp_key_handle_t key = NULL; if (!keyinfo) { continue; } std::string pub_status = json_obj_get_str(keyinfo, "public"); std::string sec_status = json_obj_get_str(keyinfo, "secret"); const char *fphex = json_obj_get_str(keyinfo, "fingerprint"); if (pub_status == "new") { new_pub_keys.insert(fphex); updated = true; } if (sec_status == "new") { new_sec_keys.insert(fphex); updated = true; } if (pub_status == "updated" || sec_status == "updated") { updated_keys.insert(fphex); updated = true; } if (pub_status == "unchanged" || sec_status == "unchanged") { if (!new_pub_keys.count(fphex) && !new_sec_keys.count(fphex) && !updated_keys.count(fphex)) { unchanged_keys++; continue; } } if (rnp_locate_key(rnp->ffi, "fingerprint", fphex, &key) || !key) { ERR_MSG("failed to locate key with fingerprint %s", fphex); continue; } cli_rnp_print_key_info(stdout, rnp->ffi, key, true, false); rnp_key_handle_destroy(key); } json_object_put(jso); } while (1); // print statistics ERR_MSG("Import finished: %lu key%s processed, %lu new public keys, %lu new secret keys, " "%lu updated, %lu unchanged.", processed_keys, (processed_keys != 1) ? "s" : "", new_pub_keys.size(), new_sec_keys.size(), updated_keys.size(), unchanged_keys); if (updated) { // set default key if we didn't have one if (rnp->defkey().empty()) { rnp->set_defkey(); } // save public and secret keyrings if (!cli_rnp_save_keyrings(rnp)) { ERR_MSG("failed to save keyrings"); } } return res; } static bool import_sigs(cli_rnp_t *rnp, rnp_input_t input, const std::string &inname) { bool res = false; char * results = NULL; json_object *jso = NULL; json_object *sigs = NULL; int unknown_sigs = 0; int new_sigs = 0; int old_sigs = 0; if (rnp_import_signatures(rnp->ffi, input, 0, &results)) { ERR_MSG("Failed to import signatures from %s", inname.c_str()); goto done; } // print information about imported signature(s) jso = json_tokener_parse(results); if (!jso || !json_object_object_get_ex(jso, "sigs", &sigs)) { ERR_MSG("Invalid signature import result"); goto done; } for (size_t idx = 0; idx < (size_t) json_object_array_length(sigs); idx++) { json_object *siginfo = json_object_array_get_idx(sigs, idx); if (!siginfo) { continue; } const char *status = json_obj_get_str(siginfo, "public"); std::string pub_status = status ? status : "unknown"; status = json_obj_get_str(siginfo, "secret"); std::string sec_status = status ? status : "unknown"; if ((pub_status == "new") || (sec_status == "new")) { new_sigs++; } else if ((pub_status == "unchanged") || (sec_status == "unchanged")) { old_sigs++; } else { unknown_sigs++; } } // print status information ERR_MSG("Import finished: %d new signature%s, %d unchanged, %d unknown.", new_sigs, (new_sigs != 1) ? "s" : "", old_sigs, unknown_sigs); // save public and secret keyrings if ((new_sigs > 0) && !cli_rnp_save_keyrings(rnp)) { ERR_MSG("Failed to save keyrings"); goto done; } res = true; done: json_object_put(jso); rnp_buffer_destroy(results); return res; } static bool import(cli_rnp_t *rnp, const std::string &spec, int cmd) { if (spec.empty()) { ERR_MSG("Import path isn't specified"); return false; } rnp_input_t input = cli_rnp_input_from_specifier(*rnp, spec, NULL); if (!input) { ERR_MSG("Failed to create input for %s", spec.c_str()); return false; } if (cmd == CMD_IMPORT) { char *contents = NULL; if (rnp_guess_contents(input, &contents)) { ERR_MSG("Warning! Failed to guess content type to import. Assuming keys."); } cmd = (contents && !strcmp(contents, "signature")) ? CMD_IMPORT_SIGS : CMD_IMPORT_KEYS; rnp_buffer_destroy(contents); } bool res = false; switch (cmd) { case CMD_IMPORT_KEYS: res = import_keys(rnp, input, spec); break; case CMD_IMPORT_SIGS: res = import_sigs(rnp, input, spec); break; default: ERR_MSG("Unexpected command: %d", cmd); } rnp_input_destroy(input); return res; } /* print a usage message */ void print_usage(const char *usagemsg) { cli_rnp_print_praise(); puts(usagemsg); } /* do a command once for a specified file 'f' */ bool rnp_cmd(cli_rnp_t *rnp, optdefs_t cmd, const char *f) { std::string fs; switch (cmd) { case CMD_LIST_KEYS: if (!f && rnp->cfg().get_count(CFG_USERID)) { fs = rnp->cfg().get_str(CFG_USERID, 0); f = fs.c_str(); } return print_keys_info(rnp, stdout, f); case CMD_EXPORT_KEY: { if (!f && rnp->cfg().get_count(CFG_USERID)) { fs = rnp->cfg().get_str(CFG_USERID, 0); f = fs.c_str(); } if (!f) { ERR_MSG("No key specified."); return 0; } return cli_rnp_export_keys(rnp, f); } case CMD_IMPORT: case CMD_IMPORT_KEYS: case CMD_IMPORT_SIGS: return import(rnp, f ? f : "", cmd); case CMD_GENERATE_KEY: { if (!f) { size_t count = rnp->cfg().get_count(CFG_USERID); if (count == 1) { fs = rnp->cfg().get_str(CFG_USERID, 0); f = fs.c_str(); } else if (count > 1) { ERR_MSG("Only single userid is supported for generated keys"); return false; } } return cli_rnp_generate_key(rnp, f); } case CMD_EXPORT_REV: { if (!f) { ERR_MSG("You need to specify key to generate revocation for."); return false; } return cli_rnp_export_revocation(rnp, f); } case CMD_REVOKE_KEY: { if (!f) { ERR_MSG("You need to specify key or subkey to revoke."); return false; } return cli_rnp_revoke_key(rnp, f); } case CMD_REMOVE_KEY: { if (!f) { ERR_MSG("You need to specify key or subkey to remove."); return false; } return cli_rnp_remove_key(rnp, f); } case CMD_EDIT_KEY: { if (!f) { ERR_MSG("You need to specify a key or subkey to edit."); return false; } return rnp->edit_key(f); } case CMD_VERSION: cli_rnp_print_praise(); return true; case CMD_HELP: default: print_usage(usage); return true; } } /* set the option */ bool setoption(rnp_cfg &cfg, optdefs_t *cmd, int val, const char *arg) { switch (val) { case OPT_COREDUMPS: #ifdef _WIN32 ERR_MSG("warning: --coredumps doesn't make sense on windows systems."); #endif cfg.set_bool(CFG_COREDUMPS, true); return true; case CMD_GENERATE_KEY: cfg.set_bool(CFG_NEEDSSECKEY, true); *cmd = (optdefs_t) val; return true; case OPT_EXPERT: cfg.set_bool(CFG_EXPERT, true); return true; case CMD_LIST_KEYS: case CMD_EXPORT_KEY: case CMD_EXPORT_REV: case CMD_REVOKE_KEY: case CMD_REMOVE_KEY: case CMD_EDIT_KEY: case CMD_IMPORT: case CMD_IMPORT_KEYS: case CMD_IMPORT_SIGS: case CMD_HELP: case CMD_VERSION: *cmd = (optdefs_t) val; return true; /* options */ case OPT_KEY_STORE_FORMAT: cfg.set_str(CFG_KEYSTOREFMT, arg); return true; case OPT_USERID: cfg.add_str(CFG_USERID, arg); return true; case OPT_HOMEDIR: cfg.set_str(CFG_HOMEDIR, arg); return true; case OPT_NUMBITS: { int bits = 0; if (!rnp::str_to_int(arg, bits) || (bits < 1024) || (bits > 16384)) { ERR_MSG("wrong bits value: %s", arg); return false; } cfg.set_int(CFG_NUMBITS, bits); return true; } case OPT_ALLOW_WEAK_HASH: cfg.set_bool(CFG_WEAK_HASH, true); return true; case OPT_HASH_ALG: return cli_rnp_set_hash(cfg, arg); case OPT_S2K_ITER: { int iterations = atoi(arg); if (!iterations) { ERR_MSG("Wrong iterations value: %s", arg); return false; } cfg.set_int(CFG_S2K_ITER, iterations); return true; } case OPT_EXPIRATION: cfg.set_str(CFG_KG_PRIMARY_EXPIRATION, arg); cfg.set_str(CFG_KG_SUBKEY_EXPIRATION, arg); return true; case OPT_S2K_MSEC: { int msec = 0; if (!rnp::str_to_int(arg, msec) || !msec) { ERR_MSG("Invalid s2k msec value: %s", arg); return false; } cfg.set_int(CFG_S2K_MSEC, msec); return true; } case OPT_PASSWDFD: cfg.set_str(CFG_PASSFD, arg); return true; case OPT_PASSWD: cfg.set_str(CFG_PASSWD, arg); return true; case OPT_RESULTS: cfg.set_str(CFG_IO_RESS, arg); return true; case OPT_CIPHER: return cli_rnp_set_cipher(cfg, arg); case OPT_DEBUG: ERR_MSG("Option --debug is deprecated, ignoring."); return true; case OPT_OUTPUT: if (!arg) { ERR_MSG("No output filename argument provided"); return false; } cfg.set_str(CFG_OUTFILE, arg); return true; case OPT_OVERWRITE: cfg.set_bool(CFG_OVERWRITE, true); return true; case OPT_FORCE: cfg.set_bool(CFG_FORCE, true); return true; case OPT_SECRET: cfg.set_bool(CFG_SECRET, true); return true; case OPT_WITH_SIGS: cfg.set_bool(CFG_WITH_SIGS, true); return true; case OPT_REV_TYPE: { std::string revtype = arg; if (revtype == "0") { revtype = "no"; } else if (revtype == "1") { revtype = "superseded"; } else if (revtype == "2") { revtype = "compromised"; } else if (revtype == "3") { revtype = "retired"; } cfg.set_str(CFG_REV_TYPE, revtype); return true; } case OPT_REV_REASON: cfg.set_str(CFG_REV_REASON, arg); return true; case OPT_PERMISSIVE: cfg.set_bool(CFG_PERMISSIVE, true); return true; case OPT_NOTTY: cfg.set_bool(CFG_NOTTY, true); return true; case OPT_FIX_25519_BITS: cfg.set_bool(CFG_FIX_25519_BITS, true); return true; case OPT_CHK_25519_BITS: cfg.set_bool(CFG_CHK_25519_BITS, true); return true; case OPT_CURTIME: cfg.set_str(CFG_CURTIME, arg); return true; case OPT_ADD_SUBKEY: cfg.set_bool(CFG_ADD_SUBKEY, true); return true; case OPT_SET_EXPIRE: cfg.set_str(CFG_SET_KEY_EXPIRE, arg); return true; default: *cmd = CMD_HELP; return true; } } bool rnpkeys_init(cli_rnp_t &rnp, const rnp_cfg &cfg) { rnp_cfg rnpcfg; rnpcfg.load_defaults(); rnpcfg.set_int(CFG_NUMBITS, DEFAULT_RSA_NUMBITS); rnpcfg.set_str(CFG_IO_RESS, ""); rnpcfg.copy(cfg); if (!cli_cfg_set_keystore_info(rnpcfg)) { ERR_MSG("fatal: cannot set keystore info"); return false; } if (!rnp.init(rnpcfg)) { ERR_MSG("fatal: failed to initialize rnpkeys"); return false; } if (!cli_rnp_check_weak_hash(&rnp)) { ERR_MSG("Weak hash algorithm detected. Pass --allow-weak-hash option if you really " "want to use it."); return false; } /* TODO: at some point we should check for error here */ (void) rnp.load_keyrings(true); return true; }