Auto-reconnect after connection failures to gpg-agent.
authorWerner Koch <wk@gnupg.org>
Mon, 25 Feb 2019 17:15:47 +0000 (18:15 +0100)
committerWerner Koch <wk@gnupg.org>
Mon, 25 Feb 2019 17:15:47 +0000 (18:15 +0100)
* src/get-path.c (gnupg_version_string): New.
(read_first_line): New.
(extract_version_string): New.  Taken from gpgme.
(get_gpgconf_path): Retrieve GnuPG version.
(get_gnupg_version): New.
(is_gnupg_older_than): New.
(get_bindir): Re-implement using read_first_line.
* src/support.h: Include gpg-error.h
* src/agent.c (agent_version_major, agent_version_minor): Remove.
(agent_connect): Implement using read_first_line.  Use combined launch
command with fallback to tow command for GnuPG < 2.2.14.
(read_version_cb): Remove.
(agent_configure): Don't send GETINFO.
(scute_agent_get_agent_version): Remove.  Change callers to use the
new get_gnupg_version.
(check_broken_pipe): New.
(ensure_agent_connection): New.
(scute_agent_learn): Ensure that an agent has been started and detect
a broekn pipe.
(scute_agent_check_status): Ditto.
(scute_agent_sign): Ditto.
(scute_agent_decrypt): Ditto.
(scute_agent_is_trusted): Ditto.
(scute_agent_get_cert): Ditto.
(scute_agent_get_random): Ditto.

Signed-off-by: Werner Koch <wk@gnupg.org>
src/agent.c
src/agent.h
src/get-path.c
src/p11-getslotinfo.c
src/support.h

index b1035e4..ab64b70 100644 (file)
 /* The global agent context.  */
 static assuan_context_t agent_ctx;
 
-/* The version number of the agent.  */
-static int agent_version_major;
-static int agent_version_minor;
-
 
 \f
 /* Hack required for Windows.  */
@@ -79,33 +75,25 @@ agent_connect (assuan_context_t *ctx_r)
   gpg_error_t err = 0;
   assuan_context_t ctx = NULL;
   char buffer[512];
-  FILE *fp;
 
-  /* Use gpgconf to obtain the socket name.  */
-  snprintf (buffer, sizeof buffer, "%s --null --list-dirs agent-socket",
+  /* Use gpgconf to make sure that gpg-agent is started and to obtain
+   * the socket name.  For older version of gnupg we will fallback to
+   * using two gpgconf commands with the same effect.  */
+  snprintf (buffer, sizeof buffer, "%s --show-socket --launch gpg-agent",
             get_gpgconf_path ());
-#ifdef HAVE_W32_SYSTEM
-  fp = _popen (buffer, "r");
-#else
-  fp = popen (buffer, "r");
-#endif
-  if (fp)
+  err = read_first_line (buffer, buffer, sizeof buffer);
+  if (gpg_err_code (err) == GPG_ERR_NO_AGENT && is_gnupg_older_than (2, 2, 14))
     {
-      int i, c;
-
-      for (i=0; i < sizeof buffer - 1  && (c = getc (fp)) != EOF; i++)
-        buffer[i] = c;
-      if (c == EOF && ferror (fp))       /* I/O error? */
-        err = gpg_error_from_syserror ();
-      else if (!(i < sizeof buffer - 1))
-        err = gpg_error (GPG_ERR_NO_AGENT);  /* Path too long.  */
-      else if (!i || buffer[i-1])
-        err = gpg_error (GPG_ERR_NO_AGENT);  /* No terminating nul. */
-
-      pclose (fp);
+      snprintf (buffer, sizeof buffer, "%s --launch gpg-agent",
+                get_gpgconf_path ());
+      err = read_first_line (buffer, NULL, 0);
+      if (!err)
+        {
+          snprintf (buffer, sizeof buffer, "%s --list-dirs agent-socket",
+                    get_gpgconf_path ());
+          err = read_first_line (buffer, buffer, sizeof buffer);
+        }
     }
-  else
-    err = gpg_error_from_syserror ();
 
   /* Then connect to the socket we got. */
   if (!err)
@@ -180,28 +168,6 @@ agent_simple_cmd (assuan_context_t ctx, const char *fmt, ...)
 }
 
 
