[git] GnuPG - branch, STABLE-BRANCH-2-2, updated. gnupg-2.2.3-20-g20b52be

by Werner Koch cvs at cvs.gnupg.org
Mon Dec 11 14:40:03 CET 2017


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "The GNU Privacy Guard".

The branch, STABLE-BRANCH-2-2 has been updated
       via  20b52be9ca29b0bc843fc68a279cb72728ede72f (commit)
       via  f2997adee0455c8c0fa391a853ec1b0c9fc43342 (commit)
       via  7a663c296e687f12ccd9a21d414de780feb4dfcf (commit)
       via  6c1dcd79cf0977844179d9a7b189c10af5e42a7e (commit)
      from  3e72143023aa8a01d3e648797df89ae106e24e88 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 20b52be9ca29b0bc843fc68a279cb72728ede72f
Author: Werner Koch <wk at gnupg.org>
Date:   Tue Nov 14 16:24:12 2017 +0100

    dirmngr: Check for WKD support at session end
    
    * dirmngr/domaininfo.c (insert_or_update): Copy the name.
    * dirmngr/misc.c (copy_stream): Allow arg OUT to be NULL.
    * dirmngr/server.c (set_error): Protect CTX.
    (dirmngr_status): Protect against missing ASSUAN_CTX.
    (dirmngr_status_help): Ditto.
    (dirmngr_status_printf): Ditto.
    (cmd_wkd_get): Factor code out to ...
    (proc_wkd_get): new func.  Support silent operation with no CTX.
    (task_check_wkd_support): New.
    --
    
    This finalizes the feature to efficiently cache WKD checks.  If a
    standard WKD query returns no data, we queue a test to be run after
    the end of the session (so that we do not delay the calling client).
    This check tests whether the server responsible for the queried
    address has WKD at all enabled.  The test is done by checking whether
    the "policy" file exists.  We do not check the "submission-address"
    file because that is not necessary for the web key operation.  The
    policy file is now required.
    
    Signed-off-by: Werner Koch <wk at gnupg.org>
    (cherry picked from commit d4e2302d8f4a1ff52d56da4f8e3a5d1c6303822d)

diff --git a/dirmngr/domaininfo.c b/dirmngr/domaininfo.c
index 90cdb85..a2effff 100644
--- a/dirmngr/domaininfo.c
+++ b/dirmngr/domaininfo.c
@@ -158,6 +158,7 @@ insert_or_update (const char *domain,
   di_new = xtrycalloc (1, sizeof *di + strlen (domain));
   if (!di_new)
     return;  /* Out of core - we ignore this.  */
+  strcpy (di_new->name, domain);
 
   /* Need to do another lookup because the malloc is a system call and
    * thus the hash array may have been changed by another thread.  */
diff --git a/dirmngr/ks-action.c b/dirmngr/ks-action.c
index 857aab1..38cd02f 100644
--- a/dirmngr/ks-action.c
+++ b/dirmngr/ks-action.c
@@ -296,7 +296,8 @@ ks_action_get (ctrl_t ctrl, uri_item_t keyservers,
 
 
 /* Retrieve keys from URL and write the result to the provided output
-   stream OUTFP.  */
+ * stream OUTFP.  If OUTFP is NULL the data is written to the bit
+ * bucket. */
 gpg_error_t
 ks_action_fetch (ctrl_t ctrl, const char *url, estream_t outfp)
 {
diff --git a/dirmngr/misc.c b/dirmngr/misc.c
index 1716141..6291a9a 100644
--- a/dirmngr/misc.c
+++ b/dirmngr/misc.c
@@ -636,7 +636,9 @@ armor_data (char **r_string, const void *data, size_t datalen)
   return 0;
 }
 
-/* Copy all data from IN to OUT.  */
+
+/* Copy all data from IN to OUT.  OUT may be NULL to use this fucntion
+ * as a dummy reader.  */
 gpg_error_t
 copy_stream (estream_t in, estream_t out)
 {
@@ -647,9 +649,8 @@ copy_stream (estream_t in, estream_t out)
     {
       if (!nread)
         return 0; /* EOF */
-      if (es_write (out, buffer, nread, NULL))
+      if (out && es_write (out, buffer, nread, NULL))
         break;
-
     }
   return gpg_error_from_syserror ();
 }
diff --git a/dirmngr/server.c b/dirmngr/server.c
index 1fbd007..3d0768b 100644
--- a/dirmngr/server.c
+++ b/dirmngr/server.c
@@ -80,7 +80,8 @@
 
 #define PARM_ERROR(t) assuan_set_error (ctx, \
                                         gpg_error (GPG_ERR_ASS_PARAMETER), (t))
-#define set_error(e,t) assuan_set_error (ctx, gpg_error (e), (t))
+#define set_error(e,t) (ctx ? assuan_set_error (ctx, gpg_error (e), (t)) \
+                        /**/: gpg_error (e))
 
 
 
@@ -828,15 +829,11 @@ cmd_dns_cert (assuan_context_t ctx, char *line)
 
 
 

-static const char hlp_wkd_get[] =
-  "WKD_GET [--submission-address|--policy-flags] <user_id>\n"
-  "\n"
-  "Return the key or other info for <user_id>\n"
-  "from the Web Key Directory.";
+/* Core of cmd_wkd_get and task_check_wkd_support.  If CTX is NULL
+ * this function will not write anything to the assuan output.  */
 static gpg_error_t
