ssh: Add support for Putty.
authorWerner Koch <wk@gnupg.org>
Wed, 3 Jul 2013 11:29:47 +0000 (13:29 +0200)
committerWerner Koch <wk@gnupg.org>
Wed, 3 Jul 2013 11:29:47 +0000 (13:29 +0200)
* agent/gpg-agent.c [W32]: Include Several Windows header.
(opts): Change help text for enable-ssh-support.
(opts, main): Add option --enable-putty-support
(putty_support, PUTTY_IPC_MAGIC, PUTTY_IPC_MAXLEN): New for W32.
(agent_init_default_ctrl): Add and asssert call.
(putty_message_proc, putty_message_thread): New.
(handle_connections) [W32]: Start putty message thread.
* common/sysutils.c (w32_get_user_sid): New for W32 only
* tools/gpgconf-comp.c (gc_options_gpg_agent): Add
--enable-ssh-support and --enable-putty-support.  Make the
configuration group visible at basic level.
* agent/command-ssh.c (serve_mmapped_ssh_request): New for W32 only.
--

This patch enables support for Putty.  It has been tested with Putty
0.62 using an Unix created ssh key copied to the private-keys-v1.d
directory on Windows and with a manually crafted sshcontrol file.  It
also works with a smartcard key.

May thanks to gniibe who implemented a proxy in Python to test the
putty/gpg-agent communication.

Signed-off-by: Werner Koch <wk@gnupg.org>
NEWS
agent/agent.h
agent/command-ssh.c
agent/gpg-agent.c
common/sysutils.c
common/sysutils.h
tools/gpgconf-comp.c

diff --git a/NEWS b/NEWS
index 4295ee9..adaa257 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -3,6 +3,9 @@ Noteworthy changes in version 2.0.21 (unreleased)
 
  * The included ssh agent does now support ECDSA keys.
 
+ * New option --enable-putty-support to allow gpg-agent to act as a
+   Pageant replacement including full smartcard support.
+
 
 Noteworthy changes in version 2.0.20 (2013-05-10)
 -------------------------------------------------
index 15cf8bf..4afe6e9 100644 (file)
@@ -220,6 +220,10 @@ gpg_error_t agent_write_status (ctrl_t ctrl, const char *keyword, ...)
 void bump_key_eventcounter (void);
 void bump_card_eventcounter (void);
 void start_command_handler (ctrl_t, gnupg_fd_t, gnupg_fd_t);
+#ifdef HAVE_W32_SYSTEM
+int serve_mmapped_ssh_request (ctrl_t ctrl,
+                               unsigned char *request, size_t maxreqlen);
+#endif /*HAVE_W32_SYSTEM*/
 
 /*-- command-ssh.c --*/
 void start_command_handler_ssh (ctrl_t, gnupg_fd_t);
index 4cd3537..6533730 100644 (file)
@@ -3362,3 +3362,149 @@ start_command_handler_ssh (ctrl_t ctrl, gnupg_fd_t sock_client)
   if (stream_sock)
     es_fclose (stream_sock);
 }
