Add support to talking to LDAP key servers.
authorNeal H. Walfield <neal@g10code.de>
Thu, 19 Mar 2015 10:02:46 +0000 (11:02 +0100)
committerNeal H. Walfield <neal@g10code.de>
Mon, 23 Mar 2015 18:58:29 +0000 (19:58 +0100)
* g10/call-dirmngr.c (record_output): New function.
(ks_put_inq_cb): Use it here to generate a --with-colons like output
instead of a custom format.
* dirmngr/ks-action.c: Include "ldap-parse-uri.h".
(ks_action_help): If the provided URI is an LDAP URI, then use
ldap_parse_uri to parse.  Call ks_ldap_help.
(ks_action_search): If passed an LDAP URI, then call ks_ldap_search.
(ks_action_get): Likewise.
(ks_action_put): Likewise.  Also, change data from a 'const void *' to
a 'void *' and add info and infolen parameters.  Add note that
function may modify DATA.
* dirmngr/ks-action.h (ks_action_put): Update declaration accordingly.
* dirmngr/server.c: Include "ldap-parse-uri.h".
(cmd_keyserver): If ITEM->URI is an LDAP URI, parse it using
ldap_parse_uri.
(hlp_ks_put): Improve documentation.
(cmd_ks_put): Also pass info and infolen to ks_action_put.  Improve
documentation.
* dirmngr/ks-engine.h (ks_ldap_help): New declaration.
(ks_ldap_search): Likewise.
(ks_ldap_get): Likewise.
(ks_ldap_put): Likewise.
* dirmngr/ks-engine-ldap.c: New file.
* dirmngr/Makefile.am (dirmngr_SOURCES): Add ks-engine-ldap.c,
ldap-parse-uri.c and ldap-parse-uri.h.
(dirmngr_LDADD) [USE_LDAP]: Add $(ldaplibs).

--
Signed-off-by: Neal H. Walfield <neal@g10code.de>
dirmngr/Makefile.am
dirmngr/ks-action.c
dirmngr/ks-action.h
dirmngr/ks-engine-ldap.c [new file with mode: 0644]
dirmngr/ks-engine.h
dirmngr/ldap-parse-uri.c
dirmngr/server.c
g10/call-dirmngr.c

index c0918a7..c8613bb 100644 (file)
@@ -62,7 +62,9 @@ dirmngr_SOURCES = dirmngr.c dirmngr.h server.c crlcache.c crlfetch.c  \
        cdb.h cdblib.c misc.c dirmngr-err.h  \
        ocsp.c ocsp.h validate.c validate.h  \
        ks-action.c ks-action.h ks-engine.h \
-        ks-engine-hkp.c ks-engine-http.c ks-engine-finger.c ks-engine-kdns.c
+        ks-engine-hkp.c ks-engine-http.c ks-engine-finger.c ks-engine-kdns.c \
+       ks-engine-ldap.c \
+       ldap-parse-uri.c ldap-parse-uri.h
 
 if USE_LDAP
 dirmngr_SOURCES += ldapserver.h ldapserver.c ldap.c w32-ldap-help.h \
@@ -77,6 +79,9 @@ dirmngr_LDADD = $(libcommontlsnpth) $(libcommonpth) \
         $(DNSLIBS) $(LIBASSUAN_LIBS) \
        $(LIBGCRYPT_LIBS) $(KSBA_LIBS) $(NPTH_LIBS) \
        $(NTBTLS_LIBS) $(LIBGNUTLS_LIBS) $(LIBINTL) $(LIBICONV)
+if USE_LDAP
+dirmngr_LDADD += $(ldaplibs)
+endif
 if !USE_LDAPWRAPPER
 dirmngr_LDADD += $(ldaplibs)
 endif
@@ -104,7 +109,8 @@ no-libgcrypt.c : $(top_srcdir)/tools/no-libgcrypt.c
 
 t_common_src = t-support.h
 # We need libcommontls, because we use the http functions.
-t_common_ldadd = $(libcommontls) $(libcommon) no-libgcrypt.o $(GPG_ERROR_LIBS)
+t_common_ldadd = $(libcommontls) $(libcommon) no-libgcrypt.o \
+       $(GPG_ERROR_LIBS) $(NTBTLS_LIBS) $(LIBGNUTLS_LIBS) $(DNSLIBS)
 
 module_tests = t-ldap-parse-uri
 t_ldap_parse_uri_SOURCES = \
index 8e2f520..6cfb598 100644 (file)
@@ -1,6 +1,7 @@
 /* ks-action.c - OpenPGP keyserver actions
  * Copyright (C) 2011 Free Software Foundation, Inc.
  * Copyright (C) 2011, 2014 Werner Koch
+ * Copyright (C) 2015  g10 Code GmbH
  *
  * This file is part of GnuPG.
  *
@@ -29,7 +30,7 @@
 #include "misc.h"
 #include "ks-engine.h"
 #include "ks-action.h"
-
+#include "ldap-parse-uri.h"
 
 /* Called by the engine's help functions to print the actual help.  */
 gpg_error_t
@@ -72,7 +73,11 @@ ks_action_help (ctrl_t ctrl, const char *url)
     }
   else
     {
-      err = http_parse_uri (&parsed_uri, url, 1);
+      if (ldap_uri_p (url))
+       err = ldap_parse_uri (&parsed_uri, url);
+      else
+       err = http_parse_uri (&parsed_uri, url, 1);
+
       if (err)
         return err;
     }
@@ -85,6 +90,8 @@ ks_action_help (ctrl_t ctrl, const char *url)
     err = ks_finger_help (ctrl, parsed_uri);
   if (!err)
     err = ks_kdns_help (ctrl, parsed_uri);