-/* Read and stroe the agent's version number.  */
-static gpg_error_t
-read_version_cb (void *opaque, const void *buffer, size_t length)
-{
-  char version[20];
-  const char *s;
-
-  (void) opaque;
-
-  if (length > sizeof (version) -1)
-    length = sizeof (version) - 1;
-  strncpy (version, buffer, length);
-  version[length] = 0;
-
-  agent_version_major = atoi (version);
-  s = strchr (version, '.');
-  agent_version_minor = s? atoi (s+1) : 0;
-
-  return 0;
-}
-
-
 /* Configure the GPG agent at connection CTX.  */
 static gpg_error_t
 agent_configure (assuan_context_t ctx)
@@ -301,21 +267,51 @@ agent_configure (assuan_context_t ctx)
   if (err && gpg_err_code (err) != GPG_ERR_UNKNOWN_OPTION)
     return err;
 
-  err = assuan_transact (ctx, "GETINFO version",
-                         read_version_cb, NULL,
-                         NULL, NULL, NULL, NULL);
-  if (gpg_err_code (err) == GPG_ERR_UNKNOWN_OPTION)
-    err = 0;
-  else if (err)
-    return err;
+  return err;
+}
 
 
+/* Check for a broken pipe, that is a lost connection to the agent.
+ * Update the gloabls so that a re-connect is done the next time.
+ * Returns ERR or the modified code GPG_ERR_NO_AGENT.  */
+static gpg_error_t
+check_broken_pipe (gpg_error_t err)
+{
+  /* Note that Scute _currently_ uses GPG_ERR_SOURCE_ANY.  */
+  if (gpg_err_code (err) == GPG_ERR_EPIPE
+      && gpg_err_source (err) == GPG_ERR_SOURCE_ANY)
+    {
+      DEBUG (DBG_INFO, "Broken connection to the gpg-agent");
+      scute_agent_finalize ();
+      err = gpg_error (GPG_ERR_NO_AGENT);
+    }
   return err;
 }
 
 
+/* If the connection to the agent was lost earlier and detected by
+ * check_broken_pipe we try to reconnect.  */
+static gpg_error_t
+ensure_agent_connection (void)
+{
+  gpg_error_t err;
+
+  if (agent_ctx)
+    return 0;  /* Connection still known.  */
+
+  DEBUG (DBG_INFO, "Re-connecting to gpg-agent");
+  err = agent_connect (&agent_ctx);
+  if (err)
+    return err;
+
+  err = agent_configure (agent_ctx);
+  return check_broken_pipe (err);
+}
+
+
 /* Try to connect to the agent via socket.  Handle the server's
-   initial greeting.  */
+   initial greeting.  This is used only once when SCute is loaded.
+   Re-connection is done using ensure_agent_connection.  */
 gpg_error_t
 scute_agent_initialize (void)
 {
@@ -340,14 +336,6 @@ scute_agent_initialize (void)
 }
 
 