+
+
+#ifdef HAVE_W32_SYSTEM
+/* Serve one ssh-agent request.  This is used for the Putty support.
+   REQUEST is the the mmapped memory which may be accessed up to a
+   length of MAXREQLEN.  Returns 0 on success which also indicates
+   that a valid SSH response message is now in REQUEST.  */
+int
+serve_mmapped_ssh_request (ctrl_t ctrl,
+                           unsigned char *request, size_t maxreqlen)
+{
+  gpg_error_t err;
+  int send_err = 0;
+  int valid_response = 0;
+  ssh_request_spec_t *spec;
+  u32 msglen;
+  estream_t request_stream, response_stream;
+
+  if (setup_ssh_env (ctrl))
+    goto leave; /* Error setting up the environment.  */
+
+  if (maxreqlen < 5)
+    goto leave; /* Caller error.  */
+
+  msglen = uint32_construct (request[0], request[1], request[2], request[3]);
+  if (msglen < 1 || msglen > maxreqlen - 4)
+    {
+      log_error ("ssh message len (%u) out of range", (unsigned int)msglen);
+      goto leave;
+    }
+
+  spec = request_spec_lookup (request[4]);
+  if (!spec)
+    {
+      send_err = 1;  /* Unknown request type.  */
+      goto leave;
+    }
+
+  /* Create a stream object with the data part of the request.  */
+  if (spec->secret_input)
+    request_stream = es_mopen (NULL, 0, 0, 1, realloc_secure, gcry_free, "r+");
+  else
+    request_stream = es_mopen (NULL, 0, 0, 1, gcry_realloc, gcry_free, "r+");
+  if (!request_stream)
+    {
+      err = gpg_error_from_syserror ();
+      goto leave;
+    }
+  /* We have to disable the estream buffering, because the estream
+     core doesn't know about secure memory.  */
+  if (es_setvbuf (request_stream, NULL, _IONBF, 0))
+    {
+      err = gpg_error_from_syserror ();
+      goto leave;
+    }
+  /* Copy the request to the stream but omit the request type.  */
+  err = stream_write_data (request_stream, request + 5, msglen - 1);
+  if (err)
+    goto leave;
+  es_rewind (request_stream);
+
+  response_stream = es_fopenmem (0, "r+b");
+  if (!response_stream)
+    {
+      err = gpg_error_from_syserror ();
+      goto leave;
+    }
+
+  if (opt.verbose)
+    log_info ("ssh request handler for %s (%u) started\n",
+              spec->identifier, spec->type);
+
+  err = (*spec->handler) (ctrl, request_stream, response_stream);
+
+  if (opt.verbose)
+    {
+      if (err)
+        log_info ("ssh request handler for %s (%u) failed: %s\n",
+                  spec->identifier, spec->type, gpg_strerror (err));
+      else
+        log_info ("ssh request handler for %s (%u) ready\n",
+                  spec->identifier, spec->type);
+    }
+
+  es_fclose (request_stream);
+  request_stream = NULL;
+
+  if (err)
+    {
+      send_err = 1;
+      goto leave;
+    }
+
+  /* Put the response back into the mmapped buffer.  */
+  {
+    void *response_data;
+    size_t response_size;
+
+    /* NB: In contrast to the request-stream, the response stream
+       includes the the message type byte.  */
+    if (es_fclose_snatch (response_stream, &response_data, &response_size))
+      {
+        log_error ("snatching ssh response failed: %s",
+                   gpg_strerror (gpg_error_from_syserror ()));
+        send_err = 1; /* Ooops.  */
+        goto leave;
+      }
+
+    if (opt.verbose > 1)
+      log_info ("sending ssh response of length %u\n",
+                (unsigned int)response_size);
+    if (response_size > maxreqlen - 4)
+      {
+        log_error ("invalid length of the ssh response: %s",
+                   gpg_strerror (GPG_ERR_INTERNAL));
+        es_free (response_data);
+        send_err = 1;
+        goto leave;
+      }
+
+    request[0] = response_size >> 24;
+    request[1] = response_size >> 16;
+    request[2] = response_size >>  8;
+    request[3] = response_size >>  0;
+    memcpy (request+4, response_data, response_size);
+    es_free (response_data);
+    valid_response = 1;
+  }
+
+ leave:
+  if (send_err)
+    {
+      request[0] = 0;
+      request[1] = 0;
+      request[2] = 0;
+      request[3] = 1;
+      request[4] = SSH_RESPONSE_FAILURE;
+      valid_response = 1;
+    }
+
+  /* Reset the SCD in case it has been used. */
+  agent_reset_scd (ctrl);
+
+  return valid_response? 0 : -1;
+}
+#endif /*HAVE_W32_SYSTEM*/
index ba25875..9d53de9 100644 (file)
@@ -1,6 +1,7 @@
 /* gpg-agent.c  -  The GnuPG Agent
  * Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005,
  *               2006, 2007, 2009, 2010 Free Software Foundation, Inc.
+ * Copyright (C) 2013 Werner Koch
  *
  * This file is part of GnuPG.
  *
 #include <time.h>
 #include <fcntl.h>
 #include <sys/stat.h>
-#ifndef HAVE_W32_SYSTEM
+#ifdef HAVE_W32_SYSTEM
+# ifndef WINVER
+#  define WINVER 0x0500  /* Same as in common/sysutils.c */
+# endif
+# ifdef HAVE_WINSOCK2_H
+#  include <winsock2.h>
+# endif
+# include <aclapi.h>
+# include <sddl.h>
+#else /*!HAVE_W32_SYSTEM*/
 # include <sys/socket.h>
 # include <sys/un.h>
 #endif /*!HAVE_W32_SYSTEM*/