-cmd_wkd_get (assuan_context_t ctx, char *line)
+proc_wkd_get (ctrl_t ctrl, assuan_context_t ctx, char *line)
 {
-  ctrl_t ctrl = assuan_get_pointer (ctx);
   gpg_error_t err = 0;
   char *mbox = NULL;
   char *domainbuf = NULL;
@@ -895,7 +892,8 @@ cmd_wkd_get (assuan_context_t ctx, char *line)
       domainlen = strlen (domain);
       for (i = 0; i < srvscount; i++)
         {
-          log_debug ("srv: trying '%s:%hu'\n", srvs[i].target, srvs[i].port);
+          if (DBG_DNS)
+            log_debug ("srv: trying '%s:%hu'\n", srvs[i].target, srvs[i].port);
           targetlen = strlen (srvs[i].target);
           if ((targetlen > domainlen + 1
                && srvs[i].target[targetlen - domainlen - 1] == '.'
@@ -972,19 +970,24 @@ cmd_wkd_get (assuan_context_t ctx, char *line)
   {
     estream_t outfp;
 
-    outfp = es_fopencookie (ctx, "w", data_line_cookie_functions);
-    if (!outfp)
+    outfp = ctx? es_fopencookie (ctx, "w", data_line_cookie_functions) : NULL;
+    if (!outfp && ctx)
       err = set_error (GPG_ERR_ASS_GENERAL,
                        "error setting up a data stream");
     else
       {
-        if (no_log)
-          ctrl->server_local->inhibit_data_logging = 1;
-        ctrl->server_local->inhibit_data_logging_now = 0;
-        ctrl->server_local->inhibit_data_logging_count = 0;
+        if (ctrl->server_local)
+          {
+            if (no_log)
+              ctrl->server_local->inhibit_data_logging = 1;
+            ctrl->server_local->inhibit_data_logging_now = 0;
+            ctrl->server_local->inhibit_data_logging_count = 0;
+          }
         err = ks_action_fetch (ctrl, uri, outfp);
         es_fclose (outfp);
-        ctrl->server_local->inhibit_data_logging = 0;
+        if (ctrl->server_local)
+          ctrl->server_local->inhibit_data_logging = 0;
+
         /* Register the result under the domain name of MBOX. */
         switch (gpg_err_code (err))
           {
@@ -998,8 +1001,9 @@ cmd_wkd_get (assuan_context_t ctx, char *line)
             break;
 
           case GPG_ERR_NO_DATA:
-            if (is_wkd_query) /* Mark that and schedule a check.  */
+            if (is_wkd_query && ctrl->server_local)
               {
+                /* Mark that and schedule a check.  */
                 domaininfo_set_wkd_not_found (domain_orig);
                 workqueue_add_task (task_check_wkd_support, domain_orig,
                                     ctrl->server_local->session_id, 1);
@@ -1020,6 +1024,23 @@ cmd_wkd_get (assuan_context_t ctx, char *line)
   xfree (encodedhash);
   xfree (mbox);
   xfree (domainbuf);
+  return err;
+}
+
+
+static const char hlp_wkd_get[] =
+  "WKD_GET [--submission-address|--policy-flags] <user_id>\n"
+  "\n"
+  "Return the key or other info for <user_id>\n"
+  "from the Web Key Directory.";
+static gpg_error_t
+cmd_wkd_get (assuan_context_t ctx, char *line)
+{
+  ctrl_t ctrl = assuan_get_pointer (ctx);
+  gpg_error_t err;
+
+  err = proc_wkd_get (ctrl, ctx, line);
+
   return leave_cmd (ctx, err);
 }
 
@@ -1029,10 +1050,19 @@ cmd_wkd_get (assuan_context_t ctx, char *line)
 static const char *
 task_check_wkd_support (ctrl_t ctrl, const char *domain)
 {
+  char *string;
+
   if (!ctrl || !domain)
     return "check_wkd_support";
 
-  log_debug ("FIXME: Implement %s\n", __func__);
+  string = strconcat ("--policy-flags foo@", domain, NULL);
+  if (!string)
+    log_error ("%s: %s\n", __func__, gpg_strerror (gpg_error_from_syserror ()));
+  else
+    {
+      proc_wkd_get (ctrl, NULL, string);
+      xfree (string);
+    }
 
   return NULL;
 }
@@ -2800,12 +2830,12 @@ dirmngr_status (ctrl_t ctrl, const char *keyword, ...)
   gpg_error_t err = 0;
   va_list arg_ptr;
   const char *text;
+  assuan_context_t ctx;
 
   va_start (arg_ptr, keyword);
 
-  if (ctrl->server_local)
+  if (ctrl->server_local && (ctx = ctrl->server_local->assuan_ctx))
     {
-      assuan_context_t ctx = ctrl->server_local->assuan_ctx;
       char buf[950], *p;
       size_t n;
 
@@ -2835,10 +2865,10 @@ gpg_error_t
 dirmngr_status_help (ctrl_t ctrl, const char *text)
 {
   gpg_error_t err = 0;
+  assuan_context_t ctx;
 
-  if (ctrl->server_local)
+  if (ctrl->server_local && (ctx = ctrl->server_local->assuan_ctx))
     {
-      assuan_context_t ctx = ctrl->server_local->assuan_ctx;
       char buf[950], *p;
       size_t n;
 
@@ -2888,7 +2918,10 @@ dirmngr_status_printf (ctrl_t ctrl, const char *keyword,
 {
   gpg_error_t err;
   va_list arg_ptr;
-  assuan_context_t ctx = ctrl->server_local->assuan_ctx;
+  assuan_context_t ctx;
+
+  if (!ctrl->server_local || !(ctx = ctrl->server_local->assuan_ctx))
+    return 0;
 
   va_start (arg_ptr, format);
   err = vprint_assuan_status (ctx, keyword, format, arg_ptr);

commit f2997adee0455c8c0fa391a853ec1b0c9fc43342
Author: Werner Koch <wk at gnupg.org>
Date:   Tue Nov 14 13:42:18 2017 +0100

    dirmngr: Add a background task framework.
    
    * dirmngr/workqueue.c: New.
    * dirmngr/Makefile.am (dirmngr_SOURCES): Add new file.
    * dirmngr/server.c (server_local_s): New field session_id.
    (cmd_wkd_get): Add a task.
    (task_check_wkd_support): New stub function.
    (cmd_getinfo): New sub-commands "session_id" and "workqueue".
    (start_command_handler): Add arg session_id and store it in
    SERVER_LOCAL.
    (dirmngr_status_helpf): New.
    * dirmngr/dirmngr.h (wqtask_t): New type.
    * dirmngr/dirmngr.c (main): Pass 0 as session_id to
    start_command_handler.
    (start_connection_thread): Introduce a session_id and pass it to
    start_command_handler.  Run post session tasks.
    (housekeeping_thread): Run global workqueue tasks.
    --
    
    Signed-off-by: Werner Koch <wk at gnupg.org>
    (cherry picked from commit 96a4fbecd1acf946dcde20bef4752c539dae196b)

diff --git a/dirmngr/Makefile.am b/dirmngr/Makefile.am
index 421a325..43f59bd 100644
--- a/dirmngr/Makefile.am
+++ b/dirmngr/Makefile.am
@@ -60,6 +60,7 @@ noinst_HEADERS = dirmngr.h crlcache.h crlfetch.h misc.h
 dirmngr_SOURCES = dirmngr.c dirmngr.h server.c crlcache.c crlfetch.c	\
 	certcache.c certcache.h \
 	domaininfo.c \
+	workqueue.c \
 	loadswdb.c \
 	cdb.h cdblib.c misc.c dirmngr-err.h  \
 	ocsp.c ocsp.h validate.c validate.h  \
diff --git a/dirmngr/dirmngr.c b/dirmngr/dirmngr.c
index 2b64655..9cb0203 100644
--- a/dirmngr/dirmngr.c
+++ b/dirmngr/dirmngr.c
@@ -1134,7 +1134,7 @@ main (int argc, char **argv)
       cert_cache_init (hkp_cacert_filenames);
       crl_cache_init ();
       http_register_netactivity_cb (netactivity_action);
-      start_command_handler (ASSUAN_INVALID_FD);
+      start_command_handler (ASSUAN_INVALID_FD, 0);
       shutdown_reaper ();
     }
 #ifndef HAVE_W32_SYSTEM
@@ -1939,7 +1939,10 @@ housekeeping_thread (void *arg)
       network_activity_seen = 0;
       if (opt.allow_version_check)
         dirmngr_load_swdb (&ctrlbuf, 0);
+      workqueue_run_global_tasks (&ctrlbuf, 1);
     }
+  else
+    workqueue_run_global_tasks (&ctrlbuf, 0);
 
   dirmngr_deinit_default_ctrl (&ctrlbuf);
 
@@ -2034,6 +2037,8 @@ check_nonce (assuan_fd_t fd, assuan_sock_nonce_t *nonce)
 static void *
 start_connection_thread (void *arg)
 {
+  static unsigned int last_session_id;
+  unsigned int session_id;
   union int_and_ptr_u argval;
   gnupg_fd_t fd;
 
@@ -2055,12 +2060,17 @@ start_connection_thread (void *arg)
   if (opt.verbose)
     log_info (_("handler for fd %d started\n"), FD2INT (fd));
 
-  start_command_handler (fd);
+  session_id = ++last_session_id;
+  if (!session_id)
+    session_id = ++last_session_id;
+  start_command_handler (fd, session_id);
 
   if (opt.verbose)
     log_info (_("handler for fd %d terminated\n"), FD2INT (fd));
   active_connections--;
 
+  workqueue_run_post_session_tasks (session_id);
+
 #ifndef HAVE_W32_SYSTEM
   argval.afd = ASSUAN_INVALID_FD;
   npth_setspecific (my_tlskey_current_fd, argval.aptr);
diff --git a/dirmngr/dirmngr.h b/dirmngr/dirmngr.h
index b08e4fe..5189f93 100644
--- a/dirmngr/dirmngr.h
+++ b/dirmngr/dirmngr.h
@@ -228,9 +228,11 @@ ksba_cert_t get_cert_local_ski (ctrl_t ctrl,
 gpg_error_t get_istrusted_from_client (ctrl_t ctrl, const char *hexfpr);
 int dirmngr_assuan_log_monitor (assuan_context_t ctx, unsigned int cat,
                                 const char *msg);
-void start_command_handler (gnupg_fd_t fd);
+void start_command_handler (gnupg_fd_t fd, unsigned int session_id);
 gpg_error_t dirmngr_status (ctrl_t ctrl, const char *keyword, ...);
 gpg_error_t dirmngr_status_help (ctrl_t ctrl, const char *text);
+gpg_error_t dirmngr_status_helpf (ctrl_t ctrl, const char *format,
+                                  ...) GPGRT_ATTR_PRINTF(2,3);
 gpg_error_t dirmngr_status_printf (ctrl_t ctrl, const char *keyword,
                                    const char *format,
                                    ...) GPGRT_ATTR_PRINTF(3,4);
@@ -258,6 +260,15 @@ void domaininfo_set_wkd_supported (const char *domain);
 void domaininfo_set_wkd_not_supported (const char *domain);
 void domaininfo_set_wkd_not_found (const char *domain);
 
+/*-- workqueue.c --*/
+typedef const char *(*wqtask_t)(ctrl_t ctrl, const char *args);
+
+void workqueue_dump_queue (ctrl_t ctrl);
+gpg_error_t workqueue_add_task (wqtask_t func, const char *args,
+                                unsigned int session_id, int need_network);
+void workqueue_run_global_tasks (ctrl_t ctrl, int with_network);
+void workqueue_run_post_session_tasks (unsigned int session_id);
+
 
 
 #endif /*DIRMNGR_H*/
diff --git a/dirmngr/server.c b/dirmngr/server.c
index 18a5f72..1fbd007 100644
--- a/dirmngr/server.c
+++ b/dirmngr/server.c
@@ -90,6 +90,9 @@ struct server_local_s
   /* Data used to associate an Assuan context with local server data */
   assuan_context_t assuan_ctx;
 
+  /* The session id (a counter).  */
+  unsigned int session_id;
+
   /* Per-session LDAP servers.  */
   ldap_server_t ldapservers;
 
@@ -125,6 +128,9 @@ static es_cookie_io_functions_t data_line_cookie_functions =
   };
 
 
+/* Local prototypes */
+static const char *task_check_wkd_support (ctrl_t ctrl, const char *domain);
+
 
 
 

@@ -992,8 +998,12 @@ cmd_wkd_get (assuan_context_t ctx, char *line)
             break;
 
           case GPG_ERR_NO_DATA:
-            if (is_wkd_query) /* Mark that - we will latter do a check.  */
-              domaininfo_set_wkd_not_found (domain_orig);
+            if (is_wkd_query) /* Mark that and schedule a check.  */
+              {
+                domaininfo_set_wkd_not_found (domain_orig);
+                workqueue_add_task (task_check_wkd_support, domain_orig,
+                                    ctrl->server_local->session_id, 1);
+              }
             else if (opt_policy_flags) /* No policy file - no support.  */
               domaininfo_set_wkd_not_supported (domain_orig);
             break;
@@ -1014,6 +1024,20 @@ cmd_wkd_get (assuan_context_t ctx, char *line)
 }
 
 
+/* A task to check whether DOMAIN supports WKD.  This is done by
+ * checking whether the policy flags file can be read.  */
+static const char *
+task_check_wkd_support (ctrl_t ctrl, const char *domain)
+{
+  if (!ctrl || !domain)
+    return "check_wkd_support";
+
+  log_debug ("FIXME: Implement %s\n", __func__);
+
+  return NULL;
+}
+
+
 

 static const char hlp_ldapserver[] =
   "LDAPSERVER <data>\n"
@@ -2428,12 +2452,15 @@ static const char hlp_getinfo[] =
   "pid         - Return the process id of the server.\n"
   "tor         - Return OK if running in Tor mode\n"
   "dnsinfo     - Return info about the DNS resolver\n"
-  "socket_name - Return the name of the socket.\n";
+  "socket_name - Return the name of the socket.\n"
+  "session_id  - Return the current session_id.\n"
+  "workqueue   - Inspect the work queue\n";
 static gpg_error_t
 cmd_getinfo (assuan_context_t ctx, char *line)
 {
   ctrl_t ctrl = assuan_get_pointer (ctx);
   gpg_error_t err;
+  char numbuf[50];
 
   if (!strcmp (line, "version"))
     {
@@ -2442,8 +2469,6 @@ cmd_getinfo (assuan_context_t ctx, char *line)
     }
   else if (!strcmp (line, "pid"))
     {
-      char numbuf[50];
-
       snprintf (numbuf, sizeof numbuf, "%lu", (unsigned long)getpid ());
       err = assuan_send_data (ctx, numbuf, strlen (numbuf));
     }
@@ -2452,6 +2477,11 @@ cmd_getinfo (assuan_context_t ctx, char *line)
       const char *s = dirmngr_get_current_socket_name ();
       err = assuan_send_data (ctx, s, strlen (s));
     }
+  else if (!strcmp (line, "session_id"))
+    {
+      snprintf (numbuf, sizeof numbuf, "%u", ctrl->server_local->session_id);
+      err = assuan_send_data (ctx, numbuf, strlen (numbuf));
+    }
   else if (!strcmp (line, "tor"))
     {
       int use_tor;
@@ -2487,6 +2517,11 @@ cmd_getinfo (assuan_context_t ctx, char *line)
         }
       err = 0;
     }
+  else if (!strcmp (line, "workqueue"))
+    {
+      workqueue_dump_queue (ctrl);
+      err = 0;
+    }
   else
     err = set_error (GPG_ERR_ASS_PARAMETER, "unknown value for WHAT");
 
@@ -2614,9 +2649,10 @@ dirmngr_assuan_log_monitor (assuan_context_t ctx, unsigned int cat,
 
 
 /* Startup the server and run the main command loop.  With FD = -1,
-   use stdin/stdout. */
+ * use stdin/stdout.  SESSION_ID is either 0 or a unique number
+ * identifying a session.  */
 void
-start_command_handler (assuan_fd_t fd)
+start_command_handler (assuan_fd_t fd, unsigned int session_id)
 {
   static const char hello[] = "Dirmngr " VERSION " at your service";
   static char *hello_line;
@@ -2693,6 +2729,8 @@ start_command_handler (assuan_fd_t fd)
   assuan_register_option_handler (ctx, option_handler);
   assuan_register_reset_notify (ctx, reset_notify);
 
+  ctrl->server_local->session_id = session_id;
+
   for (;;)
     {
       rc = assuan_accept (ctx);
@@ -2792,8 +2830,7 @@ dirmngr_status (ctrl_t ctrl, const char *keyword, ...)
 }
 
 
-/* Print a help status line.  TEXTLEN gives the length of the text
-   from TEXT to be printed.  The function splits text at LFs.  */
+/* Print a help status line.  The function splits text at LFs.  */
 gpg_error_t
 dirmngr_status_help (ctrl_t ctrl, const char *text)
 {
@@ -2823,6 +2860,26 @@ dirmngr_status_help (ctrl_t ctrl, const char *text)
 }
 
 
+/* Print a help status line using a printf like format.  The function
+ * splits text at LFs.  */
+gpg_error_t
+dirmngr_status_helpf (ctrl_t ctrl, const char *format, ...)
+{
+  va_list arg_ptr;
+  gpg_error_t err;
+  char *buf;
+
+  va_start (arg_ptr, format);
+  buf = es_vbsprintf (format, arg_ptr);
+  err = buf? 0 : gpg_error_from_syserror ();
+  va_end (arg_ptr);
+  if (!err)
+    err = dirmngr_status_help (ctrl, buf);
+  es_free (buf);
+  return err;
+}
+
+
 /* This function is similar to print_assuan_status but takes a CTRL
  * arg instead of an assuan context as first argument.  */
 gpg_error_t
diff --git a/dirmngr/workqueue.c b/dirmngr/workqueue.c
new file mode 100644
index 0000000..2cb8573
--- /dev/null
+++ b/dirmngr/workqueue.c
@@ -0,0 +1,214 @@
+/* workqueue.c - Maintain a queue of background tasks
+ * Copyright (C) 2017 Werner Koch
+ *
+ * This file is part of GnuPG.
+ *
+ * GnuPG is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuPG 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0+
+ */
+
+#include <config.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "dirmngr.h"
+
+
+/* An object for one item in the workqueue.  */
+struct wqitem_s
+{
+  struct wqitem_s *next;
+
+  /* This flag is set if the task requires network access.  */
+  unsigned int need_network:1;
+
+  /* The id of the session which created this task.  If this is 0 the
+   * task is not associated with a specific session.  */
+  unsigned int session_id;
+
+  /* The function to perform the backgrount task.  */
+  wqtask_t func;
+
+  /* A string with the string argument for that task.  */
+  char args[1];
+};
+typedef struct wqitem_s *wqitem_t;
+
+
+/* The workque is a simple linked list.  */
+static wqitem_t workqueue;
+
+
+/* Dump the queue using Assuan status comments.  */
+void
+workqueue_dump_queue (ctrl_t ctrl)
+{
+  wqitem_t saved_workqueue;
+  wqitem_t item;
+  unsigned int count;
+
+  /* Temporay detach the entiere workqueue so that other threads don't
+   * get into our way.  */
+  saved_workqueue = workqueue;
+  workqueue = NULL;
+
+  for (count=0, item = saved_workqueue; item; item = item->next)
+    count++;
+
+  dirmngr_status_helpf (ctrl, "wq: number of entries: %u", count);
+  for (item = saved_workqueue; item; item = item->next)
+    dirmngr_status_helpf (ctrl, "wq: sess=%u net=%d %s(\"%.100s%s\")",
+                          item->session_id, item->need_network,
+                          item->func? item->func (NULL, NULL): "nop",
+                          item->args, strlen (item->args) > 100? "[...]":"");
+
+  /* Restore then workqueue.  Actually we append the saved queue do a
+   * possibly updated workqueue.  */
+  if (!(item=workqueue))
+    workqueue = saved_workqueue;
+  else
+    {
+      while (item->next)
+        item = item->next;
+      item->next = saved_workqueue;
+    }
+}
+
+
+/* Append the task (FUNC,ARGS) to the work queue.  FUNC shall return
+ * its name when called with (NULL, NULL).  */
+gpg_error_t
+workqueue_add_task (wqtask_t func, const char *args, unsigned int session_id,
+                    int need_network)
+{
+  wqitem_t item, wi;
+
+  item = xtrycalloc (1, sizeof *item + strlen (args));
+  if (!item)
+    return gpg_error_from_syserror ();
+  strcpy (item->args, args);
+  item->func = func;
+  item->session_id = session_id;
+  item->need_network = !!need_network;
+
+  if (!(wi=workqueue))
+    workqueue = item;
+  else
+    {
+      while (wi->next)
+        wi = wi->next;
+      wi->next = item;
+    }
+  return 0;
+}
+
+
+/* Run the task described by ITEM.  ITEM must have been detached from
+ * the workqueue; its ownership is transferred to this fucntion.  */
+static void
+run_a_task (ctrl_t ctrl, wqitem_t item)
+{
+  log_assert (!item->next);
+
+  if (opt.verbose)
+    log_info ("session %u: running %s(\"%s%s\")\n",
+              item->session_id,
+              item->func? item->func (NULL, NULL): "nop",
+              item->args, strlen (item->args) > 100? "[...]":"");
+  if (item->func)
+    item->func (ctrl, item->args);
+
+  xfree (item);
+}
+
+
+/* Run tasks not associated with a session.  This is called from the
+ * ticker every few minutes.  If WITH_NETWORK is not set tasks which
+ * require the network are not run.  */
+void
+workqueue_run_global_tasks (ctrl_t ctrl, int with_network)
+{
+  wqitem_t item, prev;
+
+  with_network = !!with_network;
+
+  if (opt.verbose)
+    log_info ("running scheduled tasks%s\n", with_network?" (with network)":"");
+
+  for (;;)
+    {
+      prev = NULL;
+      for (item = workqueue; item; prev = item, item = item->next)
+        if (!item->session_id
+            && (!item->need_network || (item->need_network && with_network)))
+          break;
+      if (!item)
+        break;  /* No more tasks to run.  */
+
+      /* Detach that item from the workqueue.  */
+      if (!prev)
+        workqueue = item->next;
+      else
+        prev->next = item->next;
+      item->next = NULL;
+
+      /* Run the task.  */
+      run_a_task (ctrl, item);
+    }
+}
+
+
+/* Run tasks scheduled for running after a session.  Those tasks are
+ * identified by the SESSION_ID.  */
+void
+workqueue_run_post_session_tasks (unsigned int session_id)
+{
+  struct server_control_s ctrlbuf;
+  ctrl_t ctrl = NULL;
+  wqitem_t item, prev;
+
+  if (!session_id)
+    return;
+
+  for (;;)
+    {
+      prev = NULL;
+      for (item = workqueue; item; prev = item, item = item->next)
+        if (item->session_id == session_id)
+          break;
+      if (!item)
+        break;  /* No more tasks for this session.  */
+
+      /* Detach that item from the workqueue.  */
+      if (!prev)
+        workqueue = item->next;
+      else
+        prev->next = item->next;
+      item->next = NULL;
+
+      /* Create a CTRL object the first time we need it.  */
+      if (!ctrl)
+        {
+          memset (&ctrlbuf, 0, sizeof ctrlbuf);
+          ctrl = &ctrlbuf;
+          dirmngr_init_default_ctrl (ctrl);
+        }
+
+      /* Run the task.  */
+      run_a_task (ctrl, item);
+    }
+
+  dirmngr_deinit_default_ctrl (ctrl);
+}

commit 7a663c296e687f12ccd9a21d414de780feb4dfcf
Author: Werner Koch <wk at gnupg.org>
Date:   Tue Nov 14 08:37:27 2017 +0100

    dirmngr: Limit the number of cached domains for WKD.
    
    * dirmngr/domaininfo.c (MAX_DOMAINBUCKET_LEN): New.
    (insert_or_update): Limit the length of a bucket chain.
    (domaininfo_print_stats): Print just one summary line.
    
    Signed-off-by: Werner Koch <wk at gnupg.org>
    (cherry picked from commit 26f08343fbccdbaa177c3507a3c5e24a5cf94a2d)

diff --git a/dirmngr/domaininfo.c b/dirmngr/domaininfo.c
index 393db8c..90cdb85 100644
--- a/dirmngr/domaininfo.c
+++ b/dirmngr/domaininfo.c
@@ -26,7 +26,18 @@
 #include "dirmngr.h"
 
 
-#define NO_OF_DOMAINBUCKETS 103
+/* Number of bucket for the hash array and limit for the length of a
+ * bucket chain.  For debugging values of 13 and 10 are more suitable
+ * and a command like
+ *   for j   in a b c d e f g h i j k l m n o p q r s t u v w z y z; do \
+ *     for i in a b c d e f g h i j k l m n o p q r s t u v w z y z; do \
+ *       gpg-connect-agent --dirmngr "wkd_get foo@$i.$j.gnupg.net" /bye \
+ *       >/dev/null ; done; done
+ * will quickly add a couple of domains.
+ */
+#define NO_OF_DOMAINBUCKETS  103
+#define MAX_DOMAINBUCKET_LEN  20
+
 
 /* Object to keep track of a domain name.  */
 struct domaininfo_s
@@ -74,13 +85,18 @@ domaininfo_print_stats (void)
   int bidx;
   domaininfo_t di;
   int count, no_name, wkd_not_found, wkd_supported, wkd_not_supported;
+  int len, minlen, maxlen;
 
+  count = no_name = wkd_not_found = wkd_supported = wkd_not_supported = 0;
+  maxlen = 0;
+  minlen = -1;
   for (bidx = 0; bidx < NO_OF_DOMAINBUCKETS; bidx++)
     {
-      count = no_name = wkd_not_found = wkd_supported = wkd_not_supported = 0;
+      len = 0;
       for (di = domainbuckets[bidx]; di; di = di->next)
         {
           count++;
+          len++;
           if (di->no_name)
             no_name++;
           if (di->wkd_not_found)
@@ -90,11 +106,16 @@ domaininfo_print_stats (void)
           if (di->wkd_not_supported)
             wkd_not_supported++;
         }
-      if (count)
-        log_info ("domaininfo: chain %3d length=%d nn=%d nf=%d s=%d ns=%d\n",
-                  bidx, count, no_name,
-                  wkd_not_found, wkd_supported, wkd_not_supported);
+      if (len > maxlen)
+        maxlen = len;
+      if (minlen == -1 || len < minlen)
+        minlen = len;
     }
+  log_info ("domaininfo: items=%d chainlen=%d..%d nn=%d nf=%d ns=%d s=%d\n",
+            count,
+            minlen > 0? minlen : 0,
+            maxlen,
+            no_name, wkd_not_found, wkd_not_supported, wkd_supported);
 }
 
 
@@ -122,7 +143,9 @@ insert_or_update (const char *domain,
 {
   domaininfo_t di;
   domaininfo_t di_new;
+  domaininfo_t di_cut;
   u32 hash;
+  int count;
 
   hash = hash_domain (domain);
   for (di = domainbuckets[hash]; di; di = di->next)
@@ -138,7 +161,8 @@ insert_or_update (const char *domain,
 
   /* Need to do another lookup because the malloc is a system call and
    * thus the hash array may have been changed by another thread.  */
-  for (di = domainbuckets[hash]; di; di = di->next)
+  di_cut = NULL;
+  for (count=0, di = domainbuckets[hash]; di; di = di->next, count++)
     if (!strcmp (di->name, domain))
       {
         callback (di, 0);  /* Update */
@@ -146,10 +170,32 @@ insert_or_update (const char *domain,
         return;
       }
 
-  callback (di_new, 1);  /* Insert */
+  /* Before we insert we need to check whether the chain gets too long.  */
+  di_cut = NULL;
+  if (count >= MAX_DOMAINBUCKET_LEN)
+    {
+      for (count=0, di = domainbuckets[hash]; di; di = di->next, count++)
+        if (count >= MAX_DOMAINBUCKET_LEN/2)
+          {
+            di_cut = di->next;
+            di->next = NULL;
+            break;
+          }
+    }
+
+  /* Insert */
+  callback (di_new, 1);
   di = di_new;
   di->next = domainbuckets[hash];
   domainbuckets[hash] = di;
+
+  /* Remove the rest of the cutted chain.  */
+  while (di_cut)
+    {
+      di = di_cut->next;
+      xfree (di_cut);
+      di_cut = di;
+    }
 }
 
 

commit 6c1dcd79cf0977844179d9a7b189c10af5e42a7e
Author: Werner Koch <wk at gnupg.org>
Date:   Mon Nov 13 16:09:32 2017 +0100

    dirmngr: Keep track of domains used for WKD queries
    
    * dirmngr/domaininfo.c: New file.
    * dirmngr/Makefile.am (dirmngr_SOURCES): Add file.
    * dirmngr/server.c (cmd_wkd_get): Check whether the domain is already
    known and tell domaininfo about the results.
    --
    
    This adds a registry for domain information to eventually avoid
    useless queries for domains which do not support WKD.  The missing
    part is a background task to check whether a queried domain supports
    WKD at all and to expire old entries.
    
    Signed-off-by: Werner Koch <wk at gnupg.org>
    (cherry picked from commit 65038e6852185c20413d8f6602218ee636413b77)

diff --git a/dirmngr/Makefile.am b/dirmngr/Makefile.am
index b404165..421a325 100644
--- a/dirmngr/Makefile.am
+++ b/dirmngr/Makefile.am
@@ -16,6 +16,8 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, see <https://www.gnu.org/licenses/>.
+#
+# SPDX-License-Identifier: GPL-3.0+
 
 ## Process this file with automake to produce Makefile.in
 
@@ -57,6 +59,7 @@ noinst_HEADERS = dirmngr.h crlcache.h crlfetch.h misc.h
 
 dirmngr_SOURCES = dirmngr.c dirmngr.h server.c crlcache.c crlfetch.c	\
 	certcache.c certcache.h \
+	domaininfo.c \
 	loadswdb.c \
 	cdb.h cdblib.c misc.c dirmngr-err.h  \
 	ocsp.c ocsp.h validate.c validate.h  \
diff --git a/dirmngr/dirmngr.c b/dirmngr/dirmngr.c
index 5317c21..2b64655 100644
--- a/dirmngr/dirmngr.c
+++ b/dirmngr/dirmngr.c
@@ -17,6 +17,8 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0+
  */
 
 #include <config.h>
@@ -1871,6 +1873,7 @@ handle_signal (int signo)
 
     case SIGUSR1:
       cert_cache_print_stats ();
+      domaininfo_print_stats ();
       break;
 
     case SIGUSR2:
diff --git a/dirmngr/dirmngr.h b/dirmngr/dirmngr.h
index 1f660de..b08e4fe 100644
--- a/dirmngr/dirmngr.h
+++ b/dirmngr/dirmngr.h
@@ -17,6 +17,8 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0+
  */
 
 #ifndef DIRMNGR_H
@@ -248,4 +250,14 @@ gpg_error_t gnupg_http_tls_verify_cb (void *opaque,
 gpg_error_t dirmngr_load_swdb (ctrl_t ctrl, int force);
 
 
+/*-- domaininfo.c --*/
+void domaininfo_print_stats (void);
+int  domaininfo_is_wkd_not_supported (const char *domain);
+void domaininfo_set_no_name (const char *domain);
+void domaininfo_set_wkd_supported (const char *domain);
+void domaininfo_set_wkd_not_supported (const char *domain);
+void domaininfo_set_wkd_not_found (const char *domain);
+
+
+
 #endif /*DIRMNGR_H*/
diff --git a/dirmngr/domaininfo.c b/dirmngr/domaininfo.c
new file mode 100644
index 0000000..393db8c
--- /dev/null
+++ b/dirmngr/domaininfo.c
@@ -0,0 +1,244 @@
+/* domaininfo.c - Gather statistics about accessed domains
+ * Copyright (C) 2017 Werner Koch
+ *
+ * This file is part of GnuPG.
+ *
+ * GnuPG is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuPG 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0+
+ */
+
+#include <config.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "dirmngr.h"
+
+
+#define NO_OF_DOMAINBUCKETS 103
+
+/* Object to keep track of a domain name.  */
+struct domaininfo_s
+{
+  struct domaininfo_s *next;
+  unsigned int no_name:1;            /* Domain name not found.            */
+  unsigned int wkd_not_found:1;      /* A WKD query failed.               */
+  unsigned int wkd_supported:1;      /* One WKD entry was found.          */
+  unsigned int wkd_not_supported:1;  /* Definitely does not support WKD.  */
+  char name[1];
+};
+typedef struct domaininfo_s *domaininfo_t;
+
+/* And the hashed array.  */
+static domaininfo_t domainbuckets[NO_OF_DOMAINBUCKETS];
+
+
+/* The hash function we use.  Must not call a system function.  */
+static inline u32
+hash_domain (const char *domain)
+{
+  const unsigned char *s = (const unsigned char*)domain;
+  u32 hashval = 0;
+  u32 carry;
+
+  for (; *s; s++)
+    {
+      if (*s == '.')
+        continue;
+      hashval = (hashval << 4) + *s;
+      if ((carry = (hashval & 0xf0000000)))
+        {
+          hashval ^= (carry >> 24);
+          hashval ^= carry;
+        }
+    }
+
+  return hashval % NO_OF_DOMAINBUCKETS;
+}
+
+
+void
+domaininfo_print_stats (void)
+{
+  int bidx;
+  domaininfo_t di;
+  int count, no_name, wkd_not_found, wkd_supported, wkd_not_supported;
+
+  for (bidx = 0; bidx < NO_OF_DOMAINBUCKETS; bidx++)
+    {
+      count = no_name = wkd_not_found = wkd_supported = wkd_not_supported = 0;
+      for (di = domainbuckets[bidx]; di; di = di->next)
+        {
+          count++;
+          if (di->no_name)
+            no_name++;
+          if (di->wkd_not_found)
+            wkd_not_found++;
+          if (di->wkd_supported)
+            wkd_supported++;
+          if (di->wkd_not_supported)
+            wkd_not_supported++;
+        }
+      if (count)
+        log_info ("domaininfo: chain %3d length=%d nn=%d nf=%d s=%d ns=%d\n",
+                  bidx, count, no_name,
+                  wkd_not_found, wkd_supported, wkd_not_supported);
+    }
+}
+
+
+/* Return true if DOMAIN definitely does not support WKD.  Noet that
+ * DOMAIN is expected to be lowercase.  */
+int
+domaininfo_is_wkd_not_supported (const char *domain)
+{
+  domaininfo_t di;
+
+  for (di = domainbuckets[hash_domain (domain)]; di; di = di->next)
+    if (!strcmp (di->name, domain))
+      return !!di->wkd_not_supported;
+
+  return 0;  /* We don't know.  */
+}
+
+
+/* Core update function.  DOMAIN is expected to be lowercase.
+ * CALLBACK is called to update the existing or the newly inserted
+ * item.  */
+static void
+insert_or_update (const char *domain,
+                  void (*callback)(domaininfo_t di, int insert_mode))
+{
+  domaininfo_t di;
+  domaininfo_t di_new;
+  u32 hash;
+
+  hash = hash_domain (domain);
+  for (di = domainbuckets[hash]; di; di = di->next)
+    if (!strcmp (di->name, domain))
+      {
+        callback (di, 0);  /* Update */
+        return;
+      }
+
+  di_new = xtrycalloc (1, sizeof *di + strlen (domain));
+  if (!di_new)
+    return;  /* Out of core - we ignore this.  */
+
+  /* Need to do another lookup because the malloc is a system call and
+   * thus the hash array may have been changed by another thread.  */
+  for (di = domainbuckets[hash]; di; di = di->next)
+    if (!strcmp (di->name, domain))
+      {
+        callback (di, 0);  /* Update */
+        xfree (di_new);
+        return;
+      }
+
+  callback (di_new, 1);  /* Insert */
+  di = di_new;
+  di->next = domainbuckets[hash];
+  domainbuckets[hash] = di;
+}
+
+
+/* Helper for domaininfo_set_no_name.  */
+static void
+set_no_name_cb (domaininfo_t di, int insert_mode)
+{
+  (void)insert_mode;
+
+  di->no_name = 1;
+  /* Obviously the domain is in this case also not supported.  */
+  di->wkd_not_supported = 1;
+
+  /* The next should already be 0 but we clear it anyway in the case
+   * of a temporary DNS failure.  */
+  di->wkd_supported = 0;
+}
+
+
+/* Mark DOMAIN as not existent.  */
+void
+domaininfo_set_no_name (const char *domain)
+{
+  insert_or_update (domain, set_no_name_cb);
+}
+
+
+/* Helper for domaininfo_set_wkd_supported.  */
+static void
+set_wkd_supported_cb (domaininfo_t di, int insert_mode)
+{
+  (void)insert_mode;
+
+  di->wkd_supported = 1;
+  /* The next will already be set unless the domain enabled WKD in the
+   * meantime.  Thus we need to clear it.  */
+  di->wkd_not_supported = 0;
+}
+
+
+/* Mark DOMAIN as supporting WKD.  */
+void
+domaininfo_set_wkd_supported (const char *domain)
+{
+  insert_or_update (domain, set_wkd_supported_cb);
+}
+
+
+/* Helper for domaininfo_set_wkd_not_supported.  */
+static void
+set_wkd_not_supported_cb (domaininfo_t di, int insert_mode)
+{
+  (void)insert_mode;
+
+  di->wkd_not_supported = 1;
+  di->wkd_supported = 0;
+}
+
+
+/* Mark DOMAIN as not supporting WKD queries (e.g. no policy file).  */
+void
+domaininfo_set_wkd_not_supported (const char *domain)
+{
+  insert_or_update (domain, set_wkd_not_supported_cb);
+}
+
+
+
+/* Helper for domaininfo_set_wkd_not_found.  */
+static void
+set_wkd_not_found_cb (domaininfo_t di, int insert_mode)
+{
+  /* Set the not found flag but there is no need to do this if we
+   * already know that the domain either does not support WKD or we
+   * know that it supports WKD.  */
+  if (insert_mode)
+    di->wkd_not_found = 1;
+  else if (!di->wkd_not_supported && !di->wkd_supported)
+    di->wkd_not_found = 1;
+
+  /* Better clear this flag in case we had a DNS failure in the
+   * past.  */
+  di->no_name = 0;
+}
+
+
+/* Update a counter for DOMAIN to keep track of failed WKD queries.  */
+void
+domaininfo_set_wkd_not_found (const char *domain)
+{
+  insert_or_update (domain, set_wkd_not_found_cb);
+}
diff --git a/dirmngr/server.c b/dirmngr/server.c
index 7ed6cde..18a5f72 100644
--- a/dirmngr/server.c
+++ b/dirmngr/server.c
@@ -18,6 +18,8 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0+
  */
 
 #include <config.h>
@@ -833,11 +835,13 @@ cmd_wkd_get (assuan_context_t ctx, char *line)
   char *mbox = NULL;
   char *domainbuf = NULL;
   char *domain;     /* Points to mbox or domainbuf.  */
+  char *domain_orig;/* Points to mbox.  */
   char sha1buf[20];
   char *uri = NULL;
   char *encodedhash = NULL;
   int opt_submission_addr;
   int opt_policy_flags;
+  int is_wkd_query;   /* True if this is a real WKD query.  */
   int no_log = 0;
   char portstr[20] = { 0 };
 
@@ -846,6 +850,7 @@ cmd_wkd_get (assuan_context_t ctx, char *line)
   if (has_option (line, "--quick"))
     ctrl->timeout = opt.connect_quick_timeout;
   line = skip_options (line);
+  is_wkd_query = !(opt_policy_flags || opt_submission_addr);
 
   mbox = mailbox_from_userid (line);
   if (!mbox || !(domain = strchr (mbox, '@')))
@@ -854,6 +859,18 @@ cmd_wkd_get (assuan_context_t ctx, char *line)
       goto leave;
     }
   *domain++ = 0;
+  domain_orig = domain;
+
+  /* First check whether we already know that the domain does not
+   * support WKD.  */
+  if (is_wkd_query)
+    {
+      if (domaininfo_is_wkd_not_supported (domain_orig))
+        {
+          err = gpg_error (GPG_ERR_NO_DATA);
+          goto leave;
+        }
+    }
 
   /* Check for SRV records.  */
   if (1)
@@ -962,6 +979,29 @@ cmd_wkd_get (assuan_context_t ctx, char *line)
         err = ks_action_fetch (ctrl, uri, outfp);
         es_fclose (outfp);
         ctrl->server_local->inhibit_data_logging = 0;
+        /* Register the result under the domain name of MBOX. */
+        switch (gpg_err_code (err))
+          {
+          case 0:
+            domaininfo_set_wkd_supported (domain_orig);
+            break;
+
+          case GPG_ERR_NO_NAME:
+            /* There is no such domain.  */
+            domaininfo_set_no_name (domain_orig);
+            break;
+
+          case GPG_ERR_NO_DATA:
+            if (is_wkd_query) /* Mark that - we will latter do a check.  */
+              domaininfo_set_wkd_not_found (domain_orig);
+            else if (opt_policy_flags) /* No policy file - no support.  */
+              domaininfo_set_wkd_not_supported (domain_orig);
+            break;
+
+          default:
+            /* Don't register other errors.  */
+            break;
+          }
       }
   }
 

-----------------------------------------------------------------------

Summary of changes:
 dirmngr/Makefile.am  |   4 +
 dirmngr/dirmngr.c    |  17 ++-
 dirmngr/dirmngr.h    |  25 ++++-
 dirmngr/domaininfo.c | 291 +++++++++++++++++++++++++++++++++++++++++++++++++++
 dirmngr/ks-action.c  |   3 +-
 dirmngr/misc.c       |   7 +-
 dirmngr/server.c     | 186 +++++++++++++++++++++++++++-----
 dirmngr/workqueue.c  | 214 +++++++++++++++++++++++++++++++++++++
 8 files changed, 712 insertions(+), 35 deletions(-)
 create mode 100644 dirmngr/domaininfo.c
 create mode 100644 dirmngr/workqueue.c


hooks/post-receive
-- 
The GNU Privacy Guard
http://git.gnupg.org




More information about the Gnupg-commits mailing list