+  if (!err)
+    err = ks_ldap_help (ctrl, parsed_uri);
 
   if (!parsed_uri)
     ks_print_help (ctrl,
@@ -142,10 +149,18 @@ ks_action_search (ctrl_t ctrl, strlist_t patterns, estream_t outfp)
      stop at the first error. */
   for (uri = ctrl->keyservers; !err && uri; uri = uri->next)
     {
-      if (uri->parsed_uri->is_http)
+      int is_http = uri->parsed_uri->is_http;
+      int is_ldap = (strcmp (uri->parsed_uri->scheme, "ldap") == 0
+                    || strcmp (uri->parsed_uri->scheme, "ldaps") == 0
+                    || strcmp (uri->parsed_uri->scheme, "ldapi") == 0);
+      if (is_http || is_ldap)
         {
           any_server = 1;
-          err = ks_hkp_search (ctrl, uri->parsed_uri, patterns->d, &infp);
+         if (is_http)
+           err = ks_hkp_search (ctrl, uri->parsed_uri, patterns->d, &infp);
+         else if (is_ldap)
+           err = ks_ldap_search (ctrl, uri->parsed_uri, patterns->d, &infp);
+
           if (!err)
             {
               err = copy_stream (infp, outfp);
@@ -185,12 +200,20 @@ ks_action_get (ctrl_t ctrl, strlist_t patterns, estream_t outfp)
      Need to think about a better strategy.  */
   for (uri = ctrl->keyservers; !err && uri; uri = uri->next)
     {
-      if (uri->parsed_uri->is_http)
+      int is_http = uri->parsed_uri->is_http;
+      int is_ldap = (strcmp (uri->parsed_uri->scheme, "ldap") == 0
+                    || strcmp (uri->parsed_uri->scheme, "ldaps") == 0
+                    || strcmp (uri->parsed_uri->scheme, "ldapi") == 0);
+      if (is_http || is_ldap)
         {
           any_server = 1;
           for (sl = patterns; !err && sl; sl = sl->next)
             {
-              err = ks_hkp_get (ctrl, uri->parsed_uri, sl->d, &infp);
+             if (is_http)
+               err = ks_hkp_get (ctrl, uri->parsed_uri, sl->d, &infp);
+             else
+               err = ks_ldap_get (ctrl, uri->parsed_uri, sl->d, &infp);
+
               if (err)
                 {
                   /* It is possible that a server does not carry a
@@ -282,9 +305,14 @@ ks_action_fetch (ctrl_t ctrl, const char *url, estream_t outfp)
 
 
 /* Send an OpenPGP key to all keyservers.  The key in {DATA,DATALEN}
-   is expected in OpenPGP binary transport format.  */
+   is expected to be in OpenPGP binary transport format.  The metadata
+   in {INFO,INFOLEN} is in colon-separated format (concretely, it is
+   the output of 'for x in keys sigs; do gpg --list-$x --with-colons
+   KEYID; done'.  This function may modify DATA and INFO.  If this is
+   a problem, then the caller should create a copy.  */
 gpg_error_t
-ks_action_put (ctrl_t ctrl, const void *data, size_t datalen)
+ks_action_put (ctrl_t ctrl, void *data, size_t datalen,
+              void *info, size_t infolen)
 {
   gpg_error_t err = 0;
   gpg_error_t first_err = 0;
@@ -293,10 +321,20 @@ ks_action_put (ctrl_t ctrl, const void *data, size_t datalen)
 
   for (uri = ctrl->keyservers; !err && uri; uri = uri->next)
     {
-      if (uri->parsed_uri->is_http)
+      int is_http = uri->parsed_uri->is_http;
+      int is_ldap = (strcmp (uri->parsed_uri->scheme, "ldap") == 0
+                    || strcmp (uri->parsed_uri->scheme, "ldaps") == 0
+                    || strcmp (uri->parsed_uri->scheme, "ldapi") == 0);
+
+      if (is_http || is_ldap)
         {
           any_server = 1;
-          err = ks_hkp_put (ctrl, uri->parsed_uri, data, datalen);
+         if (is_http)
+           err = ks_hkp_put (ctrl, uri->parsed_uri, data, datalen);
+         else
+           err = ks_ldap_put (ctrl, uri->parsed_uri, data, datalen,
+                              info, infolen);
+
           if (err)
             {
               first_err = err;
index 5c8a5cd..2def3dc 100644 (file)
@@ -1,5 +1,6 @@
 /* ks-action.h - OpenPGP keyserver actions definitions
  * Copyright (C) 2011 Free Software Foundation, Inc.
+ *               2015 g10 Code GmbH
  *
  * This file is part of GnuPG.
  *
@@ -25,7 +26,8 @@ gpg_error_t ks_action_resolve (ctrl_t ctrl);
 gpg_error_t ks_action_search (ctrl_t ctrl, strlist_t patterns, estream_t outfp);
 gpg_error_t ks_action_get (ctrl_t ctrl, strlist_t patterns, 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, const void *data, size_t datalen);
+gpg_error_t ks_action_put (ctrl_t ctrl, void *data, size_t datalen,
+                          void *info, size_t infolen);
 
 
 #endif /*DIRMNGR_KS_ACTION_H*/
diff --git a/dirmngr/ks-engine-ldap.c b/dirmngr/ks-engine-ldap.c
new file mode 100644 (file)
index 0000000..68a1bb7
--- /dev/null
@@ -0,0 +1,2055 @@
+/* ks-engine-ldap.c - talk to a LDAP keyserver
+ * Copyright (C) 2001, 2002, 2004, 2005, 2006
+ *               2007  Free Software Foundation, Inc.
+ * Copyright (C) 2015  g10 Code GmbH
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#ifdef HAVE_GETOPT_H
+# include <getopt.h>
+#endif
+#include <stdlib.h>
+#include <errno.h>
+#include <assert.h>
+
+#ifdef _WIN32
+# include <winsock2.h>
+# include <winldap.h>
+#else
+# ifdef NEED_LBER_H
+#  include <lber.h>
+# endif
+/* For OpenLDAP, to enable the API that we're using. */
+# define LDAP_DEPRECATED 1
+# include <ldap.h>
+#endif
+
+#include "dirmngr.h"
+#include "misc.h"
+#include "userids.h"
+#include "ks-engine.h"
+#include "ldap-parse-uri.h"
+
+#ifdef __riscos__
+# include "util.h"
+#endif
+
+#ifndef HAVE_TIMEGM
+time_t timegm(struct tm *tm);
+#endif
+\f
+/* Convert an LDAP error to a GPG error.  */
+static int
+ldap_err_to_gpg_err (int code)
+{
+  gpg_err_code_t ec;
+
+  switch (code)
+    {
+#ifdef LDAP_X_CONNECTING
+    case LDAP_X_CONNECTING: ec = GPG_ERR_LDAP_X_CONNECTING; break;
+#endif
+
+    case LDAP_REFERRAL_LIMIT_EXCEEDED: ec = GPG_ERR_LDAP_REFERRAL_LIMIT; break;
+    case LDAP_CLIENT_LOOP: ec = GPG_ERR_LDAP_CLIENT_LOOP; break;
+    case LDAP_NO_RESULTS_RETURNED: ec = GPG_ERR_LDAP_NO_RESULTS; break;
+    case LDAP_CONTROL_NOT_FOUND: ec = GPG_ERR_LDAP_CONTROL_NOT_FOUND; break;
+    case LDAP_NOT_SUPPORTED: ec = GPG_ERR_LDAP_NOT_SUPPORTED; break;
+    case LDAP_CONNECT_ERROR: ec = GPG_ERR_LDAP_CONNECT; break;
+    case LDAP_NO_MEMORY: ec = GPG_ERR_LDAP_NO_MEMORY; break;
+    case LDAP_PARAM_ERROR: ec = GPG_ERR_LDAP_PARAM; break;
+    case LDAP_USER_CANCELLED: ec = GPG_ERR_LDAP_USER_CANCELLED; break;
+    case LDAP_FILTER_ERROR: ec = GPG_ERR_LDAP_FILTER; break;
+    case LDAP_AUTH_UNKNOWN: ec = GPG_ERR_LDAP_AUTH_UNKNOWN; break;
+    case LDAP_TIMEOUT: ec = GPG_ERR_LDAP_TIMEOUT; break;
+    case LDAP_DECODING_ERROR: ec = GPG_ERR_LDAP_DECODING; break;
+    case LDAP_ENCODING_ERROR: ec = GPG_ERR_LDAP_ENCODING; break;
+    case LDAP_LOCAL_ERROR: ec = GPG_ERR_LDAP_LOCAL; break;
+    case LDAP_SERVER_DOWN: ec = GPG_ERR_LDAP_SERVER_DOWN; break;
+
+    case LDAP_SUCCESS: ec = GPG_ERR_LDAP_SUCCESS; break;
+
+    case LDAP_OPERATIONS_ERROR: ec = GPG_ERR_LDAP_OPERATIONS; break;
+    case LDAP_PROTOCOL_ERROR: ec = GPG_ERR_LDAP_PROTOCOL; break;
+    case LDAP_TIMELIMIT_EXCEEDED: ec = GPG_ERR_LDAP_TIMELIMIT; break;
+    case LDAP_SIZELIMIT_EXCEEDED: ec = GPG_ERR_LDAP_SIZELIMIT; break;
+    case LDAP_COMPARE_FALSE: ec = GPG_ERR_LDAP_COMPARE_FALSE; break;
+    case LDAP_COMPARE_TRUE: ec = GPG_ERR_LDAP_COMPARE_TRUE; break;
+    case LDAP_AUTH_METHOD_NOT_SUPPORTED: ec=GPG_ERR_LDAP_UNSUPPORTED_AUTH;break;
+    case LDAP_STRONG_AUTH_REQUIRED: ec = GPG_ERR_LDAP_STRONG_AUTH_RQRD; break;
+    case LDAP_PARTIAL_RESULTS: ec = GPG_ERR_LDAP_PARTIAL_RESULTS; break;
+    case LDAP_REFERRAL: ec = GPG_ERR_LDAP_REFERRAL; break;
+
+#ifdef LDAP_ADMINLIMIT_EXCEEDED
+    case LDAP_ADMINLIMIT_EXCEEDED: ec = GPG_ERR_LDAP_ADMINLIMIT; break;
+#endif
+
+#ifdef LDAP_UNAVAILABLE_CRITICAL_EXTENSION
+    case LDAP_UNAVAILABLE_CRITICAL_EXTENSION:
+                               ec = GPG_ERR_LDAP_UNAVAIL_CRIT_EXTN; break;
+#endif
+
+    case LDAP_CONFIDENTIALITY_REQUIRED: ec = GPG_ERR_LDAP_CONFIDENT_RQRD; break;
+    case LDAP_SASL_BIND_IN_PROGRESS: ec = GPG_ERR_LDAP_SASL_BIND_INPROG; break;
+    case LDAP_NO_SUCH_ATTRIBUTE: ec = GPG_ERR_LDAP_NO_SUCH_ATTRIBUTE; break;
+    case LDAP_UNDEFINED_TYPE: ec = GPG_ERR_LDAP_UNDEFINED_TYPE; break;
+    case LDAP_INAPPROPRIATE_MATCHING: ec = GPG_ERR_LDAP_BAD_MATCHING; break;
+    case LDAP_CONSTRAINT_VIOLATION: ec = GPG_ERR_LDAP_CONST_VIOLATION; break;
+
+#ifdef LDAP_TYPE_OR_VALUE_EXISTS
+    case LDAP_TYPE_OR_VALUE_EXISTS: ec = GPG_ERR_LDAP_TYPE_VALUE_EXISTS; break;
+#endif
+
+    case LDAP_INVALID_SYNTAX: ec = GPG_ERR_LDAP_INV_SYNTAX; break;
+    case LDAP_NO_SUCH_OBJECT: ec = GPG_ERR_LDAP_NO_SUCH_OBJ; break;
+    case LDAP_ALIAS_PROBLEM: ec = GPG_ERR_LDAP_ALIAS_PROBLEM; break;
+    case LDAP_INVALID_DN_SYNTAX: ec = GPG_ERR_LDAP_INV_DN_SYNTAX; break;
+    case LDAP_IS_LEAF: ec = GPG_ERR_LDAP_IS_LEAF; break;
+    case LDAP_ALIAS_DEREF_PROBLEM: ec = GPG_ERR_LDAP_ALIAS_DEREF; break;
+
+#ifdef LDAP_X_PROXY_AUTHZ_FAILURE
+    case LDAP_X_PROXY_AUTHZ_FAILURE: ec = GPG_ERR_LDAP_X_PROXY_AUTH_FAIL; break;
+#endif
+
+    case LDAP_INAPPROPRIATE_AUTH: ec = GPG_ERR_LDAP_BAD_AUTH; break;
+    case LDAP_INVALID_CREDENTIALS: ec = GPG_ERR_LDAP_INV_CREDENTIALS; break;
+
+#ifdef LDAP_INSUFFICIENT_ACCESS
+    case LDAP_INSUFFICIENT_ACCESS: ec = GPG_ERR_LDAP_INSUFFICIENT_ACC; break;
+#endif
+
+    case LDAP_BUSY: ec = GPG_ERR_LDAP_BUSY; break;
+    case LDAP_UNAVAILABLE: ec = GPG_ERR_LDAP_UNAVAILABLE; break;
+    case LDAP_UNWILLING_TO_PERFORM: ec = GPG_ERR_LDAP_UNWILL_TO_PERFORM; break;
+    case LDAP_LOOP_DETECT: ec = GPG_ERR_LDAP_LOOP_DETECT; break;
+    case LDAP_NAMING_VIOLATION: ec = GPG_ERR_LDAP_NAMING_VIOLATION; break;
+    case LDAP_OBJECT_CLASS_VIOLATION: ec = GPG_ERR_LDAP_OBJ_CLS_VIOLATION; break;
+    case LDAP_NOT_ALLOWED_ON_NONLEAF: ec=GPG_ERR_LDAP_NOT_ALLOW_NONLEAF;break;
+    case LDAP_NOT_ALLOWED_ON_RDN: ec = GPG_ERR_LDAP_NOT_ALLOW_ON_RDN; break;
+    case LDAP_ALREADY_EXISTS: ec = GPG_ERR_LDAP_ALREADY_EXISTS; break;
+    case LDAP_NO_OBJECT_CLASS_MODS: ec = GPG_ERR_LDAP_NO_OBJ_CLASS_MODS; break;
+    case LDAP_RESULTS_TOO_LARGE: ec = GPG_ERR_LDAP_RESULTS_TOO_LARGE; break;
+    case LDAP_AFFECTS_MULTIPLE_DSAS: ec = GPG_ERR_LDAP_AFFECTS_MULT_DSAS; break;
+
+#ifdef LDAP_VLV_ERROR
+    case LDAP_VLV_ERROR: ec = GPG_ERR_LDAP_VLV; break;
+#endif
+
+    case LDAP_OTHER: ec = GPG_ERR_LDAP_OTHER; break;
+
+#ifdef LDAP_CUP_RESOURCES_EXHAUSTED
+    case LDAP_CUP_RESOURCES_EXHAUSTED: ec=GPG_ERR_LDAP_CUP_RESOURCE_LIMIT;break;
+    case LDAP_CUP_SECURITY_VIOLATION: ec=GPG_ERR_LDAP_CUP_SEC_VIOLATION; break;
+    case LDAP_CUP_INVALID_DATA: ec = GPG_ERR_LDAP_CUP_INV_DATA; break;
+    case LDAP_CUP_UNSUPPORTED_SCHEME: ec = GPG_ERR_LDAP_CUP_UNSUP_SCHEME; break;
+    case LDAP_CUP_RELOAD_REQUIRED: ec = GPG_ERR_LDAP_CUP_RELOAD; break;
+#endif
+
+#ifdef LDAP_CANCELLED
+    case LDAP_CANCELLED: ec = GPG_ERR_LDAP_CANCELLED; break;
+#endif
+
+#ifdef LDAP_NO_SUCH_OPERATION
+    case LDAP_NO_SUCH_OPERATION: ec = GPG_ERR_LDAP_NO_SUCH_OPERATION; break;
+#endif
+
+#ifdef LDAP_TOO_LATE
+    case LDAP_TOO_LATE: ec = GPG_ERR_LDAP_TOO_LATE; break;
+#endif
+
+#ifdef LDAP_CANNOT_CANCEL
+    case LDAP_CANNOT_CANCEL: ec = GPG_ERR_LDAP_CANNOT_CANCEL; break;
+#endif
+
+#ifdef LDAP_ASSERTION_FAILED
+    case LDAP_ASSERTION_FAILED: ec = GPG_ERR_LDAP_ASSERTION_FAILED; break;
+#endif
+
+#ifdef LDAP_PROXIED_AUTHORIZATION_DENIED
+    case LDAP_PROXIED_AUTHORIZATION_DENIED:
+                                      ec = GPG_ERR_LDAP_PROX_AUTH_DENIED; break;
+#endif
+
+    default:
+#if defined(LDAP_E_ERROR) && defined(LDAP_X_ERROR)
+      if (LDAP_E_ERROR (code))
+        ec = GPG_ERR_LDAP_E_GENERAL;
+      else if (LDAP_X_ERROR (code))
+        ec = GPG_ERR_LDAP_X_GENERAL;
+      else
+#endif
+        ec = GPG_ERR_LDAP_GENERAL;
+      break;
+    }
+
+  return ec;
+}
+
+/* Retrieve an LDAP error and return it's GPG equivalent.  */
+static int
+ldap_to_gpg_err (LDAP *ld)
+{
+#if defined(HAVE_LDAP_GET_OPTION) && defined(LDAP_OPT_ERROR_NUMBER)
+  int err;
+
+  if (ldap_get_option (ld, LDAP_OPT_ERROR_NUMBER, &err) == 0)
+    return ldap_err_to_gpg_err (err);
+  else
+    return GPG_ERR_GENERAL;
+#elif defined(HAVE_LDAP_LD_ERRNO)
+  return ldap_err_to_gpg_err (ld->ld_errno);
+#else
+  /* We should never get here since the LDAP library should always
+     have either ldap_get_option or ld_errno, but just in case... */
+  return GPG_ERR_GENERAL;
+#endif
+}
+\f
+static time_t
+ldap2epochtime (const char *timestr)
+{
+  struct tm pgptime;
+  time_t answer;
+
+  memset (&pgptime, 0, sizeof(pgptime));
+
+  /* YYYYMMDDHHmmssZ */
+
+  sscanf (timestr, "%4d%2d%2d%2d%2d%2d",
+         &pgptime.tm_year,
+         &pgptime.tm_mon,
+         &pgptime.tm_mday,
+         &pgptime.tm_hour,
+         &pgptime.tm_min,
+         &pgptime.tm_sec);
+
+  pgptime.tm_year -= 1900;
+  pgptime.tm_isdst = -1;
+  pgptime.tm_mon--;
+
+  /* mktime() takes the timezone into account, so we use timegm() */
+
+  answer = timegm (&pgptime);
+
+  return answer;
+}
+
+/* Caller must free the result.  */
+static char *
+tm2ldaptime (struct tm *tm)
+{
+  struct tm tmp = *tm;
+  char buf[16];
+
+  /* YYYYMMDDHHmmssZ */
+
+  tmp.tm_year += 1900;
+  tmp.tm_mon ++;
+
+  sprintf (buf, "%04d%02d%02d%02d%02d%02dZ",
+          tmp.tm_year,
+          tmp.tm_mon,
+          tmp.tm_mday,
+          tmp.tm_hour,
+          tmp.tm_min,
+          tmp.tm_sec);
+
+  return xstrdup (buf);
+}
+
+#if 0
+/* Caller must free */
+static char *
+epoch2ldaptime (time_t stamp)
+{
+  struct tm tm;
+  if (gmtime_r (&stamp, &tm))
+    return tm2ldaptime (&tm);
+  else
+    return xstrdup ("INVALID TIME");
+}
+#endif
+\f
+/* Print a help output for the schemata supported by this module. */
+gpg_error_t
+ks_ldap_help (ctrl_t ctrl, parsed_uri_t uri)
+{
+  const char const data[] =
+    "Handler for LDAP URLs:\n"
+    "  ldap://host:port/[BASEDN]???[bindname=BINDNAME,password=PASSWORD]\n"
+    "\n"
+    "Note: basedn, bindname and password need to be percent escaped. In\n"
+    "particular, spaces need to be replaced with %20 and commas with %2c.\n"
+    "bindname will typically be of the form:\n"
+    "\n"
+    "  uid=user%2cou=PGP%20Users%2cdc=EXAMPLE%2cdc=ORG\n"
+    "\n"
+    "The ldaps:// and ldapi:// schemes are also supported.  If ldaps is used\n"
+    "then the server's certificate will be checked.  If it is not valid, any\n"
+    "operation will be aborted.\n"
+    "\n"
+    "Supported methods: search, get, put\n";
+  gpg_error_t err;
+
+  if (strcmp (uri->scheme, "ldap") == 0
+      || strcmp (uri->scheme, "ldaps") == 0
+      || strcmp (uri->scheme, "ldapi") == 0)
+    err = ks_print_help (ctrl, data);
+  else
+    err = 0;
+
+  return err;
+}
+\f
+/* Convert a keyspec to a filter.  Return an error if the keyspec is
+   bad or is not supported.  The filter is escaped and returned in
+   *filter.  It is the caller's responsibility to free *filter.
+   *filter is only set if this function returns success (i.e., 0).  */
+static gpg_error_t
+keyspec_to_ldap_filter (const char *keyspec, char **filter, int only_exact)
+{
+  /* Remove search type indicator and adjust PATTERN accordingly.
+     Note: don't include a preceding 0x when searching by keyid.  */
+
+  /* XXX: Should we include disabled / revoke options?  */
+  KEYDB_SEARCH_DESC desc;
+  char *f = NULL;
+
+  gpg_error_t err = classify_user_id (keyspec, &desc, 1);
+  if (err)
+    return err;
+
+  switch (desc.mode)
+    {
+    case KEYDB_SEARCH_MODE_EXACT:
+      f = xasprintf ("(pgpUserID=%s)",
+                    ldap_escape_filter (desc.u.name));
+      break;
+
+    case KEYDB_SEARCH_MODE_SUBSTR:
+      if (! only_exact)
+       f = xasprintf ("(pgpUserID=*%s*)",
+                      ldap_escape_filter (desc.u.name));
+      break;
+
+    case KEYDB_SEARCH_MODE_MAIL:
+      if (! only_exact)
+       f = xasprintf ("(pgpUserID=*<%s>*)",
+                      ldap_escape_filter (desc.u.name));
+      break;
+
+    case KEYDB_SEARCH_MODE_MAILSUB:
+      if (! only_exact)
+       f = xasprintf ("(pgpUserID=*<*%s*>*)",
+                      ldap_escape_filter (desc.u.name));
+      break;
+
+    case KEYDB_SEARCH_MODE_MAILEND:
+      if (! only_exact)
+       f = xasprintf ("(pgpUserID=*<*%s>*)",
+                      ldap_escape_filter (desc.u.name));
+      break;
+
+    case KEYDB_SEARCH_MODE_SHORT_KID:
+      f = xasprintf ("(pgpKeyID=%08lX)", (ulong) desc.u.kid[1]);
+      break;
+    case KEYDB_SEARCH_MODE_LONG_KID:
+      f = xasprintf ("(pgpCertID=%08lX%08lX)",
+                    (ulong) desc.u.kid[0], (ulong) desc.u.kid[1]);
+      break;
+
+    case KEYDB_SEARCH_MODE_FPR16:
+    case KEYDB_SEARCH_MODE_FPR20:
+    case KEYDB_SEARCH_MODE_FPR:
+    case KEYDB_SEARCH_MODE_ISSUER:
+    case KEYDB_SEARCH_MODE_ISSUER_SN:
+    case KEYDB_SEARCH_MODE_SN:
+    case KEYDB_SEARCH_MODE_SUBJECT:
+    case KEYDB_SEARCH_MODE_KEYGRIP:
+    case KEYDB_SEARCH_MODE_WORDS:
+    case KEYDB_SEARCH_MODE_FIRST:
+    case KEYDB_SEARCH_MODE_NEXT:
+    default:
+      break;
+    }
+
+  if (! f)
+    {
+      log_error ("Unsupported search mode.\n");
+      return gpg_error (GPG_ERR_NOT_SUPPORTED);
+    }
+
+  *filter = f;
+
+  return 0;
+}
+\f
+/* Connect to an LDAP server and interrogate it.
+
+     - uri describes the server to connect to and various options
+       including whether to use TLS and the username and password (see
+       ldap_parse_uri for a description of the various fields).
+
+   This function returns:
+
+     - The ldap connection handle in *LDAP_CONNP.
+
+     - The base DN for the PGP key space by querying the
+       pgpBaseKeySpaceDN attribute (This is normally
+       'ou=PGP Keys,dc=EXAMPLE,dc=ORG').
+
+     - The attribute to lookup to find the pgp key.  This is either
+       'pgpKey' or 'pgpKeyV2'.
+
+     - Whether this is a real ldap server.  (It's unclear what this
+       exactly means.)
+
+   The values are returned in the passed variables.  If you pass NULL,
+   then the value won't be returned.  It is the caller's
+   responsibility to release *LDAP_CONNP with ldap_unbind and xfree
+   *BASEDNP and *PGPKEYATTRP.
+
+   If this function successfully interrogated the server, it returns
+   0.  If there was an LDAP error, it returns the LDAP error code.  If
+   an error occured, *basednp, etc., are undefined (and don't need to
+   be freed.)
+
+   If no LDAP error occured, you still need to check that *basednp is
+   valid.  If it is NULL, then the server does not appear to be an
+   OpenPGP Keyserver.  In this case, you also do not need to free
+   *pgpkeyattrp.  */
+static int
+ldap_connect (parsed_uri_t uri, LDAP **ldap_connp,
+             char **basednp, char **pgpkeyattrp, int *real_ldapp)
+{
+  int err = 0;
+
+  LDAP *ldap_conn = NULL;
+
+  char *user = uri->auth;
+  struct uri_tuple_s *password_param = uri_query_lookup (uri, "password");
+  char *password = password_param ? password_param->value : NULL;
+
+  char *basedn = NULL;
+  /* Whether to look for the pgpKey or pgpKeyv2 attribute.  */
+  char *pgpkeyattr = "pgpKey";
+  int real_ldap = 0;
+
+  log_debug ("ldap_connect(%s:%d/%s????%s%s%s%s%s)\n",
+            uri->host, uri->port,
+            uri->path ?: "",
+            uri->auth ? "bindname=" : "", uri->auth ?: "",
+            uri->auth && password ? "," : "",
+            password ? "password=" : "", password ?: "");
+
+  /* If the uri specifies a secure connection and we don't support
+     TLS, then fail; don't silently revert to an insecure
+     connection.  */
+  if (uri->use_tls)
+    {
+#ifndef HAVE_LDAP_START_TLS_S
+      log_error ("Can't use LDAP to connect to the server: no TLS support.");
+      err = GPG_ERR_LDAP_NOT_SUPPORTED;
+      goto out;
+#endif
+    }
+
+  ldap_conn = ldap_init (uri->host, uri->port);
+  if (! ldap_conn)
+    {
+      log_error ("Failed to open connection to LDAP server (%s://%s:%d)\n",
+                uri->scheme, uri->host, uri->port);
+      err = gpg_err_code_from_errno (errno);
+      goto out;
+    }
+
+#ifdef HAVE_LDAP_SET_OPTION
+  {
+    int ver = LDAP_VERSION3;
+
+    err = ldap_set_option (ldap_conn, LDAP_OPT_PROTOCOL_VERSION, &ver);
+    if (err != LDAP_SUCCESS)
+      {
+       log_error ("gpgkeys: unable to go to LDAP 3: %s\n",
+                  ldap_err2string (err));
+       goto out;
+      }
+  }
+#endif
+
+  /* XXX: It would be nice to have an option to provide the server's
+     certificate.  */
+#if 0
+#if defined(LDAP_OPT_X_TLS_CACERTFILE) && defined(HAVE_LDAP_SET_OPTION)
+  err = ldap_set_option (NULL, LDAP_OPT_X_TLS_CACERTFILE, ca_cert_file);
+  if (err)
+    {
+      log_error ("unable to set ca-cert-file to '%s': %s\n",
+                ca_cert_file, ldap_err2string (err));
+      goto out;
+    }
+#endif /* LDAP_OPT_X_TLS_CACERTFILE && HAVE_LDAP_SET_OPTION */
+#endif
+
+#ifndef HAVE_LDAP_START_TLS_S
+  if (uri->use_tls)
+    {
+      /* XXX: We need an option to determine whether to abort if the
+        certificate is bad or not.  Right now we conservatively
+        default to checking the certificate and aborting.  */
+      int check_cert = LDAP_OPT_X_TLS_HARD; // LDAP_OPT_X_TLS_NEVER
+
+      err = ldap_set_option (ldap_conn,
+                            LDAP_OPT_X_TLS_REQUIRE_CERT, &check_cert);
+      if (err)
+       {
+         log_error ("Failed to set TLS option on LDAP connection.\n");
+         goto out;
+       }
+
+      err = ldap_start_tls_s (ldap_conn, NULL, NULL);
+      if (err)
+       {
+         log_error ("Failed to connect to LDAP server with TLS.\n");
+         goto out;
+       }
+    }
+#endif
+
+  /* By default we don't bind as there is usually no need to.  */
+  if (uri->auth)
+    {
+      log_debug ("LDAP bind to %s, password %s\n",
+                user, password ? ">not shown<" : ">none<");
+
+      err = ldap_simple_bind_s (ldap_conn, user, password);
+      if (err != LDAP_SUCCESS)
+       {
+         log_error ("Internal LDAP bind error: %s\n",
+                    ldap_err2string (err));
+         goto out;
+       }
+    }
+
+  if (uri->path && *uri->path)
+    /* User specified base DN.  */
+    {
+      basedn = xstrdup (uri->path);
+
+      /* If the user specifies a base DN, then we know the server is a
+        real LDAP server.  */
+      real_ldap = 1;
+    }
+  else
+    {
+      LDAPMessage *res = NULL;
+      /* Look for namingContexts.  */
+      char *attr[] = { "namingContexts", NULL };
+
+      err = ldap_search_s (ldap_conn, "", LDAP_SCOPE_BASE,
+                          "(objectClass=*)", attr, 0, &res);
+      if (err == LDAP_SUCCESS)
+       {
+         char **context = ldap_get_values (ldap_conn, res, "namingContexts");
+         if (context)
+           /* We found some, so try each namingContext as the search
+              base and look for pgpBaseKeySpaceDN.  Because we found
+              this, we know we're talking to a regular-ish LDAP
+              server and not an LDAP keyserver.  */
+           {
+             int i;
+             char *attr2[] =
+               { "pgpBaseKeySpaceDN", "pgpVersion", "pgpSoftware", NULL };
+
+             real_ldap = 1;
+
+             for (i = 0; context[i] && ! basedn; i++)
+               {
+                 char **vals;
+                 LDAPMessage *si_res;
+
+                 char *object = xasprintf ("cn=pgpServerInfo,%s", context[i]);
+                 err = ldap_search_s (ldap_conn, object, LDAP_SCOPE_BASE,
+                                      "(objectClass=*)", attr2, 0, &si_res);
+                 free (object);
+
+                 if (err == LDAP_SUCCESS)
+                   {
+                     vals = ldap_get_values (ldap_conn, si_res,
+                                             "pgpBaseKeySpaceDN");
+                     if (vals)
+                       {
+                         basedn = strdup (vals[0]);
+                         ldap_value_free (vals);
+                       }
+
+                     vals = ldap_get_values (ldap_conn, si_res,
+                                             "pgpSoftware");
+                     if (vals)
+                       {
+                         log_debug ("Server: \t%s\n", vals[0]);
+                         ldap_value_free (vals);
+                       }
+
+                     vals = ldap_get_values (ldap_conn, si_res,
+                                             "pgpVersion");
+                     if (vals)
+                       {
+                         log_debug ("Version:\t%s\n", vals[0]);
+                         ldap_value_free (vals);
+                       }
+                   }
+
+                 /* From man ldap_search_s: "res parameter of
+                    ldap_search_ext_s() and ldap_search_s() should be
+                    freed with ldap_msgfree() regardless of return
+                    value of these functions.  */
+                 ldap_msgfree (si_res);
+               }
+
+             ldap_value_free (context);
+           }
+       }
+      else
+       {
+         /* We don't have an answer yet, which means the server might
+            be an LDAP keyserver. */
+         char **vals;
+         LDAPMessage *si_res = NULL;
+
+         char *attr2[] = { "pgpBaseKeySpaceDN", "version", "software", NULL };
+
+         err = ldap_search_s (ldap_conn, "cn=pgpServerInfo", LDAP_SCOPE_BASE,
+                              "(objectClass=*)", attr2, 0, &si_res);
+         if (err == LDAP_SUCCESS)
+           {
+             /* For the LDAP keyserver, this is always
+                "OU=ACTIVE,O=PGP KEYSPACE,C=US", but it might not be
+                in the future. */
+
+             vals = ldap_get_values (ldap_conn, si_res, "baseKeySpaceDN");
+             if (vals)
+               {
+                 basedn = strdup (vals[0]);
+                 ldap_value_free (vals);
+               }
+
+             vals = ldap_get_values (ldap_conn, si_res, "software");
+             if (vals)
+               {
+                 log_debug ("ldap: Server: \t%s\n", vals[0]);
+                 ldap_value_free (vals);
+               }
+
+             vals = ldap_get_values (ldap_conn, si_res, "version");
+             if (vals)
+               {
+                 log_debug ("ldap: Version:\t%s\n", vals[0]);
+
+                 /* If the version is high enough, use the new
+                    pgpKeyV2 attribute.  This design is iffy at best,
+                    but it matches how PGP does it.  I figure the NAI
+                    folks assumed that there would never be an LDAP
+                    keyserver vendor with a different numbering
+                    scheme. */
+                 if (atoi (vals[0]) > 1)
+                   pgpkeyattr = "pgpKeyV2";
+
+                 ldap_value_free (vals);
+               }
+           }
+
+         ldap_msgfree (si_res);
+       }
+
+      /* From man ldap_search_s: "res parameter of ldap_search_ext_s()
+        and ldap_search_s() should be freed with ldap_msgfree()
+        regardless of return value of these functions.  */
+      ldap_msgfree (res);
+    }
+
+ out:
+  if (! err)
+    {
+      log_debug ("ldap_conn: %p\n", ldap_conn);
+      log_debug ("real_ldap: %d\n", real_ldap);
+      log_debug ("basedn: %s\n", basedn);
+      log_debug ("pgpkeyattr: %s\n", pgpkeyattr);
+    }
+
+  if (! err && real_ldapp)
+    *real_ldapp = real_ldap;
+
+  if (err)
+    xfree (basedn);
+  else
+    {
+      if (pgpkeyattrp)
+       {
+         if (basedn)
+           *pgpkeyattrp = xstrdup (pgpkeyattr);
+         else
+           *pgpkeyattrp = NULL;
+       }
+
+      if (basednp)
+       *basednp = basedn;
+      else
+       xfree (basedn);
+    }
+
+  if (err)
+    {
+      if (ldap_conn)
+       ldap_unbind (ldap_conn);
+    }
+  else
+    *ldap_connp = ldap_conn;
+
+  return err;
+}
+
+/* Extract keys from an LDAP reply and write them out to the output
+   stream OUTPUT in a format GnuPG can import (either the OpenPGP
+   binary format or armored format).  */
+static void
+extract_keys (estream_t output,
+             LDAP *ldap_conn, const char *certid, LDAPMessage *message)
+{
+  char **vals;
+
+  es_fprintf (output, "INFO %s BEGIN\n", certid);
+  es_fprintf (output, "pub:%s:", certid);
+
+  /* Note: ldap_get_values returns a NULL terminates array of
+     strings.  */
+  vals = ldap_get_values (ldap_conn, message, "pgpkeytype");
+  if (vals && vals[0])
+    {
+      if (strcmp (vals[0], "RSA") == 0)
+       es_fprintf  (output, "1");
+      else if (strcmp (vals[0],"DSS/DH") == 0)
+       es_fprintf (output, "17");
+      ldap_value_free (vals);
+    }
+
+  es_fprintf (output, ":");
+
+  vals = ldap_get_values (ldap_conn, message, "pgpkeysize");
+  if (vals && vals[0])
+    {
+      int v = atoi (vals[0]);
+      if (v > 0)
+       es_fprintf (output, "%d", v);
+      ldap_value_free (vals);
+    }
+
+  es_fprintf (output, ":");
+
+  vals = ldap_get_values (ldap_conn, message, "pgpkeycreatetime");
+  if (vals && vals[0])
+    {
+      if (strlen (vals[0]) == 15)
+       es_fprintf (output, "%u", (unsigned int) ldap2epochtime (vals[0]));
+      ldap_value_free (vals);
+    }
+
+  es_fprintf (output, ":");
+
+  vals = ldap_get_values (ldap_conn, message, "pgpkeyexpiretime");
+  if (vals && vals[0])
+    {
+      if (strlen (vals[0]) == 15)
+       es_fprintf (output, "%u", (unsigned int) ldap2epochtime (vals[0]));
+      ldap_value_free (vals);
+    }
+
+  es_fprintf (output, ":");
+
+  vals = ldap_get_values (ldap_conn, message, "pgprevoked");
+  if (vals && vals[0])
+    {
+      if (atoi (vals[0]) == 1)
+       es_fprintf (output, "r");
+      ldap_value_free (vals);
+    }
+
+  es_fprintf (output, "\n");
+
+  vals = ldap_get_values (ldap_conn, message, "pgpuserid");
+  if (vals && vals[0])
+    {
+      int i;
+      for (i = 0; vals[i]; i++)
+       es_fprintf (output, "uid:%s\n", vals[i]);
+      ldap_value_free (vals);
+    }
+
+  es_fprintf (output, "INFO %s END\n", certid);
+}
+
+/* Get the key described key the KEYSPEC string from the keyserver
+   identified by URI.  On success R_FP has an open stream to read the
+   data.  */
+gpg_error_t
+ks_ldap_get (ctrl_t ctrl, parsed_uri_t uri, const char *keyspec,
+            estream_t *r_fp)
+{
+  gpg_error_t err = 0;
+  int ldap_err;
+
+  char *filter = NULL;
+
+  LDAP *ldap_conn = NULL;
+
+  char *basedn = NULL;
+  char *pgpkeyattr = NULL;
+
+  estream_t fp = NULL;
+
+  LDAPMessage *message = NULL;
+
+  (void) ctrl;
+
+  /* Before connecting to the server, make sure we have a sane
+     keyspec.  If not, there is no need to establish a network
+     connection.  */
+  err = keyspec_to_ldap_filter (keyspec, &filter, 1);
+  if (err)
+    return (err);
+
+  /* Make sure we are talking to an OpenPGP LDAP server.  */
+  ldap_err = ldap_connect (uri, &ldap_conn, &basedn, &pgpkeyattr, NULL);
+  if (ldap_err || !basedn)
+    {
+      if (ldap_err)
+       err = ldap_err_to_gpg_err (ldap_err);
+      else
+       err = GPG_ERR_GENERAL;
+      goto out;
+    }
+
+  {
+    /* The ordering is significant.  Specifically, "pgpcertid" needs
+       to be the second item in the list, since everything after it
+       may be discarded we aren't in verbose mode. */
+    char *attrs[] =
+      {
+       pgpkeyattr,
+       "pgpcertid", "pgpuserid", "pgpkeyid", "pgprevoked", "pgpdisabled",
+       "pgpkeycreatetime", "modifytimestamp", "pgpkeysize", "pgpkeytype",
+       NULL
+      };
+    /* 1 if we want just attribute types; 0 if we want both attribute
+       types and values.  */
+    int attrsonly = 0;
+
+    int count;
+
+    ldap_err = ldap_search_s (ldap_conn, basedn, LDAP_SCOPE_SUBTREE,
+                             filter, attrs, attrsonly, &message);
+    if (ldap_err)
+      {
+       err = ldap_err_to_gpg_err (ldap_err);
+
+       log_error ("gpgkeys: LDAP search error: %s\n",
+                  ldap_err2string (ldap_err));
+       goto out;
+      }
+
+    count = ldap_count_entries (ldap_conn, message);
+    if (count < 1)
+      {
+       log_error ("gpgkeys: key %s not found on keyserver\n", keyspec);
+
+       if (count == -1)
+         err = ldap_to_gpg_err (ldap_conn);
+       else
+         err = gpg_error (GPG_ERR_NO_DATA);
+
+       goto out;
+      }
+
+    {
+      /* There may be more than one unique result for a given keyID,
+        so we should fetch them all (test this by fetching short key
+        id 0xDEADBEEF). */
+
+      /* The set of entries that we've seen.  */
+      strlist_t seen = NULL;
+      LDAPMessage *each;
+
+      for (each = ldap_first_entry (ldap_conn, message);
+          each;
+          each = ldap_next_entry (ldap_conn, each))
+       {
+         char **vals;
+         char **certid;
+
+         /* Use the long keyid to remove duplicates.  The LDAP
+            server returns the same keyid more than once if there
+            are multiple user IDs on the key.  Note that this does
+            NOT mean that a keyid that exists multiple times on the
+            keyserver will not be fetched.  It means that each KEY,
+            no matter how many user IDs share its keyid, will be
+            fetched only once.  If a keyid that belongs to more
+            than one key is fetched, the server quite properly
+            responds with all matching keys. -ds */
+
+         certid = ldap_get_values (ldap_conn, each, "pgpcertid");
+         if (certid && certid[0])
+           {
+             if (! strlist_find (seen, certid[0]))
+               {
+                 /* It's not a duplicate, add it */
+
+                 add_to_strlist (&seen, certid[0]);
+
+                 if (! fp)
+                   fp = es_fopenmem(0, "rw");
+
+                 extract_keys (fp, ldap_conn, certid[0], each);
+
+                 vals = ldap_get_values (ldap_conn, each, pgpkeyattr);
+                 if (! vals)
+                   {
+                     err = ldap_to_gpg_err (ldap_conn);
+                     log_error("gpgkeys: unable to retrieve key %s "
+                               "from keyserver\n", certid[0]);
+                     goto out;
+                   }
+                 else
+                   {
+                     /* We should strip the new lines.  */
+                     es_fprintf (fp, "KEY 0x%s BEGIN\n", certid[0]);
+                     es_fputs (vals[0], fp);
+                     es_fprintf (fp, "\nKEY 0x%s END\n", certid[0]);
+
+                     ldap_value_free (vals);
+                   }
+               }
+           }
+
+         ldap_value_free (certid);
+       }
+
+      free_strlist (seen);
+
+      if (! fp)
+       err = gpg_error (GPG_ERR_NO_DATA);
+    }
+  }
+
+ out:
+  if (message)
+    ldap_msgfree (message);
+
+  if (err)
+    {
+      if (fp)
+       es_fclose (fp);
+    }
+  else
+    {
+      if (fp)
+       es_fseek (fp, 0, SEEK_SET);
+
+      *r_fp = fp;
+    }
+
+  xfree (pgpkeyattr);
+  xfree (basedn);
+
+  if (ldap_conn)
+    ldap_unbind (ldap_conn);
+
+  xfree (filter);
+
+  return err;
+}
+
+/* Search the keyserver identified by URI for keys matching PATTERN.
+   On success R_FP has an open stream to read the data.  */
+gpg_error_t
+ks_ldap_search (ctrl_t ctrl, parsed_uri_t uri, const char *pattern,
+               estream_t *r_fp)
+{
+  gpg_error_t err;
+  int ldap_err;
+
+  char *filter = NULL;
+
+  LDAP *ldap_conn = NULL;
+
+  char *basedn = NULL;
+
+  estream_t fp = NULL;
+
+  (void) ctrl;
+
+  /* Before connecting to the server, make sure we have a sane
+     keyspec.  If not, there is no need to establish a network
+     connection.  */
+  err = keyspec_to_ldap_filter (pattern, &filter, 0);
+  if (err)
+    {
+      log_error ("Bad search pattern: '%s'\n", pattern);
+      return (err);
+    }
+
+  /* Make sure we are talking to an OpenPGP LDAP server.  */
+  ldap_err = ldap_connect (uri, &ldap_conn, &basedn, NULL, NULL);
+  if (ldap_err || !basedn)
+    {
+      if (ldap_err)
+       err = ldap_err_to_gpg_err (ldap_err);
+      else
+       err = GPG_ERR_GENERAL;
+      goto out;
+    }
+
+  /* Even if we have no results, we want to return a stream.  */
+  fp = es_fopenmem(0, "rw");
+
+  {
+    char **vals;
+    LDAPMessage *res, *each;
+    int count = 0;
+    strlist_t dupelist = NULL;
+
+    /* The maximum size of the search, including the optional stuff
+       and the trailing \0 */
+    char *attrs[] =
+      {
+       "pgpcertid", "pgpuserid", "pgprevoked", "pgpdisabled",
+       "pgpkeycreatetime", "pgpkeyexpiretime", "modifytimestamp",
+       "pgpkeysize", "pgpkeytype", NULL
+      };
+
+    log_debug ("SEARCH '%s' => '%s' BEGIN\n", pattern, filter);
+
+    ldap_err = ldap_search_s (ldap_conn, basedn,
+                             LDAP_SCOPE_SUBTREE, filter, attrs, 0, &res);
+
+    xfree (filter);
+    filter = NULL;
+
+    if (err != LDAP_SUCCESS && err != LDAP_SIZELIMIT_EXCEEDED)
+      {
+       err = ldap_err_to_gpg_err (ldap_err);
+
+       log_error ("SEARCH %s FAILED %d\n", pattern, err);
+       log_error ("gpgkeys: LDAP search error: %s\n",
+                  ldap_err2string (err));
+       goto out;
+    }
+
+    /* The LDAP server doesn't return a real count of unique keys, so we
+       can't use ldap_count_entries here. */
+    for (each = ldap_first_entry (ldap_conn, res);
+        each;
+        each = ldap_next_entry (ldap_conn, each))
+      {
+       char **certid = ldap_get_values (ldap_conn, each, "pgpcertid");
+       if (certid && certid[0] && ! strlist_find (dupelist, certid[0]))
+         {
+           add_to_strlist (&dupelist, certid[0]);
+           count++;
+         }
+      }
+
+    if (err == LDAP_SIZELIMIT_EXCEEDED)
+      {
+       if (count == 1)
+         log_error ("gpgkeys: search results exceeded server limit."
+                    "  First 1 result shown.\n");
+       else
+         log_error ("gpgkeys: search results exceeded server limit."
+                    "  First %d results shown.\n", count);
+      }
+
+    free_strlist (dupelist);
+    dupelist = NULL;
+
+    if (count < 1)
+      es_fputs ("info:1:0\n", fp);
+    else
+      {
+       es_fprintf (fp, "info:1:%d\n", count);
+
+       for (each = ldap_first_entry (ldap_conn, res);
+            each;
+            each = ldap_next_entry (ldap_conn, each))
+         {
+           char **certid;
+           LDAPMessage *uids;
+
+           certid = ldap_get_values (ldap_conn, each, "pgpcertid");
+           if (! certid || ! certid[0])
+             continue;
+
+           /* Have we seen this certid before? */
+           if (! strlist_find (dupelist, certid[0]))
+             {
+               add_to_strlist (&dupelist, certid[0]);
+
+               es_fprintf (fp, "pub:%s:",certid[0]);
+
+                 vals = ldap_get_values (ldap_conn, each, "pgpkeytype");
+                 if (vals)
+                   {
+                     /* The LDAP server doesn't exactly handle this
+                        well. */
+                     if (strcasecmp (vals[0], "RSA") == 0)
+                       es_fputs ("1", fp);
+                     else if (strcasecmp (vals[0], "DSS/DH") == 0)
+                       es_fputs ("17", fp);
+                     ldap_value_free (vals);
+                   }
+
+                 es_fputc (':', fp);
+
+                 vals = ldap_get_values (ldap_conn, each, "pgpkeysize");
+                 if (vals)
+                   {
+                     /* Not sure why, but some keys are listed with a
+                        key size of 0.  Treat that like an
+                        unknown. */
+                     if (atoi (vals[0]) > 0)
+                       es_fprintf (fp, "%d", atoi (vals[0]));
+                     ldap_value_free (vals);
+                   }
+
+                 es_fputc (':', fp);
+
+                 /* YYYYMMDDHHmmssZ */
+
+                 vals = ldap_get_values (ldap_conn, each, "pgpkeycreatetime");
+                 if(vals && strlen (vals[0]) == 15)
+                   {
+                     es_fprintf (fp, "%u",
+                                 (unsigned int) ldap2epochtime(vals[0]));
+                     ldap_value_free (vals);
+                   }
+
+                 es_fputc (':', fp);
+
+                 vals = ldap_get_values (ldap_conn, each, "pgpkeyexpiretime");
+                 if (vals && strlen (vals[0]) == 15)
+                   {
+                     es_fprintf (fp, "%u",
+                                 (unsigned int) ldap2epochtime (vals[0]));
+                     ldap_value_free (vals);
+                   }
+
+                 es_fputc (':', fp);
+
+                 vals = ldap_get_values (ldap_conn, each, "pgprevoked");
+                 if (vals)
+                   {
+                     if (atoi (vals[0]) == 1)
+                       es_fprintf (fp, "r");
+                     ldap_value_free (vals);
+                   }
+
+                 vals = ldap_get_values (ldap_conn, each, "pgpdisabled");
+                 if (vals)
+                   {
+                     if (atoi (vals[0]) ==1)
+                       es_fprintf (fp, "d");
+                     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 && strlen (vals[0]) == 15)
+                   {
+                     es_fprintf (fp, "%u",
+                                 (unsigned int) ldap2epochtime (vals[0]));
+                     ldap_value_free (vals);
+                   }
+#endif
+
+                 es_fprintf (fp, "\n");
+
+                 /* Now print all the uids that have this certid */
+                 for (uids = ldap_first_entry (ldap_conn, res);
+                      uids;
+                      uids = ldap_next_entry (ldap_conn, uids))
+                   {
+                     vals = ldap_get_values (ldap_conn, uids, "pgpcertid");
+                     if (! vals)
+                       continue;
+
+                     if (strcasecmp (certid[0], vals[0]) == 0)
+                       {
+                         char **uidvals;
+
+                         es_fprintf (fp, "uid:");
+
+                         uidvals = ldap_get_values (ldap_conn,
+                                                    uids, "pgpuserid");
+                         if (uidvals)
+                           {
+                             /* Need to escape any colons */
+                             char *quoted = percent_escape (uidvals[0], NULL);
+                             es_fputs (quoted, fp);
+                             xfree (quoted);
+                             ldap_value_free (uidvals);
+                           }
+
+                         es_fprintf (fp, "\n");
+                       }
+
+                     ldap_value_free(vals);
+                   }
+             }
+
+             ldap_value_free (certid);
+         }
+      }
+
+    ldap_msgfree (res);
+    free_strlist (dupelist);
+  }
+
+  log_debug ("SEARCH %s END\n", pattern);
+
+ out:
+  if (err)
+    {
+      if (fp)
+       es_fclose (fp);
+    }
+  else
+    {
+      /* Return the read stream.  */
+      if (fp)
+       es_fseek (fp, 0, SEEK_SET);
+
+      *r_fp = fp;
+    }
+
+  xfree (basedn);
+
+  if (ldap_conn)
+    ldap_unbind (ldap_conn);
+
+  xfree (filter);
+
+  return err;
+}
+\f
+/* A modlist describes a set of changes to an LDAP entry.  (An entry
+   consists of 1 or more attributes.  Attributes are <name, value>
+   pairs.  Note: an attribute may be multi-valued in which case
+   multiple values are associated with a single name.)
+
+   A modlist is a NULL terminated array of struct LDAPMod's.
+
+   Thus, if we have:
+
+     LDAPMod **modlist;
+
+   Then:
+
+     modlist[i]
+
+   Is the ith modification.
+
+   Each LDAPMod describes a change to a single attribute.  Further,
+   there is one modification for each attribute that we want to
+   change.  The attribute's new value is stored in LDAPMod.mod_values.
+   If the attribute is multi-valued, we still only use a single
+   LDAPMod structure: mod_values is a NULL-terminated array of
+   strings.  To delete an attribute from an entry, we set mod_values
+   to NULL.
+
+   Thus, if:
+
+     modlist[i]->mod_values == NULL
+
+   then we remove the attribute.
+
+   (Using LDAP_MOD_DELETE doesn't work here as we don't know if the
+   attribute in question exists or not.)
+
+   Note: this function does NOT copy or free ATTR.  It does copy
+   VALUE.  */
+static void
+modlist_add (LDAPMod ***modlistp, char *attr, const char *value)
+{
+  LDAPMod **modlist = *modlistp;
+
+  LDAPMod **m;
+  int nummods = 0;
+
+  /* Search modlist for the attribute we're playing with.  If modlist
+     is NULL, then the list is empty.  Recall: modlist is a NULL
+     terminated array.  */
+  for (m = modlist; m && *m; m++, nummods ++)
+    {
+      /* The attribute is already on the list.  */
+      char **ptr;
+      int numvalues = 0;
+
+      if (strcasecmp ((*m)->mod_type, attr) != 0)
+       continue;
+
+      /* We have this attribute already, so when the REPLACE happens,
+        the server attributes will be replaced anyway. */
+      if (! value)
+       return;
+
+      /* Attributes can be multi-valued.  See if the value is already
+        present.  mod_values is a NULL terminated array of pointers.
+        Note: mod_values can be NULL.  */
+      for (ptr = (*m)->mod_values; ptr && *ptr; ptr++)
+       {
+         if (strcmp (*ptr, value) == 0)
+           /* Duplicate value, we're done.  */
+           return;
+         numvalues ++;
+       }
+
+      /* Append the value.  */
+      ptr = xrealloc ((*m)->mod_values, sizeof (char *) * (numvalues + 2));
+
+      (*m)->mod_values = ptr;
+      ptr[numvalues] = xstrdup (value);
+
+      ptr[numvalues + 1] = NULL;
+
+      return;
+    }
+
+  /* We didn't find the attr, so make one and add it to the end */
+
+  /* Like attribute values, the list of attributes is NULL terminated
+     array of pointers.  */
+  modlist = xrealloc (modlist, sizeof (LDAPMod *) * (nummods + 2));
+
+  *modlistp = modlist;
+  modlist[nummods] = xmalloc (sizeof (LDAPMod));
+
+  modlist[nummods]->mod_op = LDAP_MOD_REPLACE;
+  modlist[nummods]->mod_type = attr;
+  if (value)
+    {
+      modlist[nummods]->mod_values = xmalloc (sizeof(char *) * 2);
+
+      /* XXX: Is this the right thing?  Can a UTF8-encoded user ID
+        have embedded nulls? */
+      modlist[nummods]->mod_values[0] = xstrdup (value);
+      modlist[nummods]->mod_values[1] = NULL;
+    }
+  else
+    modlist[nummods]->mod_values = NULL;
+
+  modlist[nummods + 1] = NULL;
+
+  return;
+}
+
+/* Look up the value of an attribute in the specified modlist.  If the
+   attribute is not on the mod list, returns NULL.  The result is a
+   NULL-terminated array of strings.  Don't change it.  */
+static char **
+modlist_lookup (LDAPMod **modlist, const char *attr)
+{
+  LDAPMod **m;
+  for (m = modlist; m && *m; m++)
+    {
+      if (strcasecmp ((*m)->mod_type, attr) != 0)
+       continue;
+
+      return (*m)->mod_values;
+    }
+
+  return NULL;
+}
+
+/* Dump a modlist to a file.  This is useful for debugging.  */
+static estream_t modlist_dump (LDAPMod **modlist, estream_t output)
+  __attribute__ ((used));
+
+static estream_t
+modlist_dump (LDAPMod **modlist, estream_t output)
+{
+  LDAPMod **m;
+
+  int opened = 0;
+  if (! output)
+    {
+      output = es_fopenmem (0, "rw");
+      opened = 1;
+    }
+
+  for (m = modlist; m && *m; m++)
+    {
+      es_fprintf (output, "  %s:", (*m)->mod_type);
+
+      if (! (*m)->mod_values)
+       es_fprintf(output, " delete.\n");
+      else
+       {
+         char **ptr;
+         int i;
+
+         int multi = 0;
+         if ((*m)->mod_values[0] && (*m)->mod_values[1])
+           /* Have at least 2.  */
+           multi = 1;
+
+         if (multi)
+           es_fprintf (output, "\n");
+
+         for ((ptr = (*m)->mod_values), (i = 1); ptr && *ptr; ptr++, i ++)
+           {
+             /* At most about 10 lines.  */
+             const int max_len = 10 * 70;
+             size_t value_len = strlen (*ptr);
+             char buffer[max_len + 4];
+             char *temp;
+             int elided = 0;
+             if (value_len > max_len)
+               {
+                 temp = buffer;
+                 memcpy (temp, *ptr, max_len);
+                 temp[max_len] = temp[max_len + 1] = temp[max_len + 2] = '.';
+                 temp[max_len + 3] = 0;
+                 elided = 1;
+               }
+             else
+               temp = *ptr;
+
+             if (multi)
+               es_fprintf (output, "    %d. ", i);
+             es_fprintf (output, "`%s'", temp);
+             if (elided)
+               es_fprintf (output, " (%zd bytes elided)",
+                           value_len - max_len);
+             es_fprintf (output, "\n");
+           }
+       }
+    }
+
+  if (opened)
+    es_fseek (output, 0, SEEK_SET);
+
+  return output;
+}
+
+/* Free all of the memory allocated by the mod list.  This assumes
+   that the attribute names don't have to be freed, but the attributes
+   values do.  (Which is what modlist_add does.)  */
+static void
+modlist_free (LDAPMod **modlist)
+{
+  LDAPMod **ml;
+
+  if (! modlist)
+    return;
+
+  /* Unwind and free the whole modlist structure */
+
+  /* The modlist is a NULL terminated array of pointers.  */
+  for (ml = modlist; *ml; ml++)
+    {
+      LDAPMod *mod = *ml;
+      char **ptr;
+
+      /* The list of values is a NULL termianted array of pointers.
+        If the list is NULL, there are no values.  */
+
+      if (mod->mod_values)
+       {
+         for (ptr = mod->mod_values; *ptr; ptr++)
+           free (*ptr);
+
+         free (mod->mod_values);
+       }
+
+      free (mod);
+    }
+  free (modlist);
+}
+
+/* Append two onto the end of one.  Two is not freed, but its pointers
+   are now part of one.  Make sure you don't free them both!
+
+   As long as you don't add anything to ONE, TWO is still valid.
+   After that all bets are off.  */
+static void
+modlists_join (LDAPMod ***one, LDAPMod **two)
+{
+  int i, one_count = 0, two_count = 0;
+  LDAPMod **grow;
+
+  if (!*two)
+    /* two is empty.  Nothing to do.  */
+    return;
+
+  if (!*one)
+    /* one is empty.  Just set it equal to *two.  */
+    {
+      *one = two;
+      return;
+    }
+
+  for (grow = *one; *grow; grow++)
+    one_count ++;
+
+  for (grow = two; *grow; grow++)
+    two_count ++;
+
+  grow = xrealloc (*one, sizeof(LDAPMod *) * (one_count + two_count + 1));
+
+  for (i = 0; i < two_count; i++)
+    grow[one_count + i] = two[i];
+
+  grow[one_count + i] = NULL;
+
+  *one = grow;
+}
+
+/* Given a string, unescape C escapes.  In particular, \xXX.  This
+   modifies the string in place.  */
+static void
+uncescape (char *str)
+{
+  int r = 0;
+  int w = 0;
+
+  char *first = strchr (str, '\\');
+  if (! first)
+    /* No backslashes => no escaping.  We're done.  */
+    return;
+
+  /* Start at the first '\\'.  */
+  r = w = (uintptr_t) first - (uintptr_t) str;
+
+  while (str[r])
+    {
+      /* XXX: What to do about bad escapes?  */
+      if (str[r] == '\\' && str[r + 1] == 'x'
+         && (('0' <= str[r + 2] && str[r + 2] <= '9')
+             || ('a' <= str[r + 2] && str[r + 2] <= 'f')
+             || ('A' <= str[r + 2] && str[r + 2] <= 'F'))
+         && (('0' <= str[r + 3] && str[r + 3] <= '9')
+             || ('a' <= str[r + 3] && str[r + 3] <= 'f')
+             || ('A' <= str[r + 3] && str[r + 3] <= 'F')))
+       {
+         int x = hextobyte (&str[r + 2]);
+         assert (0 <= x && x <= 0xff);
+
+         str[w] = x;
+
+         /* We consumed 4 characters and wrote 1.  */
+         r += 4;
+         w ++;
+       }
+      else
+       str[w ++] = str[r ++];
+    }
+
+  str[w] = '\0';
+}
+
+/* Given one line from an info block (`gpg --list-{keys,sigs}
+   --with-colons KEYID'), pull it apart and fill in the modlist with
+   the relevant (for the LDAP schema) attributes.  */
+static void
+extract_attributes (LDAPMod ***modlist, char *line)
+{
+  int i;
+
+  int field_count;
+  char **fields;
+
+  char *keyid;
+
+  int is_pub, is_sub, is_uid, is_sig;
+
+  /* Remove trailing whitespace */
+  for (i = strlen (line) - 1; i >= 0 && ascii_isspace (line[i]); i--)
+    line[i] = '\0';
+
+  fields = strsplit (line, ':', '\0', &field_count);
+  if (field_count == 1)
+    /* We only have a single field.  There is definately nothing to
+       do.  */
+    goto out;
+
+  if (field_count < 7)
+    goto out;
+
+  is_pub = strcasecmp ("pub", fields[0]) == 0;
+  is_sub = strcasecmp ("sub", fields[0]) == 0;
+  is_uid = strcasecmp ("uid", fields[0]) == 0;
+  is_sig = strcasecmp ("sig", fields[0]) == 0;
+
+  if (!is_pub && !is_sub && !is_uid && !is_sig)
+    /* Not a relevant line.  */
+    goto out;
+
+  keyid = fields[4];
+
+  if (is_uid && strlen (keyid) == 0)
+    /* The uid record type can have an empty keyid.  */
+    ;
+  else if (strlen (keyid) == 16
+          && strspn (keyid, "0123456789aAbBcCdDeEfF") == 16)
+    /* Otherwise, we expect exactly 16 hex characters.  */
+    ;
+  else
+    {
+      log_error ("malformed record!\n");
+      goto out;
+    }
+
+  if (is_pub)
+    {
+      int disabled = 0, revoked = 0;
+      char *flags;
+      for (flags = fields[1]; *flags; flags ++)
+       switch (*flags)
+         {
+         case 'r':
+         case 'R':
+           revoked = 1;
+           break;
+
+         case 'd':
+         case 'D':
+           disabled = 1;
+           break;
+         }
+
+      /* Note: we always create the pgpDisabled and pgpRevoked
+       attributes, regardless of whether the key is disabled/revoked
+       or not.  This is because a very common search is like
+       "(&(pgpUserID=*isabella*)(pgpDisabled=0))"  */
+
+      if (is_pub)
+       {
+         modlist_add (modlist,"pgpDisabled", disabled ? "1" : "0");
+         modlist_add (modlist,"pgpRevoked", revoked ? "1" : "0");
+       }
+    }
+
+  if (is_pub || is_sub)
+    {
+      char *size = fields[2];
+      int val = atoi (size);
+      size = NULL;
+
+      if (val > 0)
+       {
+         /* We zero pad this on the left to make PGP happy. */
+         char padded[6];
+         if (val < 99999 && val > 0)
+           {
+             sprintf (padded, "%05u", val);
+             size = padded;
+           }
+       }
+
+      if (size)
+       {
+         if (is_pub || is_sub)
+           modlist_add (modlist, "pgpKeySize", size);
+       }
+    }
+
+  if (is_pub)
+    {
+      char *algo = fields[3];
+      int val = atoi (algo);
+      switch (val)
+       {
+       case 1:
+         algo = "RSA";
+         break;
+
+       case 17:
+         algo = "DSS/DH";
+         break;
+
+       default:
+         algo = NULL;
+         break;
+       }
+
+      if (algo)
+       {
+         if (is_pub)
+           modlist_add (modlist, "pgpKeyType", algo);
+       }
+    }
+
+  if (is_pub || is_sub || is_sig)
+    {
+      if (is_pub)
+       {
+         modlist_add (modlist, "pgpCertID", keyid);
+         modlist_add (modlist, "pgpKeyID", &keyid[8]);
+       }
+
+      if (is_sub)
+       modlist_add (modlist, "pgpSubKeyID", keyid);
+
+      if (is_sig)
+       modlist_add (modlist, "pgpSignerID", keyid);
+    }
+
+  if (is_pub)
+    {
+      char *create_time = fields[5];
+
+      if (strlen (create_time) == 0)
+       create_time = NULL;
+      else
+       {
+         char *create_time_orig = create_time;
+         struct tm tm;
+         time_t t;
+         char *end;
+
+         memset (&tm, 0, sizeof (tm));
+
+         /* parse_timestamp handles both seconds fromt he epoch and
+            ISO 8601 format.  We also need to handle YYYY-MM-DD
+            format (as generated by gpg1 --with-colons --list-key).
+            Check that first and then if it fails, then try
+            parse_timestamp.  */
+
+         if (strptime (create_time, "%Y-%m-%d", &tm))
+           create_time = tm2ldaptime (&tm);
+         else if ((t = parse_timestamp (create_time, &end)) != (time_t) -1
+                  && *end == '\0')
+           {
+             if (! gmtime_r (&t, &tm))
+               create_time = NULL;
+             else
+               create_time = tm2ldaptime (&tm);
+           }
+         else
+           create_time = NULL;
+
+         if (! create_time)
+           /* Failed to parse string.  */
+           log_error ("Failed to parse creation time ('%s')",
+                      create_time_orig);
+       }
+
+      if (create_time)
+       {
+         modlist_add (modlist, "pgpKeyCreateTime", create_time);
+         xfree (create_time);
+       }
+    }
+
+  if (is_pub)
+    {
+      char *expire_time = fields[6];
+
+      if (strlen (expire_time) == 0)
+       expire_time = NULL;
+      else
+       {
+         char *expire_time_orig = expire_time;
+         struct tm tm;
+         time_t t;
+         char *end;
+
+         memset (&tm, 0, sizeof (tm));
+
+         /* parse_timestamp handles both seconds fromt he epoch and
+            ISO 8601 format.  We also need to handle YYYY-MM-DD
+            format (as generated by gpg1 --with-colons --list-key).
+            Check that first and then if it fails, then try
+            parse_timestamp.  */
+
+         if (strptime (expire_time, "%Y-%m-%d", &tm))
+           expire_time = tm2ldaptime (&tm);
+         else if ((t = parse_timestamp (expire_time, &end)) != (time_t) -1
+                  && *end == '\0')
+           {
+             if (! gmtime_r (&t, &tm))
+               expire_time = NULL;
+             else
+               expire_time = tm2ldaptime (&tm);
+           }
+         else
+           expire_time = NULL;
+
+         if (! expire_time)
+           /* Failed to parse string.  */
+           log_error ("Failed to parse creation time ('%s')",
+                      expire_time_orig);
+       }
+
+      if (expire_time)
+       {
+         modlist_add (modlist, "pgpKeyExpireTime", expire_time);
+         xfree (expire_time);
+       }
+    }
+
+  if ((is_uid || is_pub) && field_count >= 10)
+    {
+      char *uid = fields[9];
+
+      if (is_pub && strlen (uid) == 0)
+       /* When using gpg --list-keys, the uid is included.  When
+          passed via gpg, it is not.  It is important to process it
+          when it is present, because gpg 1 won't print a UID record
+          if there is only one key.  */
+       ;
+      else
+       {
+         uncescape (uid);
+         modlist_add (modlist, "pgpUserID", uid);
+       }
+    }
+
+ out:
+  free (fields);
+}
+
+/* Send the key in {KEY,KEYLEN} with the metadata {INFO,INFOLEN} to
+   the keyserver identified by URI.  See server.c:cmd_ks_put for the
+   format of the data and metadata.  */
+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 err = 0;
+  int ldap_err;
+
+  LDAP *ldap_conn = NULL;
+  char *basedn = NULL;
+  char *pgpkeyattr = NULL;
+  int real_ldap;
+
+  LDAPMod **modlist = NULL;
+  LDAPMod **addlist = NULL;
+
+  char *data_armored = NULL;
+
+  /* The last byte of the info block.  */
+  const char *infoend = (const char *) info + infolen - 1;
+
+  /* Enable this code to dump the modlist to /tmp/modlist.txt.  */
+#if 0
+# warning Disable debug code before checking in.
+  const int dump_modlist = 1;
+#else
+  const int dump_modlist = 0;
+#endif
+  estream_t dump = NULL;
+
+  /* Elide a warning.  */
+  (void) ctrl;
+
+  ldap_err = ldap_connect (uri, &ldap_conn, &basedn, &pgpkeyattr, &real_ldap);
+  if (ldap_err || !basedn)
+    {
+      if (ldap_err)
+       err = ldap_err_to_gpg_err (ldap_err);
+      else
+       err = GPG_ERR_GENERAL;
+      goto out;
+    }
+
+  if (! real_ldap)
+    /* We appear to have an OpenPGP Keyserver, which can unpack the key
+       on its own (not just a dumb LDAP server).  */
+    {
+      LDAPMod mod, *attrs[2];
+      char *key[] = { data, NULL };
+      char *dn;
+
+      memset (&mod, 0, sizeof (mod));
+      mod.mod_op = LDAP_MOD_ADD;
+      mod.mod_type = pgpkeyattr;
+      mod.mod_values = key;
+      attrs[0] = &mod;
+      attrs[1] = NULL;
+
+      dn = xasprintf ("pgpCertid=virtual,%s", basedn);
+      ldap_err = ldap_add_s (ldap_conn, dn, attrs);
+      xfree (dn);
+
+      if (ldap_err != LDAP_SUCCESS)
+       {
+         err = ldap_err_to_gpg_err (err);
+         goto out;
+       }
+
+      goto out;
+    }
+
+  modlist = xmalloc (sizeof (LDAPMod *));
+  *modlist = NULL;
+
+  if (dump_modlist)
+    {
+      dump = es_fopen("/tmp/modlist.txt", "w");
+      if (! dump)
+       log_error ("Failed to open /tmp/modlist.txt: %s\n",
+                  strerror (errno));
+
+      if (dump)
+       {
+         es_fprintf(dump, "data (%zd bytes)\n", datalen);
+         es_fprintf(dump, "info (%zd bytes): '\n", infolen);
+         es_fwrite(info, infolen, 1, dump);
+         es_fprintf(dump, "'\n");
+       }
+    }
+
+  /* Start by nulling out all attributes.  We try and do a modify
+     operation first, so this ensures that we don't leave old
+     attributes lying around. */
+  modlist_add (&modlist, "pgpDisabled", NULL);
+  modlist_add (&modlist, "pgpKeyID", NULL);
+  modlist_add (&modlist, "pgpKeyType", NULL);
+  modlist_add (&modlist, "pgpUserID", NULL);
+  modlist_add (&modlist, "pgpKeyCreateTime", NULL);
+  modlist_add (&modlist, "pgpSignerID", NULL);
+  modlist_add (&modlist, "pgpRevoked", NULL);
+  modlist_add (&modlist, "pgpSubKeyID", NULL);
+  modlist_add (&modlist, "pgpKeySize", NULL);
+  modlist_add (&modlist, "pgpKeyExpireTime", NULL);
+  modlist_add (&modlist, "pgpCertID", NULL);
+
+  /* Assemble the INFO stuff into LDAP attributes */
+
+  while (infolen > 0)
+    {
+      char *temp = NULL;
+
+      char *newline = memchr (info, '\n', infolen);
+      if (! newline)
+       /* The last line is not \n terminated!  Make a copy so we can
+          add a NUL terminator.  */
+       {
+         temp = alloca (infolen + 1);
+         memcpy (temp, info, infolen);
+         info = temp;
+         newline = (char *) info + infolen;
+       }
+
+      *newline = '\0';
+
+      extract_attributes (&modlist, info);
+
+      infolen = infolen - ((uintptr_t) newline - (uintptr_t) info + 1);
+      info = newline + 1;
+
+      /* Sanity check.  */
+      if (! temp)
+       assert ((char *) info + infolen - 1 == infoend);
+      else
+       assert (infolen == -1);
+    }
+
+  modlist_add (&addlist, "objectClass", "pgpKeyInfo");
+
+  err = armor_data (&data_armored, data, datalen);
+  if (err)
+    goto out;
+
+  modlist_add (&addlist, pgpkeyattr, data_armored);
+
+  /* Now append addlist onto modlist.  */
+  modlists_join (&modlist, addlist);
+
+  if (dump)
+    {
+      estream_t input = modlist_dump (modlist, NULL);
+      copy_stream (input, dump);
+      es_fclose (input);
+    }
+
+  /* Going on the assumption that modify operations are more frequent
+     than adds, we try a modify first.  If it's not there, we just
+     turn around and send an add command for the same key.  Otherwise,
+     the modify brings the server copy into compliance with our copy.
+     Note that unlike the LDAP keyserver (and really, any other
+     keyserver) this does NOT merge signatures, but replaces the whole
+     key.  This should make some people very happy. */
+  {
+    char **certid;
+    char *dn;
+
+    certid = modlist_lookup (modlist, "pgpCertID");
+    if (/* We should have a value.  */
+       ! certid
+       /* Exactly one.  */
+       || !(certid[0] && !certid[1]))
+      {
+       log_error ("Bad certid.\n");
+       err = GPG_ERR_GENERAL;
+       goto out;
+      }
+
+    dn = xasprintf ("pgpCertID=%s,%s", certid[0], basedn);
+
+    err = ldap_modify_s (ldap_conn, dn, modlist);
+    if (err == LDAP_NO_SUCH_OBJECT)
+      err = ldap_add_s (ldap_conn, dn, addlist);
+
+    xfree (dn);
+
+    if (err != LDAP_SUCCESS)
+      {
+       log_error ("gpgkeys: error adding key to keyserver: %s\n",
+                  ldap_err2string (err));
+       err = ldap_err_to_gpg_err (err);
+      }
+  }
+
+ out:
+  if (dump)
+    es_fclose (dump);
+
+  if (ldap_conn)
+    ldap_unbind (ldap_conn);
+
+  xfree (basedn);
+  xfree (pgpkeyattr);
+
+  modlist_free (modlist);
+  xfree (addlist);
+
+  xfree (data_armored);
+
+  return err;
+}
index dc950cf..5bfe29b 100644 (file)
@@ -1,5 +1,6 @@
 /* ks-engine.h - Keyserver engines definitions
  * Copyright (C) 2011 Free Software Foundation, Inc.
+ * Copyright (C) 2015  g10 Code GmbH
  *
  * This file is part of GnuPG.
  *
@@ -52,6 +53,15 @@ gpg_error_t ks_finger_fetch (ctrl_t ctrl, parsed_uri_t uri, estream_t *r_fp);
 gpg_error_t ks_kdns_help (ctrl_t ctrl, parsed_uri_t uri);
 gpg_error_t ks_kdns_fetch (ctrl_t ctrl, parsed_uri_t uri, estream_t *r_fp);
 
+/*-- ks-engine-ldap.c --*/
+gpg_error_t ks_ldap_help (ctrl_t ctrl, parsed_uri_t uri);
+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, 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);
 
 
 #endif /*DIRMNGR_KS_ENGINE_H*/
index 4be58fd..2a341ad 100644 (file)
@@ -127,7 +127,7 @@ ldap_parse_uri (parsed_uri_t *purip, const char *uri)
 
   len = 0;
 
-#define add(s) ({ if (s) len += strlen (s) + 1; })
+#define add(s) { if (s) len += strlen (s) + 1; }
 
   add (scheme);
   add (host);
@@ -144,27 +144,30 @@ ldap_parse_uri (parsed_uri_t *purip, const char *uri)
 
   buffer = puri->buffer;
 
-#define copy(s)                                        \
-  ({                                           \
-    char *copy_result = NULL;                  \
-    if (s)                                     \
-      {                                                \
-       copy_result = buffer;                   \
-       buffer = stpcpy (buffer, s) + 1;        \
-      }                                                \
-    copy_result;                               \
-  })
-
-  puri->scheme = ascii_strlwr (copy (scheme));
-  puri->host = copy (host);
-  puri->path = copy (dn);
-  puri->auth = copy (bindname);
+#define copy(to, s)                            \
+  do                                           \
+    {                                          \
+      if (s)                                   \
+       {                                       \
+         to = buffer;                          \
+         buffer = stpcpy (buffer, s) + 1;      \
+       }                                       \
+    }                                          \
+  while (0)
+
+  copy (puri->scheme, scheme);
+  /* Make sure the scheme is lower case.  */
+  ascii_strlwr (puri->scheme);
+
+  copy (puri->host, host);
+  copy (puri->path, dn);
+  copy (puri->auth, bindname);
 
   if (password)
     {
       puri->query = calloc (sizeof (*puri->query), 1);
       puri->query->name = "password";
-      puri->query->value = copy (password);
+      copy (puri->query->value, password);
       puri->query->valuelen = strlen (password) + 1;
     }
 
index 6094bc9..b5d1653 100644 (file)
@@ -2,6 +2,7 @@
  * Copyright (C) 2002 Klarälvdalens Datakonsult AB
  * Copyright (C) 2003, 2004, 2005, 2007, 2008, 2009, 2011 g10 Code GmbH
  * Copyright (C) 2014 Werner Koch
+ * Copyright (C) 2015  g10 Code GmbH
  *
  * This file is part of GnuPG.
  *
@@ -48,6 +49,7 @@
 #endif
 #include "ks-action.h"
 #include "ks-engine.h"  /* (ks_hkp_print_hosttable) */
+#include "ldap-parse-uri.h"
 
 /* To avoid DoS attacks we limit the size of a certificate to
    something reasonable. */
@@ -1524,7 +1526,10 @@ cmd_keyserver (assuan_context_t ctx, char *line)
       item->parsed_uri = NULL;
       strcpy (item->uri, line);
 
-      err = http_parse_uri (&item->parsed_uri, line, 1);
+      if (ldap_uri_p (item->uri))
+       err = ldap_parse_uri (&item->parsed_uri, line);
+      else
+       err = http_parse_uri (&item->parsed_uri, line, 1);
       if (err)
         {
           xfree (item);
@@ -1709,13 +1714,15 @@ static const char hlp_ks_put[] =
   "\n"
   "  INQUIRE KEYBLOCK\n"
   "\n"
-  "The client shall respond with a binary version of the keyblock.  For LDAP\n"
+  "The client shall respond with a binary version of the keyblock (e.g.,\n"
+  "the output of `gpg --export KEYID').  For LDAP\n"
   "keyservers Dirmngr may ask for meta information of the provided keyblock\n"
   "using:\n"
   "\n"
   "  INQUIRE KEYBLOCK_INFO\n"
   "\n"
-  "The client shall respond with a colon delimited info lines";
+  "The client shall respond with a colon delimited info lines (the output\n"
+  "of 'for x in keys sigs; do gpg --list-$x --with-colons KEYID; done').\n";
 static gpg_error_t
 cmd_ks_put (assuan_context_t ctx, char *line)
 {
@@ -1755,7 +1762,7 @@ cmd_ks_put (assuan_context_t ctx, char *line)
     }
 
   /* Send the key.  */
-  err = ks_action_put (ctrl, value, valuelen);
+  err = ks_action_put (ctrl, value, valuelen, info, infolen);
 
  leave:
   xfree (info);
index b802f81..e2f63fb 100644 (file)
@@ -1,5 +1,6 @@
 /* call-dirmngr.c - GPG operations to the Dirmngr.
  * Copyright (C) 2011 Free Software Foundation, Inc.
+ * Copyright (C) 2015  g10 Code GmbH
  *
  * This file is part of GnuPG.
  *
@@ -585,6 +586,117 @@ gpg_dirmngr_ks_fetch (ctrl_t ctrl, const char *url, estream_t *r_fp)
 
 
 \f
+static void
+record_output (estream_t output,
+              pkttype_t type,
+              const char *validity,
+              /* The public key length or -1.  */
+              int pub_key_length,
+              /* The public key algo or -1.  */
+              int pub_key_algo,
+              /* 2 ulongs or NULL.  */
+              const u32 *keyid,
+              /* The creation / expiration date or 0.  */
+              u32 creation_date,
+              u32 expiration_date,
+              const char *userid)
+{
+  const char *type_str = NULL;
+  char *pub_key_length_str = NULL;
+  char *pub_key_algo_str = NULL;
+  char *keyid_str = NULL;
+  char *creation_date_str = NULL;
+  char *expiration_date_str = NULL;
+  char *userid_escaped = NULL;
+
+  switch (type)
+    {
+    case PKT_PUBLIC_KEY:
+      type_str = "pub";
+      break;
+    case PKT_PUBLIC_SUBKEY:
+      type_str = "sub";
+      break;
+    case PKT_USER_ID:
+      type_str = "uid";
+      break;
+    case PKT_SIGNATURE:
+      type_str = "sig";
+      break;
+    default:
+      assert (! "Unhandled type.");
+    }
+
+  if (pub_key_length > 0)
+    pub_key_length_str = xasprintf ("%d", pub_key_length);
+
+  if (pub_key_algo != -1)
+    pub_key_algo_str = xasprintf ("%d", pub_key_algo);
+
+  if (keyid)
+    keyid_str = xasprintf ("%08lX%08lX", (ulong) keyid[0], (ulong) keyid[1]);
+
+  if (creation_date)
+    creation_date_str = xstrdup (colon_strtime (creation_date));
+
+  if (expiration_date)
+    expiration_date_str = xstrdup (colon_strtime (expiration_date));
+
+  /* Quote ':', '%', and any 8-bit characters.  */
+  if (userid)
+    {
+      int r;
+      int w = 0;
+
+      int len = strlen (userid);
+      /* A 100k character limit on the uid should be way more than
+        enough.  */
+      if (len > 100 * 1024)
+       len = 100 * 1024;
+
+      /* The minimum amount of space that we need.  */
+      userid_escaped = xmalloc (len * 3 + 1);
+
+      for (r = 0; r < len; r++)
+       {
+         if (userid[r] == ':' || userid[r]== '%' || (userid[r] & 0x80))
+           {
+             sprintf (&userid_escaped[w], "%%%02X", (byte) userid[r]);
+             w += 3;
+           }
+         else
+           userid_escaped[w ++] = userid[r];
+       }
+      userid_escaped[w] = '\0';
+    }
+
+  es_fprintf (output, "%s:%s:%s:%s:%s:%s:%s:%s:%s:%s:%s:%s:%s:%s:%s:%s:%s\n",
+             type_str,
+             validity ?: "",
+             pub_key_length_str ?: "",
+             pub_key_algo_str ?: "",
+             keyid_str ?: "",
+             creation_date_str ?: "",
+             expiration_date_str ?: "",
+             "" /* Certificate S/N */,
+             "" /* Ownertrust.  */,
+             userid_escaped ?: "",
+             "" /* Signature class.  */,
+             "" /* Key capabilities.  */,
+             "" /* Issuer certificate fingerprint.  */,
+             "" /* Flag field.  */,
+             "" /* S/N of a token.  */,
+             "" /* Hash algo.  */,
+             "" /* Curve name.  */);
+
+  xfree (userid_escaped);
+  xfree (expiration_date_str);
+  xfree (creation_date_str);
+  xfree (keyid_str);
+  xfree (pub_key_algo_str);
+  xfree (pub_key_length_str);
+}
+
 /* Handle the KS_PUT inquiries. */
 static gpg_error_t
 ks_put_inq_cb (void *opaque, const char *line)
@@ -607,53 +719,80 @@ ks_put_inq_cb (void *opaque, const char *line)
       if (!fp)
         err = gpg_error_from_syserror ();
 
+      /* Note: the output format for the INFO block follows the colon
+        format as described in doc/DETAILS.  We don't actually reuse
+        the functionality from g10/keylist.c to produce the output,
+        because we don't need all of it and some of it is quite
+        expensive to generate.
+
+        The fields are (the starred fields are the ones we need):
+
+          * Field 1 - Type of record
+           * Field 2 - Validity
+           * Field 3 - Key length
+           * Field 4 - Public key algorithm
+           * Field 5 - KeyID
+           * Field 6 - Creation date
+           * Field 7 - Expiration date
+             Field 8 - Certificate S/N, UID hash, trust signature info
+             Field 9 -  Ownertrust
+          * Field 10 - User-ID
+             Field 11 - Signature class
+             Field 12 - Key capabilities
+             Field 13 - Issuer certificate fingerprint or other info
+             Field 14 - Flag field
+             Field 15 - S/N of a token
+             Field 16 - Hash algorithm
+             Field 17 - Curve name
+       */
       for (node = parm->keyblock; !err && node; node=node->next)
         {
-          switch(node->pkt->pkttype)
+          switch (node->pkt->pkttype)
             {
             case PKT_PUBLIC_KEY:
             case PKT_PUBLIC_SUBKEY:
               {
                 PKT_public_key *pk = node->pkt->pkt.public_key;
 
+               char validity[3];
+               int i;
+
+               i = 0;
+               if (pk->flags.revoked)
+                 validity[i ++] = 'r';
+               if (pk->has_expired)
+                 validity[i ++] = 'e';
+               validity[i] = '\0';
+
                 keyid_from_pk (pk, NULL);
 
-                es_fprintf (fp, "%s:%08lX%08lX:%u:%u:%u:%u:%s%s:\n",
-                            node->pkt->pkttype==PKT_PUBLIC_KEY? "pub" : "sub",
-                            (ulong)pk->keyid[0], (ulong)pk->keyid[1],
-                            pk->pubkey_algo,
-                            nbits_from_pk (pk),
-                            pk->timestamp,
-                            pk->expiredate,
-                            pk->flags.revoked? "r":"",
-                            pk->has_expired? "e":"");
+               record_output (fp, node->pkt->pkttype, validity,
+                              nbits_from_pk (pk), pk->pubkey_algo,
+                              pk->keyid, pk->timestamp, pk->expiredate,
+                              NULL);
               }
               break;
 
             case PKT_USER_ID:
               {
                 PKT_user_id *uid = node->pkt->pkt.user_id;
-                int r;
 
                 if (!uid->attrib_data)
                   {
-                    es_fprintf (fp, "uid:");
-
-                    /* Quote ':', '%', and any 8-bit characters.  */
-                    for (r=0; r < uid->len; r++)
-                      {
-                        if (uid->name[r] == ':'
-                            || uid->name[r]== '%'
-                            || (uid->name[r]&0x80))
-                          es_fprintf (fp, "%%%02X", (byte)uid->name[r]);
-                        else
-                          es_putc (uid->name[r], fp);
-                      }
-
-                    es_fprintf (fp, ":%u:%u:%s%s:\n",
-                                uid->created,uid->expiredate,
-                                uid->is_revoked? "r":"",
-                                uid->is_expired? "e":"");
+                   char validity[3];
+                   int i;
+
+                   i = 0;
+                   if (uid->is_revoked)
+                     validity[i ++] = 'r';
+                   if (uid->is_expired)
+                     validity[i ++] = 'e';
+                   validity[i] = '\0';
+
+                   record_output (fp, node->pkt->pkttype, validity,
+                                  -1, -1, NULL,
+                                  uid->created, uid->expiredate,
+                                  uid->name);
                   }
               }
               break;
@@ -667,12 +806,9 @@ ks_put_inq_cb (void *opaque, const char *line)
                 PKT_signature *sig = node->pkt->pkt.signature;
 
                 if (IS_UID_SIG (sig))
-                  {
-                    es_fprintf (fp, "sig:%08lX%08lX:%X:%u:%u:\n",
-                                (ulong)sig->keyid[0],(ulong)sig->keyid[1],
-                                sig->sig_class, sig->timestamp,
-                                sig->expiredate);
-                  }
+                 record_output (fp, node->pkt->pkttype, NULL,
+                                -1, -1, sig->keyid,
+                                sig->timestamp, sig->expiredate, NULL);
               }
               break;