@@ -106,6 +116,7 @@ enum cmd_and_opt_values
   oKeepTTY,
   oKeepDISPLAY,
   oSSHSupport,
+  oPuttySupport,
   oDisableScdaemon,
   oWriteEnvFile
 };
@@ -177,7 +188,14 @@ static ARGPARSE_OPTS opts[] = {
                              N_("allow clients to mark keys as \"trusted\"")},
   { oAllowPresetPassphrase, "allow-preset-passphrase", 0,
                              N_("allow presetting passphrase")},
-  { oSSHSupport, "enable-ssh-support", 0, N_("enable ssh-agent emulation") },
+  { oSSHSupport, "enable-ssh-support", 0, N_("enable ssh support") },
+  { oPuttySupport, "enable-putty-support", 0,
+#ifdef HAVE_W32_SYSTEM
+      N_("enable putty support")
+#else
+      "@"
+#endif
+  },
   { oWriteEnvFile, "write-env-file", 2|8,
             N_("|FILE|write environment settings also to FILE")},
   {0}
@@ -202,6 +220,17 @@ static ARGPARSE_OPTS opts[] = {
 #endif
 
 
+#ifdef HAVE_W32_SYSTEM
+/* Flag indicating that support for Putty has been enabled.  */
+static int putty_support;
+/* A magic value used with WM_COPYDATA.  */
+#define PUTTY_IPC_MAGIC 0x804e50ba
+/* To avoid surprises we limit the size of the mapped IPC file to this
+   value.  Putty currently (0.62) uses 8k, thus 16k should be enough
+   for the foreseeable future.  */
+#define PUTTY_IPC_MAXLEN 16384
+#endif /*HAVE_W32_SYSTEM*/
+
 /* The list of open file descriptors at startup.  Note that this list
    has been allocated using the standard malloc.  */
 static int *startup_fd_list;
@@ -805,6 +834,13 @@ main (int argc, char **argv )
         case oKeepDISPLAY: opt.keep_display = 1; break;
 
        case oSSHSupport:  opt.ssh_support = 1; break;
+        case oPuttySupport:
+#        ifdef HAVE_W32_SYSTEM
+          putty_support = 1;
+          opt.ssh_support = 1;
+#        endif
+          break;
+
         case oWriteEnvFile:
           if (pargs.r_type)
             env_file_name = pargs.r.ret_str;
@@ -928,6 +964,11 @@ main (int argc, char **argv )
               GC_OPT_FLAG_NONE|GC_OPT_FLAG_RUNTIME);
       printf ("disable-scdaemon:%lu:\n",
               GC_OPT_FLAG_NONE|GC_OPT_FLAG_RUNTIME);
+#ifdef HAVE_W32_SYSTEM
+      printf ("enable-putty-support:%lu:\n", GC_OPT_FLAG_NONE);
+#else
+      printf ("enable-ssh-support:%lu:\n", GC_OPT_FLAG_NONE);
+#endif
 
       agent_exit (0);
     }
@@ -1292,6 +1333,8 @@ agent_exit (int rc)
 static void
 agent_init_default_ctrl (ctrl_t ctrl)
 {
+  assert (ctrl->session_env);
+
   /* Note we ignore malloc errors because we can't do much about it
      and the request will fail anyway shortly after this
      initialization. */
@@ -1309,7 +1352,6 @@ agent_init_default_ctrl (ctrl_t ctrl)
     xfree (ctrl->lc_messages);
   ctrl->lc_messages = default_lc_messages? xtrystrdup (default_lc_messages)
                                     /**/ : NULL;
-
 }
 
 
