/* gnupg-key-manage.c - Managment tool for keys
* Copyright (C) 2025 g10 Code GmbH
*
* This file is part of GPGME.
*
* GPGME is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* GPGME is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, see .
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
/* This tool provides some specialized commands for key management
* tasks. Although this could be done using scripting, it avoids
* problems maintaining such scripts for Unix and Windows.
*/
#include
#include
#include
#include
#include
#include
#ifdef HAVE_LOCALE_H
#include
#endif
#define GPGRT_ENABLE_ES_MACROS 1
#define GPGRT_ENABLE_LOG_MACROS 1
#define GPGRT_ENABLE_ARGPARSE_MACROS 1
#include
static struct
{
int verbose; /* The verbosity level. */
int debug; /* True if debug mode is active. */
int pgp; /* Select PGP keys. */
int x509; /* Select X.509 keys. */
int all; /* Work on all keys in the keyring. */
int dryrun; /* No actual changes. */
int with_secret; /* Also process secret keys. */
} opt;
/* An object to store keys in an array. */
struct keyarray_s
{
size_t size;
size_t used;
gpgme_key_t *keys; /* Allocated with SIZE elements. */
};
typedef struct keyarray_s *keyarray_t;
/*
* Helper macros and functions
*/
#define xtrystrdup(a) gpgrt_strdup ((a))
#define xcalloc(a,b) ({ \
void *_r = gpgrt_calloc ((a), (b)); \
if (!_r) \
xoutofcore ("calloc"); \
_r; })
#define xstrdup(a) ({ \
char *_r = gpgrt_strdup ((a)); \
if (!_r) \
xoutofcore ("strdup"); \
_r; })
#define xstrconcat(a, ...) ({ \
char *_r = gpgrt_strconcat ((a), __VA_ARGS__); \
if (!_r) \
xoutofcore ("strconcat"); \
_r; })
#define xfree(a) gpgrt_free ((a))
#define xtrymalloc(a) gpgrt_malloc ((a))
#define xmalloc(a) ({ \
void *_r = gpgrt_malloc ((a)); \
if (!_r) \
xoutofcore ("malloc"); \
_r; })
#define spacep(p) (*(p) == ' ' || *(p) == '\t')
static void
xoutofcore (const char *type)
{
gpg_error_t err = gpg_error_from_syserror ();
log_error ("%s failed: %s\n", type, gpg_strerror (err));
exit (2);
}
/* Note that this is a copy from ../src/json-utils.c */
static const char *
data_type_to_string (gpgme_data_type_t dt)
{
const char *s = "[?]";
switch (dt)
{
case GPGME_DATA_TYPE_INVALID : s = "invalid"; break;
case GPGME_DATA_TYPE_UNKNOWN : s = "unknown"; break;
case GPGME_DATA_TYPE_PGP_SIGNED : s = "PGP-signed"; break;
case GPGME_DATA_TYPE_PGP_SIGNATURE: s = "PGP-signature"; break;
case GPGME_DATA_TYPE_PGP_ENCRYPTED: s = "PGP-encrypted"; break;
case GPGME_DATA_TYPE_PGP_OTHER : s = "PGP"; break;
case GPGME_DATA_TYPE_PGP_KEY : s = "PGP-key"; break;
case GPGME_DATA_TYPE_CMS_SIGNED : s = "CMS-signed"; break;
case GPGME_DATA_TYPE_CMS_ENCRYPTED: s = "CMS-encrypted"; break;
case GPGME_DATA_TYPE_CMS_OTHER : s = "CMS"; break;
case GPGME_DATA_TYPE_X509_CERT : s = "X.509"; break;
case GPGME_DATA_TYPE_PKCS12 : s = "PKCS12"; break;
}
return s;
}
/* Return a new context object or die. */
static gpgme_ctx_t
create_context (void)
{
gpg_error_t err;
gpgme_ctx_t ctx;
err = gpgme_new (&ctx);
if (err)
{
log_error ("error creating a new context: %s\n", gpg_strerror (err));
exit (2);
}
return ctx;
}
/* Create a new key array object or die. */
static keyarray_t
create_keyarray (void)
{
keyarray_t array;
array = xcalloc (1, sizeof *array);
return array;
}
/* Release the array object and all keys. */
static void
free_keyarray (keyarray_t array)
{
size_t n;
if (!array)
return;
for (n=0; n < array->used; n++)
gpgme_key_unref (array->keys[n]);
xfree (array->keys);
array->keys = 0;
array->size = 0;
array->used = 0;
xfree (array);
}
/* Add KEY to the ARRAY. Enlarge array as needed. A new ref is taken
* for the key. */
static void
add_to_keyarray (keyarray_t array, gpgme_key_t key)
{
if (!array->keys || array->used == array->size)
{
size_t incr = 128;
void *p = gpgrt_reallocarray (array->keys, array->size,
array->size+incr, sizeof *array->keys);
if (!p)
xoutofcore ("reallocarray");
array->keys = p;
array->size += incr;
}
gpgme_key_ref (key);
array->keys[array->used++] = key;
}
/*
* The identify command.
*/
static gpg_error_t
cmd_identify (const char *fname)
{
gpg_error_t err;
estream_t fp;
gpgme_data_t data;
gpgme_data_type_t dt;
if (fname)
{
fp = es_fopen (fname, "rb");
if (!fp)
{
err = gpg_error_from_syserror ();
log_error ("can't open '%s': %s\n", fname, gpg_strerror (err));
return err;
}
err = gpgme_data_new_from_estream (&data, fp);
}
else
{
char *buffer;
int n;
fp = NULL;
es_set_binary (es_stdin);
/* Urgs: gpgme_data_identify does a seek and that fails for stdin. */
buffer = xmalloc (2048+1);
n = es_fread (buffer, 1, 2048, es_stdin);
if (n < 0 || es_ferror (es_stdin))
{
err = gpg_error_from_syserror ();
log_error ("error reading '%s': %s\n", "[stdin]", gpg_strerror (err));
xfree (buffer);
return err;
}
buffer[n] = 0;
err = gpgme_data_new_from_mem (&data, buffer, n, 1);
xfree (buffer);
}
if (err)
{
log_error ("error creating data object: %s\n", gpg_strerror (err));
return err;
}
dt = gpgme_data_identify (data, 0);
if (fname && dt == GPGME_DATA_TYPE_UNKNOWN
&& gpgme_data_seek (data, 0, SEEK_SET) != (gpgme_off_t)(-1))
{
/* This might be a PGP or PEM file with a long ascii lead in.
* Search for the dashes and try again. We do this only if a
* file was given to complications with the already ready
* buffered stdin. */
/* FIXME: We need a buffered read for gpgme_data-t. */
}
if (dt == GPGME_DATA_TYPE_INVALID)
log_error ("%s: error identifying data\n", fname? fname:"-");
if (fname)
es_printf ("%s: ", fname);
es_printf ("%s\n", data_type_to_string (dt));
gpgme_data_release (data);
es_fclose (fp);
return 0;
}
/*
* The delete-expired command.
*
* Walk over all keys and delete those which have expired. By default
* only X.509 keys are considered because PGP keys can be prolonged.
* To work on PGP keys the option --pgp is required. */
static gpg_error_t
cmd_delexpired (const char *pattern)
{
gpg_error_t err, firsterr;
gpgme_ctx_t ctx = create_context ();
keyarray_t expiredkeys = create_keyarray ();
gpgme_key_t key = NULL;
gpgme_protocol_t proto;
const char *protostr;
size_t n;
/* This command defaults to X.509. */
proto = opt.pgp? GPGME_PROTOCOL_OPENPGP:GPGME_PROTOCOL_CMS;
protostr = (proto == GPGME_PROTOCOL_OPENPGP)? "(pgp)":"(x.509)";
err = gpgme_set_protocol (ctx, proto);
if (err)
{
log_error ("error setting the protocol: %s\n", gpg_strerror (err));
goto leave;
}
err = gpgme_set_keylist_mode (ctx, GPGME_KEYLIST_MODE_LOCAL);
if (err)
{
log_error ("error setting setting the listing mode: %s\n",
gpg_strerror (err));
goto leave;
}
gpgme_set_offline (ctx, 1);
err = gpgme_op_keylist_start (ctx, pattern, 0);
if (err)
{
if (pattern)
log_error ("error listing keys with pattern '%s': %s\n",
pattern, gpg_strerror (err));
else
log_error ("error listing all keys: %s\n", gpg_strerror (err));
goto leave;
}
for (;;)
{
gpgme_key_unref (key);
err = gpgme_op_keylist_next (ctx, &key);
if (err)
break;
if (!key->subkeys)
{
log_error ("internal error: subkey object missing\n");
continue;
}
if (!key->expired)
continue;
if (key->secret && !opt.with_secret)
{
if (opt.verbose)
log_info ("key %s %s with secret part skipped\n",
key->subkeys->fpr, protostr);
continue;
}
if (opt.verbose || opt.dryrun)
log_info ("key %s %s has expired\n", key->subkeys->fpr, protostr);
add_to_keyarray (expiredkeys, key);
}
if (gpgme_err_code (err) != GPG_ERR_EOF)
{
log_error ("error listing keys: %s\n", gpg_strerror (err));
goto leave;
}
err = gpgme_op_keylist_end (ctx);
if (err)
{
log_error ("error finishing the key listing: %s\n", gpg_strerror (err));
goto leave;
}
if (opt.verbose)
log_info ("number of keys to delete: %zu\n", expiredkeys->used);
if (opt.dryrun)
{
log_info ("no keys deleted due to option --dry-run\n");
goto leave;
}
firsterr = 0;
for (n=0; n < expiredkeys->used; n++)
{
err = gpgme_op_delete_ext (ctx, expiredkeys->keys[n], 0);
if (err)
{
if (!firsterr)
firsterr = err;
log_error ("error deleting key %s %s: %s\n",
expiredkeys->keys[n]->subkeys->fpr, protostr,
gpg_strerror (err));
}
else if (opt.verbose)
log_error ("key %s %s deleted\n",
expiredkeys->keys[n]->subkeys->fpr, protostr);
}
if (firsterr)
err = firsterr;
leave:
free_keyarray (expiredkeys);
gpgme_key_unref (key);
gpgme_release (ctx);
return err;
}
static const char *
my_strusage( int level )
{
const char *p;
switch (level)
{
case 9: p = "LGPL-2.1-or-later"; break;
case 11: p = "gnupg-key-manage"; break;
case 13: p = PACKAGE_VERSION; break;
case 14: p = "Copyright (C) 2025 g10 Code GmbH"; break;
case 19: p = "Please report bugs to <" PACKAGE_BUGREPORT ">.\n"; break;
case 1:
case 40:
p = "Usage: gnupg-key-manage COMMAND [OPTIONS]";
break;
case 41:
p = ("Syntax: gnupg-key-manage COMMAND [OPTIONS]\n\n"
"A fine selection of commands for common key management tasks.");
break;
default: p = NULL; break;
}
return p;
}
int
main (int argc, char *argv[])
{
enum { CMD_DEFAULT = 0,
CMD_IDENTIFY = 500,
CMD_DELEXPIRED,
CMD_LIBVERSION
} cmd = CMD_DEFAULT;
enum {
OPT_DRYRUN = 'n',
OPT_VERBOSE = 'v',
OPT_DEBUG = 600,
OPT_PGP,
OPT_X509,
OPT_WITH_SECRET,
OPT_ALL
};
static gpgrt_opt_t opts[] = {
ARGPARSE_header (NULL, "Commands"),
ARGPARSE_c (CMD_IDENTIFY, "identify", "Identify the input"),
ARGPARSE_c (CMD_DELEXPIRED, "delete-expired",
"Delete expired keys (defaults to X.509)"),
ARGPARSE_c (CMD_LIBVERSION, "lib-version", "@"),
ARGPARSE_header (NULL, "Options"),
ARGPARSE_s_n(OPT_PGP, "pgp", "Select PGP keys"),
ARGPARSE_s_n(OPT_X509, "x509", "Select X.509 keys"),
ARGPARSE_s_n(OPT_ALL, "all", "Work on the entire keyring"),
ARGPARSE_s_n(OPT_WITH_SECRET, "with-secret",
"Work also on secret keys"),
ARGPARSE_s_n(OPT_DRYRUN, "dry-run", "Print only what would be done"),
ARGPARSE_s_n(OPT_VERBOSE, "verbose", "verbose mode"),
ARGPARSE_s_n(OPT_DEBUG, "debug", "enable debug output"),
ARGPARSE_end()
};
gpgrt_argparse_t pargs = { &argc, &argv};
int i;
gpgrt_set_strusage (my_strusage);
/* We disable logging enabled via a registry key. */
log_set_prefix (gpgrt_strusage (11), (GPGRT_LOG_WITH_PREFIX
|GPGRT_LOG_NO_REGISTRY));
#ifdef HAVE_SETLOCALE
setlocale (LC_ALL, "");
#endif
gpgme_check_version (NULL);
#ifdef LC_CTYPE
gpgme_set_locale (NULL, LC_CTYPE, setlocale (LC_CTYPE, NULL));
#endif
#ifdef LC_MESSAGES
gpgme_set_locale (NULL, LC_MESSAGES, setlocale (LC_MESSAGES, NULL));
#endif
#if GPGRT_VERSION_NUMBER >= 0x013000 /* >= 1.48 */
pargs.flags |= ARGPARSE_FLAG_COMMAND;
#endif
while (gpgrt_argparse (NULL, &pargs, opts))
{
switch (pargs.r_opt)
{
case CMD_IDENTIFY:
case CMD_DELEXPIRED:
case CMD_LIBVERSION:
cmd = pargs.r_opt;
break;
case OPT_VERBOSE: opt.verbose++; break;
case OPT_DEBUG: opt.debug = 1; break;
case OPT_PGP: opt.pgp = 1; break;
case OPT_X509: opt.x509 = 1; break;
case OPT_WITH_SECRET: opt.with_secret = 1; break;
case OPT_ALL: opt.all = 1; break;
case OPT_DRYRUN: opt.dryrun = 1; break;
default:
pargs.err = ARGPARSE_PRINT_ERROR;
break;
}
}
gpgrt_argparse (NULL, &pargs, NULL);
if (opt.pgp && opt.x509)
{
log_error ("error: Only one protocol may be specified\n");
exit (2);
}
switch (cmd)
{
case CMD_DEFAULT:
log_info ("Please use the \"help\" command for a list commands\n");
break;
case CMD_IDENTIFY:
if (!argc || !strcmp (*argv, "-"))
cmd_identify (NULL); /* read from stdin */
else
{
for (i=0; i < argc; i++)
cmd_identify (argv[i]);
}
break;
case CMD_DELEXPIRED:
if (!argc && opt.all)
cmd_delexpired (NULL);
else if (!argc)
log_error ("error: option --all is required to work on"
" the entire keyring\n");
else
{
for (i=0; i < argc; i++)
cmd_delexpired (argv[i]);
}
break;
case CMD_LIBVERSION:
es_printf ("Version from header: %s (0x%06x)\n",
GPGME_VERSION, GPGME_VERSION_NUMBER);
es_printf ("Version from binary: %s\n", gpgme_check_version (NULL));
es_printf ("Copyright blurb ...:%s\n", gpgme_check_version ("\x01\x01"));
break;
}
if (opt.debug)
log_debug ("ready\n");
return 0;
}