-int
-scute_agent_get_agent_version (int *minor)
-{
-  *minor = agent_version_minor;
-  return agent_version_major;
-}
-
-
 \f
 /* Return a new malloced string by unescaping the string S.  Escaping
    is percent escaping and '+'/space mapping.  A binary nul will
@@ -743,10 +731,12 @@ scute_agent_learn (struct agent_card_info_s *info)
   gpg_error_t err;
 
   memset (info, 0, sizeof (*info));
-  err = assuan_transact (agent_ctx, "SCD LEARN --force",
-                        NULL, NULL,
-                         default_inq_cb, NULL,
-                        learn_status_cb, info);
+  err = ensure_agent_connection ();
+  if (!err)
+    err = assuan_transact (agent_ctx, "SCD LEARN --force",
+                           NULL, NULL,
+                           default_inq_cb, NULL,
+                           learn_status_cb, info);
   if (gpg_err_source(err) == GPG_ERR_SOURCE_SCD
       && gpg_err_code (err) == GPG_ERR_CARD_REMOVED)
     {
@@ -764,7 +754,7 @@ scute_agent_learn (struct agent_card_info_s *info)
     }
   if (!err)
     {
-      /* Also try to get the human readabale serial number.  */
+      /* Also try to get the human readable serial number.  */
       err = assuan_transact (agent_ctx, "SCD GETATTR $DISPSERIALNO",
                              NULL, NULL,
                              default_inq_cb, NULL,
@@ -774,8 +764,7 @@ scute_agent_learn (struct agent_card_info_s *info)
         err = 0; /* Not implemented or GETATTR not supported.  */
     }
 
-
-  return err;
+  return check_broken_pipe (err);
 }
 
 
@@ -836,14 +825,21 @@ scute_agent_check_status (void)
   int any = 0;
   char flag = '-';
 
+  err = ensure_agent_connection ();
+  if (err)
+    return err;
+
   /* First we look at the eventcounter to see if anything happened at
      all.  This is a low overhead function which won't even clutter a
      gpg-agent log file.  There is no need for error checking here. */
   if (last_flag)
-    assuan_transact (agent_ctx, "GETEVENTCOUNTER",
-                     NULL, NULL,
-                     NULL, NULL,
-                     geteventcounter_status_cb, &any);
+    {
+      err = assuan_transact (agent_ctx, "GETEVENTCOUNTER",
+                             NULL, NULL,
+                             NULL, NULL,
+                             geteventcounter_status_cb, &any);
+      check_broken_pipe (err);
+    }
 
   if (any || !last_flag)
     {
@@ -851,6 +847,7 @@ scute_agent_check_status (void)
                              read_status_cb, &flag,
                              default_inq_cb, NULL,
                              NULL, NULL);
+      err = check_broken_pipe (err);
       if (err)
         return err;
       last_flag = flag;
@@ -1071,8 +1068,12 @@ scute_agent_sign (const char *hexgrip, unsigned char *data, int len,
 
   snprintf (cmd, sizeof (cmd), "SIGKEY %s", hexgrip);
 
+  err = ensure_agent_connection ();
+  if (err)
+    return err;
   err = assuan_transact (agent_ctx, cmd, NULL, NULL, default_inq_cb,
-                        NULL, NULL, NULL);
+                         NULL, NULL, NULL);
+  err = check_broken_pipe (err);
   if (err)
     return err;
 