@@ -1788,6 +1830,199 @@ check_nonce (ctrl_t ctrl, assuan_sock_nonce_t *nonce)
 }
 
 
+#ifdef HAVE_W32_SYSTEM
+/* The window message processing function for Putty.  Warning: This
+   code runs as a native Windows thread.  Use of our own functions
+   needs to be bracket with pth_leave/pth_enter. */
+static LRESULT CALLBACK
+putty_message_proc (HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
+{
+  int ret = 0;
+  int w32rc;
+  COPYDATASTRUCT *cds;
+  const char *mapfile;
+  HANDLE maphd;
+  PSID mysid = NULL;
+  PSID mapsid = NULL;
+  void *data = NULL;
+  PSECURITY_DESCRIPTOR psd = NULL;
+  ctrl_t ctrl = NULL;
+
+  if (msg != WM_COPYDATA)
+    {
+      /* pth_leave (); */
+      /* log_debug ("putty loop: received WM_%u\n", msg ); */
+      /* pth_enter (); */
+      return DefWindowProc (hwnd, msg, wparam, lparam);
+    }
+
+  cds = (COPYDATASTRUCT*)lparam;
+  if (cds->dwData != PUTTY_IPC_MAGIC)
+    return 0;  /* Ignore data with the wrong magic.  */
+  mapfile = cds->lpData;
+  if (!cds->cbData || mapfile[cds->cbData - 1])
+    return 0;  /* Ignore empty and non-properly terminated strings.  */
+
+  if (DBG_ASSUAN)
+    {
+      pth_leave ();
+      log_debug ("ssh map file '%s'", mapfile);
+      pth_enter ();
+    }
+
+  maphd = OpenFileMapping (FILE_MAP_ALL_ACCESS, FALSE, mapfile);
+  if (DBG_ASSUAN)
+    {
+      pth_leave ();
+      log_debug ("ssh map handle %p\n", maphd);
+      pth_enter ();
+    }
+
+  if (!maphd || maphd == INVALID_HANDLE_VALUE)
+    return 0;
+
+  pth_leave ();
+
+  mysid = w32_get_user_sid ();
+  if (!mysid)
+    {
+      log_error ("error getting my sid\n");
+      goto leave;
+    }
+
+  w32rc = GetSecurityInfo (maphd, SE_KERNEL_OBJECT,
+                           OWNER_SECURITY_INFORMATION,
+                           &mapsid, NULL, NULL, NULL,
+                           &psd);
+  if (w32rc)
+    {
+      log_error ("error getting sid of ssh map file: rc=%d", w32rc);
+      goto leave;
+    }
+
+  if (DBG_ASSUAN)
+    {
+      char *sidstr;
+
+      if (!ConvertSidToStringSid (mysid, &sidstr))
+        sidstr = NULL;
+      log_debug ("          my sid: '%s'", sidstr? sidstr: "[error]");
+      LocalFree (sidstr);
+      if (!ConvertSidToStringSid (mapsid, &sidstr))
+        sidstr = NULL;
+      log_debug ("ssh map file sid: '%s'", sidstr? sidstr: "[error]");
+      LocalFree (sidstr);
+    }
+
+  if (!EqualSid (mysid, mapsid))
+    {
+      log_error ("ssh map file has a non-matching sid\n");
+      goto leave;
+    }
+
+  data = MapViewOfFile (maphd, FILE_MAP_ALL_ACCESS, 0, 0, 0);
+  if (DBG_ASSUAN)
+    log_debug ("ssh IPC buffer at %p\n", data);
+  if (!data)
+    goto leave;
+
+  /* log_printhex ("request:", data, 20); */
+
+  ctrl = xtrycalloc (1, sizeof *ctrl);
+  if (!ctrl)
+    {
+      log_error ("error allocating connection control data: %s\n",
+                 strerror (errno) );
+      goto leave;
+    }
+  ctrl->session_env = session_env_new ();
+  if (!ctrl->session_env)
+    {
+      log_error ("error allocating session environment block: %s\n",
+                 strerror (errno) );
+      goto leave;
+    }
+
+  agent_init_default_ctrl (ctrl);
+  if (!serve_mmapped_ssh_request (ctrl, data, PUTTY_IPC_MAXLEN))
+    ret = 1; /* Valid ssh message has been constructed.  */
+  agent_deinit_default_ctrl (ctrl);
+  /* log_printhex ("  reply:", data, 20); */
+
+ leave:
+  xfree (ctrl);
+  if (data)
+    UnmapViewOfFile (data);
+  xfree (mapsid);
+  if (psd)
+    LocalFree (psd);
+  xfree (mysid);
+  CloseHandle (maphd);
+
+  pth_enter ();
+
+  return ret;
+}
+#endif /*HAVE_W32_SYSTEM*/
+
+
+#ifdef HAVE_W32_SYSTEM
+/* The thread handling Putty's IPC requests.  */
+static void *
+putty_message_thread (void *arg)
+{
+  WNDCLASS wndwclass = {0, putty_message_proc, 0, 0,
+                        NULL, NULL, NULL, NULL, NULL, "Pageant"};
+  HWND hwnd;
+  MSG msg;
+
+  (void)arg;
+
+  if (opt.verbose)
+    log_info ("putty message loop thread 0x%lx started\n", pth_thread_id ());
+
+  /* The message loop runs as thread independet from out Pth system.
+     This also meand that we need to make sure that we switch back to
+     our system before calling any no-windows function.  */
+  pth_enter ();
+
+  /* First create a window to make sure that a message queue exists
+     for this thread.  */
+  if (!RegisterClass (&wndwclass))
+    {
+      pth_leave ();
+      log_error ("error registering Pageant window class");
+      return NULL;
+    }
+  hwnd = CreateWindowEx (0, "Pageant", "Pageant", 0,
+                         0, 0, 0, 0,
+                         HWND_MESSAGE,  /* hWndParent */
+                         NULL,          /* hWndMenu   */
+                         NULL,          /* hInstance  */
+                         NULL);         /* lpParm     */
+  if (!hwnd)
+    {
+      pth_leave ();
+      log_error ("error creating Pageant window");
+      return NULL;
+    }
+
+  while (GetMessage(&msg, NULL, 0, 0))
+    {
+      TranslateMessage(&msg);
+      DispatchMessage(&msg);
+    }
+
+  /* Back to Pth.  */
+  pth_leave ();
+
+  if (opt.verbose)
+    log_info ("putty message loop thread 0x%lx stopped\n", pth_thread_id ());
+  return NULL;
+}
+#endif /*HAVE_W32_SYSTEM*/
+
+
 /* This is the standard connection thread's main function.  */
 static void *
 start_connection_thread (void *arg)
@@ -1897,6 +2132,21 @@ handle_connections (gnupg_fd_t listen_fd, gnupg_fd_t listen_fd_ssh)
 #endif
   time_ev = NULL;
 
+  /* On Windows we need to fire up a separate thread to listen for
+     requests from Putty (an SSH client), so we can replace Putty's
+     Pageant (its ssh-agent implementation). */
+#ifdef HAVE_W32_SYSTEM
+  if (putty_support)
+    {
+      pth_attr_set (tattr, PTH_ATTR_NAME, "putty message loop");
+      if (!pth_spawn (tattr, putty_message_thread, NULL))
+        {
+          log_error ("error spawning putty message loop: %s\n",
+                     strerror (errno) );
+        }
+    }
+#endif /*HAVE_W32_SYSTEM*/
+
   /* Set a flag to tell call-scd.c that it may enable event
      notifications.  */
   opt.sigusr2_enabled = 1;
index 82bc81f..8f93ff5 100644 (file)
@@ -1,6 +1,7 @@
 /* sysutils.c -  system helpers
  * Copyright (C) 1998, 1999, 2000, 2001, 2003, 2004,
  *               2007, 2008  Free Software Foundation, Inc.
+ * Copyright (C) 2013 Werner Koch
  *
  * This file is part of GnuPG.
  *
@@ -495,3 +496,59 @@ gnupg_allow_set_foregound_window (pid_t pid)
                (unsigned long)pid, w32_strerror (-1));
 #endif
 }
+
+
+#ifdef HAVE_W32_SYSTEM
+/* Return the user's security identifier from the current process.  */
+PSID
+w32_get_user_sid (void)
+{
+  int okay = 0;
+  HANDLE proc = NULL;
+  HANDLE token = NULL;
+  TOKEN_USER *user = NULL;
+  PSID sid = NULL;
+  DWORD tokenlen, sidlen;
+
+  proc = OpenProcess (PROCESS_QUERY_INFORMATION, FALSE, GetCurrentProcessId());
+  if (!proc)
+    goto leave;
+
+  if (!OpenProcessToken (proc, TOKEN_QUERY, &token))
+    goto leave;
+
+  if (!GetTokenInformation (token, TokenUser, NULL, 0, &tokenlen)
+      && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
+    goto leave;
+
+  user = xtrymalloc (tokenlen);
+  if (!user)
+    goto leave;
+
+  if (!GetTokenInformation (token, TokenUser, user, tokenlen, &tokenlen))
+    goto leave;
+  if (!IsValidSid (user->User.Sid))
+    goto leave;
+  sidlen = GetLengthSid (user->User.Sid);
+  sid = xtrymalloc (sidlen);
+  if (!sid)
+    goto leave;
+  if (!CopySid (sidlen, sid, user->User.Sid))
+    goto leave;
+  okay = 1;
+
+ leave:
+  xfree (user);
+  if (token)
+    CloseHandle (token);
+  if (proc)
+    CloseHandle (proc);
+
+  if (!okay)
+    {
+      xfree (sid);
+      sid = NULL;
+    }
+  return sid;
+}
+#endif /*HAVE_W32_SYSTEM*/
index fd4340f..0a05e2b 100644 (file)
@@ -51,8 +51,9 @@ void gnupg_allow_set_foregound_window (pid_t pid);
 
 
 #ifdef HAVE_W32_SYSTEM
+void *w32_get_user_sid (void);
 
-#include "../jnlib/w32help.h"
+# include "../jnlib/w32help.h"
 
 #endif /*HAVE_W32_SYSTEM*/
 
index 49c082b..72e7134 100644 (file)
@@ -482,7 +482,7 @@ static gc_option_t gc_options_gpg_agent[] =
      GC_ARG_TYPE_NONE, GC_BACKEND_GPG_AGENT },
 
    { "Configuration",
-     GC_OPT_FLAG_GROUP, GC_LEVEL_EXPERT,
+     GC_OPT_FLAG_GROUP, GC_LEVEL_BASIC,
      "gnupg", N_("Options controlling the configuration") },
    { "options", GC_OPT_FLAG_NONE, GC_LEVEL_EXPERT,
      "gnupg", "|FILE|read options from FILE",
@@ -490,6 +490,12 @@ static gc_option_t gc_options_gpg_agent[] =
    { "disable-scdaemon", GC_OPT_FLAG_NONE, GC_LEVEL_ADVANCED,
      "gnupg", "do not use the SCdaemon",
      GC_ARG_TYPE_NONE, GC_BACKEND_GPG_AGENT },
+   { "enable-ssh-support", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC,
+     "gnupg", "enable ssh support",
+     GC_ARG_TYPE_NONE, GC_BACKEND_GPG_AGENT },
+   { "enable-putty-support", GC_OPT_FLAG_NONE, GC_LEVEL_BASIC,
+     "gnupg", "enable putty support",
+     GC_ARG_TYPE_NONE, GC_BACKEND_GPG_AGENT },
 
    { "Debug",
      GC_OPT_FLAG_GROUP, GC_LEVEL_ADVANCED,