Re: [PATCH] Add key expiration date to key menu for classic PGP and GPGME.

Alejandro Colomar via Mutt-dev <[email protected]> Mon, 6 Jul 2026 16:44:14 +0200
Newsgroups gmane.mail.mutt.devel
Message-ID <aku5OQsmkciVoO_M@devuan>
--qgmvxbrkopjchm3k
Content-Type: text/plain; protected-headers=v1; charset=utf-8
Content-Disposition: inline
Content-Transfer-Encoding: quoted-printable
From: Alejandro Colomar <[email protected]>
To: [email protected]
Subject: Re: [PATCH] Add key expiration date to key menu for classic PGP and
 GPGME.
Message-ID: <aku5OQsmkciVoO_M@devuan>
References: <[email protected]>
MIME-Version: 1.0
In-Reply-To: <[email protected]>

Hi Kevin,

On 2026-07-06T21:41:33+0800, Kevin J. McCarthy wrote:
> Create expando %(<datefmt>) for the classic PGP and GPGME select key
> menu in $pgp_entry_format.
>=20
> Add "expires" to $pgp_sort_keys methods.
>=20
> Refactor the datetime parser in gnupgparse.c and apply it to both the
> creation and expiration dates in the output.
> ---
>  crypt-gpgme.c | 37 ++++++++++++++++++--
>  gnupgparse.c  | 96 +++++++++++++++++++++++++++++----------------------
>  init.h        |  3 ++
>  pgpkey.c      | 27 +++++++++++++--
>  pgplib.h      |  1 +
>  sort.h        |  1 +
>  6 files changed, 118 insertions(+), 47 deletions(-)
>=20
> diff --git a/crypt-gpgme.c b/crypt-gpgme.c
> index f5ee87bd..6badab78 100644
> --- a/crypt-gpgme.c
> +++ b/crypt-gpgme.c
> @@ -3287,12 +3287,14 @@ static const char *crypt_entry_fmt(char *dest,
>    switch (ascii_tolower(op))
>      {
>      case '[':
> +    case '(':
>        {
>          const char *cp;
>          char buf2[SHORT_STRING], *p;
>          int do_locales;
>          struct tm *tm;
>          size_t len;
> +        int expires =3D (op =3D=3D '(');
> =20
>          p =3D dest;
> =20
> @@ -3306,7 +3308,7 @@ static const char *crypt_entry_fmt(char *dest,
>            do_locales =3D 1;
> =20
>          len =3D destlen - 1;
> -        while (len > 0 && *cp !=3D ']')
> +        while (len > 0 && *cp !=3D (expires ? ')' : ']'))
>          {
>            if (*cp =3D=3D '%')
>            {
> @@ -3332,7 +3334,9 @@ static const char *crypt_entry_fmt(char *dest,
>          {
>            time_t tt =3D 0;
> =20
> -          if (key->kobj->subkeys && (key->kobj->subkeys->timestamp > 0))
> +          if (expires && key->kobj->subkeys && (key->kobj->subkeys->expi=
res > 0))
> +            tt =3D key->kobj->subkeys->expires;
> +          else if (!expires && key->kobj->subkeys && (key->kobj->subkeys=
->timestamp > 0))
>              tt =3D key->kobj->subkeys->timestamp;
> =20
>            tm =3D localtime(&tt);
> @@ -3539,6 +3543,32 @@ static int crypt_compare_date(const void *a, const=
 void *b)
>                                           :  _crypt_compare_date(a, b));
>  }
> =20
> +/* Compare 2 creation dates and the addresses.  For sorting. */
> +static int _crypt_compare_expires(const void *a, const void *b)
> +{
> +  const crypt_key_t * const *s =3D (const crypt_key_t * const *) a;
> +  const crypt_key_t * const *t =3D (const crypt_key_t * const *) b;
> +  unsigned long ts =3D 0, tt =3D 0;

Why don't we use time_t?  Can it overflow?

> +
> +  if ((*s)->kobj->subkeys && ((*s)->kobj->subkeys->expires > 0))
> +    ts =3D (*s)->kobj->subkeys->expires;
> +  if ((*t)->kobj->subkeys && ((*t)->kobj->subkeys->expires > 0))
> +    tt =3D (*t)->kobj->subkeys->expires;
> +
> +  if (ts > tt)
> +    return 1;
> +  if (ts < tt)
> +    return -1;
> +
> +  return mutt_strcasecmp((*s)->uid, (*t)->uid);
> +}
> +
> +static int crypt_compare_expires(const void *a, const void *b)
> +{
> +  return ((PgpSortKeys & SORT_REVERSE) ? !_crypt_compare_expires(a, b)
> +                                         :  _crypt_compare_expires(a, b)=
);
> +}
> +
>  /* Compare two trust values, the key length, the creation dates. the
>     addresses and the key IDs.  For sorting. */
>  static int _crypt_compare_trust(const void *a, const void *b)
> @@ -4539,6 +4569,9 @@ static crypt_key_t *crypt_select_key(crypt_key_t *k=
eys,
>      case SORT_DATE:
>        f =3D crypt_compare_date;
>        break;
> +    case SORT_EXPIRES:
> +      f =3D crypt_compare_expires;
> +      break;
>      case SORT_KEYID:
>        f =3D crypt_compare_keyid;
>        break;
> diff --git a/gnupgparse.c b/gnupgparse.c
> index 81b25503..f3c3d37c 100644
> --- a/gnupgparse.c
> +++ b/gnupgparse.c
> @@ -116,6 +116,53 @@ static void fix_uid(char *uid)
>    }
>  }
> =20
> +static int parse_timestamp(char *p, time_t *timestamp)
> +{
> +  if (strchr(p, '-'))   /* gpg pre-2.0.10 used format (yyyy-mm-dd) */
> +  {
> +    char tstr[11];
> +    struct tm time;
> +
> +    time.tm_sec =3D 0;
> +    time.tm_min =3D 0;
> +    time.tm_hour =3D 12;



> +    strncpy(tstr, p, 11);
> +    tstr[4] =3D '\0';
> +    tstr[7] =3D '\0';

strncpy(3) should never be used for copying strings with truncation.

Some of its issues are:

-  It forces you to terminate explicitly, which is error-prone.
-  It doesn't detect truncation.
-  It makes it more difficult to know if the zeroing is superfluous or
   necessary.

In this case, I'd use the following to replace the code above:

	const char  *y, *m, *d;

	if (strlen(p) >=3D sizeof(tstr))
		goto bail;
	strcpy(tstr, p);
	p =3D tstr;
	y =3D strsep(&p, "-");
	m =3D strsep(&p, "-");
	d =3D strsep(&p, "-");
	if (p !=3D NULL)
		goto bail;

> +    if (mutt_atoi(tstr, &time.tm_year, 0) < 0)

And then here, I'd use y (and in the next two calls, m and d).
> +    {
> +      p =3D tstr;

Setting p here seems dead code; am I missing something?

> +      goto bail;
> +    }
> +    time.tm_year -=3D 1900;
> +    if (mutt_atoi(tstr+5, &time.tm_mon, 0) < 0)
> +    {
> +      p =3D tstr+5;
> +      goto bail;
> +    }
> +    time.tm_mon -=3D 1;
> +    if (mutt_atoi(tstr+8, &time.tm_mday, 0) < 0)
> +    {
> +      p =3D tstr+8;
> +      goto bail;
> +    }
> +    *timestamp =3D mutt_mktime(&time, 0);
> +  }
> +  else                  /* gpg 2.0.10+ uses seconds since 1970-01-01 */
> +  {
> +    unsigned long long secs;
> +
> +    if (mutt_atoull(p, &secs, MUTT_ATOI_ALLOW_EMPTY) < 0)
> +      goto bail;
> +    *timestamp =3D (time_t)secs;
> +  }
> +
> +  return 0;
> +
> +bail:
> +  return -1;

Why not return -1 directly instead of goto?

> +}
> +
>  static pgp_key_t parse_pub_line(char *buf, int *is_subkey, pgp_key_t k)
>  {
>    pgp_uid_t *uid =3D NULL;
> @@ -245,50 +292,15 @@ static pgp_key_t parse_pub_line(char *buf, int *is_=
subkey, pgp_key_t k)
> =20
>        }
>        case 6:                   /* timestamp (1998-02-28) */
> -      {
>          muttdbg(2, "time stamp: %s", p);
> -
> -        if (strchr(p, '-'))   /* gpg pre-2.0.10 used format (yyyy-mm-dd)=
 */
> -        {
> -          char tstr[11];
> -          struct tm time;
> -
> -          time.tm_sec =3D 0;
> -          time.tm_min =3D 0;
> -          time.tm_hour =3D 12;
> -          strncpy(tstr, p, 11);
> -          tstr[4] =3D '\0';
> -          tstr[7] =3D '\0';
> -          if (mutt_atoi(tstr, &time.tm_year, 0) < 0)
> -          {
> -            p =3D tstr;
> -            goto bail;

Ohhh, now I see where it comes from.  :)

It would be nice to have a second commit that cleans up and removes dead
code (if you want, I can send that).


Have a lovely day!
Alex

> -          }
> -          time.tm_year -=3D 1900;
> -          if (mutt_atoi(tstr+5, &time.tm_mon, 0) < 0)
> -          {
> -            p =3D tstr+5;
> -            goto bail;
> -          }
> -          time.tm_mon -=3D 1;
> -          if (mutt_atoi(tstr+8, &time.tm_mday, 0) < 0)
> -          {
> -            p =3D tstr+8;
> -            goto bail;
> -          }
> -          tmp.gen_time =3D mutt_mktime(&time, 0);
> -        }
> -        else                  /* gpg 2.0.10+ uses seconds since 1970-01-=
01 */
> -        {
> -          unsigned long long secs;
> -
> -          if (mutt_atoull(p, &secs, MUTT_ATOI_ALLOW_EMPTY) < 0)
> -            goto bail;
> -          tmp.gen_time =3D (time_t)secs;
> -        }
> +        if (parse_timestamp(p, &tmp.gen_time))
> +          goto bail;
>          break;
> -      }
> -      case 7:                   /* valid for n days */
> +
> +      case 7:                   /* expires timestamp */
> +        muttdbg(2, "expires time stamp: %s", p);
> +        if (parse_timestamp(p, &tmp.exp_time))
> +          goto bail;
>          break;
>        case 8:                   /* Local id         */
>          break;
> diff --git a/init.h b/init.h
> index af684866..087ebfa8 100644
> --- a/init.h
> +++ b/init.h
> @@ -168,6 +168,7 @@ const struct mapping_t SortAliasMethods[] =3D {  /* D=
T_SORT_ALIAS */
>  const struct mapping_t SortKeyMethods[] =3D {  /* DT_SORT_KEYS */
>    { "address",  SORT_ADDRESS },
>    { "date",     SORT_DATE },
> +  { "expires",  SORT_EXPIRES },
>    { "keyid",    SORT_KEYID },
>    { "trust",    SORT_TRUST },
>    { NULL,       0 }
> @@ -2641,6 +2642,7 @@ struct option_t MuttVars[] =3D {
>    ** .dt %c     .dd capabilities
>    ** .dt %t     .dd trust/validity of the key-uid association
>    ** .dt %[<s>] .dd date of the key where <s> is an \fCstrftime(3)\fP ex=
pression
> +  ** .dt %(<s>) .dd expires date of the key where <s> is an \fCstrftime(=
3)\fP expression
>    ** .de
>    ** .pp
>    ** (PGP only)
> @@ -2834,6 +2836,7 @@ struct option_t MuttVars[] =3D {
>    ** .dt address .dd sort alphabetically by user id
>    ** .dt keyid   .dd sort alphabetically by key id
>    ** .dt date    .dd sort by key creation date
> +  ** .dt expires .dd sort by key expiration date
>    ** .dt trust   .dd sort by the trust of the key
>    ** .de
>    ** .pp
> diff --git a/pgpkey.c b/pgpkey.c
> index a6532e4c..06789ea3 100644
> --- a/pgpkey.c
> +++ b/pgpkey.c
> @@ -152,13 +152,14 @@ static const char *pgp_entry_fmt(char *dest,
>    switch (ascii_tolower(op))
>    {
>      case '[':
> -
> +    case '(':
>      {
>        const char *cp;
>        char buf2[SHORT_STRING], *p;
>        int do_locales;
>        struct tm *tm;
>        size_t len;
> +      int expires =3D (op =3D=3D '(');
> =20
>        p =3D dest;
> =20
> @@ -172,7 +173,7 @@ static const char *pgp_entry_fmt(char *dest,
>          do_locales =3D 1;
> =20
>        len =3D destlen - 1;
> -      while (len > 0 && *cp !=3D ']')
> +      while (len > 0 && *cp !=3D (expires ? ')' : ']'))
>        {
>          if (*cp =3D=3D '%')
>          {
> @@ -196,7 +197,7 @@ static const char *pgp_entry_fmt(char *dest,
>        *p =3D 0;
> =20
> =20
> -      tm =3D localtime(&key->gen_time);
> +      tm =3D localtime(expires ? &key->exp_time : &key->gen_time);
> =20
>        if (!do_locales)
>          setlocale(LC_TIME, "C");
> @@ -355,6 +356,23 @@ static int pgp_compare_date(const void *a, const voi=
d *b)
>            : _pgp_compare_date(a, b));
>  }
> =20
> +static int _pgp_compare_expires(const void *a, const void *b)
> +{
> +  int r;
> +  const pgp_uid_t * const *s =3D (const pgp_uid_t * const *) a;
> +  const pgp_uid_t * const *t =3D (const pgp_uid_t * const *) b;
> +
> +  if ((r =3D mutt_numeric_cmp((*s)->parent->exp_time, (*t)->parent->exp_=
time)))
> +    return r;
> +  return (mutt_strcasecmp((*s)->addr, (*t)->addr));
> +}
> +
> +static int pgp_compare_expires(const void *a, const void *b)
> +{
> +  return ((PgpSortKeys & SORT_REVERSE) ? !_pgp_compare_expires(a, b)
> +          : _pgp_compare_expires(a, b));
> +}
> +
>  static int _pgp_compare_trust(const void *a, const void *b)
>  {
>    int r;
> @@ -502,6 +520,9 @@ static pgp_key_t pgp_select_key(pgp_key_t keys,
>      case SORT_DATE:
>        f =3D pgp_compare_date;
>        break;
> +    case SORT_EXPIRES:
> +      f =3D pgp_compare_expires;
> +      break;
>      case SORT_KEYID:
>        f =3D pgp_compare_keyid;
>        break;
> diff --git a/pgplib.h b/pgplib.h
> index 4a7fa94e..cccd8d9d 100644
> --- a/pgplib.h
> +++ b/pgplib.h
> @@ -38,6 +38,7 @@ struct pgp_keyinfo
>    int flags;
>    short keylen;
>    time_t gen_time;
> +  time_t exp_time;
>    int numalg;
>    const char *algorithm;
>    struct pgp_keyinfo *parent;
> diff --git a/sort.h b/sort.h
> index 863a6655..9fe1e557 100644
> --- a/sort.h
> +++ b/sort.h
> @@ -37,6 +37,7 @@
>  #define SORT_LABEL      19
>  #define SORT_AUX        20  /* $sort_thread_groups delegation to $sort_a=
ux */
>  #define SORT_UID        21  /* used internally by the IMAP code */
> +#define SORT_EXPIRES    22
> =20
>  /* Sort and sort_aux are shorts, and are a composite of a
>   * constant sort operation number and a set of compounded
> --=20
> 2.55.0
>=20

--=20
<https://www.alejandro-colomar.es>

--qgmvxbrkopjchm3k
Content-Type: application/pgp-signature; name="signature.asc"

-----BEGIN PGP SIGNATURE-----

iQIzBAABCgAdFiEES7Jt9u9GbmlWADAi64mZXMKQwqkFAmpLvzgACgkQ64mZXMKQ
wqkdpRAAr3iRvD2YkI5nMdomMl7BGeB+lxrGKIPXDfa8fD8gTFeo+8UNn1/xuooh
DvRQiUOqUpGw0n5my98GgmkOro9VINC65VnFi/VTuXHb6Vx3FMQJahwe+Hcg4mlr
O1nnZagSDG1cgHIc7yEJQL5Fq+jVSkRzZyXOWzBLeAgjrXyZ8FqQOOn3+QXenRFZ
P10InshbhT/xJqgxwIcE7xKtoSVbfuwl0LpexvVNVFE0c30bNHIOQ/ARm2EXhKz6
yxjebU8ETzmMYsT4Yp3xW4tNSUywpmfEW+mB1esj3BbEzRek9gACT+ESoNuk+MMo
gCA8J5BvsaeG+b78gcYnEXYIn2VY0w0rkjQJMk1RgsY1tbx9+S5up3oqco06n60P
orLPK5edePNNQk5fsfxQ6lJ1+QzdmqfLaJvX36c2v4GZ6k5OIahPd3XQmgK9bWe8
aqCgCayesWDTFsdraIHe5a8cvz2Jdoo9LZ+hxU8JHRKbaZD6tANehMaURdGfyhWB
o/8T+GdScIrz6uNO9O3C2WzvAhLnfGPmxtRPXf6O0XxaJ34TBCP/BcwUgJ8TbFl8
Mt704UiSe9kC7qEpb9L0ogSP20WI7p1dMd/ITcNceNi6c3cWyR6gbg/mSj8BfUzy
/CwuixEf72/78qLEUvPdaG+/FVH5UYTwXd9LuWLpT2YFGq7Fvds=
=TEsH
-----END PGP SIGNATURE-----

--qgmvxbrkopjchm3k--