@@ -1083,11 +1084,13 @@ scute_agent_sign (const char *hexgrip, unsigned char *data, int len,
   snprintf (cmd, sizeof (cmd), "SETHASH --hash=%s %s", hash, pretty_data);
   err = assuan_transact (agent_ctx, cmd, NULL, NULL, default_inq_cb,
                         NULL, NULL, NULL);
+  err = check_broken_pipe (err);
   if (err)
     return err;
 
   err = assuan_transact (agent_ctx, "PKSIGN",
                         pksign_cb, &sig, default_inq_cb, NULL, NULL, NULL);
+  err = check_broken_pipe (err);
   if (err)
     return err;
 
@@ -1231,11 +1234,16 @@ scute_agent_decrypt (const char *hexgrip,
       return 0;
     }
 
+  err = ensure_agent_connection ();
+  if (err)
+    return err;
+
   snprintf (cmd, sizeof (cmd), "SETKEY %s", hexgrip);
   err = assuan_transact (agent_ctx, cmd,
                          NULL, NULL,
                          default_inq_cb, NULL,
                         NULL, NULL);
+  err = check_broken_pipe (err);
   if (err)
     return err;
 
@@ -1269,6 +1277,7 @@ scute_agent_decrypt (const char *hexgrip,
                         pkdecrypt_data_cb, &pkdecrypt,
                          pkdecrypt_inq_cb, &pkdecrypt,
                          NULL, NULL);
+  err = check_broken_pipe (err);
   if (!err)
     err = pkdecrypt_parse_result (&pkdecrypt, r_plaindata, r_plaindatalen);
 
@@ -1285,9 +1294,14 @@ scute_agent_is_trusted (const char *fpr, bool *is_trusted)
   bool trusted = false;
   char cmd[150];
 
+  err = ensure_agent_connection ();
+  if (err)
+    return err;
+
   snprintf (cmd, sizeof (cmd), "ISTRUSTED %s", fpr);
   err = assuan_transact (agent_ctx, cmd, NULL, NULL, default_inq_cb,
                         NULL, NULL, NULL);
+  err = check_broken_pipe (err);
   if (err && gpg_err_code (err) != GPG_ERR_NOT_TRUSTED)
     return err;
   else if (!err)
@@ -1356,9 +1370,14 @@ scute_agent_get_cert (const char *certref, struct cert *cert)
   cert_s.cert_der_len = 0;
   cert_s.cert_der_size = 0;
 
+  err = ensure_agent_connection ();
+  if (err)
+    return err;
+
   snprintf (cmd, sizeof (cmd), "SCD READCERT %s", certref);
   err = assuan_transact (agent_ctx, cmd, get_cert_data_cb, &cert_s,
                         NULL, NULL, NULL, NULL);
+  err = check_broken_pipe (err);
   /* Just to be safe... */
   if (!err && (cert_s.cert_der_len <= 16 || cert_s.cert_der[0] != 0x30))
     {
@@ -1409,13 +1428,17 @@ scute_agent_get_random (unsigned char *data, size_t len)
     gpg_error_t err;
     struct random_request request;
 
+    err = ensure_agent_connection ();
+    if (err)
+      return err;
+
     snprintf (command, sizeof(command), "SCD RANDOM %zu", len);
 
     request.buffer = data;
     request.len = len;
     err = assuan_transact (agent_ctx, command, get_challenge_data_cb,
                            &request, NULL, NULL, NULL, NULL);
-
+    err = check_broken_pipe (err);
     return err;
 }
 
index 0dfce0e..710f14c 100644 (file)
@@ -82,9 +82,6 @@ typedef struct agent_card_info_s *agent_card_info_t;
    initial greeting.  */
 gpg_error_t scute_agent_initialize (void);
 
-/* Return the major and minor version of the agent.  */
-int scute_agent_get_agent_version (int *minor);
-
 /* Tear down the agent connection and release all associated
    resources.  */
 void scute_agent_finalize (void);
index 3647b30..9768c11 100644 (file)
 #include "support.h"
 
 
+/* Malloced string with GnuPG's version.  NULL if gnupg is notproperly
+ * installed.  */
+static char *gnupg_version_string;
+
+
+
+
 #ifndef HAVE_STPCPY
 static char *
 my_stpcpy (char *a, const char *b)
@@ -289,7 +296,111 @@ find_program_at_standard_place (const char *name)
 #endif
 
 
-/* Return the file name of the gpgconf utility.  */
+/* Read a line form the output of COMMAND via popen and return that
+ * line at BUFFER which has been allocated by the caller with BUFSIZE
+ * bytes.  On success BUFFER contains a string with the first line.
+ * Command and buffer may have the same address.  If no output is
+ * expected BUFFER can be given as NULL. */
+gpg_error_t
+read_first_line (const char *command, char *buffer, size_t bufsize)
+{
+  gpg_error_t err;
+  FILE *fp;
+
+  if (buffer && bufsize < 2)
+    return gpg_error (GPG_ERR_BUFFER_TOO_SHORT);
+
+#ifdef HAVE_W32_SYSTEM
+  fp = _popen (command, "r");
+#else
+  fp = popen (command, "r");
+#endif
+  if (fp)
+    {
+      int i, c;
+
+      if (buffer)
+        {
+          for (i=0; i < bufsize - 1 && (c=getc(fp)) != EOF && c != '\n'; i++)
+            buffer[i] = c;
+          buffer [i] = 0;  /* Terminate string.  */
+          if (c == EOF && ferror (fp))
+            err = gpg_error_from_syserror ();    /* Read error.  */
+          else if (!(i < bufsize - 1))
+            err = gpg_error (GPG_ERR_NO_AGENT);  /* Path too long.  */
+          else if (!i || c != '\n')
+            err = gpg_error (GPG_ERR_NO_AGENT);  /* No terminating LF. */
+          else
+            err = 0;
+        }
+      else
+        err = 0;
+      pclose (fp);
+    }
+  else
+    {
+      err = gpg_error_from_syserror ();
+      DEBUG (DBG_CRIT, "popen(%s) failed: %s",
+             command, gpg_strerror (err));
+    }
+
+  return err;
+}
+
+
+
+/* Extract the version string of a program from STRING.  The version
+ * number is expected to be in GNU style format:
+ *
+ *   foo 1.2.3
+ *   foo (bar system) 1.2.3
+ *   foo 1.2.3 cruft
+ *   foo (bar system) 1.2.3 cruft.
+ *
+ * Spaces and tabs are skipped and used as delimiters, a term in
+ * (nested) parenthesis before the version string is skipped, the
+ * version string may consist of any non-space and non-tab characters
+ * but needs to start with a digit.
+ */
+static const char *
+extract_version_string (const char *string, size_t *r_len)
+{
+  const char *s;
+  int count, len;
+
+  for (s=string; *s; s++)
+    if (*s == ' ' || *s == '\t')
+        break;
+  while (*s == ' ' || *s == '\t')
+    s++;
+  if (*s == '(')
+    {
+      for (count=1, s++; count && *s; s++)
+        if (*s == '(')
+          count++;
+        else if (*s == ')')
+          count--;
+    }
+  /* For robustness we look for a digit.  */
+  while ( *s && !(*s >= '0' && *s <= '9') )
+    s++;
+  if (*s >= '0' && *s <= '9')
+    {
+      for (len=0; s[len]; len++)
+        if (s[len] == ' ' || s[len] == '\t')
+          break;
+    }
+  else
+    len = 0;
+
+  *r_len = len;
+  return s;
+}
+
+
+/* Return the file name of the gpgconf utility.  As a side-effect the
+ * version number of gnupg is also figured out the first time this
+ * function is called.  */
 const char *
 get_gpgconf_path (void)
 {
@@ -303,10 +414,85 @@ get_gpgconf_path (void)
 #endif
   if (!pgmname)
     pgmname = "gpgconf";
+  if (!gnupg_version_string)
+    {
+      char buffer[512];
+      const char *s;
+      size_t n;
+
+      snprintf (buffer, sizeof buffer, "%s --version", pgmname);
+      if (!read_first_line (buffer, buffer, sizeof buffer))
+        {
+          s = extract_version_string (buffer, &n);
+          gnupg_version_string = malloc (n+1);
+          if (gnupg_version_string)
+            {
+              memcpy (gnupg_version_string, s, n);
+              gnupg_version_string[n] = 0;
+            }
+        }
+    }
   return pgmname;
 }
 
 
+/* Return the version of GnuPG. */
+int
+get_gnupg_version (int *minor)
+{
+  int major;
+  const char *s;
+
+  if (!gnupg_version_string)
+    {
+      *minor = 0;
+      return 0;
+    }
+
+  major = atoi (gnupg_version_string);
+  s = strchr (gnupg_version_string, '.');
+  *minor = s? atoi (s+1) : 0;
+
+  return major;
+}
+
+
+/* Return true if GnuPG is older than MAJOR.MINOR.MICRO. */
+int
+is_gnupg_older_than (int major, int minor, int micro)
+{
+  int my_major, my_minor, my_micro;
+  const char *s;
+
+  if (!gnupg_version_string)
+    return 1;
+
+  my_minor = my_micro = 0;
+  my_major = atoi (gnupg_version_string);
+  s = strchr (gnupg_version_string, '.');
+  if (s)
+    {
+      my_minor = atoi (++s);
+      s = strchr (s, '.');
+      if (s)
+        my_micro = atoi (++s);
+    }
+  if (my_major < major)
+    return 1;
+  if (my_major > major)
+    return 0;
+
+  if (my_minor < minor)
+    return 1;
+  if (my_minor > minor)
+    return 0;
+
+  if (my_micro < micro)
+    return 1;
+  return 0;
+}
+
+
 /* Return the bindir where the main binaries are installed.  This may
  * return NULL.  */
 static const char *
@@ -315,37 +501,18 @@ get_bindir (void)
   static char *bindir;
   gpg_error_t err = 0;
   char buffer[512];
-  FILE *fp;
 
   if (!bindir)
     {
-      snprintf (buffer, sizeof buffer, "%s --null --list-dirs bindir",
+      snprintf (buffer, sizeof buffer, "%s --list-dirs bindir",
                 get_gpgconf_path ());
-#ifdef HAVE_W32_SYSTEM
-      fp = _popen (buffer, "r");
-#else
-      fp = popen (buffer, "r");
-#endif
-      if (fp)
+      err = read_first_line (buffer, buffer, sizeof buffer);
+      if (!err)
         {
-          int i, c;
-
-          for (i=0; i < sizeof buffer - 1  && (c = getc (fp)) != EOF; i++)
-            buffer[i] = c;
-          if (c == EOF && ferror (fp))
+          bindir = strdup (buffer);
+          if (!bindir)
             err = gpg_error_from_syserror ();
-          else if (!(i < sizeof buffer - 1))
-            err = gpg_error (GPG_ERR_NO_AGENT);  /* Path too long.  */
-          else if (!i || buffer[i-1])
-            err = gpg_error (GPG_ERR_NO_AGENT);  /* No terminating nul. */
-          else if (!(bindir = strdup (buffer)))
-            err = gpg_error_from_syserror ();
-
-          pclose (fp);
         }
-      else
-        err = gpg_error_from_syserror ();
-
       if (err)
         DEBUG (DBG_CRIT, "error locating GnuPG's installation directory: %s",
                gpg_strerror (err));
index c5bd8ed..27c7784 100644 (file)
@@ -63,7 +63,7 @@ C_GetSlotInfo (CK_SLOT_ID slotID, CK_SLOT_INFO_PTR pInfo)
     pInfo->flags |= CKF_TOKEN_PRESENT;
 
   /* Use the gpg-agent version for the hardware version.. */
-  pInfo->hardwareVersion.major = scute_agent_get_agent_version (&minor);
+  pInfo->hardwareVersion.major = get_gnupg_version (&minor);
   pInfo->hardwareVersion.minor = minor;
 
   /* Use Scute version as Firmware version.  */
index 4cd222f..4b437fa 100644 (file)
@@ -30,6 +30,8 @@
 #ifndef SUPPORT_H
 #define SUPPORT_H      1
 
+#include <gpg-error.h>
+
 #define spacep(p)   (*(p) == ' ' || *(p) == '\t')
 #define digitp(p)   (*(p) >= '0' && *(p) <= '9')
 #define hexdigitp(a) (digitp (a)                       \
@@ -71,7 +73,10 @@ ttyname (int fd)
 #endif /* !HAVE_TTYNAME */
 
 
+gpg_error_t read_first_line (const char *command, char *buffer, size_t bufsize);
 const char *get_gpgconf_path (void);
+int get_gnupg_version (int *minor);
+int is_gnupg_older_than (int major, int minor, int micro);
 const char *get_gpgsm_path (void);