From 56d309133f0e54ac5e2f95871fb74f8cb97e2636 Mon Sep 17 00:00:00 2001 From: Werner Koch Date: Tue, 4 Apr 2023 08:49:55 +0200 Subject: [PATCH] dirmngr: Return modifyTimestamp and add server option --newer. * dirmngr/server.c (cmd_ks_get): Add option --newer. (cmd_ad_query): Ditto. * dirmngr/ldap-misc.c (isotime2rfc4517): New. (rfc4517toisotime): New. * dirmngr/ks-action.c (ks_action_get): Add arg newer and pass on. (ks_action_query): Ditto. * dirmngr/ks-engine-ldap.c (extract_keys): Print new "chg" record. (ks_ldap_get): Add arg newer. Modify filter with newer arg. (ks_ldap_search): Print the modifyTimestamp. (ks_ldap_query): Add arg newer. Modify filter with newer arg. -- Note that the modifyTimestamp is also available on Windows, where its value is more commonly known as whenChanged. Both are constructed attributes. Note that the --newer option is a bit of a misnomer because LDAP has only a greater-or-equal and no greater-than operator. --- dirmngr/ks-action.c | 10 +++-- dirmngr/ks-action.h | 5 ++- dirmngr/ks-engine-ldap.c | 80 ++++++++++++++++++++++++++++++------ dirmngr/ks-engine.h | 5 ++- dirmngr/ldap-misc.c | 87 ++++++++++++++++++++++++++++++++++++++++ dirmngr/ldap-misc.h | 2 + dirmngr/server.c | 33 ++++++++++++--- 7 files changed, 195 insertions(+), 27 deletions(-) diff --git a/dirmngr/ks-action.c b/dirmngr/ks-action.c index 6b5f78f49..002f1a7a5 100644 --- a/dirmngr/ks-action.c +++ b/dirmngr/ks-action.c @@ -337,7 +337,8 @@ ks_action_search (ctrl_t ctrl, uri_item_t keyservers, keyservers and write the result to the provided output stream. */ gpg_error_t ks_action_get (ctrl_t ctrl, uri_item_t keyservers, - strlist_t patterns, unsigned int ks_get_flags, estream_t outfp) + strlist_t patterns, unsigned int ks_get_flags, + gnupg_isotime_t newer, estream_t outfp) { gpg_error_t err = 0; gpg_error_t first_err = 0; @@ -382,7 +383,7 @@ ks_action_get (ctrl_t ctrl, uri_item_t keyservers, #if USE_LDAP if (is_ldap) err = ks_ldap_get (ctrl, uri->parsed_uri, sl->d, ks_get_flags, - &infp); + newer, &infp); else #endif if (is_hkp_s) @@ -549,7 +550,8 @@ ks_action_put (ctrl_t ctrl, uri_item_t keyservers, * the filter expression FILTER. Write the result to OUTFP. */ gpg_error_t ks_action_query (ctrl_t ctrl, const char *url, unsigned int ks_get_flags, - const char *filter, char **attrs, estream_t outfp) + const char *filter, char **attrs, + gnupg_isotime_t newer, estream_t outfp) { #if USE_LDAP gpg_error_t err; @@ -576,7 +578,7 @@ ks_action_query (ctrl_t ctrl, const char *url, unsigned int ks_get_flags, || puri->parsed_uri->opaque) { err = ks_ldap_query (ctrl, puri->parsed_uri, ks_get_flags, filter, - attrs, &infp); + attrs, newer, &infp); if (!err) err = copy_stream (infp, outfp); } diff --git a/dirmngr/ks-action.h b/dirmngr/ks-action.h index f7d731d7a..223aae2da 100644 --- a/dirmngr/ks-action.h +++ b/dirmngr/ks-action.h @@ -28,14 +28,15 @@ gpg_error_t ks_action_search (ctrl_t ctrl, uri_item_t keyservers, strlist_t patterns, estream_t outfp); gpg_error_t ks_action_get (ctrl_t ctrl, uri_item_t keyservers, strlist_t patterns, unsigned int ks_get_flags, - estream_t outfp); + gnupg_isotime_t newer, estream_t outfp); gpg_error_t ks_action_fetch (ctrl_t ctrl, const char *url, estream_t outfp); gpg_error_t ks_action_put (ctrl_t ctrl, uri_item_t keyservers, void *data, size_t datalen, void *info, size_t infolen); gpg_error_t ks_action_query (ctrl_t ctrl, const char *ldapserver, unsigned int ks_get_flags, - const char *filter, char **attr, estream_t outfp); + const char *filter, char **attr, + gnupg_isotime_t newer, estream_t outfp); #endif /*DIRMNGR_KS_ACTION_H*/ diff --git a/dirmngr/ks-engine-ldap.c b/dirmngr/ks-engine-ldap.c index a056b1163..1ffd30ecb 100644 --- a/dirmngr/ks-engine-ldap.c +++ b/dirmngr/ks-engine-ldap.c @@ -1004,6 +1004,15 @@ extract_keys (estream_t output, } my_ldap_value_free (vals); + vals = ldap_get_values (ldap_conn, message, "modifyTimestamp"); + if (vals && vals[0]) + { + gnupg_isotime_t atime; + if (!rfc4517toisotime (atime, vals[0])) + es_fprintf (output, "chg:%s:\n", atime); + } + my_ldap_value_free (vals); + es_fprintf (output, "INFO %s END\n", certid); } @@ -1368,7 +1377,7 @@ fetch_rootdse (ctrl_t ctrl, parsed_uri_t uri) || puri->parsed_uri->opaque) { err = ks_ldap_query (ctrl, puri->parsed_uri, KS_GET_FLAG_ROOTDSE, - "^&base&(objectclass=*)", NULL, &infp); + "^&base&(objectclass=*)", NULL, NULL, &infp); if (err) log_error ("ldap: reading the rootDES failed: %s\n", gpg_strerror (err)); @@ -1417,7 +1426,7 @@ basedn_from_rootdse (ctrl_t ctrl, parsed_uri_t uri) * data. KS_GET_FLAGS conveys flags from the client. */ gpg_error_t ks_ldap_get (ctrl_t ctrl, parsed_uri_t uri, const char *keyspec, - unsigned int ks_get_flags, estream_t *r_fp) + unsigned int ks_get_flags, gnupg_isotime_t newer, estream_t *r_fp) { gpg_error_t err; unsigned int serverinfo; @@ -1442,7 +1451,7 @@ ks_ldap_get (ctrl_t ctrl, parsed_uri_t uri, const char *keyspec, { "dummy", /* (to be be replaced.) */ "pgpcertid", "pgpuserid", "pgpkeyid", "pgprevoked", "pgpdisabled", - "pgpkeycreatetime", "modifytimestamp", "pgpkeysize", "pgpkeytype", + "pgpkeycreatetime", "modifyTimestamp", "pgpkeysize", "pgpkeytype", "gpgfingerprint", NULL }; @@ -1542,6 +1551,28 @@ ks_ldap_get (ctrl_t ctrl, parsed_uri_t uri, const char *keyspec, if (err) goto leave; + if (*newer) + { + char *tstr, *fstr; + + tstr = isotime2rfc4517 (newer); + if (!tstr) + { + err = gpg_error_from_syserror (); + goto leave; + } + fstr = strconcat ("(&", filter, + "(modifyTimestamp>=", tstr, "))", NULL); + xfree (tstr); + if (!fstr) + { + err = gpg_error_from_syserror (); + goto leave; + } + xfree (filter); + filter = fstr; + } + if (opt.debug) log_debug ("ks-ldap: using filter: %s\n", filter); @@ -1697,7 +1728,7 @@ ks_ldap_search (ctrl_t ctrl, parsed_uri_t uri, const char *pattern, char *attrs[] = { "pgpcertid", "pgpuserid", "pgprevoked", "pgpdisabled", - "pgpkeycreatetime", "pgpkeyexpiretime", "modifytimestamp", + "pgpkeycreatetime", "pgpkeyexpiretime", "modifyTimestamp", "pgpkeysize", "pgpkeytype", "gpgfingerprint", NULL }; @@ -1851,19 +1882,17 @@ ks_ldap_search (ctrl_t ctrl, parsed_uri_t uri, const char *pattern, } my_ldap_value_free (vals); -#if 0 - /* This is not yet specified in the keyserver - protocol, but may be someday. */ es_fputc (':', fp); - vals = ldap_get_values (ldap_conn, each, "modifytimestamp"); - if(vals && vals[0] strlen (vals[0]) == 15) + vals = ldap_get_values (ldap_conn, each, "modifyTimestamp"); + if(vals && vals[0]) { - es_fprintf (fp, "%u", - (unsigned int) ldap2epochtime (vals[0])); + gnupg_isotime_t atime; + if (rfc4517toisotime (atime, vals[0])) + *atime = 0; + es_fprintf (fp, "%s", atime); } my_ldap_value_free (vals); -#endif es_fprintf (fp, "\n"); @@ -2785,7 +2814,8 @@ ks_ldap_put (ctrl_t ctrl, parsed_uri_t uri, * return or NULL for all. */ gpg_error_t ks_ldap_query (ctrl_t ctrl, parsed_uri_t uri, unsigned int ks_get_flags, - const char *filter_arg, char **attrs, estream_t *r_fp) + const char *filter_arg, char **attrs, + gnupg_isotime_t newer, estream_t *r_fp) { gpg_error_t err; unsigned int serverinfo; @@ -2823,6 +2853,30 @@ ks_ldap_query (ctrl_t ctrl, parsed_uri_t uri, unsigned int ks_get_flags, err = ldap_parse_extfilter (filter_arg, 0, &basedn, &scope, &filter); if (err) goto leave; + if (newer && *newer) + { + char *tstr, *fstr; + + tstr = isotime2rfc4517 (newer); + if (!tstr) + { + err = gpg_error_from_syserror (); + goto leave; + } + if (filter && *filter) + fstr = strconcat ("(&", filter, + "(modifyTimestamp>=", tstr, "))", NULL); + else + fstr = strconcat ("(modifyTimestamp>=", tstr, ")", NULL); + xfree (tstr); + if (!fstr) + { + err = gpg_error_from_syserror (); + goto leave; + } + xfree (filter); + filter = fstr; + } } diff --git a/dirmngr/ks-engine.h b/dirmngr/ks-engine.h index 5fbe300c0..03588a4d3 100644 --- a/dirmngr/ks-engine.h +++ b/dirmngr/ks-engine.h @@ -76,13 +76,14 @@ gpg_error_t ks_ldap_search (ctrl_t ctrl, parsed_uri_t uri, const char *pattern, estream_t *r_fp); gpg_error_t ks_ldap_get (ctrl_t ctrl, parsed_uri_t uri, const char *keyspec, unsigned int ks_get_flags, - estream_t *r_fp); + gnupg_isotime_t newer, estream_t *r_fp); gpg_error_t ks_ldap_put (ctrl_t ctrl, parsed_uri_t uri, void *data, size_t datalen, void *info, size_t infolen); gpg_error_t ks_ldap_query (ctrl_t ctrl, parsed_uri_t uri, unsigned int ks_get_flags, - const char *filter, char **attrs, estream_t *r_fp); + const char *filter, char **attrs, + gnupg_isotime_t newer, estream_t *r_fp); #endif /*DIRMNGR_KS_ENGINE_H*/ diff --git a/dirmngr/ldap-misc.c b/dirmngr/ldap-misc.c index 90f1d1f3c..6b0939a3b 100644 --- a/dirmngr/ldap-misc.c +++ b/dirmngr/ldap-misc.c @@ -332,3 +332,90 @@ ldap_parse_extfilter (const char *string, int silent, } return err; } + + + +/* Scan an ISO timestamp and return a Generalized Time according to + * RFC-4517. The only supported format is "yyyymmddThhmmss[Z]" + * delimited by white space, nul, a colon or a comma. Returns a + * malloced string or NULL for an invalid string or on memory + * error. */ +char * +isotime2rfc4517 (const char *string) +{ + int year, month, day, hour, minu, sec; + + if (!isotime_p (string)) + { + errno = 0; + return NULL; + } + + year = atoi_4 (string); + month = atoi_2 (string + 4); + day = atoi_2 (string + 6); + hour = atoi_2 (string + 9); + minu = atoi_2 (string + 11); + sec = atoi_2 (string + 13); + + /* Basic checks (1600 due to the LDAP time format base) */ + if (year < 1600 || month < 1 || month > 12 || day < 1 || day > 31 + || hour > 23 || minu > 59 || sec > 61 ) + { + errno = 0; + return NULL; + } + + return gpgrt_bsprintf ("%04d%02d%02d%02d%02d%02d.0Z", + year, month, day, hour, minu, sec); +} + + +/* Parse an LDAP Generalized Time string and update the provided + * isotime buffer. On error return and error code. */ +gpg_error_t +rfc4517toisotime (gnupg_isotime_t timebuf, const char *string) +{ + int i; + int year, month, day, hour, minu, sec; + const char *s; + + for (i=0, s=string; i < 10; i++, s++) /* Need yyyymmddhh */ + if (!digitp (s)) + return gpg_error (GPG_ERR_INV_TIME); + year = atoi_4 (string); + month = atoi_2 (string + 4); + day = atoi_2 (string + 6); + hour = atoi_2 (string + 9); + minu = 0; + sec = 0; + if (digitp (s) && digitp (s+1)) + { + minu = atoi_2 (s); + s += 2; + if (digitp (s) && digitp (s+1)) + { + sec = atoi_2 (s); + s += 2; + } + } + if (*s == '.' || *s == ',') + { + s++; + if (!digitp (s)) /* At least one digit of the fraction required. */ + return gpg_error (GPG_ERR_INV_TIME); + s++; + while (digitp (s)) + s++; + } + if (*s == 'Z' && (!s[1] || spacep (s+1))) + ; /* stop here. */ + else if (*s == '-' || *s == '+') + return gpg_error (GPG_ERR_NOT_IMPLEMENTED); /* FIXME */ + else + return gpg_error (GPG_ERR_INV_TIME); + + snprintf (timebuf, sizeof (gnupg_isotime_t), "%04d%02d%02dT%02d%02d%02d", + year, month, day, hour, minu, sec); + return 0; +} diff --git a/dirmngr/ldap-misc.h b/dirmngr/ldap-misc.h index d555caf49..03efe5fa7 100644 --- a/dirmngr/ldap-misc.h +++ b/dirmngr/ldap-misc.h @@ -38,6 +38,8 @@ gpg_err_code_t ldap_err_to_gpg_err (int code); gpg_err_code_t ldap_to_gpg_err (LDAP *ld); gpg_error_t ldap_parse_extfilter (const char *string, int silent, char **r_base, int *r_scope, char **r_filter); +char *isotime2rfc4517 (const char *string); +gpg_error_t rfc4517toisotime (gnupg_isotime_t timebuf, const char *string); #endif /*DIRMNGR_LDAP_MISC_H*/ diff --git a/dirmngr/server.c b/dirmngr/server.c index c93437247..2c5a41b07 100644 --- a/dirmngr/server.c +++ b/dirmngr/server.c @@ -2461,22 +2461,28 @@ cmd_ks_search (assuan_context_t ctx, char *line) static const char hlp_ks_get[] = - "KS_GET [--quick] [--ldap] [--first|--next] {}\n" + "KS_GET [--quick] [--newer=TIME] [--ldap] [--first|--next] {}\n" "\n" "Get the keys matching PATTERN from the configured OpenPGP keyservers\n" "(see command KEYSERVER). Each pattern should be a keyid, a fingerprint,\n" "or an exact name indicated by the '=' prefix. Option --quick uses a\n" "shorter timeout; --ldap will use only ldap servers. With --first only\n" - "the first item is returned; --next is used to return the next item"; + "the first item is returned; --next is used to return the next item\n" + "Option --newer works only with certain LDAP servers."; static gpg_error_t cmd_ks_get (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err; - strlist_t list, sl; + strlist_t list = NULL; + strlist_t sl; + const char *s; char *p; estream_t outfp; unsigned int flags = 0; + gnupg_isotime_t opt_newer; + + *opt_newer = 0; if (has_option (line, "--quick")) ctrl->timeout = opt.connect_quick_timeout; @@ -2486,13 +2492,18 @@ cmd_ks_get (assuan_context_t ctx, char *line) flags |= KS_GET_FLAG_FIRST; if (has_option (line, "--next")) flags |= KS_GET_FLAG_NEXT; + if ((s = option_value (line, "--newer")) + && !string2isotime (opt_newer, s)) + { + err = set_error (GPG_ERR_SYNTAX, "invalid time format"); + goto leave; + } line = skip_options (line); /* Break the line into a strlist. Each pattern is by definition percent-plus escaped. However we only support keyids and fingerprints and thus the client has no need to apply the escaping. */ - list = NULL; for (p=line; *p; line = p) { while (*p && *p != ' ') @@ -2569,7 +2580,7 @@ cmd_ks_get (assuan_context_t ctx, char *line) ctrl->server_local->inhibit_data_logging_now = 0; ctrl->server_local->inhibit_data_logging_count = 0; err = ks_action_get (ctrl, ctrl->server_local->keyservers, - list, flags, outfp); + list, flags, opt_newer, outfp); es_fclose (outfp); ctrl->server_local->inhibit_data_logging = 0; } @@ -2710,6 +2721,10 @@ cmd_ad_query (assuan_context_t ctx, char *line) estream_t outfp = NULL; char *p; char **opt_attr = NULL; + const char *s; + gnupg_isotime_t opt_newer; + + *opt_newer = 0; /* No options for now. */ if (has_option (line, "--first")) @@ -2718,6 +2733,12 @@ cmd_ad_query (assuan_context_t ctx, char *line) flags |= KS_GET_FLAG_NEXT; if (has_option (line, "--rootdse")) flags |= KS_GET_FLAG_ROOTDSE; + if ((s = option_value (line, "--newer")) + && !string2isotime (opt_newer, s)) + { + err = set_error (GPG_ERR_SYNTAX, "invalid time format"); + goto leave; + } err = get_option_value (line, "--attr", &p); if (err) goto leave; @@ -2758,7 +2779,7 @@ cmd_ad_query (assuan_context_t ctx, char *line) err = ks_action_query (ctrl, (flags & KS_GET_FLAG_ROOTDSE)? NULL : "ldap:///", - flags, filter, opt_attr, outfp); + flags, filter, opt_attr, opt_newer, outfp); leave: es_fclose (outfp);