diff --git a/dirmngr/http.c b/dirmngr/http.c index c662b1b95..dc1873448 100644 --- a/dirmngr/http.c +++ b/dirmngr/http.c @@ -1,3783 +1,3798 @@ /* http.c - HTTP protocol handler - * Copyright (C) 1999, 2001, 2002, 2003, 2004, 2006, 2009, 2010, + * Copyright (C) 1999, 2001-2004, 2006, 2009, 2010, * 2011 Free Software Foundation, Inc. - * Copyright (C) 2014 Werner Koch - * Copyright (C) 2015-2019 g10 Code GmbH + * Copyright (C) 1999, 2001-2004, 2006, 2009, 2010, 2011, 2014 Werner Koch + * Copyright (C) 2015-2017, 2021 g10 Code GmbH * * This file is part of GnuPG. * * This file is free software; you can redistribute it and/or modify * it under the terms of either * * - the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at * your option) any later version. * * or * * - the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at * your option) any later version. * * or both in parallel, as here. * * This file 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 . */ /* Simple HTTP client implementation. We try to keep the code as self-contained as possible. There are some constraints however: - estream is required. We now require estream because it provides a very useful and portable asprintf implementation and the fopencookie function. - stpcpy is required - fixme: list other requirements. - Either HTTP_USE_NTBTLS or HTTP_USE_GNUTLS must be defined to select which TLS library to use. - With HTTP_NO_WSASTARTUP the socket initialization is not done under Windows. This is useful if the socket layer has already been initialized elsewhere. This also avoids the installation of an exit handler to cleanup the socket layer. */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #ifdef HAVE_W32_SYSTEM # ifdef HAVE_WINSOCK2_H # include # endif # include #else /*!HAVE_W32_SYSTEM*/ # include # include # include # include # include # include # include # include #endif /*!HAVE_W32_SYSTEM*/ #ifdef WITHOUT_NPTH /* Give the Makefile a chance to build without Pth. */ # undef USE_NPTH #endif #ifdef USE_NPTH # include #endif #if defined (HTTP_USE_GNUTLS) && defined (HTTP_USE_NTBTLS) # error Both, HTTP_USE_GNUTLS and HTTP_USE_NTBTLS, are defined. #endif #ifdef HTTP_USE_NTBTLS # include #elif HTTP_USE_GNUTLS # include # include #endif /*HTTP_USE_GNUTLS*/ #include /* We need the socket wrapper. */ #include "../common/util.h" #include "../common/i18n.h" #include "../common/sysutils.h" /* (gnupg_fd_t) */ #include "dns-stuff.h" #include "dirmngr-status.h" /* (dirmngr_status_printf) */ #include "http.h" #include "http-common.h" #ifdef USE_NPTH # define my_select(a,b,c,d,e) npth_select ((a), (b), (c), (d), (e)) # define my_accept(a,b,c) npth_accept ((a), (b), (c)) #else # define my_select(a,b,c,d,e) select ((a), (b), (c), (d), (e)) # define my_accept(a,b,c) accept ((a), (b), (c)) #endif #ifdef HAVE_W32_SYSTEM #define sock_close(a) closesocket(a) #else #define sock_close(a) close(a) #endif #ifndef EAGAIN #define EAGAIN EWOULDBLOCK #endif #ifndef INADDR_NONE /* Slowaris is missing that. */ #define INADDR_NONE ((unsigned long)(-1)) #endif /*INADDR_NONE*/ #define HTTP_PROXY_ENV "http_proxy" #define MAX_LINELEN 20000 /* Max. length of a HTTP header line. */ #define VALID_URI_CHARS "abcdefghijklmnopqrstuvwxyz" \ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ "01234567890@" \ "!\"#$%&'()*+,-./:;<=>?[\\]^_{|}~" #if HTTP_USE_NTBTLS typedef ntbtls_t tls_session_t; #elif HTTP_USE_GNUTLS typedef gnutls_session_t tls_session_t; #else # error building without TLS is not supported #endif static gpg_err_code_t do_parse_uri (parsed_uri_t uri, int only_local_part, int no_scheme_check, int force_tls); static gpg_error_t parse_uri (parsed_uri_t *ret_uri, const char *uri, int no_scheme_check, int force_tls); static int remove_escapes (char *string); static int insert_escapes (char *buffer, const char *string, const char *special); static uri_tuple_t parse_tuple (char *string); static gpg_error_t send_request (ctrl_t ctrl, http_t hd, const char *httphost, const char *auth,const char *proxy, const char *srvtag, unsigned int timeout, strlist_t headers); static char *build_rel_path (parsed_uri_t uri); static gpg_error_t parse_response (http_t hd); static gpg_error_t connect_server (ctrl_t ctrl, const char *server, unsigned short port, unsigned int flags, const char *srvtag, unsigned int timeout, assuan_fd_t *r_sock); static gpgrt_ssize_t read_server (assuan_fd_t sock, void *buffer, size_t size); static gpg_error_t write_server (assuan_fd_t sock, const char *data, size_t length); static gpgrt_ssize_t cookie_read (void *cookie, void *buffer, size_t size); static gpgrt_ssize_t cookie_write (void *cookie, const void *buffer, size_t size); static int cookie_close (void *cookie); #if defined(HAVE_W32_SYSTEM) && defined(HTTP_USE_NTBTLS) static gpgrt_ssize_t simple_cookie_read (void *cookie, void *buffer, size_t size); static gpgrt_ssize_t simple_cookie_write (void *cookie, const void *buffer, size_t size); #endif /* A socket object used to a allow ref counting of sockets. */ struct my_socket_s { assuan_fd_t fd; /* The actual socket - shall never be ASSUAN_INVALID_FD. */ int refcount; /* Number of references to this socket. */ }; typedef struct my_socket_s *my_socket_t; /* Cookie function structure and cookie object. */ static es_cookie_io_functions_t cookie_functions = { cookie_read, cookie_write, NULL, cookie_close }; struct cookie_s { /* Socket object or NULL if already closed. */ my_socket_t sock; /* The session object or NULL if not used. */ http_session_t session; /* True if TLS is to be used. */ int use_tls; /* The remaining content length and a flag telling whether to use the content length. */ uint64_t content_length; unsigned int content_length_valid:1; }; typedef struct cookie_s *cookie_t; /* Simple cookie functions. Here the cookie is an int with the * socket. */ #if defined(HAVE_W32_SYSTEM) && defined(HTTP_USE_NTBTLS) static es_cookie_io_functions_t simple_cookie_functions = { simple_cookie_read, simple_cookie_write, NULL, NULL }; #endif #if SIZEOF_UNSIGNED_LONG == 8 # define HTTP_SESSION_MAGIC 0x0068545470534553 /* "hTTpSES" */ #else # define HTTP_SESSION_MAGIC 0x68547365 /* "hTse" */ #endif /* The session object. */ struct http_session_s { unsigned long magic; int refcount; /* Number of references to this object. */ tls_session_t tls_session; struct { int done; /* Verifciation has been done. */ int rc; /* TLS verification return code. */ unsigned int status; /* Verification status. */ } verify; char *servername; /* Malloced server name. */ /* A callback function to log details of TLS certifciates. */ void (*cert_log_cb) (http_session_t, gpg_error_t, const char *, const void **, size_t *); /* The flags passed to the session object. */ unsigned int flags; /* A per-session TLS verification callback. */ http_verify_cb_t verify_cb; void *verify_cb_value; /* The connect timeout */ unsigned int connect_timeout; #ifdef HTTP_USE_GNUTLS gnutls_certificate_credentials_t certcred; #endif /*HTTP_USE_GNUTLS*/ }; /* An object to save header lines. */ struct header_s { struct header_s *next; char *value; /* The value of the header (malloced). */ char name[1]; /* The name of the header (canonicalized). */ }; typedef struct header_s *header_t; #if SIZEOF_UNSIGNED_LONG == 8 # define HTTP_CONTEXT_MAGIC 0x0068545470435458 /* "hTTpCTX" */ #else # define HTTP_CONTEXT_MAGIC 0x68546378 /* "hTcx" */ #endif /* Our handle context. */ struct http_context_s { unsigned long magic; unsigned int status_code; my_socket_t sock; unsigned int in_data:1; unsigned int is_http_0_9:1; estream_t fp_read; estream_t fp_write; void *write_cookie; void *read_cookie; http_session_t session; parsed_uri_t uri; http_req_t req_type; char *buffer; /* Line buffer. */ size_t buffer_size; unsigned int flags; header_t headers; /* Received headers. */ }; /* Two flags to enable verbose and debug mode. Although currently not * set-able a value > 1 for OPT_DEBUG enables debugging of the session * reference counting. */ static int opt_verbose; static int opt_debug; /* The global callback for the verification function. */ static gpg_error_t (*tls_callback) (http_t, http_session_t, int); /* The list of files with trusted CA certificates. */ static strlist_t tls_ca_certlist; /* The list of files with extra trusted CA certificates. */ static strlist_t cfg_ca_certlist; /* The global callback for net activity. */ static void (*netactivity_cb)(void); #if defined(HAVE_W32_SYSTEM) && !defined(HTTP_NO_WSASTARTUP) #if GNUPG_MAJOR_VERSION == 1 #define REQ_WINSOCK_MAJOR 1 #define REQ_WINSOCK_MINOR 1 #else #define REQ_WINSOCK_MAJOR 2 #define REQ_WINSOCK_MINOR 2 #endif static void deinit_sockets (void) { WSACleanup(); } static void init_sockets (void) { static int initialized; static WSADATA wsdata; if (initialized) return; if ( WSAStartup( MAKEWORD (REQ_WINSOCK_MINOR, REQ_WINSOCK_MAJOR), &wsdata ) ) { log_error ("error initializing socket library: ec=%d\n", (int)WSAGetLastError () ); return; } if ( LOBYTE(wsdata.wVersion) != REQ_WINSOCK_MAJOR || HIBYTE(wsdata.wVersion) != REQ_WINSOCK_MINOR ) { log_error ("socket library version is %x.%x - but %d.%d needed\n", LOBYTE(wsdata.wVersion), HIBYTE(wsdata.wVersion), REQ_WINSOCK_MAJOR, REQ_WINSOCK_MINOR); WSACleanup(); return; } atexit ( deinit_sockets ); initialized = 1; } #endif /*HAVE_W32_SYSTEM && !HTTP_NO_WSASTARTUP*/ /* Create a new socket object. Returns NULL and closes FD if not enough memory is available. */ static my_socket_t _my_socket_new (int lnr, assuan_fd_t fd) { my_socket_t so; so = xtrymalloc (sizeof *so); if (!so) { int save_errno = errno; assuan_sock_close (fd); gpg_err_set_errno (save_errno); return NULL; } so->fd = fd; so->refcount = 1; if (opt_debug) log_debug ("http.c:%d:socket_new: object %p for fd %d created\n", lnr, so, (int)so->fd); return so; } #define my_socket_new(a) _my_socket_new (__LINE__, (a)) /* Bump up the reference counter for the socket object SO. */ static my_socket_t _my_socket_ref (int lnr, my_socket_t so) { so->refcount++; if (opt_debug > 1) log_debug ("http.c:%d:socket_ref: object %p for fd %d refcount now %d\n", lnr, so, (int)so->fd, so->refcount); return so; } #define my_socket_ref(a) _my_socket_ref (__LINE__,(a)) /* Bump down the reference counter for the socket object SO. If SO has no more references, close the socket and release the object. */ static void _my_socket_unref (int lnr, my_socket_t so, void (*preclose)(void*), void *preclosearg) { if (so) { so->refcount--; if (opt_debug > 1) log_debug ("http.c:%d:socket_unref: object %p for fd %d ref now %d\n", lnr, so, (int)so->fd, so->refcount); if (!so->refcount) { if (preclose) preclose (preclosearg); assuan_sock_close (so->fd); xfree (so); } } } #define my_socket_unref(a,b,c) _my_socket_unref (__LINE__,(a),(b),(c)) #ifdef HTTP_USE_GNUTLS static ssize_t my_gnutls_read (gnutls_transport_ptr_t ptr, void *buffer, size_t size) { my_socket_t sock = ptr; #if USE_NPTH return npth_read (sock->fd, buffer, size); #else return read (sock->fd, buffer, size); #endif } static ssize_t my_gnutls_write (gnutls_transport_ptr_t ptr, const void *buffer, size_t size) { my_socket_t sock = ptr; #if USE_NPTH return npth_write (sock->fd, buffer, size); #else return write (sock->fd, buffer, size); #endif } #endif /*HTTP_USE_GNUTLS*/ #ifdef HTTP_USE_NTBTLS /* Connect the ntbls callback to our generic callback. */ static gpg_error_t my_ntbtls_verify_cb (void *opaque, ntbtls_t tls, unsigned int verify_flags) { http_t hd = opaque; (void)verify_flags; log_assert (hd && hd->session && hd->session->verify_cb); log_assert (hd->magic == HTTP_CONTEXT_MAGIC); log_assert (hd->session->magic == HTTP_SESSION_MAGIC); return hd->session->verify_cb (hd->session->verify_cb_value, hd, hd->session, (hd->flags | hd->session->flags), tls); } #endif /*HTTP_USE_NTBTLS*/ /* This notification function is called by estream whenever stream is closed. Its purpose is to mark the closing in the handle so that a http_close won't accidentally close the estream. The function http_close removes this notification so that it won't be called if http_close was used before an es_fclose. */ static void fp_onclose_notification (estream_t stream, void *opaque) { http_t hd = opaque; log_assert (hd->magic == HTTP_CONTEXT_MAGIC); if (hd->fp_read && hd->fp_read == stream) hd->fp_read = NULL; else if (hd->fp_write && hd->fp_write == stream) hd->fp_write = NULL; } /* * Helper function to create an HTTP header with hex encoded data. A * new buffer is returned. This buffer is the concatenation of the * string PREFIX, the hex-encoded DATA of length LEN and the string * SUFFIX. On error NULL is returned and ERRNO set. */ static char * make_header_line (const char *prefix, const char *suffix, const void *data, size_t len ) { static unsigned char bintoasc[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789+/"; const unsigned char *s = data; char *buffer, *p; buffer = xtrymalloc (strlen (prefix) + (len+2)/3*4 + strlen (suffix) + 1); if (!buffer) return NULL; p = stpcpy (buffer, prefix); for ( ; len >= 3 ; len -= 3, s += 3 ) { *p++ = bintoasc[(s[0] >> 2) & 077]; *p++ = bintoasc[(((s[0] <<4)&060)|((s[1] >> 4)&017))&077]; *p++ = bintoasc[(((s[1]<<2)&074)|((s[2]>>6)&03))&077]; *p++ = bintoasc[s[2]&077]; *p = 0; } if ( len == 2 ) { *p++ = bintoasc[(s[0] >> 2) & 077]; *p++ = bintoasc[(((s[0] <<4)&060)|((s[1] >> 4)&017))&077]; *p++ = bintoasc[((s[1]<<2)&074)]; *p++ = '='; } else if ( len == 1 ) { *p++ = bintoasc[(s[0] >> 2) & 077]; *p++ = bintoasc[(s[0] <<4)&060]; *p++ = '='; *p++ = '='; } *p = 0; strcpy (p, suffix); return buffer; } /* Set verbosity and debug mode for this module. */ void http_set_verbose (int verbose, int debug) { opt_verbose = verbose; opt_debug = debug; } /* Register a non-standard global TLS callback function. If no verification is desired a callback needs to be registered which always returns NULL. */ void http_register_tls_callback (gpg_error_t (*cb)(http_t, http_session_t, int)) { tls_callback = cb; } /* Register a CA certificate for future use. The certificate is expected to be in FNAME. PEM format is assume if FNAME has a suffix of ".pem". If FNAME is NULL the list of CA files is removed. */ void http_register_tls_ca (const char *fname) { gpg_err_code_t ec; strlist_t sl; if (!fname) { free_strlist (tls_ca_certlist); tls_ca_certlist = NULL; } else { /* Warn if we can't access right now, but register it anyway in case it becomes accessible later */ if ((ec = gnupg_access (fname, F_OK))) log_info (_("can't access '%s': %s\n"), fname, gpg_strerror (ec)); sl = add_to_strlist (&tls_ca_certlist, fname); if (*sl->d && !strcmp (sl->d + strlen (sl->d) - 4, ".pem")) sl->flags = 1; } } /* Register a CA certificate for future use. The certificate is * expected to be in FNAME. PEM format is assume if FNAME has a * suffix of ".pem". If FNAME is NULL the list of CA files is * removed. This is a variant of http_register_tls_ca which puts the * certificate into a separate list enabled using HTTP_FLAG_TRUST_CFG. */ void http_register_cfg_ca (const char *fname) { gpg_err_code_t ec; strlist_t sl; if (!fname) { free_strlist (cfg_ca_certlist); cfg_ca_certlist = NULL; } else { /* Warn if we can't access right now, but register it anyway in case it becomes accessible later */ if ((ec = gnupg_access (fname, F_OK))) log_info (_("can't access '%s': %s\n"), fname, gpg_strerror (ec)); sl = add_to_strlist (&cfg_ca_certlist, fname); if (*sl->d && !strcmp (sl->d + strlen (sl->d) - 4, ".pem")) sl->flags = 1; } } /* Register a callback which is called every time the HTTP mode has * made a successful connection to some server. */ void http_register_netactivity_cb (void (*cb)(void)) { netactivity_cb = cb; } /* Call the netactivity callback if any. */ static void notify_netactivity (void) { if (netactivity_cb) netactivity_cb (); } /* Free the TLS session associated with SESS, if any. */ static void close_tls_session (http_session_t sess) { if (sess->tls_session) { #if HTTP_USE_NTBTLS /* FIXME!! Possibly, ntbtls_get_transport and close those streams. Somehow get SOCK to call my_socket_unref. */ ntbtls_release (sess->tls_session); #elif HTTP_USE_GNUTLS my_socket_t sock = gnutls_transport_get_ptr (sess->tls_session); my_socket_unref (sock, NULL, NULL); gnutls_deinit (sess->tls_session); if (sess->certcred) gnutls_certificate_free_credentials (sess->certcred); #endif /*HTTP_USE_GNUTLS*/ xfree (sess->servername); sess->tls_session = NULL; } } /* Release a session. Take care not to release it while it is being used by a http context object. */ static void session_unref (int lnr, http_session_t sess) { if (!sess) return; log_assert (sess->magic == HTTP_SESSION_MAGIC); sess->refcount--; if (opt_debug > 1) log_debug ("http.c:%d:session_unref: sess %p ref now %d\n", lnr, sess, sess->refcount); if (sess->refcount) return; close_tls_session (sess); sess->magic = 0xdeadbeef; xfree (sess); } #define http_session_unref(a) session_unref (__LINE__, (a)) void http_session_release (http_session_t sess) { http_session_unref (sess); } /* Create a new session object which is currently used to enable TLS * support. It may eventually allow reusing existing connections. * Valid values for FLAGS are: * HTTP_FLAG_TRUST_DEF - Use the CAs set with http_register_tls_ca * HTTP_FLAG_TRUST_SYS - Also use the CAs defined by the system * HTTP_FLAG_TRUST_CFG - Also use CAs set with http_register_cfg_ca * HTTP_FLAG_NO_CRL - Do not consult CRLs for https. */ gpg_error_t http_session_new (http_session_t *r_session, const char *intended_hostname, unsigned int flags, http_verify_cb_t verify_cb, void *verify_cb_value) { gpg_error_t err; http_session_t sess; *r_session = NULL; sess = xtrycalloc (1, sizeof *sess); if (!sess) return gpg_error_from_syserror (); sess->magic = HTTP_SESSION_MAGIC; sess->refcount = 1; sess->flags = flags; sess->verify_cb = verify_cb; sess->verify_cb_value = verify_cb_value; sess->connect_timeout = 0; #if HTTP_USE_NTBTLS { (void)intended_hostname; /* Not needed because we do not preload * certificates. */ err = ntbtls_new (&sess->tls_session, NTBTLS_CLIENT); if (err) { log_error ("ntbtls_new failed: %s\n", gpg_strerror (err)); goto leave; } } #elif HTTP_USE_GNUTLS { const char *errpos; int rc; strlist_t sl; int add_system_cas = !!(flags & HTTP_FLAG_TRUST_SYS); int is_hkps_pool; rc = gnutls_certificate_allocate_credentials (&sess->certcred); if (rc < 0) { log_error ("gnutls_certificate_allocate_credentials failed: %s\n", gnutls_strerror (rc)); err = gpg_error (GPG_ERR_GENERAL); goto leave; } is_hkps_pool = (intended_hostname && !ascii_strcasecmp (intended_hostname, get_default_keyserver (1))); /* If we are looking for the hkps pool from sks-keyservers.net, * then forcefully use its dedicated certificate authority. */ if (is_hkps_pool) { char *pemname = make_filename_try (gnupg_datadir (), "sks-keyservers.netCA.pem", NULL); if (!pemname) { err = gpg_error_from_syserror (); log_error ("setting CA from file '%s' failed: %s\n", pemname, gpg_strerror (err)); } else { rc = gnutls_certificate_set_x509_trust_file (sess->certcred, pemname, GNUTLS_X509_FMT_PEM); if (rc < 0) log_info ("setting CA from file '%s' failed: %s\n", pemname, gnutls_strerror (rc)); xfree (pemname); } if (is_hkps_pool) add_system_cas = 0; } /* Add configured certificates to the session. */ if ((flags & HTTP_FLAG_TRUST_DEF) && !is_hkps_pool) { for (sl = tls_ca_certlist; sl; sl = sl->next) { rc = gnutls_certificate_set_x509_trust_file (sess->certcred, sl->d, (sl->flags & 1)? GNUTLS_X509_FMT_PEM : GNUTLS_X509_FMT_DER); if (rc < 0) log_info ("setting CA from file '%s' failed: %s\n", sl->d, gnutls_strerror (rc)); } /* If HKP trust is requested and there are no HKP certificates * configured, also try the standard system certificates. */ if (!tls_ca_certlist) add_system_cas = 1; } /* Add system certificates to the session. */ if (add_system_cas) { #if GNUTLS_VERSION_NUMBER >= 0x030014 static int shown; rc = gnutls_certificate_set_x509_system_trust (sess->certcred); if (rc < 0) log_info ("setting system CAs failed: %s\n", gnutls_strerror (rc)); else if (!shown) { shown = 1; log_info ("number of system provided CAs: %d\n", rc); } #endif /* gnutls >= 3.0.20 */ } /* Add other configured certificates to the session. */ if ((flags & HTTP_FLAG_TRUST_CFG) && !is_hkps_pool) { for (sl = cfg_ca_certlist; sl; sl = sl->next) { rc = gnutls_certificate_set_x509_trust_file (sess->certcred, sl->d, (sl->flags & 1)? GNUTLS_X509_FMT_PEM : GNUTLS_X509_FMT_DER); if (rc < 0) log_info ("setting extra CA from file '%s' failed: %s\n", sl->d, gnutls_strerror (rc)); } } rc = gnutls_init (&sess->tls_session, GNUTLS_CLIENT); if (rc < 0) { log_error ("gnutls_init failed: %s\n", gnutls_strerror (rc)); err = gpg_error (GPG_ERR_GENERAL); goto leave; } /* A new session has the transport ptr set to (void*(-1), we need it to be NULL. */ gnutls_transport_set_ptr (sess->tls_session, NULL); rc = gnutls_priority_set_direct (sess->tls_session, "NORMAL", &errpos); if (rc < 0) { log_error ("gnutls_priority_set_direct failed at '%s': %s\n", errpos, gnutls_strerror (rc)); err = gpg_error (GPG_ERR_GENERAL); goto leave; } rc = gnutls_credentials_set (sess->tls_session, GNUTLS_CRD_CERTIFICATE, sess->certcred); if (rc < 0) { log_error ("gnutls_credentials_set failed: %s\n", gnutls_strerror (rc)); err = gpg_error (GPG_ERR_GENERAL); goto leave; } } #else /*!HTTP_USE_GNUTLS && !HTTP_USE_NTBTLS*/ { (void)intended_hostname; (void)flags; } #endif /*!HTTP_USE_GNUTLS && !HTTP_USE_NTBTLS*/ if (opt_debug > 1) log_debug ("http.c:session_new: sess %p created\n", sess); err = 0; leave: if (err) http_session_unref (sess); else *r_session = sess; return err; } /* Increment the reference count for session SESS. Passing NULL for SESS is allowed. */ http_session_t http_session_ref (http_session_t sess) { if (sess) { sess->refcount++; if (opt_debug > 1) log_debug ("http.c:session_ref: sess %p ref now %d\n", sess, sess->refcount); } return sess; } void http_session_set_log_cb (http_session_t sess, void (*cb)(http_session_t, gpg_error_t, const char *hostname, const void **certs, size_t *certlens)) { sess->cert_log_cb = cb; } /* Set the TIMEOUT in milliseconds for the connection's connect * calls. Using 0 disables the timeout. */ void http_session_set_timeout (http_session_t sess, unsigned int timeout) { sess->connect_timeout = timeout; } /* Start a HTTP retrieval and on success store at R_HD a context pointer for completing the request and to wait for the response. If HTTPHOST is not NULL it is used for the Host header instead of a Host header derived from the URL. */ gpg_error_t http_open (ctrl_t ctrl, http_t *r_hd, http_req_t reqtype, const char *url, const char *httphost, const char *auth, unsigned int flags, const char *proxy, http_session_t session, const char *srvtag, strlist_t headers) { gpg_error_t err; http_t hd; *r_hd = NULL; if (!(reqtype == HTTP_REQ_GET || reqtype == HTTP_REQ_POST)) return gpg_err_make (default_errsource, GPG_ERR_INV_ARG); /* Create the handle. */ hd = xtrycalloc (1, sizeof *hd); if (!hd) return gpg_error_from_syserror (); hd->magic = HTTP_CONTEXT_MAGIC; hd->req_type = reqtype; hd->flags = flags; hd->session = http_session_ref (session); err = parse_uri (&hd->uri, url, 0, !!(flags & HTTP_FLAG_FORCE_TLS)); if (!err) err = send_request (ctrl, hd, httphost, auth, proxy, srvtag, hd->session? hd->session->connect_timeout : 0, headers); if (err) { my_socket_unref (hd->sock, NULL, NULL); if (hd->fp_read) es_fclose (hd->fp_read); if (hd->fp_write) es_fclose (hd->fp_write); http_session_unref (hd->session); xfree (hd); } else *r_hd = hd; return err; } /* This function is useful to connect to a generic TCP service using this http abstraction layer. This has the advantage of providing service tags and an estream interface. TIMEOUT is in milliseconds. */ gpg_error_t http_raw_connect (ctrl_t ctrl, http_t *r_hd, const char *server, unsigned short port, unsigned int flags, const char *srvtag, unsigned int timeout) { gpg_error_t err = 0; http_t hd; cookie_t cookie; *r_hd = NULL; if ((flags & HTTP_FLAG_FORCE_TOR)) { int mode; if (assuan_sock_get_flag (ASSUAN_INVALID_FD, "tor-mode", &mode) || !mode) { log_error ("Tor support is not available\n"); return gpg_err_make (default_errsource, GPG_ERR_NOT_IMPLEMENTED); } /* Non-blocking connects do not work with our Tor proxy because * we can't continue the Socks protocol after the EINPROGRESS. * Disable the timeout to use a blocking connect. */ timeout = 0; } /* Create the handle. */ hd = xtrycalloc (1, sizeof *hd); if (!hd) return gpg_error_from_syserror (); hd->magic = HTTP_CONTEXT_MAGIC; hd->req_type = HTTP_REQ_OPAQUE; hd->flags = flags; /* Connect. */ { assuan_fd_t sock; err = connect_server (ctrl, server, port, hd->flags, srvtag, timeout, &sock); if (err) { xfree (hd); return err; } hd->sock = my_socket_new (sock); if (!hd->sock) { err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); xfree (hd); return err; } } /* Setup estreams for reading and writing. */ cookie = xtrycalloc (1, sizeof *cookie); if (!cookie) { err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); goto leave; } cookie->sock = my_socket_ref (hd->sock); hd->fp_write = es_fopencookie (cookie, "w", cookie_functions); if (!hd->fp_write) { err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); my_socket_unref (cookie->sock, NULL, NULL); xfree (cookie); goto leave; } hd->write_cookie = cookie; /* Cookie now owned by FP_WRITE. */ cookie = xtrycalloc (1, sizeof *cookie); if (!cookie) { err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); goto leave; } cookie->sock = my_socket_ref (hd->sock); hd->fp_read = es_fopencookie (cookie, "r", cookie_functions); if (!hd->fp_read) { err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); my_socket_unref (cookie->sock, NULL, NULL); xfree (cookie); goto leave; } hd->read_cookie = cookie; /* Cookie now owned by FP_READ. */ /* Register close notification to interlock the use of es_fclose in http_close and in user code. */ err = es_onclose (hd->fp_write, 1, fp_onclose_notification, hd); if (!err) err = es_onclose (hd->fp_read, 1, fp_onclose_notification, hd); leave: if (err) { if (hd->fp_read) es_fclose (hd->fp_read); if (hd->fp_write) es_fclose (hd->fp_write); my_socket_unref (hd->sock, NULL, NULL); xfree (hd); } else *r_hd = hd; return err; } void http_start_data (http_t hd) { if (!hd->in_data) { if (opt_debug || (hd->flags & HTTP_FLAG_LOG_RESP)) log_debug_string ("\r\n", "http.c:request-header:"); es_fputs ("\r\n", hd->fp_write); es_fflush (hd->fp_write); hd->in_data = 1; } else es_fflush (hd->fp_write); } gpg_error_t http_wait_response (http_t hd) { gpg_error_t err; cookie_t cookie; int use_tls; /* Make sure that we are in the data. */ http_start_data (hd); /* Close the write stream. Note that the reference counted socket object keeps the actual system socket open. */ cookie = hd->write_cookie; if (!cookie) return gpg_err_make (default_errsource, GPG_ERR_INTERNAL); use_tls = cookie->use_tls; es_fclose (hd->fp_write); hd->fp_write = NULL; /* The close has released the cookie and thus we better set it to NULL. */ hd->write_cookie = NULL; /* Shutdown one end of the socket is desired. As per HTTP/1.0 this is not required but some very old servers (e.g. the original pksd keyserver didn't worked without it. */ if ((hd->flags & HTTP_FLAG_SHUTDOWN)) shutdown (FD2INT (hd->sock->fd), 1); hd->in_data = 0; /* Create a new cookie and a stream for reading. */ cookie = xtrycalloc (1, sizeof *cookie); if (!cookie) return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); cookie->sock = my_socket_ref (hd->sock); cookie->session = http_session_ref (hd->session); cookie->use_tls = use_tls; hd->read_cookie = cookie; hd->fp_read = es_fopencookie (cookie, "r", cookie_functions); if (!hd->fp_read) { err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); my_socket_unref (cookie->sock, NULL, NULL); http_session_unref (cookie->session); xfree (cookie); hd->read_cookie = NULL; return err; } err = parse_response (hd); if (!err) err = es_onclose (hd->fp_read, 1, fp_onclose_notification, hd); return err; } /* Convenience function to send a request and wait for the response. Closes the handle on error. If PROXY is not NULL, this value will be used as an HTTP proxy and any enabled $http_proxy gets ignored. */ gpg_error_t http_open_document (ctrl_t ctrl, http_t *r_hd, const char *document, const char *auth, unsigned int flags, const char *proxy, http_session_t session, const char *srvtag, strlist_t headers) { gpg_error_t err; err = http_open (ctrl, r_hd, HTTP_REQ_GET, document, NULL, auth, flags, proxy, session, srvtag, headers); if (err) return err; err = http_wait_response (*r_hd); if (err) http_close (*r_hd, 0); return err; } void http_close (http_t hd, int keep_read_stream) { if (!hd) return; log_assert (hd->magic == HTTP_CONTEXT_MAGIC); /* First remove the close notifications for the streams. */ if (hd->fp_read) es_onclose (hd->fp_read, 0, fp_onclose_notification, hd); if (hd->fp_write) es_onclose (hd->fp_write, 0, fp_onclose_notification, hd); /* Now we can close the streams. */ my_socket_unref (hd->sock, NULL, NULL); if (hd->fp_read && !keep_read_stream) es_fclose (hd->fp_read); if (hd->fp_write) es_fclose (hd->fp_write); http_session_unref (hd->session); hd->magic = 0xdeadbeef; http_release_parsed_uri (hd->uri); while (hd->headers) { header_t tmp = hd->headers->next; xfree (hd->headers->value); xfree (hd->headers); hd->headers = tmp; } xfree (hd->buffer); xfree (hd); } estream_t http_get_read_ptr (http_t hd) { return hd?hd->fp_read:NULL; } estream_t http_get_write_ptr (http_t hd) { return hd?hd->fp_write:NULL; } unsigned int http_get_status_code (http_t hd) { return hd?hd->status_code:0; } /* Return information pertaining to TLS. If TLS is not in use for HD, NULL is returned. WHAT is used ask for specific information: (NULL) := Only check whether TLS is in use. Returns an unspecified string if TLS is in use. That string may even be the empty string. */ const char * http_get_tls_info (http_t hd, const char *what) { (void)what; if (!hd) return NULL; return hd->uri->use_tls? "":NULL; } static gpg_error_t parse_uri (parsed_uri_t *ret_uri, const char *uri, int no_scheme_check, int force_tls) { gpg_err_code_t ec; *ret_uri = xtrycalloc (1, sizeof **ret_uri + 2 * strlen (uri) + 1); if (!*ret_uri) return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); strcpy ((*ret_uri)->buffer, uri); strcpy ((*ret_uri)->buffer + strlen (uri) + 1, uri); (*ret_uri)->original = (*ret_uri)->buffer + strlen (uri) + 1; ec = do_parse_uri (*ret_uri, 0, no_scheme_check, force_tls); if (ec) { http_release_parsed_uri (*ret_uri); *ret_uri = NULL; } return gpg_err_make (default_errsource, ec); } /* * Parse an URI and put the result into the newly allocated RET_URI. * On success the caller must use http_release_parsed_uri() to - * releases the resources. If NO_SCHEME_CHECK is set, the function - * tries to parse the URL in the same way it would do for an HTTP - * style URI; this can for example be used for hkps or ldap schemes. - */ + * releases the resources. If the HTTP_PARSE_NO_SCHEME_CHECK flag is + * set, the function tries to parse the URL in the same way it would + * do for an HTTP style URI. */ gpg_error_t http_parse_uri (parsed_uri_t *ret_uri, const char *uri, - int no_scheme_check) + unsigned int flags) { - return parse_uri (ret_uri, uri, no_scheme_check, 0); + return parse_uri (ret_uri, uri, !!(flags & HTTP_PARSE_NO_SCHEME_CHECK), 0); } void http_release_parsed_uri (parsed_uri_t uri) { if (uri) { uri_tuple_t r, r2; for (r = uri->params; r; r = r2) { r2 = r->next; xfree (r); } for (r = uri->query; r; r = r2) { r2 = r->next; xfree (r); } xfree (uri); } } static gpg_err_code_t do_parse_uri (parsed_uri_t uri, int only_local_part, int no_scheme_check, int force_tls) { uri_tuple_t *tail; char *p, *p2, *p3, *pp; int n; p = uri->buffer; n = strlen (uri->buffer); /* Initialize all fields to an empty string or an empty list. */ uri->scheme = uri->host = uri->path = p + n; uri->port = 0; uri->params = uri->query = NULL; uri->use_tls = 0; uri->is_http = 0; uri->is_ldap = 0; uri->opaque = 0; uri->v6lit = 0; uri->onion = 0; uri->explicit_port = 0; uri->off_host = 0; uri->off_path = 0; - /* A quick validity check. */ - if (strspn (p, VALID_URI_CHARS) != n) + /* A quick validity check unless we have the opaque scheme. */ + if (strspn (p, VALID_URI_CHARS) != n + && strncmp (p, "opaque:", 7)) return GPG_ERR_BAD_URI; /* Invalid characters found. */ if (!only_local_part) { /* Find the scheme. */ if (!(p2 = strchr (p, ':')) || p2 == p) return GPG_ERR_BAD_URI; /* No scheme. */ *p2++ = 0; for (pp=p; *pp; pp++) *pp = tolower (*(unsigned char*)pp); uri->scheme = p; if (!strcmp (uri->scheme, "http") && !force_tls) { uri->port = 80; uri->is_http = 1; } else if (!strcmp (uri->scheme, "hkp") && !force_tls) { uri->port = 11371; uri->is_http = 1; } else if (!strcmp (uri->scheme, "https") || !strcmp (uri->scheme,"hkps") || (force_tls && (!strcmp (uri->scheme, "http") || !strcmp (uri->scheme,"hkp")))) { uri->port = 443; uri->is_http = 1; uri->use_tls = 1; } + else if (!strcmp (uri->scheme, "opaque")) + { + uri->opaque = 1; + uri->path = p2; + return 0; + } else if (!no_scheme_check) return GPG_ERR_INV_URI; /* Not an http style scheme. */ else if (!strcmp (uri->scheme, "ldap") && !force_tls) { uri->port = 389; uri->is_ldap = 1; } else if (!strcmp (uri->scheme, "ldaps") || (force_tls && (!strcmp (uri->scheme, "ldap")))) { uri->port = 636; uri->is_ldap = 1; uri->use_tls = 1; } else if (!strcmp (uri->scheme, "ldapi")) /* LDAP via IPC. */ { uri->port = 0; uri->is_ldap = 1; } p = p2; if (*p == '/' && p[1] == '/' ) /* There seems to be a hostname. */ { p += 2; if ((p2 = strchr (p, '/'))) { if (p2 - uri->buffer > 10000) return GPG_ERR_BAD_URI; uri->off_path = p2 - uri->buffer; *p2++ = 0; } else { n = (p - uri->buffer) + strlen (p); if (n > 10000) return GPG_ERR_BAD_URI; uri->off_path = n; } /* Check for username/password encoding */ if ((p3 = strchr (p, '@'))) { uri->auth = p; *p3++ = '\0'; p = p3; } for (pp=p; *pp; pp++) *pp = tolower (*(unsigned char*)pp); /* Handle an IPv6 literal */ if( *p == '[' && (p3=strchr( p, ']' )) ) { *p3++ = '\0'; /* worst case, uri->host should have length 0, points to \0 */ uri->host = p + 1; if (p - uri->buffer > 10000) return GPG_ERR_BAD_URI; uri->off_host = (p + 1) - uri->buffer; uri->v6lit = 1; p = p3; } else { uri->host = p; if (p - uri->buffer > 10000) return GPG_ERR_BAD_URI; uri->off_host = p - uri->buffer; } if ((p3 = strchr (p, ':'))) { *p3++ = '\0'; uri->port = atoi (p3); uri->explicit_port = 1; } if ((n = remove_escapes (uri->host)) < 0) return GPG_ERR_BAD_URI; if (n != strlen (uri->host)) return GPG_ERR_BAD_URI; /* Hostname includes a Nul. */ p = p2 ? p2 : NULL; } else if (!no_scheme_check && (uri->is_http || uri->is_ldap)) return GPG_ERR_INV_URI; /* HTTP or LDAP w/o leading double slash. */ else { uri->opaque = 1; uri->path = p; if (is_onion_address (uri->path)) uri->onion = 1; return 0; } } /* End global URI part. */ /* Parse the pathname part if any. */ if (p && *p) { /* TODO: Here we have to check params. */ /* Do we have a query part? */ if ((p2 = strchr (p, '?'))) *p2++ = 0; uri->path = p; if ((n = remove_escapes (p)) < 0) return GPG_ERR_BAD_URI; if (n != strlen (p)) return GPG_ERR_BAD_URI; /* Path includes a Nul. */ p = p2 ? p2 : NULL; /* Parse a query string if any. */ if (p && *p) { tail = &uri->query; for (;;) { uri_tuple_t elem; if ((p2 = strchr (p, '&'))) *p2++ = 0; if (!(elem = parse_tuple (p))) return GPG_ERR_BAD_URI; *tail = elem; tail = &elem->next; if (!p2) break; /* Ready. */ p = p2; } } } if (is_onion_address (uri->host)) uri->onion = 1; return 0; } /* * Remove all %xx escapes; this is done in-place. Returns: New length * of the string. */ static int remove_escapes (char *string) { int n = 0; unsigned char *p, *s; for (p = s = (unsigned char*)string; *s; s++) { if (*s == '%') { if (s[1] && s[2] && isxdigit (s[1]) && isxdigit (s[2])) { s++; *p = *s >= '0' && *s <= '9' ? *s - '0' : *s >= 'A' && *s <= 'F' ? *s - 'A' + 10 : *s - 'a' + 10; *p <<= 4; s++; *p |= *s >= '0' && *s <= '9' ? *s - '0' : *s >= 'A' && *s <= 'F' ? *s - 'A' + 10 : *s - 'a' + 10; p++; n++; } else { *p++ = *s++; if (*s) *p++ = *s++; if (*s) *p++ = *s++; if (*s) *p = 0; return -1; /* Bad URI. */ } } else { *p++ = *s; n++; } } *p = 0; /* Make sure to keep a string terminator. */ return n; } /* If SPECIAL is NULL this function escapes in forms mode. */ static size_t escape_data (char *buffer, const void *data, size_t datalen, const char *special) { int forms = !special; const unsigned char *s; size_t n = 0; if (forms) special = "%;?&="; for (s = data; datalen; s++, datalen--) { if (forms && *s == ' ') { if (buffer) *buffer++ = '+'; n++; } else if (forms && *s == '\n') { if (buffer) memcpy (buffer, "%0D%0A", 6); n += 6; } else if (forms && *s == '\r' && datalen > 1 && s[1] == '\n') { if (buffer) memcpy (buffer, "%0D%0A", 6); n += 6; s++; datalen--; } else if (strchr (VALID_URI_CHARS, *s) && !strchr (special, *s)) { if (buffer) *(unsigned char*)buffer++ = *s; n++; } else { if (buffer) { snprintf (buffer, 4, "%%%02X", *s); buffer += 3; } n += 3; } } return n; } static int insert_escapes (char *buffer, const char *string, const char *special) { return escape_data (buffer, string, strlen (string), special); } /* Allocate a new string from STRING using standard HTTP escaping as well as escaping of characters given in SPECIALS. A common pattern for SPECIALS is "%;?&=". However it depends on the needs, for example "+" and "/: often needs to be escaped too. Returns NULL on failure and sets ERRNO. If SPECIAL is NULL a dedicated forms encoding mode is used. */ char * http_escape_string (const char *string, const char *specials) { int n; char *buf; n = insert_escapes (NULL, string, specials); buf = xtrymalloc (n+1); if (buf) { insert_escapes (buf, string, specials); buf[n] = 0; } return buf; } /* Allocate a new string from {DATA,DATALEN} using standard HTTP escaping as well as escaping of characters given in SPECIALS. A common pattern for SPECIALS is "%;?&=". However it depends on the needs, for example "+" and "/: often needs to be escaped too. Returns NULL on failure and sets ERRNO. If SPECIAL is NULL a dedicated forms encoding mode is used. */ char * http_escape_data (const void *data, size_t datalen, const char *specials) { int n; char *buf; n = escape_data (NULL, data, datalen, specials); buf = xtrymalloc (n+1); if (buf) { escape_data (buf, data, datalen, specials); buf[n] = 0; } return buf; } static uri_tuple_t parse_tuple (char *string) { char *p = string; char *p2; int n; uri_tuple_t tuple; if ((p2 = strchr (p, '='))) *p2++ = 0; if ((n = remove_escapes (p)) < 0) return NULL; /* Bad URI. */ if (n != strlen (p)) return NULL; /* Name with a Nul in it. */ tuple = xtrycalloc (1, sizeof *tuple); if (!tuple) return NULL; /* Out of core. */ tuple->name = p; if (!p2) /* We have only the name, so we assume an empty value string. */ { tuple->value = p + strlen (p); tuple->valuelen = 0; tuple->no_value = 1; /* Explicitly mark that we have seen no '='. */ } else /* Name and value. */ { if ((n = remove_escapes (p2)) < 0) { xfree (tuple); return NULL; /* Bad URI. */ } tuple->value = p2; tuple->valuelen = n; } return tuple; } /* Return true if STRING is likely "hostname:port" or only "hostname". */ static int is_hostname_port (const char *string) { int colons = 0; if (!string || !*string) return 0; for (; *string; string++) { if (*string == ':') { if (colons) return 0; if (!string[1]) return 0; colons++; } else if (!colons && strchr (" \t\f\n\v_@[]/", *string)) return 0; /* Invalid characters in hostname. */ else if (colons && !digitp (string)) return 0; /* Not a digit in the port. */ } return 1; } /* * Send a HTTP request to the server * Returns 0 if the request was successful */ static gpg_error_t send_request (ctrl_t ctrl, http_t hd, const char *httphost, const char *auth, const char *proxy, const char *srvtag, unsigned int timeout, strlist_t headers) { gpg_error_t err; const char *server; char *request, *p; unsigned short port; const char *http_proxy = NULL; char *proxy_authstr = NULL; char *authstr = NULL; assuan_fd_t sock; int have_http_proxy = 0; if (hd->uri->use_tls && !hd->session) { log_error ("TLS requested but no session object provided\n"); return gpg_err_make (default_errsource, GPG_ERR_INTERNAL); } if (hd->uri->use_tls && !hd->session->tls_session) { log_error ("TLS requested but no TLS context available\n"); return gpg_err_make (default_errsource, GPG_ERR_INTERNAL); } if (opt_debug) log_debug ("Using TLS library: %s %s\n", #if HTTP_USE_NTBTLS "NTBTLS", ntbtls_check_version (NULL) #elif HTTP_USE_GNUTLS "GNUTLS", gnutls_check_version (NULL) #endif /*HTTP_USE_GNUTLS*/ ); if ((hd->flags & HTTP_FLAG_FORCE_TOR)) { int mode; if (assuan_sock_get_flag (ASSUAN_INVALID_FD, "tor-mode", &mode) || !mode) { log_error ("Tor support is not available\n"); return gpg_err_make (default_errsource, GPG_ERR_NOT_IMPLEMENTED); } /* Non-blocking connects do not work with our Tor proxy because * we can't continue the Socks protocol after the EINPROGRESS. * Disable the timeout to use a blocking connect. */ timeout = 0; } server = *hd->uri->host ? hd->uri->host : "localhost"; port = hd->uri->port ? hd->uri->port : 80; /* Try to use SNI. */ if (hd->uri->use_tls) { #if HTTP_USE_GNUTLS int rc; #endif xfree (hd->session->servername); hd->session->servername = xtrystrdup (httphost? httphost : server); if (!hd->session->servername) { err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); return err; } #if HTTP_USE_NTBTLS err = ntbtls_set_hostname (hd->session->tls_session, hd->session->servername); if (err) { log_info ("ntbtls_set_hostname failed: %s\n", gpg_strerror (err)); return err; } #elif HTTP_USE_GNUTLS rc = gnutls_server_name_set (hd->session->tls_session, GNUTLS_NAME_DNS, hd->session->servername, strlen (hd->session->servername)); if (rc < 0) log_info ("gnutls_server_name_set failed: %s\n", gnutls_strerror (rc)); #endif /*HTTP_USE_GNUTLS*/ } if ( (proxy && *proxy) || ( (hd->flags & HTTP_FLAG_TRY_PROXY) && (http_proxy = getenv (HTTP_PROXY_ENV)) && *http_proxy )) { parsed_uri_t uri; if (proxy) http_proxy = proxy; err = parse_uri (&uri, http_proxy, 0, 0); if (gpg_err_code (err) == GPG_ERR_INV_URI && is_hostname_port (http_proxy)) { /* Retry assuming a "hostname:port" string. */ char *tmpname = strconcat ("http://", http_proxy, NULL); if (tmpname && !parse_uri (&uri, tmpname, 0, 0)) err = 0; xfree (tmpname); } if (err) ; else if (!strcmp (uri->scheme, "http")) have_http_proxy = 1; else if (!strcmp (uri->scheme, "socks4") || !strcmp (uri->scheme, "socks5h")) err = gpg_err_make (default_errsource, GPG_ERR_NOT_IMPLEMENTED); else err = gpg_err_make (default_errsource, GPG_ERR_INV_URI); if (err) { log_error ("invalid HTTP proxy (%s): %s\n", http_proxy, gpg_strerror (err)); return gpg_err_make (default_errsource, GPG_ERR_CONFIGURATION); } if (uri->auth) { remove_escapes (uri->auth); proxy_authstr = make_header_line ("Proxy-Authorization: Basic ", "\r\n", uri->auth, strlen(uri->auth)); if (!proxy_authstr) { err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); http_release_parsed_uri (uri); return err; } } err = connect_server (ctrl, *uri->host ? uri->host : "localhost", uri->port ? uri->port : 80, hd->flags, NULL, timeout, &sock); http_release_parsed_uri (uri); } else { err = connect_server (ctrl, server, port, hd->flags, srvtag, timeout, &sock); } if (err) { xfree (proxy_authstr); return err; } hd->sock = my_socket_new (sock); if (!hd->sock) { xfree (proxy_authstr); return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); } if (have_http_proxy && hd->uri->use_tls) { int saved_flags; cookie_t cookie; /* Try to use the CONNECT method to proxy our TLS stream. */ request = es_bsprintf ("CONNECT %s:%hu HTTP/1.0\r\nHost: %s:%hu\r\n%s", httphost ? httphost : server, port, httphost ? httphost : server, port, proxy_authstr ? proxy_authstr : ""); xfree (proxy_authstr); proxy_authstr = NULL; if (! request) return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); if (opt_debug || (hd->flags & HTTP_FLAG_LOG_RESP)) log_debug_string (request, "http.c:request:"); cookie = xtrycalloc (1, sizeof *cookie); if (! cookie) { err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); xfree (request); return err; } cookie->sock = my_socket_ref (hd->sock); hd->write_cookie = cookie; hd->fp_write = es_fopencookie (cookie, "w", cookie_functions); if (! hd->fp_write) { err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); my_socket_unref (cookie->sock, NULL, NULL); xfree (cookie); xfree (request); hd->write_cookie = NULL; return err; } else if (es_fputs (request, hd->fp_write) || es_fflush (hd->fp_write)) err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); xfree (request); request = NULL; /* Make sure http_wait_response doesn't close the stream. */ saved_flags = hd->flags; hd->flags &= ~HTTP_FLAG_SHUTDOWN; /* Get the response. */ err = http_wait_response (hd); /* Restore flags, destroy stream. */ hd->flags = saved_flags; es_fclose (hd->fp_read); hd->fp_read = NULL; hd->read_cookie = NULL; /* Reset state. */ hd->in_data = 0; if (err) return err; if (hd->status_code != 200) { request = es_bsprintf ("CONNECT %s:%hu", httphost ? httphost : server, port); log_error (_("error accessing '%s': http status %u\n"), request ? request : "out of core", http_get_status_code (hd)); xfree (request); return gpg_error (GPG_ERR_NO_DATA); } /* We are done with the proxy, the code below will establish a * TLS session and talk directly to the target server. */ http_proxy = NULL; } #if HTTP_USE_NTBTLS if (hd->uri->use_tls) { estream_t in, out; my_socket_ref (hd->sock); /* Until we support send/recv in estream under Windows we need * to use es_fopencookie. */ # ifdef HAVE_W32_SYSTEM in = es_fopencookie ((void*)(unsigned int)hd->sock->fd, "rb", simple_cookie_functions); # else in = es_fdopen_nc (hd->sock->fd, "rb"); # endif if (!in) { err = gpg_error_from_syserror (); xfree (proxy_authstr); return err; } # ifdef HAVE_W32_SYSTEM out = es_fopencookie ((void*)(unsigned int)hd->sock->fd, "wb", simple_cookie_functions); # else out = es_fdopen_nc (hd->sock->fd, "wb"); # endif if (!out) { err = gpg_error_from_syserror (); es_fclose (in); xfree (proxy_authstr); return err; } err = ntbtls_set_transport (hd->session->tls_session, in, out); if (err) { log_info ("TLS set_transport failed: %s <%s>\n", gpg_strerror (err), gpg_strsource (err)); xfree (proxy_authstr); return err; } if (hd->session->verify_cb) { err = ntbtls_set_verify_cb (hd->session->tls_session, my_ntbtls_verify_cb, hd); if (err) { log_error ("ntbtls_set_verify_cb failed: %s\n", gpg_strerror (err)); xfree (proxy_authstr); return err; } } while ((err = ntbtls_handshake (hd->session->tls_session))) { #if NTBTLS_VERSION_NUMBER >= 0x000200 unsigned int tlevel, ttype; const char *s = ntbtls_get_last_alert (hd->session->tls_session, &tlevel, &ttype); if (s) log_info ("TLS alert: %s (%u.%u)\n", s, tlevel, ttype); #endif switch (err) { default: log_info ("TLS handshake failed: %s <%s>\n", gpg_strerror (err), gpg_strsource (err)); xfree (proxy_authstr); return err; } } hd->session->verify.done = 0; /* Try the available verify callbacks until one returns success * or a real error. Note that NTBTLS does the verification * during the handshake via */ err = 0; /* Fixme check that the CB has been called. */ if (hd->session->verify_cb && gpg_err_source (err) == GPG_ERR_SOURCE_DIRMNGR && gpg_err_code (err) == GPG_ERR_NOT_IMPLEMENTED) err = hd->session->verify_cb (hd->session->verify_cb_value, hd, hd->session, (hd->flags | hd->session->flags), hd->session->tls_session); if (tls_callback && gpg_err_source (err) == GPG_ERR_SOURCE_DIRMNGR && gpg_err_code (err) == GPG_ERR_NOT_IMPLEMENTED) err = tls_callback (hd, hd->session, 0); if (gpg_err_source (err) == GPG_ERR_SOURCE_DIRMNGR && gpg_err_code (err) == GPG_ERR_NOT_IMPLEMENTED) err = http_verify_server_credentials (hd->session); if (err) { log_info ("TLS connection authentication failed: %s <%s>\n", gpg_strerror (err), gpg_strsource (err)); xfree (proxy_authstr); return err; } } #elif HTTP_USE_GNUTLS if (hd->uri->use_tls) { int rc; my_socket_ref (hd->sock); gnutls_transport_set_ptr (hd->session->tls_session, hd->sock); gnutls_transport_set_pull_function (hd->session->tls_session, my_gnutls_read); gnutls_transport_set_push_function (hd->session->tls_session, my_gnutls_write); handshake_again: do { rc = gnutls_handshake (hd->session->tls_session); } while (rc == GNUTLS_E_INTERRUPTED || rc == GNUTLS_E_AGAIN); if (rc < 0) { if (rc == GNUTLS_E_WARNING_ALERT_RECEIVED || rc == GNUTLS_E_FATAL_ALERT_RECEIVED) { gnutls_alert_description_t alertno; const char *alertstr; alertno = gnutls_alert_get (hd->session->tls_session); alertstr = gnutls_alert_get_name (alertno); log_info ("TLS handshake %s: %s (alert %d)\n", rc == GNUTLS_E_WARNING_ALERT_RECEIVED ? "warning" : "failed", alertstr, (int)alertno); if (alertno == GNUTLS_A_UNRECOGNIZED_NAME && server) log_info (" (sent server name '%s')\n", server); if (rc == GNUTLS_E_WARNING_ALERT_RECEIVED) goto handshake_again; } else log_info ("TLS handshake failed: %s\n", gnutls_strerror (rc)); xfree (proxy_authstr); return gpg_err_make (default_errsource, GPG_ERR_NETWORK); } hd->session->verify.done = 0; if (tls_callback) err = tls_callback (hd, hd->session, 0); else err = http_verify_server_credentials (hd->session); if (err) { log_info ("TLS connection authentication failed: %s\n", gpg_strerror (err)); xfree (proxy_authstr); return err; } } #endif /*HTTP_USE_GNUTLS*/ if (auth || hd->uri->auth) { char *myauth; if (auth) { myauth = xtrystrdup (auth); if (!myauth) { xfree (proxy_authstr); return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); } remove_escapes (myauth); } else { remove_escapes (hd->uri->auth); myauth = hd->uri->auth; } authstr = make_header_line ("Authorization: Basic ", "\r\n", myauth, strlen (myauth)); if (auth) xfree (myauth); if (!authstr) { xfree (proxy_authstr); return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); } } p = build_rel_path (hd->uri); if (!p) { xfree (authstr); xfree (proxy_authstr); return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); } if (http_proxy && *http_proxy) { request = es_bsprintf ("%s %s://%s:%hu%s%s HTTP/1.0\r\n%s%s", hd->req_type == HTTP_REQ_GET ? "GET" : hd->req_type == HTTP_REQ_HEAD ? "HEAD" : hd->req_type == HTTP_REQ_POST ? "POST" : "OOPS", hd->uri->use_tls? "https" : "http", httphost? httphost : server, port, *p == '/' ? "" : "/", p, authstr ? authstr : "", proxy_authstr ? proxy_authstr : ""); } else { char portstr[35]; if (port == (hd->uri->use_tls? 443 : 80)) *portstr = 0; else snprintf (portstr, sizeof portstr, ":%u", port); request = es_bsprintf ("%s %s%s HTTP/1.0\r\nHost: %s%s\r\n%s", hd->req_type == HTTP_REQ_GET ? "GET" : hd->req_type == HTTP_REQ_HEAD ? "HEAD" : hd->req_type == HTTP_REQ_POST ? "POST" : "OOPS", *p == '/' ? "" : "/", p, httphost? httphost : server, portstr, authstr? authstr:""); } xfree (p); if (!request) { err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); xfree (authstr); xfree (proxy_authstr); return err; } if (opt_debug || (hd->flags & HTTP_FLAG_LOG_RESP)) log_debug_string (request, "http.c:request:"); /* First setup estream so that we can write even the first line using estream. This is also required for the sake of gnutls. */ { cookie_t cookie; cookie = xtrycalloc (1, sizeof *cookie); if (!cookie) { err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); goto leave; } cookie->sock = my_socket_ref (hd->sock); hd->write_cookie = cookie; cookie->use_tls = hd->uri->use_tls; cookie->session = http_session_ref (hd->session); hd->fp_write = es_fopencookie (cookie, "w", cookie_functions); if (!hd->fp_write) { err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); my_socket_unref (cookie->sock, NULL, NULL); xfree (cookie); hd->write_cookie = NULL; } else if (es_fputs (request, hd->fp_write) || es_fflush (hd->fp_write)) err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); else err = 0; if (!err) { for (;headers; headers=headers->next) { if (opt_debug || (hd->flags & HTTP_FLAG_LOG_RESP)) log_debug_string (headers->d, "http.c:request-header:"); if ((es_fputs (headers->d, hd->fp_write) || es_fflush (hd->fp_write)) || (es_fputs("\r\n",hd->fp_write) || es_fflush(hd->fp_write))) { err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); break; } } } } leave: es_free (request); xfree (authstr); xfree (proxy_authstr); return err; } /* * Build the relative path from the parsed URI. Minimal * implementation. May return NULL in case of memory failure; errno * is then set accordingly. */ static char * build_rel_path (parsed_uri_t uri) { uri_tuple_t r; char *rel_path, *p; int n; /* Count the needed space. */ n = insert_escapes (NULL, uri->path, "%;?&"); /* TODO: build params. */ for (r = uri->query; r; r = r->next) { n++; /* '?'/'&' */ n += insert_escapes (NULL, r->name, "%;?&="); if (!r->no_value) { n++; /* '=' */ n += insert_escapes (NULL, r->value, "%;?&="); } } n++; /* Now allocate and copy. */ p = rel_path = xtrymalloc (n); if (!p) return NULL; n = insert_escapes (p, uri->path, "%;?&"); p += n; /* TODO: add params. */ for (r = uri->query; r; r = r->next) { *p++ = r == uri->query ? '?' : '&'; n = insert_escapes (p, r->name, "%;?&="); p += n; if (!r->no_value) { *p++ = '='; /* TODO: Use valuelen. */ n = insert_escapes (p, r->value, "%;?&="); p += n; } } *p = 0; return rel_path; } /* Transform a header name into a standard capitalized format; e.g. "Content-Type". Conversion stops at the colon. As usual we don't use the localized versions of ctype.h. */ static void capitalize_header_name (char *name) { int first = 1; for (; *name && *name != ':'; name++) { if (*name == '-') first = 1; else if (first) { if (*name >= 'a' && *name <= 'z') *name = *name - 'a' + 'A'; first = 0; } else if (*name >= 'A' && *name <= 'Z') *name = *name - 'A' + 'a'; } } /* Store an HTTP header line in LINE away. Line continuation is supported as well as merging of headers with the same name. This function may modify LINE. */ static gpg_err_code_t store_header (http_t hd, char *line) { size_t n; char *p, *value; header_t h; n = strlen (line); if (n && line[n-1] == '\n') { line[--n] = 0; if (n && line[n-1] == '\r') line[--n] = 0; } if (!n) /* we are never called to hit this. */ return GPG_ERR_BUG; if (*line == ' ' || *line == '\t') { /* Continuation. This won't happen too often as it is not recommended. We use a straightforward implementation. */ if (!hd->headers) return GPG_ERR_PROTOCOL_VIOLATION; n += strlen (hd->headers->value); p = xtrymalloc (n+1); if (!p) return gpg_err_code_from_syserror (); strcpy (stpcpy (p, hd->headers->value), line); xfree (hd->headers->value); hd->headers->value = p; return 0; } capitalize_header_name (line); p = strchr (line, ':'); if (!p) return GPG_ERR_PROTOCOL_VIOLATION; *p++ = 0; while (*p == ' ' || *p == '\t') p++; value = p; for (h=hd->headers; h; h = h->next) if ( !strcmp (h->name, line) ) break; if (h) { /* We have already seen a line with that name. Thus we assume * it is a comma separated list and merge them. */ p = strconcat (h->value, ",", value, NULL); if (!p) return gpg_err_code_from_syserror (); xfree (h->value); h->value = p; return 0; } /* Append a new header. */ h = xtrymalloc (sizeof *h + strlen (line)); if (!h) return gpg_err_code_from_syserror (); strcpy (h->name, line); h->value = xtrymalloc (strlen (value)+1); if (!h->value) { xfree (h); return gpg_err_code_from_syserror (); } strcpy (h->value, value); h->next = hd->headers; hd->headers = h; return 0; } /* Return the header NAME from the last response. The returned value is valid as along as HD has not been closed and no other request has been send. If the header was not found, NULL is returned. NAME must be canonicalized, that is the first letter of each dash delimited part must be uppercase and all other letters lowercase. */ const char * http_get_header (http_t hd, const char *name) { header_t h; for (h=hd->headers; h; h = h->next) if ( !strcmp (h->name, name) ) return h->value; return NULL; } /* Return a newly allocated and NULL terminated array with pointers to header names. The array must be released with xfree() and its content is only values as long as no other request has been send. */ const char ** http_get_header_names (http_t hd) { const char **array; size_t n; header_t h; for (n=0, h = hd->headers; h; h = h->next) n++; array = xtrycalloc (n+1, sizeof *array); if (array) { for (n=0, h = hd->headers; h; h = h->next) array[n++] = h->name; } return array; } /* * Parse the response from a server. * Returns: Errorcode and sets some files in the handle */ static gpg_err_code_t parse_response (http_t hd) { char *line, *p, *p2; size_t maxlen, len; cookie_t cookie = hd->read_cookie; const char *s; /* Delete old header lines. */ while (hd->headers) { header_t tmp = hd->headers->next; xfree (hd->headers->value); xfree (hd->headers); hd->headers = tmp; } /* Wait for the status line. */ do { maxlen = MAX_LINELEN; len = es_read_line (hd->fp_read, &hd->buffer, &hd->buffer_size, &maxlen); line = hd->buffer; if (!line) return gpg_err_code_from_syserror (); /* Out of core. */ if (!maxlen) return GPG_ERR_TRUNCATED; /* Line has been truncated. */ if (!len) return GPG_ERR_EOF; if (opt_debug || (hd->flags & HTTP_FLAG_LOG_RESP)) log_debug_string (line, "http.c:response:\n"); } while (!*line); if ((p = strchr (line, '/'))) *p++ = 0; if (!p || strcmp (line, "HTTP")) return 0; /* Assume http 0.9. */ if ((p2 = strpbrk (p, " \t"))) { *p2++ = 0; p2 += strspn (p2, " \t"); } if (!p2) return 0; /* Also assume http 0.9. */ p = p2; /* TODO: Add HTTP version number check. */ if ((p2 = strpbrk (p, " \t"))) *p2++ = 0; if (!isdigit ((unsigned int)p[0]) || !isdigit ((unsigned int)p[1]) || !isdigit ((unsigned int)p[2]) || p[3]) { /* Malformed HTTP status code - assume http 0.9. */ hd->is_http_0_9 = 1; hd->status_code = 200; return 0; } hd->status_code = atoi (p); /* Skip all the header lines and wait for the empty line. */ do { maxlen = MAX_LINELEN; len = es_read_line (hd->fp_read, &hd->buffer, &hd->buffer_size, &maxlen); line = hd->buffer; if (!line) return gpg_err_code_from_syserror (); /* Out of core. */ /* Note, that we can silently ignore truncated lines. */ if (!len) return GPG_ERR_EOF; /* Trim line endings of empty lines. */ if ((*line == '\r' && line[1] == '\n') || *line == '\n') *line = 0; if (opt_debug || (hd->flags & HTTP_FLAG_LOG_RESP)) log_info ("http.c:RESP: '%.*s'\n", (int)strlen(line)-(*line&&line[1]?2:0),line); if (*line) { gpg_err_code_t ec = store_header (hd, line); if (ec) return ec; } } while (len && *line); cookie->content_length_valid = 0; if (!(hd->flags & HTTP_FLAG_IGNORE_CL)) { s = http_get_header (hd, "Content-Length"); if (s) { cookie->content_length_valid = 1; cookie->content_length = string_to_u64 (s); } } return 0; } #if 0 static int start_server () { struct sockaddr_in mya; struct sockaddr_in peer; int fd, client; fd_set rfds; int addrlen; int i; if ((fd = socket (AF_INET, SOCK_STREAM, 0)) == -1) { log_error ("socket() failed: %s\n", strerror (errno)); return -1; } i = 1; if (setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, (byte *) & i, sizeof (i))) log_info ("setsockopt(SO_REUSEADDR) failed: %s\n", strerror (errno)); mya.sin_family = AF_INET; memset (&mya.sin_addr, 0, sizeof (mya.sin_addr)); mya.sin_port = htons (11371); if (bind (fd, (struct sockaddr *) &mya, sizeof (mya))) { log_error ("bind to port 11371 failed: %s\n", strerror (errno)); sock_close (fd); return -1; } if (listen (fd, 5)) { log_error ("listen failed: %s\n", strerror (errno)); sock_close (fd); return -1; } for (;;) { FD_ZERO (&rfds); FD_SET (fd, &rfds); if (my_select (fd + 1, &rfds, NULL, NULL, NULL) <= 0) continue; /* ignore any errors */ if (!FD_ISSET (fd, &rfds)) continue; addrlen = sizeof peer; client = my_accept (fd, (struct sockaddr *) &peer, &addrlen); if (client == -1) continue; /* oops */ log_info ("connect from %s\n", inet_ntoa (peer.sin_addr)); fflush (stdout); fflush (stderr); if (!fork ()) { int c; FILE *fp; fp = fdopen (client, "r"); while ((c = getc (fp)) != EOF) putchar (c); fclose (fp); exit (0); } sock_close (client); } return 0; } #endif /* Return true if SOCKS shall be used. This is the case if tor_mode * is enabled and the desired address is not the loopback address. * This function is basically a copy of the same internal function in * Libassuan. */ static int use_socks (struct sockaddr_storage *addr) { int mode; if (assuan_sock_get_flag (ASSUAN_INVALID_FD, "tor-mode", &mode) || !mode) return 0; /* Not in Tor mode. */ else if (addr->ss_family == AF_INET6) { struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *)addr; const unsigned char *s; int i; s = (unsigned char *)&addr_in6->sin6_addr.s6_addr; if (s[15] != 1) return 1; /* Last octet is not 1 - not the loopback address. */ for (i=0; i < 15; i++, s++) if (*s) return 1; /* Non-zero octet found - not the loopback address. */ return 0; /* This is the loopback address. */ } else if (addr->ss_family == AF_INET) { struct sockaddr_in *addr_in = (struct sockaddr_in *)addr; if (*(unsigned char*)&addr_in->sin_addr.s_addr == 127) return 0; /* Loopback (127.0.0.0/8) */ return 1; } else return 0; } /* Wrapper around assuan_sock_new which takes the domain from an * address parameter. */ static assuan_fd_t my_sock_new_for_addr (struct sockaddr_storage *addr, int type, int proto) { int domain; if (use_socks (addr)) { /* Libassaun always uses 127.0.0.1 to connect to the socks * server (i.e. the Tor daemon). */ domain = AF_INET; } else domain = addr->ss_family; return assuan_sock_new (domain, type, proto); } /* Call WSAGetLastError and map it to a libgpg-error. */ #ifdef HAVE_W32_SYSTEM static gpg_error_t my_wsagetlasterror (void) { int wsaerr; gpg_err_code_t ec; wsaerr = WSAGetLastError (); switch (wsaerr) { case WSAENOTSOCK: ec = GPG_ERR_EINVAL; break; case WSAEWOULDBLOCK: ec = GPG_ERR_EAGAIN; break; case ERROR_BROKEN_PIPE: ec = GPG_ERR_EPIPE; break; case WSANOTINITIALISED: ec = GPG_ERR_ENOSYS; break; case WSAENOBUFS: ec = GPG_ERR_ENOBUFS; break; case WSAEMSGSIZE: ec = GPG_ERR_EMSGSIZE; break; case WSAECONNREFUSED: ec = GPG_ERR_ECONNREFUSED; break; case WSAEISCONN: ec = GPG_ERR_EISCONN; break; case WSAEALREADY: ec = GPG_ERR_EALREADY; break; case WSAETIMEDOUT: ec = GPG_ERR_ETIMEDOUT; break; default: ec = GPG_ERR_EIO; break; } return gpg_err_make (default_errsource, ec); } #endif /*HAVE_W32_SYSTEM*/ /* Connect SOCK and return GPG_ERR_ETIMEOUT if a connection could not * be established within TIMEOUT milliseconds. 0 indicates the * system's default timeout. The other args are the usual connect * args. On success 0 is returned, on timeout GPG_ERR_ETIMEDOUT, and * another error code for other errors. On timeout the caller needs * to close the socket as soon as possible to stop an ongoing * handshake. * * This implementation is for well-behaving systems; see Stevens, * Network Programming, 2nd edition, Vol 1, 15.4. */ static gpg_error_t connect_with_timeout (assuan_fd_t sock, struct sockaddr *addr, int addrlen, unsigned int timeout) { gpg_error_t err; int syserr; socklen_t slen; fd_set rset, wset; struct timeval tval; int n; #ifndef HAVE_W32_SYSTEM int oflags; # define RESTORE_BLOCKING() do { \ fcntl (sock, F_SETFL, oflags); \ } while (0) #else /*HAVE_W32_SYSTEM*/ # define RESTORE_BLOCKING() do { \ unsigned long along = 0; \ ioctlsocket (FD2INT (sock), FIONBIO, &along); \ } while (0) #endif /*HAVE_W32_SYSTEM*/ if (!timeout) { /* Shortcut. */ if (assuan_sock_connect (sock, addr, addrlen)) err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); else err = 0; return err; } /* Switch the socket into non-blocking mode. */ #ifdef HAVE_W32_SYSTEM { unsigned long along = 1; if (ioctlsocket (FD2INT (sock), FIONBIO, &along)) return my_wsagetlasterror (); } #else oflags = fcntl (sock, F_GETFL, 0); if (fcntl (sock, F_SETFL, oflags | O_NONBLOCK)) return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); #endif /* Do the connect. */ if (!assuan_sock_connect (sock, addr, addrlen)) { /* Immediate connect. Restore flags. */ RESTORE_BLOCKING (); return 0; /* Success. */ } err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); if (gpg_err_code (err) != GPG_ERR_EINPROGRESS #ifdef HAVE_W32_SYSTEM && gpg_err_code (err) != GPG_ERR_EAGAIN #endif ) { RESTORE_BLOCKING (); return err; } FD_ZERO (&rset); FD_SET (FD2INT (sock), &rset); wset = rset; tval.tv_sec = timeout / 1000; tval.tv_usec = (timeout % 1000) * 1000; n = my_select (FD2INT(sock)+1, &rset, &wset, NULL, &tval); if (n < 0) { err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); RESTORE_BLOCKING (); return err; } if (!n) { /* Timeout: We do not restore the socket flags on timeout * because the caller is expected to close the socket. */ return gpg_err_make (default_errsource, GPG_ERR_ETIMEDOUT); } if (!FD_ISSET (sock, &rset) && !FD_ISSET (sock, &wset)) { /* select misbehaved. */ return gpg_err_make (default_errsource, GPG_ERR_SYSTEM_BUG); } slen = sizeof (syserr); if (getsockopt (FD2INT(sock), SOL_SOCKET, SO_ERROR, (void*)&syserr, &slen) < 0) { /* Assume that this is Solaris which returns the error in ERRNO. */ err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); } else if (syserr) err = gpg_err_make (default_errsource, gpg_err_code_from_errno (syserr)); else err = 0; /* Connected. */ RESTORE_BLOCKING (); return err; #undef RESTORE_BLOCKING } /* Actually connect to a server. On success 0 is returned and the * file descriptor for the socket is stored at R_SOCK; on error an * error code is returned and ASSUAN_INVALID_FD is stored at R_SOCK. * TIMEOUT is the connect timeout in milliseconds. Note that the * function tries to connect to all known addresses and the timeout is * for each one. */ static gpg_error_t connect_server (ctrl_t ctrl, const char *server, unsigned short port, unsigned int flags, const char *srvtag, unsigned int timeout, assuan_fd_t *r_sock) { gpg_error_t err; assuan_fd_t sock = ASSUAN_INVALID_FD; unsigned int srvcount = 0; int hostfound = 0; int anyhostaddr = 0; int srv, connected, v4_valid, v6_valid; gpg_error_t last_err = 0; struct srventry *serverlist = NULL; *r_sock = ASSUAN_INVALID_FD; #if defined(HAVE_W32_SYSTEM) && !defined(HTTP_NO_WSASTARTUP) init_sockets (); #endif /*Windows*/ check_inet_support (&v4_valid, &v6_valid); /* Onion addresses require special treatment. */ if (is_onion_address (server)) { #ifdef ASSUAN_SOCK_TOR if (opt_debug) log_debug ("http.c:connect_server:onion: name='%s' port=%hu\n", server, port); sock = assuan_sock_connect_byname (server, port, 0, NULL, ASSUAN_SOCK_TOR); if (sock == ASSUAN_INVALID_FD) { err = gpg_err_make (default_errsource, (errno == EHOSTUNREACH)? GPG_ERR_UNKNOWN_HOST : gpg_err_code_from_syserror ()); log_error ("can't connect to '%s': %s\n", server, gpg_strerror (err)); return err; } notify_netactivity (); *r_sock = sock; return 0; #else /*!ASSUAN_SOCK_TOR*/ err = gpg_err_make (default_errsource, GPG_ERR_ENETUNREACH); return ASSUAN_INVALID_FD; #endif /*!HASSUAN_SOCK_TOR*/ } /* Do the SRV thing */ if (srvtag) { err = get_dns_srv (ctrl, server, srvtag, NULL, &serverlist, &srvcount); if (err) log_info ("getting '%s' SRV for '%s' failed: %s\n", srvtag, server, gpg_strerror (err)); /* Note that on error SRVCOUNT is zero. */ err = 0; } if (!serverlist) { /* Either we're not using SRV, or the SRV lookup failed. Make up a fake SRV record. */ serverlist = xtrycalloc (1, sizeof *serverlist); if (!serverlist) return gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); serverlist->port = port; strncpy (serverlist->target, server, DIMof (struct srventry, target)); serverlist->target[DIMof (struct srventry, target)-1] = '\0'; srvcount = 1; } connected = 0; for (srv=0; srv < srvcount && !connected; srv++) { dns_addrinfo_t aibuf, ai; if (opt_debug) log_debug ("http.c:connect_server: trying name='%s' port=%hu\n", serverlist[srv].target, port); err = resolve_dns_name (ctrl, serverlist[srv].target, port, 0, SOCK_STREAM, &aibuf, NULL); if (err) { log_info ("resolving '%s' failed: %s\n", serverlist[srv].target, gpg_strerror (err)); last_err = err; continue; /* Not found - try next one. */ } hostfound = 1; for (ai = aibuf; ai && !connected; ai = ai->next) { if (ai->family == AF_INET && ((flags & HTTP_FLAG_IGNORE_IPv4) || !v4_valid)) continue; if (ai->family == AF_INET6 && ((flags & HTTP_FLAG_IGNORE_IPv6) || !v6_valid)) continue; if (sock != ASSUAN_INVALID_FD) assuan_sock_close (sock); sock = my_sock_new_for_addr (ai->addr, ai->socktype, ai->protocol); if (sock == ASSUAN_INVALID_FD) { if (errno == EAFNOSUPPORT) { if (ai->family == AF_INET) v4_valid = 0; if (ai->family == AF_INET6) v6_valid = 0; continue; } err = gpg_err_make (default_errsource, gpg_err_code_from_syserror ()); log_error ("error creating socket: %s\n", gpg_strerror (err)); free_dns_addrinfo (aibuf); xfree (serverlist); return err; } anyhostaddr = 1; err = connect_with_timeout (sock, (struct sockaddr *)ai->addr, ai->addrlen, timeout); if (err) { last_err = err; } else { connected = 1; notify_netactivity (); } } free_dns_addrinfo (aibuf); } xfree (serverlist); if (!connected) { if (!hostfound) log_error ("can't connect to '%s': %s\n", server, "host not found"); else if (!anyhostaddr) log_error ("can't connect to '%s': %s\n", server, "no IP address for host"); else { #ifdef HAVE_W32_SYSTEM log_error ("can't connect to '%s': ec=%d\n", server, (int)WSAGetLastError()); #else log_error ("can't connect to '%s': %s\n", server, gpg_strerror (last_err)); #endif } err = last_err? last_err : gpg_err_make (default_errsource, GPG_ERR_UNKNOWN_HOST); if (sock != ASSUAN_INVALID_FD) assuan_sock_close (sock); return err; } *r_sock = sock; return 0; } /* Helper to read from a socket. This handles npth things and * EINTR. */ static gpgrt_ssize_t read_server (assuan_fd_t sock, void *buffer, size_t size) { int nread; do { #ifdef HAVE_W32_SYSTEM /* Under Windows we need to use recv for a socket. */ # if defined(USE_NPTH) npth_unprotect (); # endif nread = recv (FD2INT (sock), buffer, size, 0); # if defined(USE_NPTH) npth_protect (); # endif #else /*!HAVE_W32_SYSTEM*/ # ifdef USE_NPTH nread = npth_read (sock, buffer, size); # else nread = read (sock, buffer, size); # endif #endif /*!HAVE_W32_SYSTEM*/ } while (nread == -1 && errno == EINTR); return nread; } static gpg_error_t write_server (assuan_fd_t sock, const char *data, size_t length) { int nleft; int nwritten; nleft = length; while (nleft > 0) { #if defined(HAVE_W32_SYSTEM) # if defined(USE_NPTH) npth_unprotect (); # endif nwritten = send (FD2INT (sock), data, nleft, 0); # if defined(USE_NPTH) npth_protect (); # endif if ( nwritten == SOCKET_ERROR ) { log_info ("network write failed: ec=%d\n", (int)WSAGetLastError ()); return gpg_error (GPG_ERR_NETWORK); } #else /*!HAVE_W32_SYSTEM*/ # ifdef USE_NPTH nwritten = npth_write (sock, data, nleft); # else nwritten = write (sock, data, nleft); # endif if (nwritten == -1) { if (errno == EINTR) continue; if (errno == EAGAIN) { struct timeval tv; tv.tv_sec = 0; tv.tv_usec = 50000; my_select (0, NULL, NULL, NULL, &tv); continue; } log_info ("network write failed: %s\n", strerror (errno)); return gpg_error_from_syserror (); } #endif /*!HAVE_W32_SYSTEM*/ nleft -= nwritten; data += nwritten; } return 0; } /* Read handler for estream. */ static gpgrt_ssize_t cookie_read (void *cookie, void *buffer, size_t size) { cookie_t c = cookie; int nread; if (c->content_length_valid) { if (!c->content_length) return 0; /* EOF */ if (c->content_length < size) size = c->content_length; } #if HTTP_USE_NTBTLS if (c->use_tls && c->session && c->session->tls_session) { estream_t in, out; ntbtls_get_stream (c->session->tls_session, &in, &out); nread = es_fread (buffer, 1, size, in); if (opt_debug) log_debug ("TLS network read: %d/%zu\n", nread, size); } else #elif HTTP_USE_GNUTLS if (c->use_tls && c->session && c->session->tls_session) { again: nread = gnutls_record_recv (c->session->tls_session, buffer, size); if (nread < 0) { if (nread == GNUTLS_E_INTERRUPTED) goto again; if (nread == GNUTLS_E_AGAIN) { struct timeval tv; tv.tv_sec = 0; tv.tv_usec = 50000; my_select (0, NULL, NULL, NULL, &tv); goto again; } if (nread == GNUTLS_E_REHANDSHAKE) goto again; /* A client is allowed to just ignore this request. */ if (nread == GNUTLS_E_PREMATURE_TERMINATION) { /* The server terminated the connection. Close the TLS session, and indicate EOF using a short read. */ close_tls_session (c->session); return 0; } log_info ("TLS network read failed: %s\n", gnutls_strerror (nread)); gpg_err_set_errno (EIO); return -1; } } else #endif /*HTTP_USE_GNUTLS*/ { nread = read_server (c->sock->fd, buffer, size); } if (c->content_length_valid && nread > 0) { if (nread < c->content_length) c->content_length -= nread; else c->content_length = 0; } return (gpgrt_ssize_t)nread; } /* Write handler for estream. */ static gpgrt_ssize_t cookie_write (void *cookie, const void *buffer_arg, size_t size) { const char *buffer = buffer_arg; cookie_t c = cookie; int nwritten = 0; #if HTTP_USE_NTBTLS if (c->use_tls && c->session && c->session->tls_session) { estream_t in, out; ntbtls_get_stream (c->session->tls_session, &in, &out); if (size == 0) es_fflush (out); else nwritten = es_fwrite (buffer, 1, size, out); if (opt_debug) log_debug ("TLS network write: %d/%zu\n", nwritten, size); } else #elif HTTP_USE_GNUTLS if (c->use_tls && c->session && c->session->tls_session) { int nleft = size; while (nleft > 0) { nwritten = gnutls_record_send (c->session->tls_session, buffer, nleft); if (nwritten <= 0) { if (nwritten == GNUTLS_E_INTERRUPTED) continue; if (nwritten == GNUTLS_E_AGAIN) { struct timeval tv; tv.tv_sec = 0; tv.tv_usec = 50000; my_select (0, NULL, NULL, NULL, &tv); continue; } log_info ("TLS network write failed: %s\n", gnutls_strerror (nwritten)); gpg_err_set_errno (EIO); return -1; } nleft -= nwritten; buffer += nwritten; } } else #endif /*HTTP_USE_GNUTLS*/ { if ( write_server (c->sock->fd, buffer, size) ) { gpg_err_set_errno (EIO); nwritten = -1; } else nwritten = size; } return (gpgrt_ssize_t)nwritten; } #if defined(HAVE_W32_SYSTEM) && defined(HTTP_USE_NTBTLS) static gpgrt_ssize_t simple_cookie_read (void *cookie, void *buffer, size_t size) { assuan_fd_t sock = (assuan_fd_t)cookie; return read_server (sock, buffer, size); } static gpgrt_ssize_t simple_cookie_write (void *cookie, const void *buffer_arg, size_t size) { assuan_fd_t sock = (assuan_fd_t)cookie; const char *buffer = buffer_arg; int nwritten; if (write_server (sock, buffer, size)) { gpg_err_set_errno (EIO); nwritten = -1; } else nwritten = size; return (gpgrt_ssize_t)nwritten; } #endif /*HAVE_W32_SYSTEM*/ #ifdef HTTP_USE_GNUTLS /* Wrapper for gnutls_bye used by my_socket_unref. */ static void send_gnutls_bye (void *opaque) { tls_session_t tls_session = opaque; int ret; again: do ret = gnutls_bye (tls_session, GNUTLS_SHUT_RDWR); while (ret == GNUTLS_E_INTERRUPTED); if (ret == GNUTLS_E_AGAIN) { struct timeval tv; tv.tv_sec = 0; tv.tv_usec = 50000; my_select (0, NULL, NULL, NULL, &tv); goto again; } } #endif /*HTTP_USE_GNUTLS*/ /* Close handler for estream. */ static int cookie_close (void *cookie) { cookie_t c = cookie; if (!c) return 0; #if HTTP_USE_NTBTLS if (c->use_tls && c->session && c->session->tls_session) { /* FIXME!! Possibly call ntbtls_close_notify for close of write stream. */ my_socket_unref (c->sock, NULL, NULL); } else #elif HTTP_USE_GNUTLS if (c->use_tls && c->session && c->session->tls_session) my_socket_unref (c->sock, send_gnutls_bye, c->session->tls_session); else #endif /*HTTP_USE_GNUTLS*/ if (c->sock) my_socket_unref (c->sock, NULL, NULL); if (c->session) http_session_unref (c->session); xfree (c); return 0; } /* Verify the credentials of the server. Returns 0 on success and store the result in the session object. */ gpg_error_t http_verify_server_credentials (http_session_t sess) { #if HTTP_USE_GNUTLS static const char errprefix[] = "TLS verification of peer failed"; int rc; unsigned int status; const char *hostname; const gnutls_datum_t *certlist; unsigned int certlistlen; gnutls_x509_crt_t cert; gpg_error_t err = 0; sess->verify.done = 1; sess->verify.status = 0; sess->verify.rc = GNUTLS_E_CERTIFICATE_ERROR; if (gnutls_certificate_type_get (sess->tls_session) != GNUTLS_CRT_X509) { log_error ("%s: %s\n", errprefix, "not an X.509 certificate"); sess->verify.rc = GNUTLS_E_UNSUPPORTED_CERTIFICATE_TYPE; return gpg_error (GPG_ERR_GENERAL); } rc = gnutls_certificate_verify_peers2 (sess->tls_session, &status); if (rc) { log_error ("%s: %s\n", errprefix, gnutls_strerror (rc)); if (!err) err = gpg_error (GPG_ERR_GENERAL); } else if (status) { log_error ("%s: status=0x%04x\n", errprefix, status); #if GNUTLS_VERSION_NUMBER >= 0x030104 { gnutls_datum_t statusdat; if (!gnutls_certificate_verification_status_print (status, GNUTLS_CRT_X509, &statusdat, 0)) { log_info ("%s: %s\n", errprefix, statusdat.data); gnutls_free (statusdat.data); } } #endif /*gnutls >= 3.1.4*/ sess->verify.status = status; if (!err) err = gpg_error (GPG_ERR_GENERAL); } hostname = sess->servername; if (!hostname || !strchr (hostname, '.')) { log_error ("%s: %s\n", errprefix, "hostname missing"); if (!err) err = gpg_error (GPG_ERR_GENERAL); } certlist = gnutls_certificate_get_peers (sess->tls_session, &certlistlen); if (!certlistlen) { log_error ("%s: %s\n", errprefix, "server did not send a certificate"); if (!err) err = gpg_error (GPG_ERR_GENERAL); /* Need to stop here. */ if (err) return err; } rc = gnutls_x509_crt_init (&cert); if (rc < 0) { if (!err) err = gpg_error (GPG_ERR_GENERAL); if (err) return err; } rc = gnutls_x509_crt_import (cert, &certlist[0], GNUTLS_X509_FMT_DER); if (rc < 0) { log_error ("%s: %s: %s\n", errprefix, "error importing certificate", gnutls_strerror (rc)); if (!err) err = gpg_error (GPG_ERR_GENERAL); } if (!gnutls_x509_crt_check_hostname (cert, hostname)) { log_error ("%s: %s\n", errprefix, "hostname does not match"); if (!err) err = gpg_error (GPG_ERR_GENERAL); } gnutls_x509_crt_deinit (cert); if (!err) sess->verify.rc = 0; if (sess->cert_log_cb) { const void *bufarr[10]; size_t buflenarr[10]; size_t n; for (n = 0; n < certlistlen && n < DIM (bufarr)-1; n++) { bufarr[n] = certlist[n].data; buflenarr[n] = certlist[n].size; } bufarr[n] = NULL; buflenarr[n] = 0; sess->cert_log_cb (sess, err, hostname, bufarr, buflenarr); } return err; #else /*!HTTP_USE_GNUTLS*/ (void)sess; return gpg_error (GPG_ERR_NOT_IMPLEMENTED); #endif } /* Return the first query variable with the specified key. If there is no such variable, return NULL. */ struct uri_tuple_s * uri_query_lookup (parsed_uri_t uri, const char *key) { struct uri_tuple_s *t; for (t = uri->query; t; t = t->next) if (strcmp (t->name, key) == 0) return t; return NULL; } +const char * +uri_query_value (parsed_uri_t url, const char *key) +{ + struct uri_tuple_s *t; + t = uri_query_lookup (url, key); + return t? t->value : NULL; +} + + /* Return true if both URI point to the same host for the purpose of * redirection check. A is the original host and B the host given in * the Location header. As a temporary workaround a fixed list of * exceptions is also consulted. */ static int same_host_p (parsed_uri_t a, parsed_uri_t b) { static struct { const char *from; /* NULL uses the last entry from the table. */ const char *to; } allow[] = { { "protonmail.com", "api.protonmail.com" }, { NULL, "api.protonmail.ch" }, { "protonmail.ch", "api.protonmail.com" }, { NULL, "api.protonmail.ch" }, { "pm.me", "api.protonmail.ch" } }; static const char *subdomains[] = { "openpgpkey." }; int i; const char *from; if (!a->host || !b->host) return 0; if (!ascii_strcasecmp (a->host, b->host)) return 1; from = NULL; for (i=0; i < DIM (allow); i++) { if (allow[i].from) from = allow[i].from; if (!from) continue; if (!ascii_strcasecmp (from, a->host) && !ascii_strcasecmp (allow[i].to, b->host)) return 1; } /* Also consider hosts the same if they differ only in a subdomain; * in both direction. This allows to have redirection between the * WKD advanced and direct lookup methods. */ for (i=0; i < DIM (subdomains); i++) { const char *subdom = subdomains[i]; size_t subdomlen = strlen (subdom); if (!ascii_strncasecmp (a->host, subdom, subdomlen) && !ascii_strcasecmp (a->host + subdomlen, b->host)) return 1; if (!ascii_strncasecmp (b->host, subdom, subdomlen) && !ascii_strcasecmp (b->host + subdomlen, a->host)) return 1; } return 0; } /* Prepare a new URL for a HTTP redirect. INFO has flags controlling * the operation, STATUS_CODE is used for diagnostics, LOCATION is the * value of the "Location" header, and R_URL reveives the new URL on * success or NULL or error. Note that INFO->ORIG_URL is * required. */ gpg_error_t http_prepare_redirect (http_redir_info_t *info, unsigned int status_code, const char *location, char **r_url) { gpg_error_t err; parsed_uri_t locuri; parsed_uri_t origuri; char *newurl; char *p; *r_url = NULL; if (!info || !info->orig_url) return gpg_error (GPG_ERR_INV_ARG); if (!info->silent) log_info (_("URL '%s' redirected to '%s' (%u)\n"), info->orig_url, location? location:"[none]", status_code); if (!info->redirects_left) { if (!info->silent) log_error (_("too many redirections\n")); return gpg_error (GPG_ERR_NO_DATA); } info->redirects_left--; if (!location || !*location) return gpg_error (GPG_ERR_NO_DATA); err = http_parse_uri (&locuri, location, 0); if (err) return err; /* Make sure that an onion address only redirects to another * onion address, or that a https address only redirects to a * https address. */ if (info->orig_onion && !locuri->onion) { dirmngr_status_printf (info->ctrl, "WARNING", "http_redirect %u" " redirect from onion to non-onion address" " rejected", err); http_release_parsed_uri (locuri); return gpg_error (GPG_ERR_FORBIDDEN); } if (!info->allow_downgrade && info->orig_https && !locuri->use_tls) { err = gpg_error (GPG_ERR_FORBIDDEN); dirmngr_status_printf (info->ctrl, "WARNING", "http_redirect %u" " redirect '%s' to '%s' rejected", err, info->orig_url, location); http_release_parsed_uri (locuri); return err; } if (info->trust_location) { /* We trust the Location - return it verbatim. */ http_release_parsed_uri (locuri); newurl = xtrystrdup (location); if (!newurl) { err = gpg_error_from_syserror (); return err; } } else if ((err = http_parse_uri (&origuri, info->orig_url, 0))) { http_release_parsed_uri (locuri); return err; } else if (same_host_p (origuri, locuri)) { /* The host is the same or on an exception list and thus we can * take the location verbatim. */ http_release_parsed_uri (origuri); http_release_parsed_uri (locuri); newurl = xtrystrdup (location); if (!newurl) { err = gpg_error_from_syserror (); return err; } } else { /* We take only the host and port from the URL given in the * Location. This limits the effects of redirection attacks by * rogue hosts returning an URL to servers in the client's own * network. We don't even include the userinfo because they * should be considered similar to the path and query parts. */ if (!(locuri->off_path - locuri->off_host)) { http_release_parsed_uri (origuri); http_release_parsed_uri (locuri); return gpg_error (GPG_ERR_BAD_URI); } if (!(origuri->off_path - origuri->off_host)) { http_release_parsed_uri (origuri); http_release_parsed_uri (locuri); return gpg_error (GPG_ERR_BAD_URI); } newurl = xtrymalloc (strlen (origuri->original) + (locuri->off_path - locuri->off_host) + 1); if (!newurl) { err = gpg_error_from_syserror (); http_release_parsed_uri (origuri); http_release_parsed_uri (locuri); return err; } /* Build new URL from * uriguri: scheme userinfo ---- ---- path rest * locuri: ------ -------- host port ---- ---- */ p = newurl; memcpy (p, origuri->original, origuri->off_host); p += origuri->off_host; memcpy (p, locuri->original + locuri->off_host, (locuri->off_path - locuri->off_host)); p += locuri->off_path - locuri->off_host; strcpy (p, origuri->original + origuri->off_path); http_release_parsed_uri (origuri); http_release_parsed_uri (locuri); if (!info->silent) log_info (_("redirection changed to '%s'\n"), newurl); dirmngr_status_printf (info->ctrl, "WARNING", "http_redirect_cleanup %u" " changed from '%s' to '%s'", 0, info->orig_url, newurl); } *r_url = newurl; return 0; } /* Return string describing the http STATUS. Returns an empty string * for an unknown status. */ const char * http_status2string (unsigned int status) { switch (status) { case 500: return "Internal Server Error"; case 501: return "Not Implemented"; case 502: return "Bad Gateway"; case 503: return "Service Unavailable"; case 504: return "Gateway Timeout"; case 505: return "HTTP version Not Supported"; case 506: return "Variant Also Negation"; case 507: return "Insufficient Storage"; case 508: return "Loop Detected"; case 510: return "Not Extended"; case 511: return "Network Authentication Required"; } return ""; } diff --git a/dirmngr/http.h b/dirmngr/http.h index 4ad0351e2..18420c925 100644 --- a/dirmngr/http.h +++ b/dirmngr/http.h @@ -1,207 +1,209 @@ /* http.h - HTTP protocol handler * Copyright (C) 1999, 2000, 2001, 2003, 2006, * 2010 Free Software Foundation, Inc. * Copyright (C) 2015 g10 Code GmbH * * This file is part of GnuPG. * * This file is free software; you can redistribute it and/or modify * it under the terms of either * * - the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at * your option) any later version. * * or * * - the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at * your option) any later version. * * or both in parallel, as here. * * This file 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 . */ #ifndef GNUPG_COMMON_HTTP_H #define GNUPG_COMMON_HTTP_H #include #include "../common/fwddecl.h" struct uri_tuple_s { struct uri_tuple_s *next; const char *name; /* A pointer into name. */ char *value; /* A pointer to value (a Nul is always appended). */ size_t valuelen; /* The real length of the value; we need it because the value may contain embedded Nuls. */ int no_value; /* True if no value has been given in the URL. */ }; typedef struct uri_tuple_s *uri_tuple_t; struct parsed_uri_s { /* All these pointers point into BUFFER; most stuff is not escaped. */ char *original; /* Unmodified copy of the parsed URI. */ char *scheme; /* Pointer to the scheme string (always lowercase). */ unsigned int is_http:1; /* This is a HTTP style URI. */ unsigned int is_ldap:1; /* This is a LDAP style URI. */ unsigned int use_tls:1; /* Whether TLS should be used. */ unsigned int opaque:1; /* Unknown scheme; PATH has the rest. */ unsigned int v6lit:1; /* Host was given as a literal v6 address. */ unsigned int onion:1; /* .onion address given. */ unsigned int explicit_port :1; /* The port was explicitly specified. */ unsigned int ad_current:1; /* Use Active Directory's current user. */ char *auth; /* username/password for basic auth. */ char *host; /* Host (converted to lowercase). */ unsigned short port; /* Port (always set if the host is set). */ unsigned short off_host; /* Offset to the HOST respective PATH parts */ unsigned short off_path; /* in the original URI buffer. */ char *path; /* Path. */ uri_tuple_t params; /* ";xxxxx" */ uri_tuple_t query; /* "?xxx=yyy" */ char buffer[1]; /* Buffer which holds a (modified) copy of the URI. */ }; typedef struct parsed_uri_s *parsed_uri_t; struct uri_tuple_s *uri_query_lookup (parsed_uri_t uri, const char *key); +const char *uri_query_value (parsed_uri_t url, const char *key); typedef enum { HTTP_REQ_GET = 1, HTTP_REQ_HEAD = 2, HTTP_REQ_POST = 3, HTTP_REQ_OPAQUE = 4 /* Internal use. */ } http_req_t; /* We put the flag values into an enum, so that gdb can display them. */ enum { HTTP_FLAG_TRY_PROXY = 1, /* Try to use a proxy. */ HTTP_FLAG_SHUTDOWN = 2, /* Close sending end after the request. */ HTTP_FLAG_FORCE_TOR = 4, /* Force a TOR connection. */ HTTP_FLAG_LOG_RESP = 8, /* Log the server response. */ HTTP_FLAG_FORCE_TLS = 16, /* Force the use of TLS. */ HTTP_FLAG_IGNORE_CL = 32, /* Ignore content-length. */ HTTP_FLAG_IGNORE_IPv4 = 64, /* Do not use IPv4. */ HTTP_FLAG_IGNORE_IPv6 = 128, /* Do not use IPv6. */ HTTP_FLAG_TRUST_DEF = 256, /* Use the CAs configured for HKP. */ HTTP_FLAG_TRUST_SYS = 512, /* Also use the system defined CAs. */ HTTP_FLAG_TRUST_CFG = 1024, /* Also use configured CAs. */ HTTP_FLAG_NO_CRL = 2048 /* Do not consult CRLs for https. */ }; struct http_session_s; typedef struct http_session_s *http_session_t; struct http_context_s; typedef struct http_context_s *http_t; /* An object used to track redirection infos. */ struct http_redir_info_s { unsigned int redirects_left; /* Number of still possible redirects. */ ctrl_t ctrl; /* The usual connection info or NULL. */ const char *orig_url; /* The original requested URL. */ unsigned int orig_onion:1; /* Original request was an onion address. */ unsigned int orig_https:1; /* Original request was a http address. */ unsigned int silent:1; /* No diagnostics. */ unsigned int allow_downgrade:1;/* Allow a downgrade from https to http. */ unsigned int trust_location:1; /* Trust the received Location header. */ }; typedef struct http_redir_info_s http_redir_info_t; /* A TLS verify callback function. */ typedef gpg_error_t (*http_verify_cb_t) (void *opaque, http_t http, http_session_t session, unsigned int flags, void *tls_context); void http_set_verbose (int verbose, int debug); void http_register_tls_callback (gpg_error_t (*cb)(http_t,http_session_t,int)); void http_register_tls_ca (const char *fname); void http_register_cfg_ca (const char *fname); void http_register_netactivity_cb (void (*cb)(void)); gpg_error_t http_session_new (http_session_t *r_session, const char *intended_hostname, unsigned int flags, http_verify_cb_t cb, void *cb_value); http_session_t http_session_ref (http_session_t sess); void http_session_release (http_session_t sess); void http_session_set_log_cb (http_session_t sess, void (*cb)(http_session_t, gpg_error_t, const char *, const void **, size_t *)); void http_session_set_timeout (http_session_t sess, unsigned int timeout); +#define HTTP_PARSE_NO_SCHEME_CHECK 1 gpg_error_t http_parse_uri (parsed_uri_t *ret_uri, const char *uri, - int no_scheme_check); + unsigned int flags); void http_release_parsed_uri (parsed_uri_t uri); gpg_error_t http_raw_connect (ctrl_t ctrl, http_t *r_hd, const char *server, unsigned short port, unsigned int flags, const char *srvtag, unsigned int timeout); gpg_error_t http_open (ctrl_t ctrl, http_t *r_hd, http_req_t reqtype, const char *url, const char *httphost, const char *auth, unsigned int flags, const char *proxy, http_session_t session, const char *srvtag, strlist_t headers); void http_start_data (http_t hd); gpg_error_t http_wait_response (http_t hd); void http_close (http_t hd, int keep_read_stream); gpg_error_t http_open_document (ctrl_t ctrl, http_t *r_hd, const char *document, const char *auth, unsigned int flags, const char *proxy, http_session_t session, const char *srvtag, strlist_t headers); estream_t http_get_read_ptr (http_t hd); estream_t http_get_write_ptr (http_t hd); unsigned int http_get_status_code (http_t hd); const char *http_get_tls_info (http_t hd, const char *what); const char *http_get_header (http_t hd, const char *name); const char **http_get_header_names (http_t hd); gpg_error_t http_verify_server_credentials (http_session_t sess); char *http_escape_string (const char *string, const char *specials); char *http_escape_data (const void *data, size_t datalen, const char *specials); gpg_error_t http_prepare_redirect (http_redir_info_t *info, unsigned int status_code, const char *location, char **r_url); const char *http_status2string (unsigned int status); #endif /*GNUPG_COMMON_HTTP_H*/ diff --git a/dirmngr/ks-action.c b/dirmngr/ks-action.c index c2d8c374d..5c097754e 100644 --- a/dirmngr/ks-action.c +++ b/dirmngr/ks-action.c @@ -1,415 +1,415 @@ /* 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. * * 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 . */ #include #include #include #include #include #include "dirmngr.h" #include "misc.h" #include "ks-engine.h" #include "ks-action.h" #if USE_LDAP # include "ldap-parse-uri.h" #endif /* Called by the engine's help functions to print the actual help. */ gpg_error_t ks_print_help (ctrl_t ctrl, const char *text) { return dirmngr_status_help (ctrl, text); } /* Called by the engine's help functions to print the actual help. */ gpg_error_t ks_printf_help (ctrl_t ctrl, const char *format, ...) { va_list arg_ptr; gpg_error_t err; char *buf; va_start (arg_ptr, format); buf = es_vbsprintf (format, arg_ptr); err = buf? 0 : gpg_error_from_syserror (); va_end (arg_ptr); if (!err) err = dirmngr_status_help (ctrl, buf); es_free (buf); return err; } /* Run the help command for the engine responsible for URI. */ gpg_error_t ks_action_help (ctrl_t ctrl, const char *url) { gpg_error_t err; parsed_uri_t parsed_uri; /* The broken down URI. */ if (!url || !*url) { ks_print_help (ctrl, "Known schemata:\n"); parsed_uri = NULL; } else { #if USE_LDAP if (ldap_uri_p (url)) err = ldap_parse_uri (&parsed_uri, url); else #endif { - err = http_parse_uri (&parsed_uri, url, 1); + err = http_parse_uri (&parsed_uri, url, HTTP_PARSE_NO_SCHEME_CHECK); } if (err) return err; } /* Call all engines to give them a chance to print a help string. */ err = ks_hkp_help (ctrl, parsed_uri); if (!err) err = ks_http_help (ctrl, parsed_uri); if (!err) err = ks_finger_help (ctrl, parsed_uri); if (!err) err = ks_kdns_help (ctrl, parsed_uri); #if USE_LDAP if (!err) err = ks_ldap_help (ctrl, parsed_uri); #endif if (!parsed_uri) ks_print_help (ctrl, "(Use an URL for engine specific help.)"); else http_release_parsed_uri (parsed_uri); return err; } /* Resolve all host names. This is useful for looking at the status of configured keyservers. */ gpg_error_t ks_action_resolve (ctrl_t ctrl, uri_item_t keyservers) { gpg_error_t err = 0; int any_server = 0; uri_item_t uri; for (uri = keyservers; !err && uri; uri = uri->next) { if (uri->parsed_uri->is_http) { any_server = 1; err = ks_hkp_resolve (ctrl, uri->parsed_uri); if (err) break; } } if (!any_server) err = gpg_error (GPG_ERR_NO_KEYSERVER); return err; } /* Search all configured keyservers for keys matching PATTERNS and write the result to the provided output stream. */ gpg_error_t ks_action_search (ctrl_t ctrl, uri_item_t keyservers, strlist_t patterns, estream_t outfp) { gpg_error_t err = 0; int any_server = 0; int any_results = 0; uri_item_t uri; estream_t infp; if (!patterns) return gpg_error (GPG_ERR_NO_USER_ID); /* FIXME: We only take care of the first pattern. To fully support multiple patterns we might either want to run several queries in parallel and merge them. We also need to decide what to do with errors - it might not be the best idea to ignore an error from one server and silently continue with another server. For now we stop at the first error, unless the server responds with '404 Not Found', in which case we try the next server. */ for (uri = keyservers; !err && uri; uri = uri->next) { int is_http = uri->parsed_uri->is_http; int is_ldap = 0; unsigned int http_status = 0; #if USE_LDAP is_ldap = (strcmp (uri->parsed_uri->scheme, "ldap") == 0 || strcmp (uri->parsed_uri->scheme, "ldaps") == 0 || strcmp (uri->parsed_uri->scheme, "ldapi") == 0); #endif if (is_http || is_ldap) { any_server = 1; #if USE_LDAP if (is_ldap) err = ks_ldap_search (ctrl, uri->parsed_uri, patterns->d, &infp); else #endif { err = ks_hkp_search (ctrl, uri->parsed_uri, patterns->d, &infp, &http_status); } if (err == gpg_error (GPG_ERR_NO_DATA) && http_status == 404 /* not found */) { /* No record found. Clear error and try next server. */ err = 0; continue; } if (!err) { err = copy_stream (infp, outfp); es_fclose (infp); any_results = 1; break; } } } if (!any_server) err = gpg_error (GPG_ERR_NO_KEYSERVER); else if (err == 0 && !any_results) err = gpg_error (GPG_ERR_NO_DATA); return err; } /* Get the requested keys (matching PATTERNS) using all configured keyservers and write the result to the provided output stream. */ gpg_error_t ks_action_get (ctrl_t ctrl, uri_item_t keyservers, strlist_t patterns, int ldap_only, estream_t outfp) { gpg_error_t err = 0; gpg_error_t first_err = 0; int any_server = 0; int any_data = 0; strlist_t sl; uri_item_t uri; estream_t infp; if (!patterns) return gpg_error (GPG_ERR_NO_USER_ID); /* FIXME: We only take care of the first keyserver. To fully support multiple keyservers we need to track the result for each pattern and use the next keyserver if one key was not found. The keyservers might not all be fully synced thus it is not clear whether the first keyserver has the freshest copy of the key. Need to think about a better strategy. */ for (uri = keyservers; !err && uri; uri = uri->next) { int is_hkp_s = (strcmp (uri->parsed_uri->scheme, "hkp") == 0 || strcmp (uri->parsed_uri->scheme, "hkps") == 0); int is_http_s = (strcmp (uri->parsed_uri->scheme, "http") == 0 || strcmp (uri->parsed_uri->scheme, "https") == 0); int is_ldap = 0; if (ldap_only) is_hkp_s = is_http_s = 0; #if USE_LDAP is_ldap = (strcmp (uri->parsed_uri->scheme, "ldap") == 0 || strcmp (uri->parsed_uri->scheme, "ldaps") == 0 || strcmp (uri->parsed_uri->scheme, "ldapi") == 0); #endif if (is_hkp_s || is_http_s || is_ldap) { any_server = 1; for (sl = patterns; !err && sl; sl = sl->next) { #if USE_LDAP if (is_ldap) err = ks_ldap_get (ctrl, uri->parsed_uri, sl->d, &infp); else #endif if (is_hkp_s) err = ks_hkp_get (ctrl, uri->parsed_uri, sl->d, &infp); else if (is_http_s) err = ks_http_fetch (ctrl, uri->parsed_uri->original, KS_HTTP_FETCH_NOCACHE, &infp); else BUG (); if (err) { /* It is possible that a server does not carry a key, thus we only save the error and continue with the next pattern. FIXME: It is an open question how to return such an error condition to the caller. */ first_err = err; err = 0; } else { err = copy_stream (infp, outfp); /* Reading from the keyserver should never fail, thus return this error. */ if (!err) any_data = 1; es_fclose (infp); infp = NULL; } } } if (any_data) break; /* Stop loop after a keyserver returned something. */ } if (!any_server) err = gpg_error (GPG_ERR_NO_KEYSERVER); else if (!err && first_err && !any_data) err = first_err; return err; } /* Retrieve keys from URL and write the result to the provided output * stream OUTFP. If OUTFP is NULL the data is written to the bit * bucket. */ gpg_error_t ks_action_fetch (ctrl_t ctrl, const char *url, estream_t outfp) { gpg_error_t err = 0; estream_t infp; parsed_uri_t parsed_uri; /* The broken down URI. */ if (!url) return gpg_error (GPG_ERR_INV_URI); - err = http_parse_uri (&parsed_uri, url, 1); + err = http_parse_uri (&parsed_uri, url, HTTP_PARSE_NO_SCHEME_CHECK); if (err) return err; if (parsed_uri->is_http) { err = ks_http_fetch (ctrl, url, KS_HTTP_FETCH_NOCACHE, &infp); if (!err) { err = copy_stream (infp, outfp); es_fclose (infp); } } else if (!parsed_uri->opaque) { err = gpg_error (GPG_ERR_INV_URI); } else if (!strcmp (parsed_uri->scheme, "finger")) { err = ks_finger_fetch (ctrl, parsed_uri, &infp); if (!err) { err = copy_stream (infp, outfp); es_fclose (infp); } } else if (!strcmp (parsed_uri->scheme, "kdns")) { err = ks_kdns_fetch (ctrl, parsed_uri, &infp); if (!err) { err = copy_stream (infp, outfp); es_fclose (infp); } } else err = gpg_error (GPG_ERR_INV_URI); http_release_parsed_uri (parsed_uri); return err; } /* Send an OpenPGP key to all keyservers. The key in {DATA,DATALEN} 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 'gpg --list-keys --with-colons KEYID'). 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, uri_item_t keyservers, void *data, size_t datalen, void *info, size_t infolen) { gpg_error_t err = 0; gpg_error_t first_err = 0; int any_server = 0; uri_item_t uri; (void) info; (void) infolen; for (uri = keyservers; !err && uri; uri = uri->next) { int is_http = uri->parsed_uri->is_http; int is_ldap = 0; #if USE_LDAP is_ldap = (strcmp (uri->parsed_uri->scheme, "ldap") == 0 || strcmp (uri->parsed_uri->scheme, "ldaps") == 0 || strcmp (uri->parsed_uri->scheme, "ldapi") == 0); #endif if (is_http || is_ldap) { any_server = 1; #if USE_LDAP if (is_ldap) err = ks_ldap_put (ctrl, uri->parsed_uri, data, datalen, info, infolen); else #endif { err = ks_hkp_put (ctrl, uri->parsed_uri, data, datalen); } if (err) { first_err = err; err = 0; } } } if (!any_server) err = gpg_error (GPG_ERR_NO_KEYSERVER); else if (!err && first_err) err = first_err; return err; } diff --git a/dirmngr/ks-engine-hkp.c b/dirmngr/ks-engine-hkp.c index 93c676b6a..e72c2c16d 100644 --- a/dirmngr/ks-engine-hkp.c +++ b/dirmngr/ks-engine-hkp.c @@ -1,1859 +1,1860 @@ /* ks-engine-hkp.c - HKP keyserver engine * Copyright (C) 2011, 2012 Free Software Foundation, Inc. * Copyright (C) 2011, 2012, 2014 Werner Koch * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #include #include #include #include #include #ifdef HAVE_W32_SYSTEM # ifdef HAVE_WINSOCK2_H # include # endif # include #else /*!HAVE_W32_SYSTEM*/ # include # include # include #endif /*!HAVE_W32_SYSTEM*/ #include #include "dirmngr.h" #include "misc.h" #include "../common/userids.h" #include "dns-stuff.h" #include "ks-engine.h" /* Substitutes for missing Mingw macro. The EAI_SYSTEM mechanism seems not to be available (probably because there is only one set of error codes anyway). For now we use WSAEINVAL. */ #ifndef EAI_OVERFLOW # define EAI_OVERFLOW EAI_FAIL #endif #ifdef HAVE_W32_SYSTEM # ifndef EAI_SYSTEM # define EAI_SYSTEM WSAEINVAL # endif #endif /* Number of seconds after a host is marked as resurrected. */ #define RESURRECT_INTERVAL (3600+1800) /* 1.5 hours */ /* To match the behaviour of our old gpgkeys helper code we escape more characters than actually needed. */ #define EXTRA_ESCAPE_CHARS "@!\"#$%&'()*+,-./:;<=>?[\\]^_{|}~" /* How many redirections do we allow. */ #define MAX_REDIRECTS 2 /* Number of retries done for a dead host etc. */ #define SEND_REQUEST_RETRIES 3 /* Number of retries done in case of transient errors. */ #define SEND_REQUEST_EXTRA_RETRIES 5 enum ks_protocol { KS_PROTOCOL_HKP, KS_PROTOCOL_HKPS, KS_PROTOCOL_MAX }; /* Objects used to maintain information about hosts. */ struct hostinfo_s; typedef struct hostinfo_s *hostinfo_t; struct hostinfo_s { time_t lastfail; /* Time we tried to connect and failed. */ time_t lastused; /* Time of last use. */ int *pool; /* An array with indices into HOSTTABLE or NULL if NAME is not a pool name. */ size_t pool_len; /* Length of POOL. */ size_t pool_size; /* Allocated size of POOL. */ #define MAX_POOL_SIZE 128 int poolidx; /* Index into POOL with the used host. -1 if not set. */ unsigned int v4:1; /* Host supports AF_INET. */ unsigned int v6:1; /* Host supports AF_INET6. */ unsigned int onion:1;/* NAME is an onion (Tor HS) address. */ unsigned int dead:1; /* Host is currently unresponsive. */ unsigned int iporname_valid:1; /* The field IPORNAME below is valid */ /* (but may be NULL) */ unsigned int did_a_lookup:1; /* Have we done an A lookup yet? */ unsigned int did_srv_lookup:2; /* One bit per protocol indicating whether we already did a SRV lookup. */ time_t died_at; /* The time the host was marked dead. If this is 0 the host has been manually marked dead. */ char *cname; /* Canonical name of the host. Only set if this is a pool or NAME has a numerical IP address. */ char *iporname; /* Numeric IP address or name for printing. */ unsigned short port[KS_PROTOCOL_MAX]; /* The port used by the host for all protocols, 0 if unknown. */ char name[1]; /* The hostname. */ }; /* An array of hostinfo_t for all hosts requested by the caller or resolved from a pool name and its allocated size.*/ static hostinfo_t *hosttable; static int hosttable_size; /* A mutex used to serialize access to the hosttable. */ static npth_mutex_t hosttable_lock; /* The number of host slots we initially allocate for HOSTTABLE. */ #define INITIAL_HOSTTABLE_SIZE 50 /* Create a new hostinfo object, fill in NAME and put it into HOSTTABLE. Return the index into hosttable on success or -1 on error. */ static int create_new_hostinfo (const char *name) { hostinfo_t hi, *newtable; int newsize; int idx, rc; hi = xtrymalloc (sizeof *hi + strlen (name)); if (!hi) return -1; strcpy (hi->name, name); hi->pool = NULL; hi->pool_len = 0; hi->pool_size = 0; hi->poolidx = -1; hi->lastused = (time_t)(-1); hi->lastfail = (time_t)(-1); hi->v4 = 0; hi->v6 = 0; hi->onion = 0; hi->dead = 0; hi->did_a_lookup = 0; hi->did_srv_lookup = 0; hi->iporname_valid = 0; hi->died_at = 0; hi->cname = NULL; hi->iporname = NULL; hi->port[KS_PROTOCOL_HKP] = 0; hi->port[KS_PROTOCOL_HKPS] = 0; /* Add it to the hosttable. */ for (idx=0; idx < hosttable_size; idx++) if (!hosttable[idx]) { hosttable[idx] = hi; return idx; } /* Need to extend the hosttable. */ newsize = hosttable_size + INITIAL_HOSTTABLE_SIZE; newtable = xtryrealloc (hosttable, newsize * sizeof *hosttable); if (!newtable) { xfree (hi); return -1; } hosttable = newtable; idx = hosttable_size; hosttable_size = newsize; rc = idx; hosttable[idx++] = hi; while (idx < hosttable_size) hosttable[idx++] = NULL; return rc; } /* Find the host NAME in our table. Return the index into the hosttable or -1 if not found. */ static int find_hostinfo (const char *name) { int idx; for (idx=0; idx < hosttable_size; idx++) if (hosttable[idx] && !ascii_strcasecmp (hosttable[idx]->name, name)) return idx; return -1; } static int sort_hostpool (const void *xa, const void *xb) { int a = *(int *)xa; int b = *(int *)xb; assert (a >= 0 && a < hosttable_size); assert (b >= 0 && b < hosttable_size); assert (hosttable[a]); assert (hosttable[b]); return ascii_strcasecmp (hosttable[a]->name, hosttable[b]->name); } /* Return true if the host with the hosttable index TBLIDX is in HI->pool. */ static int host_in_pool_p (hostinfo_t hi, int tblidx) { int i, pidx; for (i = 0; i < hi->pool_len && (pidx = hi->pool[i]) != -1; i++) if (pidx == tblidx && hosttable[pidx]) return 1; return 0; } /* Select a random host. Consult HI->pool which indices into the global hosttable. Returns index into HI->pool or -1 if no host could be selected. */ static int select_random_host (hostinfo_t hi) { int *tbl; size_t tblsize; int pidx, idx; /* CHECKTHIS(); See */ /* https://sources.debian.org/patches/gnupg2/2.2.20-1/dirmngr-idling/dirmngr-hkp-Avoid-potential-race-condition-when-some.patch/ */ /* We create a new table so that we randomly select only from currently alive hosts. */ for (idx = 0, tblsize = 0; idx < hi->pool_len && (pidx = hi->pool[idx]) != -1; idx++) if (hosttable[pidx] && !hosttable[pidx]->dead) tblsize++; if (!tblsize) return -1; /* No hosts. */ tbl = xtrymalloc (tblsize * sizeof *tbl); if (!tbl) return -1; for (idx = 0, tblsize = 0; idx < hi->pool_len && (pidx = hi->pool[idx]) != -1; idx++) if (hosttable[pidx] && !hosttable[pidx]->dead) tbl[tblsize++] = pidx; if (tblsize == 1) /* Save a get_uint_nonce. */ pidx = tbl[0]; else pidx = tbl[get_uint_nonce () % tblsize]; xfree (tbl); return pidx; } /* Figure out if a set of DNS records looks like a pool. */ static int arecords_is_pool (dns_addrinfo_t aibuf) { dns_addrinfo_t ai; int n_v6, n_v4; n_v6 = n_v4 = 0; for (ai = aibuf; ai; ai = ai->next) { if (ai->family == AF_INET6) n_v6++; else if (ai->family == AF_INET) n_v4++; } return n_v6 > 1 || n_v4 > 1; } /* Print a warning iff Tor is not running but Tor has been requested. * Also return true if it is not running. */ static int tor_not_running_p (ctrl_t ctrl) { assuan_fd_t sock; if (!dirmngr_use_tor ()) return 0; sock = assuan_sock_connect_byname (NULL, 0, 0, NULL, ASSUAN_SOCK_TOR); if (sock != ASSUAN_INVALID_FD) { assuan_sock_close (sock); return 0; } log_info ("(it seems Tor is not running)\n"); dirmngr_status (ctrl, "WARNING", "tor_not_running 0", "Tor is enabled but the local Tor daemon" " seems to be down", NULL); return 1; } /* Add the host AI under the NAME into the HOSTTABLE. If PORT is not zero, it specifies which port to use to talk to the host for PROTOCOL. If NAME specifies a pool (as indicated by IS_POOL), update the given reference table accordingly. */ static void add_host (ctrl_t ctrl, const char *name, int is_pool, const dns_addrinfo_t ai, enum ks_protocol protocol, unsigned short port) { gpg_error_t tmperr; char *tmphost; int idx, tmpidx; hostinfo_t host; int i; idx = find_hostinfo (name); host = hosttable[idx]; if (is_pool) { /* For a pool immediately convert the address to a string. */ tmperr = resolve_dns_addr (ctrl, ai->addr, ai->addrlen, (DNS_NUMERICHOST | DNS_WITHBRACKET), &tmphost); } else if (!is_ip_address (name)) { /* This is a hostname. Use the name as given without going * through resolve_dns_addr. */ tmphost = xtrystrdup (name); if (!tmphost) tmperr = gpg_error_from_syserror (); else tmperr = 0; } else { /* Do a PTR lookup on AI. If a name was not found the function * returns the numeric address (with brackets). */ tmperr = resolve_dns_addr (ctrl, ai->addr, ai->addrlen, DNS_WITHBRACKET, &tmphost); } if (tmperr) { log_info ("resolve_dns_addr failed while checking '%s': %s\n", name, gpg_strerror (tmperr)); } else if (host->pool_len + 1 >= MAX_POOL_SIZE) { log_error ("resolve_dns_addr for '%s': '%s'" " [index table full - ignored]\n", name, tmphost); } else { if (!is_pool && is_ip_address (name)) /* Update the original entry. */ tmpidx = idx; else tmpidx = find_hostinfo (tmphost); log_info ("resolve_dns_addr for '%s': '%s'%s\n", name, tmphost, tmpidx == -1? "" : " [already known]"); if (tmpidx == -1) /* Create a new entry. */ tmpidx = create_new_hostinfo (tmphost); if (tmpidx == -1) { log_error ("map_host for '%s' problem: %s - '%s' [ignored]\n", name, strerror (errno), tmphost); } else /* Set or update the entry. */ { if (port) hosttable[tmpidx]->port[protocol] = port; if (ai->family == AF_INET6) { hosttable[tmpidx]->v6 = 1; } else if (ai->family == AF_INET) { hosttable[tmpidx]->v4 = 1; } else BUG (); /* If we updated the main entry, we're done. */ if (idx == tmpidx) goto leave; /* If we updated an existing entry, we're done. */ for (i = 0; i < host->pool_len; i++) if (host->pool[i] == tmpidx) goto leave; /* Otherwise, we need to add it to the pool. Check if there is space. */ if (host->pool_len + 1 > host->pool_size) { int *new_pool; size_t new_size; if (host->pool_size == 0) new_size = 4; else new_size = host->pool_size * 2; new_pool = xtryrealloc (host->pool, new_size * sizeof *new_pool); if (new_pool == NULL) goto leave; host->pool = new_pool; host->pool_size = new_size; } /* Finally, add it. */ log_assert (host->pool_len < host->pool_size); host->pool[host->pool_len++] = tmpidx; } } leave: xfree (tmphost); } /* Sort the pool of the given hostinfo HI. */ static void hostinfo_sort_pool (hostinfo_t hi) { qsort (hi->pool, hi->pool_len, sizeof *hi->pool, sort_hostpool); } /* Map the host name NAME to the actual to be used host name. This * allows us to manage round robin DNS names. We use our own strategy * to choose one of the hosts. For example we skip those hosts which * failed for some time and we stick to one host for a time * independent of DNS retry times. If FORCE_RESELECT is true a new * host is always selected. If SRVTAG is NULL no service record * lookup will be done, if it is set that service name is used. The * selected host is stored as a malloced string at R_HOST; on error * NULL is stored. If we know the port used by the selected host from * a service record, a string representation is written to R_PORTSTR, * otherwise it is left untouched. If R_HTTPFLAGS is not NULL it will * receive flags which are to be passed to http_open. If R_HTTPHOST * is not NULL a malloced name of the host is stored there; this might * be different from R_HOST in case it has been selected from a * pool. */ static gpg_error_t map_host (ctrl_t ctrl, const char *name, const char *srvtag, int force_reselect, enum ks_protocol protocol, char **r_host, char *r_portstr, unsigned int *r_httpflags, char **r_httphost) { gpg_error_t err = 0; hostinfo_t hi; int idx; dns_addrinfo_t aibuf, ai; int is_pool; int new_hosts = 0; char *cname; *r_host = NULL; if (r_httpflags) *r_httpflags = 0; if (r_httphost) *r_httphost = NULL; /* No hostname means localhost. */ if (!name || !*name) { *r_host = xtrystrdup ("localhost"); if (!*r_host) return gpg_error_from_syserror (); if (r_httphost) { *r_httphost = xtrystrdup (*r_host); if (!*r_httphost) { err = gpg_error_from_syserror (); xfree (*r_host); *r_host = NULL; return err; } } return 0; } /* See whether the host is in our table. */ idx = find_hostinfo (name); if (idx == -1) { idx = create_new_hostinfo (name); if (idx == -1) return gpg_error_from_syserror (); hi = hosttable[idx]; hi->onion = is_onion_address (name); } else hi = hosttable[idx]; is_pool = hi->pool != NULL; if (srvtag && !is_ip_address (name) && ! hi->onion && ! (hi->did_srv_lookup & 1 << protocol)) { struct srventry *srvs; unsigned int srvscount; /* Check for SRV records. */ err = get_dns_srv (ctrl, name, srvtag, NULL, &srvs, &srvscount); if (err) { if (gpg_err_code (err) == GPG_ERR_ECONNREFUSED) tor_not_running_p (ctrl); return err; } if (srvscount > 0) { int i; if (! is_pool) is_pool = srvscount > 1; for (i = 0; i < srvscount; i++) { err = resolve_dns_name (ctrl, srvs[i].target, 0, AF_UNSPEC, SOCK_STREAM, &ai, &cname); if (err) continue; dirmngr_tick (ctrl); add_host (ctrl, name, is_pool, ai, protocol, srvs[i].port); new_hosts = 1; } xfree (srvs); } hi->did_srv_lookup |= 1 << protocol; } if (! hi->did_a_lookup && ! hi->onion) { /* Find all A records for this entry and put them into the pool list - if any. */ err = resolve_dns_name (ctrl, name, 0, 0, SOCK_STREAM, &aibuf, &cname); if (err) { log_error ("resolving '%s' failed: %s\n", name, gpg_strerror (err)); err = 0; } else { /* First figure out whether this is a pool. For a pool we use a different strategy than for a plain server: We use the canonical name of the pool as the virtual host along with the IP addresses. If it is not a pool, we use the specified name. */ if (! is_pool) is_pool = arecords_is_pool (aibuf); if (is_pool && cname) { hi->cname = cname; cname = NULL; } for (ai = aibuf; ai; ai = ai->next) { if (ai->family != AF_INET && ai->family != AF_INET6) continue; if (opt.disable_ipv4 && ai->family == AF_INET) continue; if (opt.disable_ipv6 && ai->family == AF_INET6) continue; dirmngr_tick (ctrl); add_host (ctrl, name, is_pool, ai, 0, 0); new_hosts = 1; } hi->did_a_lookup = 1; } xfree (cname); free_dns_addrinfo (aibuf); } if (new_hosts) hostinfo_sort_pool (hi); if (hi->pool) { /* Deal with the pool name before selecting a host. */ if (r_httphost) { *r_httphost = xtrystrdup (hi->name); if (!*r_httphost) return gpg_error_from_syserror (); } /* If the currently selected host is now marked dead, force a re-selection . */ if (force_reselect) hi->poolidx = -1; else if (hi->poolidx >= 0 && hi->poolidx < hosttable_size && hosttable[hi->poolidx] && hosttable[hi->poolidx]->dead) hi->poolidx = -1; /* Select a host if needed. */ if (hi->poolidx == -1) { hi->poolidx = select_random_host (hi); if (hi->poolidx == -1) { log_error ("no alive host found in pool '%s'\n", name); if (r_httphost) { xfree (*r_httphost); *r_httphost = NULL; } return gpg_error (GPG_ERR_NO_KEYSERVER); } } assert (hi->poolidx >= 0 && hi->poolidx < hosttable_size); hi = hosttable[hi->poolidx]; assert (hi); } else if (r_httphost && is_ip_address (hi->name)) { /* This is a numerical IP address and not a pool. We want to * find the canonical name so that it can be used in the HTTP * Host header. Fixme: We should store that name in the * hosttable. */ char *host; err = resolve_dns_name (ctrl, hi->name, 0, 0, SOCK_STREAM, &aibuf, NULL); if (!err) { for (ai = aibuf; ai; ai = ai->next) { if ((!opt.disable_ipv6 && ai->family == AF_INET6) || (!opt.disable_ipv4 && ai->family == AF_INET)) { err = resolve_dns_addr (ctrl, ai->addr, ai->addrlen, 0, &host); if (!err) { /* Okay, we return the first found name. */ *r_httphost = host; break; } } } } free_dns_addrinfo (aibuf); } else if (r_httphost) { *r_httphost = xtrystrdup (hi->name); if (!*r_httphost) return gpg_error_from_syserror (); } if (hi->dead) { log_error ("host '%s' marked as dead\n", hi->name); if (r_httphost) { xfree (*r_httphost); *r_httphost = NULL; } return gpg_error (GPG_ERR_NO_KEYSERVER); } if (r_httpflags) { /* If the hosttable does not indicate that a certain host supports IPv, we explicit set the corresponding http flags. The reason for this is that a host might be listed in a pool as not v6 only but actually support v6 when later the name is resolved by our http layer. */ if (!hi->v4) *r_httpflags |= HTTP_FLAG_IGNORE_IPv4; if (!hi->v6) *r_httpflags |= HTTP_FLAG_IGNORE_IPv6; /* Note that we do not set the HTTP_FLAG_FORCE_TOR for onion addresses because the http module detects this itself. This also allows us to use an onion address without Tor mode being enabled. */ } *r_host = xtrystrdup (hi->name); if (!*r_host) { err = gpg_error_from_syserror (); if (r_httphost) { xfree (*r_httphost); *r_httphost = NULL; } return err; } if (hi->port[protocol]) snprintf (r_portstr, 6 /* five digits and the sentinel */, "%hu", hi->port[protocol]); return 0; } /* Mark the host NAME as dead. NAME may be given as an URL. Returns true if a host was really marked as dead or was already marked dead (e.g. by a concurrent session). */ static int mark_host_dead (const char *name) { const char *host; char *host_buffer = NULL; parsed_uri_t parsed_uri = NULL; int done = 0; - if (name && *name && !http_parse_uri (&parsed_uri, name, 1)) + if (name && *name + && !http_parse_uri (&parsed_uri, name, HTTP_PARSE_NO_SCHEME_CHECK)) { if (parsed_uri->v6lit) { host_buffer = strconcat ("[", parsed_uri->host, "]", NULL); if (!host_buffer) log_error ("out of core in mark_host_dead"); host = host_buffer; } else host = parsed_uri->host; } else host = name; if (host && *host && strcmp (host, "localhost")) { hostinfo_t hi; int idx; idx = find_hostinfo (host); if (idx != -1) { hi = hosttable[idx]; log_info ("marking host '%s' as dead%s\n", hi->name, hi->dead? " (again)":""); hi->dead = 1; hi->died_at = gnupg_get_time (); if (!hi->died_at) hi->died_at = 1; done = 1; } } http_release_parsed_uri (parsed_uri); xfree (host_buffer); return done; } /* Mark a host in the hosttable as dead or - if ALIVE is true - as alive. */ gpg_error_t ks_hkp_mark_host (ctrl_t ctrl, const char *name, int alive) { gpg_error_t err = 0; hostinfo_t hi, hi2; int idx, idx2, idx3, n; if (!name || !*name || !strcmp (name, "localhost")) return 0; if (npth_mutex_lock (&hosttable_lock)) log_fatal ("failed to acquire mutex\n"); idx = find_hostinfo (name); if (idx == -1) { err = gpg_error (GPG_ERR_NOT_FOUND); goto leave; } hi = hosttable[idx]; if (alive && hi->dead) { hi->dead = 0; err = ks_printf_help (ctrl, "marking '%s' as alive", name); } else if (!alive && !hi->dead) { hi->dead = 1; hi->died_at = 0; /* Manually set dead. */ err = ks_printf_help (ctrl, "marking '%s' as dead", name); } /* If the host is a pool mark all member hosts. */ if (!err && hi->pool) { for (idx2 = 0; !err && idx2 < hi->pool_len && (n = hi->pool[idx2]) != -1; idx2++) { assert (n >= 0 && n < hosttable_size); if (!alive) { /* Do not mark a host from a pool dead if it is also a member in another pool. */ for (idx3=0; idx3 < hosttable_size; idx3++) { if (hosttable[idx3] && hosttable[idx3]->pool && idx3 != idx && host_in_pool_p (hosttable[idx3], n)) break; } if (idx3 < hosttable_size) continue; /* Host is also a member of another pool. */ } hi2 = hosttable[n]; if (!hi2) ; else if (alive && hi2->dead) { hi2->dead = 0; err = ks_printf_help (ctrl, "marking '%s' as alive", hi2->name); } else if (!alive && !hi2->dead) { hi2->dead = 1; hi2->died_at = 0; /* Manually set dead. */ err = ks_printf_help (ctrl, "marking '%s' as dead", hi2->name); } } } leave: if (npth_mutex_unlock (&hosttable_lock)) log_fatal ("failed to release mutex\n"); return err; } /* Debug function to print the entire hosttable. */ gpg_error_t ks_hkp_print_hosttable (ctrl_t ctrl) { gpg_error_t err; int idx, idx2; hostinfo_t hi; membuf_t mb; time_t curtime; char *p, *died; const char *diedstr; err = ks_print_help (ctrl, "hosttable (idx, ipv6, ipv4, dead, name, time):"); if (err) return err; if (npth_mutex_lock (&hosttable_lock)) log_fatal ("failed to acquire mutex\n"); curtime = gnupg_get_time (); for (idx=0; idx < hosttable_size; idx++) if ((hi=hosttable[idx])) { if (hi->dead && hi->died_at) { died = elapsed_time_string (hi->died_at, curtime); diedstr = died? died : "error"; } else diedstr = died = NULL; if (!hi->iporname_valid) { char *canon = NULL; xfree (hi->iporname); hi->iporname = NULL; /* Do a lookup just for the display purpose. */ if (hi->onion || hi->pool) ; else if (is_ip_address (hi->name)) { dns_addrinfo_t aibuf, ai; /* Turn the numerical IP address string into an AI and * then do a DNS PTR lookup. */ if (!resolve_dns_name (ctrl, hi->name, 0, 0, SOCK_STREAM, &aibuf, &canon)) { if (canon && is_ip_address (canon)) { xfree (canon); canon = NULL; } for (ai = aibuf; !canon && ai; ai = ai->next) { resolve_dns_addr (ctrl, ai->addr, ai->addrlen, DNS_WITHBRACKET, &canon); if (canon && is_ip_address (canon)) { /* We already have the numeric IP - no need to * display it a second time. */ xfree (canon); canon = NULL; } } } free_dns_addrinfo (aibuf); } else { dns_addrinfo_t aibuf, ai; /* Get the IP address as a string from a name. Note * that resolve_dns_addr allocates CANON on success * and thus terminates the loop. */ if (!resolve_dns_name (ctrl, hi->name, 0, hi->v6? AF_INET6 : AF_INET, SOCK_STREAM, &aibuf, NULL)) { for (ai = aibuf; !canon && ai; ai = ai->next) { resolve_dns_addr (ctrl, ai->addr, ai->addrlen, DNS_NUMERICHOST|DNS_WITHBRACKET, &canon); } } free_dns_addrinfo (aibuf); } hi->iporname = canon; hi->iporname_valid = 1; } err = ks_printf_help (ctrl, "%3d %s %s %s %s%s%s%s%s%s%s\n", idx, hi->onion? "O" : hi->v6? "6":" ", hi->v4? "4":" ", hi->dead? "d":" ", hi->name, hi->iporname? " (":"", hi->iporname? hi->iporname : "", hi->iporname? ")":"", diedstr? " (":"", diedstr? diedstr:"", diedstr? ")":"" ); xfree (died); if (err) goto leave; if (hi->cname) err = ks_printf_help (ctrl, " . %s", hi->cname); if (err) goto leave; if (hi->pool) { init_membuf (&mb, 256); put_membuf_printf (&mb, " . -->"); for (idx2 = 0; idx2 < hi->pool_len && hi->pool[idx2] != -1; idx2++) { put_membuf_printf (&mb, " %d", hi->pool[idx2]); if (hi->poolidx == hi->pool[idx2]) put_membuf_printf (&mb, "*"); } put_membuf( &mb, "", 1); p = get_membuf (&mb, NULL); if (!p) { err = gpg_error_from_syserror (); goto leave; } err = ks_print_help (ctrl, p); xfree (p); if (err) goto leave; } } leave: if (npth_mutex_unlock (&hosttable_lock)) log_fatal ("failed to release mutex\n"); return err; } /* Print a help output for the schemata supported by this module. */ gpg_error_t ks_hkp_help (ctrl_t ctrl, parsed_uri_t uri) { const char data[] = "Handler for HKP URLs:\n" " hkp://\n" #if HTTP_USE_GNUTLS || HTTP_USE_NTBTLS " hkps://\n" #endif "Supported methods: search, get, put\n"; gpg_error_t err; #if HTTP_USE_GNUTLS || HTTP_USE_NTBTLS const char data2[] = " hkp\n hkps"; #else const char data2[] = " hkp"; #endif if (!uri) err = ks_print_help (ctrl, data2); else if (uri->is_http && (!strcmp (uri->scheme, "hkp") || !strcmp (uri->scheme, "hkps"))) err = ks_print_help (ctrl, data); else err = 0; return err; } /* Build the remote part of the URL from SCHEME, HOST and an optional * PORT. If NO_SRV is set no SRV record lookup will be done. Returns * an allocated string at R_HOSTPORT or NULL on failure. If * R_HTTPHOST is not NULL it receives a malloced string with the * hostname; this may be different from HOST if HOST is selected from * a pool. */ static gpg_error_t make_host_part (ctrl_t ctrl, const char *scheme, const char *host, unsigned short port, int force_reselect, int no_srv, char **r_hostport, unsigned int *r_httpflags, char **r_httphost) { gpg_error_t err; const char *srvtag; char portstr[10]; char *hostname; enum ks_protocol protocol; *r_hostport = NULL; if (!strcmp (scheme, "hkps") || !strcmp (scheme,"https")) { scheme = "https"; srvtag = no_srv? NULL : "pgpkey-https"; protocol = KS_PROTOCOL_HKPS; } else /* HKP or HTTP. */ { scheme = "http"; srvtag = no_srv? NULL : "pgpkey-http"; protocol = KS_PROTOCOL_HKP; } if (npth_mutex_lock (&hosttable_lock)) log_fatal ("failed to acquire mutex\n"); portstr[0] = 0; err = map_host (ctrl, host, srvtag, force_reselect, protocol, &hostname, portstr, r_httpflags, r_httphost); if (npth_mutex_unlock (&hosttable_lock)) log_fatal ("failed to release mutex\n"); if (err) return err; /* If map_host did not return a port (from a SRV record) but a port * has been specified (implicitly or explicitly) then use that port. * In the case that a port was not specified (which is probably a * bug in https.c) we will set up defaults. */ if (*portstr) ; else if (!*portstr && port) snprintf (portstr, sizeof portstr, "%hu", port); else if (!strcmp (scheme,"https")) strcpy (portstr, "443"); else strcpy (portstr, "11371"); if (*hostname != '[' && is_ip_address (hostname) == 6) *r_hostport = strconcat (scheme, "://[", hostname, "]:", portstr, NULL); else *r_hostport = strconcat (scheme, "://", hostname, ":", portstr, NULL); xfree (hostname); if (!*r_hostport) { if (r_httphost) { xfree (*r_httphost); *r_httphost = NULL; } return gpg_error_from_syserror (); } return 0; } /* Resolve all known keyserver names and update the hosttable. This is mainly useful for debugging because the resolving is anyway done on demand. */ gpg_error_t ks_hkp_resolve (ctrl_t ctrl, parsed_uri_t uri) { gpg_error_t err; char *hostport = NULL; /* NB: With an explicitly given port we do not want to consult a * service record because that might be in conflict with the port * from such a service record. */ err = make_host_part (ctrl, uri->scheme, uri->host, uri->port, 1, uri->explicit_port, &hostport, NULL, NULL); if (err) { err = ks_printf_help (ctrl, "%s://%s:%hu: resolve failed: %s", uri->scheme, uri->host, uri->port, gpg_strerror (err)); } else { err = ks_printf_help (ctrl, "%s", hostport); xfree (hostport); } return err; } /* Housekeeping function called from the housekeeping thread. It is used to mark dead hosts alive so that they may be tried again after some time. */ void ks_hkp_housekeeping (time_t curtime) { int idx; hostinfo_t hi; if (npth_mutex_lock (&hosttable_lock)) log_fatal ("failed to acquire mutex\n"); for (idx=0; idx < hosttable_size; idx++) { hi = hosttable[idx]; if (!hi) continue; if (!hi->dead) continue; if (!hi->died_at) continue; /* Do not resurrect manually shot hosts. */ if (hi->died_at + RESURRECT_INTERVAL <= curtime || hi->died_at > curtime) { hi->dead = 0; log_info ("resurrected host '%s'", hi->name); } } if (npth_mutex_unlock (&hosttable_lock)) log_fatal ("failed to release mutex\n"); } /* Reload (SIGHUP) action for this module. We mark all host alive * even those which have been manually shot. */ void ks_hkp_reload (void) { int idx, count; hostinfo_t hi; if (npth_mutex_lock (&hosttable_lock)) log_fatal ("failed to acquire mutex\n"); for (idx=count=0; idx < hosttable_size; idx++) { hi = hosttable[idx]; if (!hi) continue; hi->iporname_valid = 0; if (!hi->dead) continue; hi->dead = 0; count++; } if (count) log_info ("number of resurrected hosts: %d", count); if (npth_mutex_unlock (&hosttable_lock)) log_fatal ("failed to release mutex\n"); } /* Send an HTTP request. On success returns an estream object at R_FP. HOSTPORTSTR is only used for diagnostics. If HTTPHOST is not NULL it will be used as HTTP "Host" header. If POST_CB is not NULL a post request is used and that callback is called to allow writing the post data. If R_HTTP_STATUS is not NULL, the http status code will be stored there. */ static gpg_error_t send_request (ctrl_t ctrl, const char *request, const char *hostportstr, const char *httphost, unsigned int httpflags, gpg_error_t (*post_cb)(void *, http_t), void *post_cb_value, estream_t *r_fp, unsigned int *r_http_status) { gpg_error_t err; http_session_t session = NULL; http_t http = NULL; http_redir_info_t redirinfo = { MAX_REDIRECTS }; estream_t fp = NULL; char *request_buffer = NULL; parsed_uri_t uri = NULL; *r_fp = NULL; err = http_parse_uri (&uri, request, 0); if (err) goto leave; redirinfo.ctrl = ctrl; redirinfo.orig_url = request; redirinfo.orig_onion = uri->onion; redirinfo.allow_downgrade = 1; /* FIXME: I am not sure whey we allow a downgrade for hkp requests. * Needs at least an explanation here.. */ once_more: err = http_session_new (&session, httphost, ((ctrl->http_no_crl? HTTP_FLAG_NO_CRL : 0) | HTTP_FLAG_TRUST_DEF), gnupg_http_tls_verify_cb, ctrl); if (err) goto leave; http_session_set_log_cb (session, cert_log_cb); http_session_set_timeout (session, ctrl->timeout); err = http_open (ctrl, &http, post_cb? HTTP_REQ_POST : HTTP_REQ_GET, request, httphost, /* fixme: AUTH */ NULL, (httpflags |(opt.honor_http_proxy? HTTP_FLAG_TRY_PROXY:0) |(dirmngr_use_tor ()? HTTP_FLAG_FORCE_TOR:0) |(opt.disable_ipv4? HTTP_FLAG_IGNORE_IPv4 : 0) |(opt.disable_ipv6? HTTP_FLAG_IGNORE_IPv6 : 0)), ctrl->http_proxy, session, NULL, /*FIXME curl->srvtag*/NULL); if (!err) { fp = http_get_write_ptr (http); /* Avoid caches to get the most recent copy of the key. We set both the Pragma and Cache-Control versions of the header, so we're good with both HTTP 1.0 and 1.1. */ es_fputs ("Pragma: no-cache\r\n" "Cache-Control: no-cache\r\n", fp); if (post_cb) err = post_cb (post_cb_value, http); if (!err) { http_start_data (http); if (es_ferror (fp)) err = gpg_error_from_syserror (); } } if (err) { /* Fixme: After a redirection we show the old host name. */ log_error (_("error connecting to '%s': %s\n"), hostportstr, gpg_strerror (err)); goto leave; } /* Wait for the response. */ dirmngr_tick (ctrl); err = http_wait_response (http); if (err) { log_error (_("error reading HTTP response for '%s': %s\n"), hostportstr, gpg_strerror (err)); goto leave; } if (http_get_tls_info (http, NULL)) { /* Update the httpflags so that a redirect won't fallback to an unencrypted connection. */ httpflags |= HTTP_FLAG_FORCE_TLS; } if (r_http_status) *r_http_status = http_get_status_code (http); switch (http_get_status_code (http)) { case 200: err = 0; break; /* Success. */ case 301: case 302: case 307: { xfree (request_buffer); err = http_prepare_redirect (&redirinfo, http_get_status_code (http), http_get_header (http, "Location"), &request_buffer); if (err) goto leave; request = request_buffer; http_close (http, 0); http = NULL; http_session_release (session); session = NULL; } goto once_more; case 501: err = gpg_error (GPG_ERR_NOT_IMPLEMENTED); goto leave; case 413: /* Payload too large */ err = gpg_error (GPG_ERR_TOO_LARGE); goto leave; default: log_error (_("error accessing '%s': http status %u\n"), request, http_get_status_code (http)); err = gpg_error (GPG_ERR_NO_DATA); goto leave; } /* FIXME: We should register a permanent redirection and whether a host has ever used TLS so that future calls will always use TLS. */ fp = http_get_read_ptr (http); if (!fp) { err = gpg_error (GPG_ERR_BUG); goto leave; } /* Return the read stream and close the HTTP context. */ *r_fp = fp; http_close (http, 1); http = NULL; leave: http_close (http, 0); http_session_release (session); xfree (request_buffer); http_release_parsed_uri (uri); return err; } /* Helper to evaluate the error code ERR from a send_request() call with REQUEST. The function returns true if the caller shall try again. TRIES_LEFT points to a variable to track the number of retries; this function decrements it and won't return true if it is down to zero. EXTRA_TRIES_LEFT does the same but only for transient http status codes. */ static int handle_send_request_error (ctrl_t ctrl, gpg_error_t err, const char *request, unsigned int http_status, unsigned int *tries_left, unsigned int *extra_tries_left) { int retry = 0; /* Fixme: Should we disable all hosts of a protocol family if a * request for an address of that family returned ENETDOWN? */ switch (gpg_err_code (err)) { case GPG_ERR_ECONNREFUSED: if (tor_not_running_p (ctrl)) break; /* A retry does not make sense. */ /* Okay: Tor is up or --use-tor is not used. */ /*FALLTHRU*/ case GPG_ERR_ENETUNREACH: case GPG_ERR_ENETDOWN: case GPG_ERR_UNKNOWN_HOST: case GPG_ERR_NETWORK: case GPG_ERR_EIO: /* Sometimes used by estream cookie functions. */ case GPG_ERR_EADDRNOTAVAIL: /* e.g. when IPv6 is disabled */ case GPG_ERR_EAFNOSUPPORT: /* e.g. when IPv6 is not compiled in */ if (mark_host_dead (request) && *tries_left) retry = 1; break; case GPG_ERR_ETIMEDOUT: if (*tries_left) { log_info ("selecting a different host due to a timeout\n"); retry = 1; } break; case GPG_ERR_EACCES: if (dirmngr_use_tor ()) { log_info ("(Tor configuration problem)\n"); dirmngr_status (ctrl, "WARNING", "tor_config_problem 0", "Please check that the \"SocksPort\" flag " "\"IPv6Traffic\" is set in torrc", NULL); } break; case GPG_ERR_NO_DATA: { switch (http_status) { case 502: /* Bad Gateway */ log_info ("marking host dead due to a %u (%s)\n", http_status, http_status2string (http_status)); if (mark_host_dead (request) && *tries_left) retry = 1; break; case 503: /* Service Unavailable */ case 504: /* Gateway Timeout */ if (*extra_tries_left) { log_info ("selecting a different host due to a %u (%s)", http_status, http_status2string (http_status)); retry = 2; } break; } } break; default: break; } if (retry == 2) { if (*extra_tries_left) --*extra_tries_left; } else { if (*tries_left) --*tries_left; } return retry; } /* Search the keyserver identified by URI for keys matching PATTERN. On success R_FP has an open stream to read the data. If R_HTTP_STATUS is not NULL, the http status code will be stored there. */ gpg_error_t ks_hkp_search (ctrl_t ctrl, parsed_uri_t uri, const char *pattern, estream_t *r_fp, unsigned int *r_http_status) { gpg_error_t err; KEYDB_SEARCH_DESC desc; char fprbuf[2+64+1]; char *hostport = NULL; char *request = NULL; estream_t fp = NULL; int reselect; unsigned int httpflags; char *httphost = NULL; unsigned int http_status; unsigned int tries = SEND_REQUEST_RETRIES; unsigned int extra_tries = SEND_REQUEST_EXTRA_RETRIES; *r_fp = NULL; /* Remove search type indicator and adjust PATTERN accordingly. Note that HKP keyservers like the 0x to be present when searching by keyid. We need to re-format the fingerprint and keyids so to remove the gpg specific force-use-of-this-key flag ("!"). */ err = classify_user_id (pattern, &desc, 1); if (err) return err; log_assert (desc.fprlen <= 64); switch (desc.mode) { case KEYDB_SEARCH_MODE_EXACT: case KEYDB_SEARCH_MODE_SUBSTR: case KEYDB_SEARCH_MODE_MAIL: case KEYDB_SEARCH_MODE_MAILSUB: pattern = desc.u.name; break; case KEYDB_SEARCH_MODE_SHORT_KID: snprintf (fprbuf, sizeof fprbuf, "0x%08lX", (ulong)desc.u.kid[1]); pattern = fprbuf; break; case KEYDB_SEARCH_MODE_LONG_KID: snprintf (fprbuf, sizeof fprbuf, "0x%08lX%08lX", (ulong)desc.u.kid[0], (ulong)desc.u.kid[1]); pattern = fprbuf; break; case KEYDB_SEARCH_MODE_FPR: fprbuf[0] = '0'; fprbuf[1] = 'x'; bin2hex (desc.u.fpr, desc.fprlen, fprbuf+2); pattern = fprbuf; break; default: return gpg_error (GPG_ERR_INV_USER_ID); } /* Build the request string. */ reselect = 0; again: { char *searchkey; xfree (hostport); hostport = NULL; xfree (httphost); httphost = NULL; err = make_host_part (ctrl, uri->scheme, uri->host, uri->port, reselect, uri->explicit_port, &hostport, &httpflags, &httphost); if (err) goto leave; searchkey = http_escape_string (pattern, EXTRA_ESCAPE_CHARS); if (!searchkey) { err = gpg_error_from_syserror (); goto leave; } xfree (request); request = strconcat (hostport, "/pks/lookup?op=index&options=mr&search=", searchkey, NULL); xfree (searchkey); if (!request) { err = gpg_error_from_syserror (); goto leave; } } /* Send the request. */ err = send_request (ctrl, request, hostport, httphost, httpflags, NULL, NULL, &fp, &http_status); if (handle_send_request_error (ctrl, err, request, http_status, &tries, &extra_tries)) { reselect = 1; goto again; } if (r_http_status) *r_http_status = http_status; if (err) { if (gpg_err_code (err) == GPG_ERR_NO_DATA) dirmngr_status (ctrl, "SOURCE", hostport, NULL); goto leave; } err = dirmngr_status (ctrl, "SOURCE", hostport, NULL); if (err) goto leave; /* Peek at the response. */ { int c = es_getc (fp); if (c == -1) { err = es_ferror (fp)?gpg_error_from_syserror ():gpg_error (GPG_ERR_EOF); log_error ("error reading response: %s\n", gpg_strerror (err)); goto leave; } if (c == '<') { /* The document begins with a '<': Assume a HTML response, which we don't support. */ err = gpg_error (GPG_ERR_UNSUPPORTED_ENCODING); goto leave; } es_ungetc (c, fp); } /* Return the read stream. */ *r_fp = fp; fp = NULL; leave: es_fclose (fp); xfree (request); xfree (hostport); xfree (httphost); return err; } /* 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. The data will be provided in a format GnuPG can import (either a binary OpenPGP message or an armored one). */ gpg_error_t ks_hkp_get (ctrl_t ctrl, parsed_uri_t uri, const char *keyspec, estream_t *r_fp) { gpg_error_t err; KEYDB_SEARCH_DESC desc; char kidbuf[2+64+1]; const char *exactname = NULL; char *searchkey = NULL; char *hostport = NULL; char *request = NULL; estream_t fp = NULL; int reselect; char *httphost = NULL; unsigned int httpflags; unsigned int http_status; unsigned int tries = SEND_REQUEST_RETRIES; unsigned int extra_tries = SEND_REQUEST_EXTRA_RETRIES; *r_fp = NULL; /* Remove search type indicator and adjust PATTERN accordingly. Note that HKP keyservers like the 0x to be present when searching by keyid. We need to re-format the fingerprint and keyids so to remove the gpg specific force-use-of-this-key flag ("!"). */ err = classify_user_id (keyspec, &desc, 1); if (err) return err; log_assert (desc.fprlen <= 64); switch (desc.mode) { case KEYDB_SEARCH_MODE_SHORT_KID: snprintf (kidbuf, sizeof kidbuf, "0x%08lX", (ulong)desc.u.kid[1]); break; case KEYDB_SEARCH_MODE_LONG_KID: snprintf (kidbuf, sizeof kidbuf, "0x%08lX%08lX", (ulong)desc.u.kid[0], (ulong)desc.u.kid[1]); break; case KEYDB_SEARCH_MODE_FPR: if (desc.fprlen < 20) { log_error ("HKP keyservers do not support v3 fingerprints\n"); return gpg_error (GPG_ERR_INV_USER_ID); } kidbuf[0] = '0'; kidbuf[1] = 'x'; bin2hex (desc.u.fpr, desc.fprlen, kidbuf+2); break; case KEYDB_SEARCH_MODE_EXACT: exactname = desc.u.name; break; default: return gpg_error (GPG_ERR_INV_USER_ID); } searchkey = http_escape_string (exactname? exactname : kidbuf, EXTRA_ESCAPE_CHARS); if (!searchkey) { err = gpg_error_from_syserror (); goto leave; } reselect = 0; again: /* Build the request string. */ xfree (hostport); hostport = NULL; xfree (httphost); httphost = NULL; err = make_host_part (ctrl, uri->scheme, uri->host, uri->port, reselect, uri->explicit_port, &hostport, &httpflags, &httphost); if (err) goto leave; xfree (request); request = strconcat (hostport, "/pks/lookup?op=get&options=mr&search=", searchkey, exactname? "&exact=on":"", NULL); if (!request) { err = gpg_error_from_syserror (); goto leave; } /* Send the request. */ err = send_request (ctrl, request, hostport, httphost, httpflags, NULL, NULL, &fp, &http_status); if (handle_send_request_error (ctrl, err, request, http_status, &tries, &extra_tries)) { reselect = 1; goto again; } if (err) { if (gpg_err_code (err) == GPG_ERR_NO_DATA) dirmngr_status (ctrl, "SOURCE", hostport, NULL); goto leave; } err = dirmngr_status (ctrl, "SOURCE", hostport, NULL); if (err) goto leave; /* Return the read stream and close the HTTP context. */ *r_fp = fp; fp = NULL; leave: es_fclose (fp); xfree (request); xfree (hostport); xfree (httphost); xfree (searchkey); return err; } /* Callback parameters for put_post_cb. */ struct put_post_parm_s { char *datastring; }; /* Helper for ks_hkp_put. */ static gpg_error_t put_post_cb (void *opaque, http_t http) { struct put_post_parm_s *parm = opaque; gpg_error_t err = 0; estream_t fp; size_t len; fp = http_get_write_ptr (http); len = strlen (parm->datastring); es_fprintf (fp, "Content-Type: application/x-www-form-urlencoded\r\n" "Content-Length: %zu\r\n", len+8 /* 8 is for "keytext" */); http_start_data (http); if (es_fputs ("keytext=", fp) || es_write (fp, parm->datastring, len, NULL)) err = gpg_error_from_syserror (); return err; } /* Send the key in {DATA,DATALEN} to the keyserver identified by URI. */ gpg_error_t ks_hkp_put (ctrl_t ctrl, parsed_uri_t uri, const void *data, size_t datalen) { gpg_error_t err; char *hostport = NULL; char *request = NULL; estream_t fp = NULL; struct put_post_parm_s parm; char *armored = NULL; int reselect; char *httphost = NULL; unsigned int httpflags; unsigned int http_status; unsigned int tries = SEND_REQUEST_RETRIES; unsigned int extra_tries = SEND_REQUEST_EXTRA_RETRIES; parm.datastring = NULL; err = armor_data (&armored, data, datalen); if (err) goto leave; parm.datastring = http_escape_string (armored, EXTRA_ESCAPE_CHARS); if (!parm.datastring) { err = gpg_error_from_syserror (); goto leave; } xfree (armored); armored = NULL; /* Build the request string. */ reselect = 0; again: xfree (hostport); hostport = NULL; xfree (httphost); httphost = NULL; err = make_host_part (ctrl, uri->scheme, uri->host, uri->port, reselect, uri->explicit_port, &hostport, &httpflags, &httphost); if (err) goto leave; xfree (request); request = strconcat (hostport, "/pks/add", NULL); if (!request) { err = gpg_error_from_syserror (); goto leave; } /* Send the request. */ err = send_request (ctrl, request, hostport, httphost, 0, put_post_cb, &parm, &fp, &http_status); if (handle_send_request_error (ctrl, err, request, http_status, &tries, &extra_tries)) { reselect = 1; goto again; } if (err) goto leave; leave: es_fclose (fp); xfree (parm.datastring); xfree (armored); xfree (request); xfree (hostport); xfree (httphost); return err; } void ks_hkp_init (void) { int err; err = npth_mutex_init (&hosttable_lock, NULL); if (err) log_fatal ("error initializing mutex: %s\n", strerror (err)); } diff --git a/dirmngr/ks-engine-ldap.c b/dirmngr/ks-engine-ldap.c index da3d9ee1b..b7ff0633c 100644 --- a/dirmngr/ks-engine-ldap.c +++ b/dirmngr/ks-engine-ldap.c @@ -1,2280 +1,2280 @@ /* ks-engine-ldap.c - talk to a LDAP keyserver * Copyright (C) 2001, 2002, 2004, 2005, 2006 * 2007 Free Software Foundation, Inc. * Copyright (C) 2015, 2020 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 . */ #include #include #include #include #include #ifdef HAVE_GETOPT_H # include #endif #include #include #include #ifdef _WIN32 # include # include #else # ifdef NEED_LBER_H # include # endif /* For OpenLDAP, to enable the API that we're using. */ # define LDAP_DEPRECATED 1 # include #endif #include #include "dirmngr.h" #include "misc.h" #include "../common/userids.h" #include "../common/mbox-util.h" #include "ks-engine.h" #include "ldap-parse-uri.h" /* Flags with infos from the connected server. */ #define SERVERINFO_REALLDAP 1 /* This is not the PGP keyserver. */ #define SERVERINFO_PGPKEYV2 2 /* Needs "pgpeyV2" instead of "pgpKey" */ #define SERVERINFO_SCHEMAV2 4 /* Version 2 of the Schema. */ #define SERVERINFO_NTDS 8 /* Server is an Active Directory. */ #ifndef HAVE_TIMEGM time_t timegm(struct tm *tm); #endif /* 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_INTERNAL; #endif } 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 ++; snprintf (buf, sizeof 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 static void my_ldap_value_free (char **vals) { if (vals) ldap_value_free (vals); } /* 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 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" + "operation will be aborted. Note that ldaps means LDAP with STARTTLS\n" "\n" "Supported methods: search, get, put\n"; gpg_error_t err; if(!uri) err = ks_print_help (ctrl, " ldap"); else if (uri->is_ldap) err = ks_print_help (ctrl, data); else err = 0; return err; } /* 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, unsigned int serverinfo) { /* 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; char *freeme = NULL; char *p; 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)", (freeme = ldap_escape_filter (desc.u.name))); break; case KEYDB_SEARCH_MODE_SUBSTR: if (! only_exact) f = xasprintf ("(pgpUserID=*%s*)", (freeme = ldap_escape_filter (desc.u.name))); break; case KEYDB_SEARCH_MODE_MAIL: freeme = ldap_escape_filter (desc.u.name); if (!freeme) break; if (*freeme == '<' && freeme[1] && freeme[2]) { /* Strip angle brackets. Note that it is does not * matter whether we work on the plan or LDAP escaped * version of the mailbox. */ p = freeme + 1; if (p[strlen(p)-1] == '>') p[strlen(p)-1] = 0; } else p = freeme; if ((serverinfo & SERVERINFO_SCHEMAV2)) f = xasprintf ("(&(gpgMailbox=%s)(!(|(pgpRevoked=1)(pgpDisabled=1))))", p); else if (!only_exact) f = xasprintf ("(pgpUserID=*<%s>*)", p); break; case KEYDB_SEARCH_MODE_MAILSUB: if (! only_exact) f = xasprintf ("(pgpUserID=*<*%s*>*)", (freeme = ldap_escape_filter (desc.u.name))); break; case KEYDB_SEARCH_MODE_MAILEND: if (! only_exact) f = xasprintf ("(pgpUserID=*<*%s>*)", (freeme = 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_FPR: if ((serverinfo & SERVERINFO_SCHEMAV2)) { freeme = bin2hex (desc.u.fpr, desc.fprlen, NULL); if (!freeme) return gpg_error_from_syserror (); f = xasprintf ("(|(gpgFingerprint=%s)(gpgSubFingerprint=%s))", freeme, freeme); /* FIXME: For an exact search and in case of a match on * gpgSubFingerprint we need to check that there is only one * matching value. */ } break; 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; } xfree (freeme); if (! f) { log_error ("Unsupported search mode.\n"); return gpg_error (GPG_ERR_NOT_SUPPORTED); } *filter = f; return 0; } /* 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. 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 occurred, *basednp, etc., are undefined (and don't need to be freed.) R_SERVERINFO receives information about the server. If no LDAP error occurred, you still need to check that *basednp is valid. If it is NULL, then the server does not appear to be an OpenPGP Keyserver. */ static int my_ldap_connect (parsed_uri_t uri, LDAP **ldap_connp, char **basednp, unsigned int *r_serverinfo) { int err = 0; LDAP *ldap_conn = NULL; char *user = uri->auth; struct uri_tuple_s *password_param; char *password; char *basedn = NULL; *r_serverinfo = 0; password_param = uri_query_lookup (uri, "password"); password = password_param ? password_param->value : NULL; if (opt.debug) log_debug ("my_ldap_connect(%s:%d/%s????%s%s%s%s%s%s)\n", uri->host, uri->port, uri->path ? uri->path : "", uri->auth ? "bindname=" : "", uri->auth ? uri->auth : "", uri->auth && password ? "," : "", password ? "password=" : "", password ? ">not shown<": "", uri->ad_current? " auth=>current_user<":""); /* 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) { err = gpg_err_code_from_syserror (); log_error ("error initializing LDAP for (%s://%s:%d)\n", uri->scheme, uri->host, uri->port); 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 ("ks-ldap: 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 #ifdef 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. */ #ifndef HAVE_W32_SYSTEM 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 ("error setting TLS option on LDAP connection\n"); goto out; } #else /* On Windows, the certificates are checked by default. If the option to disable checking mentioned above is ever implemented, the way to do that on Windows is to install a callback routine using ldap_set_option (.., LDAP_OPT_SERVER_CERTIFICATE, ..); */ #endif npth_unprotect (); err = ldap_start_tls_s (ldap_conn, #ifdef HAVE_W32_SYSTEM /* ServerReturnValue, result */ NULL, NULL, #endif /* ServerControls, ClientControls */ NULL, NULL); npth_protect (); if (err) { log_error ("error connecting to LDAP server with TLS\n"); goto out; } } #endif if (uri->ad_current) { if (opt.debug) log_debug ("LDAP bind to current user via AD\n"); #ifdef HAVE_W32_SYSTEM npth_unprotect (); err = ldap_bind_s (ldap_conn, NULL, NULL, LDAP_AUTH_NEGOTIATE); npth_protect (); if (err != LDAP_SUCCESS) { log_error ("error binding to LDAP via AD: %s\n", ldap_err2string (err)); goto out; } #else err = gpg_error (GPG_ERR_NOT_SUPPORTED); goto out; #endif } else if (uri->auth) { if (opt.debug) log_debug ("LDAP bind to %s, password %s\n", user, password ? ">not shown<" : ">none<"); npth_unprotect (); err = ldap_simple_bind_s (ldap_conn, user, password); npth_protect (); if (err != LDAP_SUCCESS) { log_error ("error binding to LDAP: %s\n", ldap_err2string (err)); goto out; } } else { /* By default we don't bind as there is usually no need to. */ } 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. */ *r_serverinfo |= SERVERINFO_REALLDAP; } else { /* Look for namingContexts. */ LDAPMessage *res = NULL; char *attr[] = { "namingContexts", NULL }; npth_unprotect (); err = ldap_search_s (ldap_conn, "", LDAP_SCOPE_BASE, "(objectClass=*)", attr, 0, &res); npth_protect (); if (err == LDAP_SUCCESS) { char **context; npth_unprotect (); context = ldap_get_values (ldap_conn, res, "namingContexts"); npth_protect (); 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 }; *r_serverinfo |= SERVERINFO_REALLDAP; for (i = 0; context[i] && ! basedn; i++) { char **vals; LDAPMessage *si_res; int is_gnupg = 0; { char *object = xasprintf ("cn=pgpServerInfo,%s", context[i]); npth_unprotect (); err = ldap_search_s (ldap_conn, object, LDAP_SCOPE_BASE, "(objectClass=*)", attr2, 0, &si_res); npth_protect (); xfree (object); } if (err == LDAP_SUCCESS) { vals = ldap_get_values (ldap_conn, si_res, "pgpBaseKeySpaceDN"); if (vals && vals[0]) { basedn = xtrystrdup (vals[0]); } my_ldap_value_free (vals); vals = ldap_get_values (ldap_conn, si_res, "pgpSoftware"); if (vals && vals[0]) { if (opt.debug) log_debug ("Server: \t%s\n", vals[0]); if (!ascii_strcasecmp (vals[0], "GnuPG")) is_gnupg = 1; } my_ldap_value_free (vals); vals = ldap_get_values (ldap_conn, si_res, "pgpVersion"); if (vals && vals[0]) { if (opt.debug) log_debug ("Version:\t%s\n", vals[0]); if (is_gnupg) { const char *fields[2]; int nfields; nfields = split_fields (vals[0], fields, DIM(fields)); if (nfields > 0 && atoi(fields[0]) > 1) *r_serverinfo |= SERVERINFO_SCHEMAV2; if (nfields > 1 && !ascii_strcasecmp (fields[1], "ntds")) *r_serverinfo |= SERVERINFO_NTDS; } } my_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 a PGP.com keyserver. */ char **vals; LDAPMessage *si_res = NULL; char *attr2[] = { "pgpBaseKeySpaceDN", "version", "software", NULL }; npth_unprotect (); err = ldap_search_s (ldap_conn, "cn=pgpServerInfo", LDAP_SCOPE_BASE, "(objectClass=*)", attr2, 0, &si_res); npth_protect (); if (err == LDAP_SUCCESS) { /* For the PGP 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 && vals[0]) { basedn = xtrystrdup (vals[0]); } my_ldap_value_free (vals); vals = ldap_get_values (ldap_conn, si_res, "software"); if (vals && vals[0]) { if (opt.debug) log_debug ("ks-ldap: PGP Server: \t%s\n", vals[0]); } my_ldap_value_free (vals); vals = ldap_get_values (ldap_conn, si_res, "version"); if (vals && vals[0]) { if (opt.debug) log_debug ("ks-ldap: PGP Server 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) *r_serverinfo |= SERVERINFO_PGPKEYV2; } my_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 && opt.debug) { log_debug ("ldap_conn: %p\n", ldap_conn); log_debug ("server_type: %s\n", ((*r_serverinfo & SERVERINFO_REALLDAP) ? "LDAP" : "PGP.com keyserver") ); log_debug ("basedn: %s\n", basedn); log_debug ("pgpkeyattr: %s\n", (*r_serverinfo & SERVERINFO_PGPKEYV2)? "pgpKeyV2":"pgpKey"); } if (err) xfree (basedn); else { 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); /* Note: ldap_get_values returns a NULL terminated array of strings. */ vals = ldap_get_values (ldap_conn, message, "gpgfingerprint"); if (vals && vals[0] && vals[0][0]) es_fprintf (output, "pub:%s:", vals[0]); else es_fprintf (output, "pub:%s:", certid); my_ldap_value_free (vals); 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"); } my_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); } my_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])); } my_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])); } my_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"); } my_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]); } my_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; unsigned int serverinfo; char *filter = NULL; LDAP *ldap_conn = NULL; char *basedn = NULL; estream_t fp = NULL; LDAPMessage *message = NULL; (void) ctrl; if (dirmngr_use_tor ()) { /* For now we do not support LDAP over Tor. */ log_error (_("LDAP access not possible due to Tor mode\n")); return gpg_error (GPG_ERR_NOT_SUPPORTED); } /* Make sure we are talking to an OpenPGP LDAP server. */ ldap_err = my_ldap_connect (uri, &ldap_conn, &basedn, &serverinfo); if (ldap_err || !basedn) { if (ldap_err) err = ldap_err_to_gpg_err (ldap_err); else err = gpg_error (GPG_ERR_GENERAL); goto out; } /* Now that we have information about the server we can construct a * query best suited for the capabilities of the server. */ err = keyspec_to_ldap_filter (keyspec, &filter, 1, serverinfo); if (err) goto out; if (opt.debug) log_debug ("ks-ldap: using filter: %s\n", filter); { /* The ordering is significant. Specifically, "pgpcertid" needs to be the second item in the list, since everything after it may be discarded if we aren't in verbose mode. */ char *attrs[] = { "dummy", "pgpcertid", "pgpuserid", "pgpkeyid", "pgprevoked", "pgpdisabled", "pgpkeycreatetime", "modifytimestamp", "pgpkeysize", "pgpkeytype", "gpgfingerprint", NULL }; /* 1 if we want just attribute types; 0 if we want both attribute * types and values. */ int attrsonly = 0; int count; /* Replace "dummy". */ attrs[0] = (serverinfo & SERVERINFO_PGPKEYV2)? "pgpKeyV2" : "pgpKey"; npth_unprotect (); ldap_err = ldap_search_s (ldap_conn, basedn, LDAP_SCOPE_SUBTREE, filter, attrs, attrsonly, &message); npth_protect (); if (ldap_err) { err = ldap_err_to_gpg_err (ldap_err); log_error ("ks-ldap: LDAP search error: %s\n", ldap_err2string (ldap_err)); goto out; } count = ldap_count_entries (ldap_conn, message); if (count < 1) { log_info ("ks-ldap: 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; int anykey = 0; for (npth_unprotect (), each = ldap_first_entry (ldap_conn, message), npth_protect (); each; npth_unprotect (), each = ldap_next_entry (ldap_conn, each), npth_protect ()) { 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, attrs[0]); if (! vals) { err = ldap_to_gpg_err (ldap_conn); log_error("ks-ldap: 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); anykey = 1; } } } my_ldap_value_free (certid); } free_strlist (seen); if (! fp) err = gpg_error (GPG_ERR_NO_DATA); if (!err && anykey) err = dirmngr_status_printf (ctrl, "SOURCE", "%s://%s", uri->scheme, uri->host? uri->host:""); } } 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 (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; unsigned int serverinfo; char *filter = NULL; LDAP *ldap_conn = NULL; char *basedn = NULL; estream_t fp = NULL; (void) ctrl; if (dirmngr_use_tor ()) { /* For now we do not support LDAP over Tor. */ log_error (_("LDAP access not possible due to Tor mode\n")); return gpg_error (GPG_ERR_NOT_SUPPORTED); } /* Make sure we are talking to an OpenPGP LDAP server. */ ldap_err = my_ldap_connect (uri, &ldap_conn, &basedn, &serverinfo); if (ldap_err || !basedn) { if (ldap_err) err = ldap_err_to_gpg_err (ldap_err); else err = GPG_ERR_GENERAL; goto out; } /* Now that we have information about the server we can construct a * query best suited for the capabilities of the server. */ err = keyspec_to_ldap_filter (pattern, &filter, 0, serverinfo); if (err) { log_error ("Bad search pattern: '%s'\n", pattern); goto out; } /* Even if we have no results, we want to return a stream. */ fp = es_fopenmem(0, "rw"); if (!fp) { err = gpg_error_from_syserror (); goto out; } { 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", "gpgfingerprint", NULL }; if (opt.debug) log_debug ("SEARCH '%s' => '%s' BEGIN\n", pattern, filter); npth_unprotect (); ldap_err = ldap_search_s (ldap_conn, basedn, LDAP_SCOPE_SUBTREE, filter, attrs, 0, &res); npth_protect (); xfree (filter); filter = NULL; if (ldap_err != LDAP_SUCCESS && ldap_err != LDAP_SIZELIMIT_EXCEEDED) { err = ldap_err_to_gpg_err (ldap_err); log_error ("SEARCH %s FAILED %d\n", pattern, err); log_error ("ks-ldap: 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 (npth_unprotect (), each = ldap_first_entry (ldap_conn, res), npth_protect (); each; npth_unprotect (), each = ldap_next_entry (ldap_conn, each), npth_protect ()) { 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++; } my_ldap_value_free (certid); } if (ldap_err == LDAP_SIZELIMIT_EXCEEDED) { if (count == 1) log_error ("ks-ldap: search results exceeded server limit." " First 1 result shown.\n"); else log_error ("ks-ldap: 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]) { my_ldap_value_free (certid); continue; } /* Have we seen this certid before? */ if (! strlist_find (dupelist, certid[0])) { add_to_strlist (&dupelist, certid[0]); vals = ldap_get_values (ldap_conn, each, "gpgfingerprint"); if (vals && vals[0] && vals[0][0]) es_fprintf (fp, "pub:%s:", vals[0]); else es_fprintf (fp, "pub:%s:", certid[0]); my_ldap_value_free (vals); vals = ldap_get_values (ldap_conn, each, "pgpkeytype"); if (vals && vals[0]) { /* 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); } my_ldap_value_free (vals); es_fputc (':', fp); vals = ldap_get_values (ldap_conn, each, "pgpkeysize"); if (vals && vals[0]) { /* 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])); } my_ldap_value_free (vals); es_fputc (':', fp); /* YYYYMMDDHHmmssZ */ vals = ldap_get_values (ldap_conn, each, "pgpkeycreatetime"); if(vals && vals[0] && strlen (vals[0]) == 15) { es_fprintf (fp, "%u", (unsigned int) ldap2epochtime(vals[0])); } my_ldap_value_free (vals); es_fputc (':', fp); vals = ldap_get_values (ldap_conn, each, "pgpkeyexpiretime"); if (vals && vals[0] && strlen (vals[0]) == 15) { es_fprintf (fp, "%u", (unsigned int) ldap2epochtime (vals[0])); } my_ldap_value_free (vals); es_fputc (':', fp); vals = ldap_get_values (ldap_conn, each, "pgprevoked"); if (vals && vals[0]) { if (atoi (vals[0]) == 1) es_fprintf (fp, "r"); } my_ldap_value_free (vals); vals = ldap_get_values (ldap_conn, each, "pgpdisabled"); if (vals && vals[0]) { if (atoi (vals[0]) ==1) es_fprintf (fp, "d"); } my_ldap_value_free (vals); #if 0 /* This is not yet specified in the keyserver protocol, but may be someday. */ es_fputc (':', fp); vals = ldap_get_values (ldap_conn, each, "modifytimestamp"); if(vals && vals[0] strlen (vals[0]) == 15) { es_fprintf (fp, "%u", (unsigned int) ldap2epochtime (vals[0])); } my_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 || !vals[0]) { my_ldap_value_free (vals); continue; } if (!ascii_strcasecmp (certid[0], vals[0])) { char **uidvals; es_fprintf (fp, "uid:"); uidvals = ldap_get_values (ldap_conn, uids, "pgpuserid"); if (uidvals) { /* Need to percent escape any colons */ char *quoted = try_percent_escape (uidvals[0], NULL); if (quoted) es_fputs (quoted, fp); xfree (quoted); } my_ldap_value_free (uidvals); es_fprintf (fp, "\n"); } ldap_value_free(vals); } } my_ldap_value_free (certid); } } ldap_msgfree (res); free_strlist (dupelist); } if (opt.debug) log_debug ("SEARCH %s END\n", pattern); out: if (err) { 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; } /* A modlist describes a set of changes to an LDAP entry. (An entry consists of 1 or more attributes. Attributes are 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); 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) GPGRT_ATTR_USED; static estream_t modlist_dump (LDAPMod **modlist, estream_t output) { LDAPMod **m; int opened = 0; if (! output) { output = es_fopenmem (0, "rw"); if (!output) return NULL; 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 ++) { /* Assuming terminals are about 80 characters wide, display at most about 10 lines of debugging output. If we do trim the buffer, append '...' to the end. */ const int max_len = 10 * 70; size_t value_len = strlen (*ptr); int elide = value_len > max_len; if (multi) es_fprintf (output, " %d. ", i); es_fprintf (output, "`%.*s", max_len, *ptr); if (elide) es_fprintf (output, "...' (%zd bytes elided)", value_len - max_len); else es_fprintf (output, "'"); 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++) xfree (*ptr); xfree (mod->mod_values); } xfree (mod); } xfree (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) { size_t r = 0; size_t 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? XXX: hextobyte already checks the string thus the hexdigitp could be removed. */ if (str[r] == '\\' && str[r + 1] == 'x' && str[r+2] && str[r+3] && hexdigitp (str + r + 2) && hexdigitp (str + r + 3)) { 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. EXTRACT_STATE should initally be set to 0 by the caller. SCHEMAV2 is set if the server supports the version 2 schema. */ static void extract_attributes (LDAPMod ***modlist, int *extract_state, char *line, int schemav2) { int field_count; char **fields; char *keyid; int is_pub, is_sub, is_uid, is_sig; /* Remove trailing whitespace */ trim_trailing_spaces (line); fields = strsplit (line, ':', '\0', &field_count); if (field_count == 1) /* We only have a single field. There is definitely nothing to do. */ goto out; if (field_count < 7) goto out; is_pub = !ascii_strcasecmp ("pub", fields[0]); is_sub = !ascii_strcasecmp ("sub", fields[0]); is_uid = !ascii_strcasecmp ("uid", fields[0]); is_sig = !ascii_strcasecmp ("sig", fields[0]); if (!ascii_strcasecmp ("fpr", fields[0])) { /* Special treatment for a fingerprint. */ if (!(*extract_state & 1)) goto out; /* Stray fingerprint line - ignore. */ *extract_state &= ~1; if (field_count >= 10 && schemav2) { if ((*extract_state & 2)) modlist_add (modlist, "gpgFingerprint", fields[9]); else modlist_add (modlist, "gpgSubFingerprint", fields[9]); } goto out; } *extract_state &= ~(1|2); if (is_pub) *extract_state |= (1|2); else if (is_sub) *extract_state |= 1; if (!is_pub && !is_sub && !is_uid && !is_sig) goto out; /* Not a relevant line. */ 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; int 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 padded[6]; int val; val = atoi (fields[2]); if (val < 99999 && val > 0) { /* We zero pad this on the left to make PGP happy. */ snprintf (padded, sizeof padded, "%05u", val); modlist_add (modlist, "pgpKeySize", padded); } } 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) modlist_add (modlist, "pgpKeyType", algo); } if (is_pub || is_sub || is_sig) { if (is_pub) { modlist_add (modlist, "pgpCertID", keyid); /* Long keyid(!) */ modlist_add (modlist, "pgpKeyID", &keyid[8]); /* Short keyid */ } if (is_sub) modlist_add (modlist, "pgpSubKeyID", keyid); /* Long 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 (!isodate_human_to_tm (create_time, &tm)) create_time = tm2ldaptime (&tm); else if ((t = parse_timestamp (create_time, &end)) != (time_t) -1 && *end == '\0') { if (!gnupg_gmtime (&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 (!isodate_human_to_tm (expire_time, &tm)) expire_time = tm2ldaptime (&tm); else if ((t = parse_timestamp (expire_time, &end)) != (time_t) -1 && *end == '\0') { if (!gnupg_gmtime (&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 && field_count >= 10) { char *uid = fields[9]; char *mbox; uncescape (uid); modlist_add (modlist, "pgpUserID", uid); if (schemav2 && (mbox = mailbox_from_userid (uid, 0))) { modlist_add (modlist, "gpgMailbox", mbox); xfree (mbox); } } out: xfree (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; unsigned int serverinfo; LDAP *ldap_conn = NULL; char *basedn = NULL; LDAPMod **modlist = NULL; LDAPMod **addlist = NULL; char *data_armored = NULL; int extract_state; /* 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; if (dirmngr_use_tor ()) { /* For now we do not support LDAP over Tor. */ log_error (_("LDAP access not possible due to Tor mode\n")); return gpg_error (GPG_ERR_NOT_SUPPORTED); } ldap_err = my_ldap_connect (uri, &ldap_conn, &basedn, &serverinfo); if (ldap_err || !basedn) { if (ldap_err) err = ldap_err_to_gpg_err (ldap_err); else err = GPG_ERR_GENERAL; goto out; } if (!(serverinfo & SERVERINFO_REALLDAP)) { /* We appear to have a PGP.com Keyserver, which can unpack the * key on its own (not just a dump LDAP server). This will * rarely be the case these days. */ LDAPMod mod; LDAPMod *attrs[2]; char *key[2]; char *dn; key[0] = data; key[1] = NULL; memset (&mod, 0, sizeof (mod)); mod.mod_op = LDAP_MOD_ADD; mod.mod_type = (serverinfo & SERVERINFO_PGPKEYV2)? "pgpKeyV2":"pgpKey"; mod.mod_values = key; attrs[0] = &mod; attrs[1] = NULL; dn = xtryasprintf ("pgpCertid=virtual,%s", basedn); if (!dn) { err = gpg_error_from_syserror (); goto out; } 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 = xtrymalloc (sizeof (LDAPMod *)); if (!modlist) { err = gpg_error_from_syserror (); goto out; } *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, "pgpRevoked", NULL); modlist_add (&modlist, "pgpSubKeyID", NULL); modlist_add (&modlist, "pgpKeySize", NULL); modlist_add (&modlist, "pgpKeyExpireTime", NULL); modlist_add (&modlist, "pgpCertID", NULL); if ((serverinfo & SERVERINFO_SCHEMAV2)) { modlist_add (&modlist, "gpgFingerprint", NULL); modlist_add (&modlist, "gpgSubFingerprint", NULL); modlist_add (&modlist, "gpgMailbox", NULL); } /* Assemble the INFO stuff into LDAP attributes */ extract_state = 0; 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 = xmalloc (infolen + 1); memcpy (temp, info, infolen); info = temp; newline = (char *) info + infolen; } *newline = '\0'; extract_attributes (&addlist, &extract_state, info, (serverinfo & SERVERINFO_SCHEMAV2)); infolen = infolen - ((uintptr_t) newline - (uintptr_t) info + 1); info = newline + 1; /* Sanity check. */ if (! temp) log_assert ((char *) info + infolen - 1 == infoend); else { log_assert (infolen == -1); xfree (temp); } } modlist_add (&addlist, "objectClass", "pgpKeyInfo"); err = armor_data (&data_armored, data, datalen); if (err) goto out; modlist_add (&addlist, (serverinfo & SERVERINFO_PGPKEYV2)? "pgpKeyV2":"pgpKey", data_armored); /* Now append addlist onto modlist. */ modlists_join (&modlist, addlist); if (dump) { estream_t input = modlist_dump (modlist, NULL); if (input) { 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 **attrval; char *dn; if ((serverinfo & SERVERINFO_NTDS)) { /* The modern way using a CN RDN with the fingerprint. This * has the advantage that we won't have duplicate 64 bit * keyids in the store. In particular NTDS requires the * DN to be unique. */ attrval = modlist_lookup (addlist, "gpgFingerprint"); /* We should have exactly one value. */ if (!attrval || !(attrval[0] && !attrval[1])) { log_error ("ks-ldap: bad gpgFingerprint provided\n"); err = GPG_ERR_GENERAL; goto out; } dn = xtryasprintf ("CN=%s,%s", attrval[0], basedn); } else /* The old style way. */ { attrval = modlist_lookup (addlist, "pgpCertID"); /* We should have exactly one value. */ if (!attrval || !(attrval[0] && !attrval[1])) { log_error ("ks-ldap: bad pgpCertID provided\n"); err = GPG_ERR_GENERAL; goto out; } dn = xtryasprintf ("pgpCertID=%s,%s", attrval[0], basedn); } if (!dn) { err = gpg_error_from_syserror (); goto out; } if (opt.debug) log_debug ("ks-ldap: using DN: %s\n", dn); npth_unprotect (); err = ldap_modify_s (ldap_conn, dn, modlist); if (err == LDAP_NO_SUCH_OBJECT) err = ldap_add_s (ldap_conn, dn, addlist); npth_protect (); xfree (dn); if (err != LDAP_SUCCESS) { log_error ("ks-ldap: 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); modlist_free (modlist); xfree (addlist); xfree (data_armored); return err; } diff --git a/dirmngr/server.c b/dirmngr/server.c index 0ccba2987..a35402271 100644 --- a/dirmngr/server.c +++ b/dirmngr/server.c @@ -1,3107 +1,3107 @@ /* server.c - LDAP and Keyserver access server * Copyright (C) 2002 Klarälvdalens Datakonsult AB * Copyright (C) 2003, 2004, 2005, 2007, 2008, 2009, 2011, 2015 g10 Code GmbH * Copyright (C) 2014, 2015, 2016 Werner Koch * Copyright (C) 2016 Bundesamt für Sicherheit in der Informationstechnik * * 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 . * * SPDX-License-Identifier: GPL-3.0+ */ #include #include #include #include #include #include #include #include #include #include #include "dirmngr.h" #include #include "crlcache.h" #include "crlfetch.h" #if USE_LDAP # include "ldapserver.h" #endif #include "ocsp.h" #include "certcache.h" #include "validate.h" #include "misc.h" #if USE_LDAP # include "ldap-wrapper.h" #endif #include "ks-action.h" #include "ks-engine.h" /* (ks_hkp_print_hosttable) */ #if USE_LDAP # include "ldap-parse-uri.h" #endif #include "dns-stuff.h" #include "../common/mbox-util.h" #include "../common/zb32.h" #include "../common/server-help.h" /* To avoid DoS attacks we limit the size of a certificate to something reasonable. The DoS was actually only an issue back when Dirmngr was a system service and not a user service. */ #define MAX_CERT_LENGTH (16*1024) /* The limit for the CERTLIST inquiry. We allow for up to 20 * certificates but also take PEM encoding into account. */ #define MAX_CERTLIST_LENGTH ((MAX_CERT_LENGTH * 20 * 4)/3) /* The same goes for OpenPGP keyblocks, but here we need to allow for much longer blocks; a 200k keyblock is not too unusual for keys with a lot of signatures (e.g. 0x5b0358a2). 9C31503C6D866396 even has 770 KiB as of 2015-08-23. To avoid adding a runtime option we now use 20MiB which should really be enough. Well, a key with several pictures could be larger (the parser as a 18MiB limit for attribute packets) but it won't be nice to the keyservers to send them such large blobs. */ #define MAX_KEYBLOCK_LENGTH (20*1024*1024) #define PARM_ERROR(t) assuan_set_error (ctx, \ gpg_error (GPG_ERR_ASS_PARAMETER), (t)) #define set_error(e,t) (ctx ? assuan_set_error (ctx, gpg_error (e), (t)) \ /**/: gpg_error (e)) /* Control structure per connection. */ struct server_local_s { /* Data used to associate an Assuan context with local server data */ assuan_context_t assuan_ctx; /* The session id (a counter). */ unsigned int session_id; /* Per-session LDAP servers. */ ldap_server_t ldapservers; /* Per-session list of keyservers. */ uri_item_t keyservers; /* If this flag is set to true this dirmngr process will be terminated after the end of this session. */ int stopme; /* State variable private to is_tor_running. */ int tor_state; /* If the first both flags are set the assuan logging of data lines * is suppressed. The count variable is used to show the number of * non-logged bytes. */ size_t inhibit_data_logging_count; unsigned int inhibit_data_logging : 1; unsigned int inhibit_data_logging_now : 1; }; /* Cookie definition for assuan data line output. */ static gpgrt_ssize_t data_line_cookie_write (void *cookie, const void *buffer, size_t size); static int data_line_cookie_close (void *cookie); static es_cookie_io_functions_t data_line_cookie_functions = { NULL, data_line_cookie_write, NULL, data_line_cookie_close }; /* Local prototypes */ static const char *task_check_wkd_support (ctrl_t ctrl, const char *domain); /* Accessor for the local ldapservers variable. */ ldap_server_t get_ldapservers_from_ctrl (ctrl_t ctrl) { if (ctrl && ctrl->server_local) return ctrl->server_local->ldapservers; else return NULL; } /* Release an uri_item_t list. */ static void release_uri_item_list (uri_item_t list) { while (list) { uri_item_t tmp = list->next; http_release_parsed_uri (list->parsed_uri); xfree (list); list = tmp; } } /* Release all configured keyserver info from CTRL. */ void release_ctrl_keyservers (ctrl_t ctrl) { if (! ctrl->server_local) return; release_uri_item_list (ctrl->server_local->keyservers); ctrl->server_local->keyservers = NULL; } /* Helper to print a message while leaving a command. */ static gpg_error_t leave_cmd (assuan_context_t ctx, gpg_error_t err) { if (err) { const char *name = assuan_get_command_name (ctx); if (!name) name = "?"; if (gpg_err_source (err) == GPG_ERR_SOURCE_DEFAULT) log_error ("command '%s' failed: %s\n", name, gpg_strerror (err)); else log_error ("command '%s' failed: %s <%s>\n", name, gpg_strerror (err), gpg_strsource (err)); } return err; } /* This is a wrapper around assuan_send_data which makes debugging the output in verbose mode easier. */ static gpg_error_t data_line_write (assuan_context_t ctx, const void *buffer_arg, size_t size) { ctrl_t ctrl = assuan_get_pointer (ctx); const char *buffer = buffer_arg; gpg_error_t err; /* If we do not want logging, enable it here. */ if (ctrl && ctrl->server_local && ctrl->server_local->inhibit_data_logging) ctrl->server_local->inhibit_data_logging_now = 1; if (opt.verbose && buffer && size) { /* Ease reading of output by sending a physical line at each LF. */ const char *p; size_t n, nbytes; nbytes = size; do { p = memchr (buffer, '\n', nbytes); n = p ? (p - buffer) + 1 : nbytes; err = assuan_send_data (ctx, buffer, n); if (err) { gpg_err_set_errno (EIO); goto leave; } buffer += n; nbytes -= n; if (nbytes && (err=assuan_send_data (ctx, NULL, 0))) /* Flush line. */ { gpg_err_set_errno (EIO); goto leave; } } while (nbytes); } else { err = assuan_send_data (ctx, buffer, size); if (err) { gpg_err_set_errno (EIO); /* For use by data_line_cookie_write. */ goto leave; } } leave: if (ctrl && ctrl->server_local && ctrl->server_local->inhibit_data_logging) { ctrl->server_local->inhibit_data_logging_now = 0; ctrl->server_local->inhibit_data_logging_count += size; } return err; } /* A write handler used by es_fopencookie to write assuan data lines. */ static gpgrt_ssize_t data_line_cookie_write (void *cookie, const void *buffer, size_t size) { assuan_context_t ctx = cookie; if (data_line_write (ctx, buffer, size)) return -1; return (gpgrt_ssize_t)size; } static int data_line_cookie_close (void *cookie) { assuan_context_t ctx = cookie; if (DBG_IPC) { ctrl_t ctrl = assuan_get_pointer (ctx); if (ctrl && ctrl->server_local && ctrl->server_local->inhibit_data_logging && ctrl->server_local->inhibit_data_logging_count) log_debug ("(%zu bytes sent via D lines not shown)\n", ctrl->server_local->inhibit_data_logging_count); } if (assuan_send_data (ctx, NULL, 0)) { gpg_err_set_errno (EIO); return -1; } return 0; } /* Copy the % and + escaped string S into the buffer D and replace the escape sequences. Note, that it is sufficient to allocate the target string D as long as the source string S, i.e.: strlen(s)+1. Note further that if S contains an escaped binary Nul the resulting string D will contain the 0 as well as all other characters but it will be impossible to know whether this is the original EOS or a copied Nul. */ static void strcpy_escaped_plus (char *d, const unsigned char *s) { while (*s) { if (*s == '%' && s[1] && s[2]) { s++; *d++ = xtoi_2 ( s); s += 2; } else if (*s == '+') *d++ = ' ', s++; else *d++ = *s++; } *d = 0; } /* This function returns true if a Tor server is running. The status * is cached for the current connection. */ static int is_tor_running (ctrl_t ctrl) { /* Check whether we can connect to the proxy. */ if (!ctrl || !ctrl->server_local) return 0; /* Ooops. */ if (!ctrl->server_local->tor_state) { assuan_fd_t sock; sock = assuan_sock_connect_byname (NULL, 0, 0, NULL, ASSUAN_SOCK_TOR); if (sock == ASSUAN_INVALID_FD) ctrl->server_local->tor_state = -1; /* Not running. */ else { assuan_sock_close (sock); ctrl->server_local->tor_state = 1; /* Running. */ } } return (ctrl->server_local->tor_state > 0); } /* Return an error if the assuan context does not belong to the owner of the process or to root. On error FAILTEXT is set as Assuan error string. */ static gpg_error_t check_owner_permission (assuan_context_t ctx, const char *failtext) { #ifdef HAVE_W32_SYSTEM /* Under Windows the dirmngr is always run under the control of the user. */ (void)ctx; (void)failtext; #else gpg_err_code_t ec; assuan_peercred_t cred; ec = gpg_err_code (assuan_get_peercred (ctx, &cred)); if (!ec && cred->uid && cred->uid != getuid ()) ec = GPG_ERR_EPERM; if (ec) return set_error (ec, failtext); #endif return 0; } /* Common code for get_cert_local and get_issuer_cert_local. */ static ksba_cert_t do_get_cert_local (ctrl_t ctrl, const char *name, const char *command) { unsigned char *value; size_t valuelen; int rc; char *buf; ksba_cert_t cert; buf = name? strconcat (command, " ", name, NULL) : xtrystrdup (command); if (!buf) rc = gpg_error_from_syserror (); else { rc = assuan_inquire (ctrl->server_local->assuan_ctx, buf, &value, &valuelen, MAX_CERT_LENGTH); xfree (buf); } if (rc) { log_error (_("assuan_inquire(%s) failed: %s\n"), command, gpg_strerror (rc)); return NULL; } if (!valuelen) { xfree (value); return NULL; } rc = ksba_cert_new (&cert); if (!rc) { rc = ksba_cert_init_from_mem (cert, value, valuelen); if (rc) { ksba_cert_release (cert); cert = NULL; } } xfree (value); return cert; } /* Ask back to return a certificate for NAME, given as a regular gpgsm * certificate identifier (e.g. fingerprint or one of the other * methods). Alternatively, NULL may be used for NAME to return the * current target certificate. Either return the certificate in a * KSBA object or NULL if it is not available. */ ksba_cert_t get_cert_local (ctrl_t ctrl, const char *name) { if (!ctrl || !ctrl->server_local || !ctrl->server_local->assuan_ctx) { if (opt.debug) log_debug ("get_cert_local called w/o context\n"); return NULL; } return do_get_cert_local (ctrl, name, "SENDCERT"); } /* Ask back to return the issuing certificate for NAME, given as a * regular gpgsm certificate identifier (e.g. fingerprint or one * of the other methods). Alternatively, NULL may be used for NAME to * return the current target certificate. Either return the certificate * in a KSBA object or NULL if it is not available. */ ksba_cert_t get_issuing_cert_local (ctrl_t ctrl, const char *name) { if (!ctrl || !ctrl->server_local || !ctrl->server_local->assuan_ctx) { if (opt.debug) log_debug ("get_issuing_cert_local called w/o context\n"); return NULL; } return do_get_cert_local (ctrl, name, "SENDISSUERCERT"); } /* Ask back to return a certificate with subject NAME and a * subjectKeyIdentifier of KEYID. */ ksba_cert_t get_cert_local_ski (ctrl_t ctrl, const char *name, ksba_sexp_t keyid) { unsigned char *value; size_t valuelen; int rc; char *buf; ksba_cert_t cert; char *hexkeyid; if (!ctrl || !ctrl->server_local || !ctrl->server_local->assuan_ctx) { if (opt.debug) log_debug ("get_cert_local_ski called w/o context\n"); return NULL; } if (!name || !keyid) { log_debug ("get_cert_local_ski called with insufficient arguments\n"); return NULL; } hexkeyid = serial_hex (keyid); if (!hexkeyid) { log_debug ("serial_hex() failed\n"); return NULL; } buf = strconcat ("SENDCERT_SKI ", hexkeyid, " /", name, NULL); if (!buf) { log_error ("can't allocate enough memory: %s\n", strerror (errno)); xfree (hexkeyid); return NULL; } xfree (hexkeyid); rc = assuan_inquire (ctrl->server_local->assuan_ctx, buf, &value, &valuelen, MAX_CERT_LENGTH); xfree (buf); if (rc) { log_error (_("assuan_inquire(%s) failed: %s\n"), "SENDCERT_SKI", gpg_strerror (rc)); return NULL; } if (!valuelen) { xfree (value); return NULL; } rc = ksba_cert_new (&cert); if (!rc) { rc = ksba_cert_init_from_mem (cert, value, valuelen); if (rc) { ksba_cert_release (cert); cert = NULL; } } xfree (value); return cert; } /* Ask the client via an inquiry to check the istrusted status of the certificate specified by the hexified fingerprint HEXFPR. Returns 0 if the certificate is trusted by the client or an error code. */ gpg_error_t get_istrusted_from_client (ctrl_t ctrl, const char *hexfpr) { unsigned char *value; size_t valuelen; int rc; char request[100]; if (!ctrl || !ctrl->server_local || !ctrl->server_local->assuan_ctx || !hexfpr) return gpg_error (GPG_ERR_INV_ARG); snprintf (request, sizeof request, "ISTRUSTED %s", hexfpr); rc = assuan_inquire (ctrl->server_local->assuan_ctx, request, &value, &valuelen, 100); if (rc) { log_error (_("assuan_inquire(%s) failed: %s\n"), request, gpg_strerror (rc)); return rc; } /* The expected data is: "1" or "1 cruft" (not a C-string). */ if (valuelen && *value == '1' && (valuelen == 1 || spacep (value+1))) rc = 0; else rc = gpg_error (GPG_ERR_NOT_TRUSTED); xfree (value); return rc; } /* Ask the client to return the certificate associated with the current command. This is sometimes needed because the client usually sends us just the cert ID, assuming that the request can be satisfied from the cache, where the cert ID is used as key. */ static int inquire_cert_and_load_crl (assuan_context_t ctx) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err; unsigned char *value = NULL; size_t valuelen; ksba_cert_t cert = NULL; err = assuan_inquire( ctx, "SENDCERT", &value, &valuelen, 0); if (err) return err; /* { */ /* FILE *fp = fopen ("foo.der", "r"); */ /* value = xmalloc (2000); */ /* valuelen = fread (value, 1, 2000, fp); */ /* fclose (fp); */ /* } */ if (!valuelen) /* No data returned; return a comprehensible error. */ return gpg_error (GPG_ERR_MISSING_CERT); err = ksba_cert_new (&cert); if (err) goto leave; err = ksba_cert_init_from_mem (cert, value, valuelen); if(err) goto leave; xfree (value); value = NULL; err = crl_cache_reload_crl (ctrl, cert); leave: ksba_cert_release (cert); xfree (value); return err; } /* Handle OPTION commands. */ static gpg_error_t option_handler (assuan_context_t ctx, const char *key, const char *value) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err = 0; if (!strcmp (key, "force-crl-refresh")) { int i = *value? atoi (value) : 0; ctrl->force_crl_refresh = i; } else if (!strcmp (key, "audit-events")) { int i = *value? atoi (value) : 0; ctrl->audit_events = i; } else if (!strcmp (key, "http-proxy")) { xfree (ctrl->http_proxy); if (!*value || !strcmp (value, "none")) ctrl->http_proxy = NULL; else if (!(ctrl->http_proxy = xtrystrdup (value))) err = gpg_error_from_syserror (); } else if (!strcmp (key, "honor-keyserver-url-used")) { /* Return an error if we are running in Tor mode. */ if (dirmngr_use_tor ()) err = gpg_error (GPG_ERR_FORBIDDEN); } else if (!strcmp (key, "http-crl")) { int i = *value? atoi (value) : 0; ctrl->http_no_crl = !i; } else err = gpg_error (GPG_ERR_UNKNOWN_OPTION); return err; } static const char hlp_dns_cert[] = "DNS_CERT \n" "DNS_CERT --pka \n" "DNS_CERT --dane \n" "\n" "Return the CERT record for . is one of\n" " * Return the first record of any supported subtype\n" " PGP Return the first record of subtype PGP (3)\n" " IPGP Return the first record of subtype IPGP (6)\n" "If the content of a certificate is available (PGP) it is returned\n" "by data lines. Fingerprints and URLs are returned via status lines.\n" "In --pka mode the fingerprint and if available an URL is returned.\n" "In --dane mode the key is returned from RR type 61"; static gpg_error_t cmd_dns_cert (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err = 0; int pka_mode, dane_mode; char *mbox = NULL; char *namebuf = NULL; char *encodedhash = NULL; const char *name; int certtype; char *p; void *key = NULL; size_t keylen; unsigned char *fpr = NULL; size_t fprlen; char *url = NULL; pka_mode = has_option (line, "--pka"); dane_mode = has_option (line, "--dane"); line = skip_options (line); if (pka_mode && dane_mode) { err = PARM_ERROR ("either --pka or --dane may be given"); goto leave; } if (pka_mode || dane_mode) ; /* No need to parse here - we do this later. */ else { p = strchr (line, ' '); if (!p) { err = PARM_ERROR ("missing arguments"); goto leave; } *p++ = 0; if (!strcmp (line, "*")) certtype = DNS_CERTTYPE_ANY; else if (!strcmp (line, "IPGP")) certtype = DNS_CERTTYPE_IPGP; else if (!strcmp (line, "PGP")) certtype = DNS_CERTTYPE_PGP; else { err = PARM_ERROR ("unknown subtype"); goto leave; } while (spacep (p)) p++; line = p; if (!*line) { err = PARM_ERROR ("name missing"); goto leave; } } if (pka_mode || dane_mode) { char *domain; /* Points to mbox. */ char hashbuf[32]; /* For SHA-1 and SHA-256. */ /* We lowercase ascii characters but the DANE I-D does not allow this. FIXME: Check after the release of the RFC whether to change this. */ mbox = mailbox_from_userid (line, 0); if (!mbox || !(domain = strchr (mbox, '@'))) { err = set_error (GPG_ERR_INV_USER_ID, "no mailbox in user id"); goto leave; } *domain++ = 0; if (pka_mode) { gcry_md_hash_buffer (GCRY_MD_SHA1, hashbuf, mbox, strlen (mbox)); encodedhash = zb32_encode (hashbuf, 8*20); if (!encodedhash) { err = gpg_error_from_syserror (); goto leave; } namebuf = strconcat (encodedhash, "._pka.", domain, NULL); if (!namebuf) { err = gpg_error_from_syserror (); goto leave; } name = namebuf; certtype = DNS_CERTTYPE_IPGP; } else { /* Note: The hash is truncated to 28 bytes and we lowercase the result only for aesthetic reasons. */ gcry_md_hash_buffer (GCRY_MD_SHA256, hashbuf, mbox, strlen (mbox)); encodedhash = bin2hex (hashbuf, 28, NULL); if (!encodedhash) { err = gpg_error_from_syserror (); goto leave; } ascii_strlwr (encodedhash); namebuf = strconcat (encodedhash, "._openpgpkey.", domain, NULL); if (!namebuf) { err = gpg_error_from_syserror (); goto leave; } name = namebuf; certtype = DNS_CERTTYPE_RR61; } } else name = line; err = get_dns_cert (ctrl, name, certtype, &key, &keylen, &fpr, &fprlen, &url); if (err) goto leave; if (key) { err = data_line_write (ctx, key, keylen); if (err) goto leave; } if (fpr) { char *tmpstr; tmpstr = bin2hex (fpr, fprlen, NULL); if (!tmpstr) err = gpg_error_from_syserror (); else { err = assuan_write_status (ctx, "FPR", tmpstr); xfree (tmpstr); } if (err) goto leave; } if (url) { err = assuan_write_status (ctx, "URL", url); if (err) goto leave; } leave: xfree (key); xfree (fpr); xfree (url); xfree (mbox); xfree (namebuf); xfree (encodedhash); return leave_cmd (ctx, err); } /* Core of cmd_wkd_get and task_check_wkd_support. If CTX is NULL * this function will not write anything to the assuan output. */ static gpg_error_t proc_wkd_get (ctrl_t ctrl, assuan_context_t ctx, char *line) { gpg_error_t err = 0; char *mbox = NULL; char *domainbuf = NULL; char *domain; /* Points to mbox or domainbuf. This is used to * connect to the host. */ char *domain_orig;/* Points to mbox. This is the used for the * query; i.e. the domain part of the * addrspec. */ char sha1buf[20]; char *uri = NULL; char *encodedhash = NULL; int opt_submission_addr; int opt_policy_flags; int is_wkd_query; /* True if this is a real WKD query. */ int no_log = 0; char portstr[20] = { 0 }; int subdomain_mode = 0; opt_submission_addr = has_option (line, "--submission-address"); opt_policy_flags = has_option (line, "--policy-flags"); if (has_option (line, "--quick")) ctrl->timeout = opt.connect_quick_timeout; line = skip_options (line); is_wkd_query = !(opt_policy_flags || opt_submission_addr); mbox = mailbox_from_userid (line, 0); if (!mbox || !(domain = strchr (mbox, '@'))) { err = set_error (GPG_ERR_INV_USER_ID, "no mailbox in user id"); goto leave; } *domain++ = 0; domain_orig = domain; /* Let's check whether we already know that the domain does not * support WKD. */ if (is_wkd_query) { if (domaininfo_is_wkd_not_supported (domain_orig)) { err = gpg_error (GPG_ERR_NO_DATA); dirmngr_status_printf (ctrl, "NOTE", "wkd_cached_result %u", err); goto leave; } } /* First try the new "openpgp" subdomain. We check that the domain * is valid because it is later used as an unescaped filename part * of the URI. */ if (is_valid_domain_name (domain_orig)) { dns_addrinfo_t aibuf; domainbuf = strconcat ( "openpgpkey.", domain_orig, NULL); if (!domainbuf) { err = gpg_error_from_syserror (); goto leave; } /* FIXME: We should put a cache into dns-stuff because the same * query (with a different port and socket type, though) will be * done later by http function. */ err = resolve_dns_name (ctrl, domainbuf, 0, 0, 0, &aibuf, NULL); if (err) { err = 0; xfree (domainbuf); domainbuf = NULL; } else /* Got a subdomain. */ { free_dns_addrinfo (aibuf); subdomain_mode = 1; domain = domainbuf; } } /* Check for SRV records unless we have a subdomain. */ if (!subdomain_mode) { struct srventry *srvs; unsigned int srvscount; size_t domainlen, targetlen; int i; err = get_dns_srv (ctrl, domain, "openpgpkey", NULL, &srvs, &srvscount); if (err) goto leave; /* Check for rogue DNS names. */ for (i = 0; i < srvscount; i++) { if (!is_valid_domain_name (srvs[i].target)) { err = gpg_error (GPG_ERR_DNS_ADDRESS); log_error ("rogue openpgpkey SRV record for '%s'\n", domain); xfree (srvs); goto leave; } } /* Find the first target which also ends in DOMAIN or is equal * to DOMAIN. */ domainlen = strlen (domain); for (i = 0; i < srvscount; i++) { if (DBG_DNS) log_debug ("srv: trying '%s:%hu'\n", srvs[i].target, srvs[i].port); targetlen = strlen (srvs[i].target); if ((targetlen > domainlen + 1 && srvs[i].target[targetlen - domainlen - 1] == '.' && !ascii_strcasecmp (srvs[i].target + targetlen - domainlen, domain)) || (targetlen == domainlen && !ascii_strcasecmp (srvs[i].target, domain))) { /* found. */ domainbuf = xtrystrdup (srvs[i].target); if (!domainbuf) { err = gpg_error_from_syserror (); xfree (srvs); goto leave; } domain = domainbuf; if (srvs[i].port) snprintf (portstr, sizeof portstr, ":%hu", srvs[i].port); break; } } xfree (srvs); } /* Prepare the hash of the local part. */ gcry_md_hash_buffer (GCRY_MD_SHA1, sha1buf, mbox, strlen (mbox)); encodedhash = zb32_encode (sha1buf, 8*20); if (!encodedhash) { err = gpg_error_from_syserror (); goto leave; } if (opt_submission_addr) { uri = strconcat ("https://", domain, portstr, "/.well-known/openpgpkey/", subdomain_mode? domain_orig : "", subdomain_mode? "/" : "", "submission-address", NULL); } else if (opt_policy_flags) { uri = strconcat ("https://", domain, portstr, "/.well-known/openpgpkey/", subdomain_mode? domain_orig : "", subdomain_mode? "/" : "", "policy", NULL); } else { char *escapedmbox; escapedmbox = http_escape_string (mbox, "%;?&="); if (escapedmbox) { uri = strconcat ("https://", domain, portstr, "/.well-known/openpgpkey/", subdomain_mode? domain_orig : "", subdomain_mode? "/" : "", "hu/", encodedhash, "?l=", escapedmbox, NULL); xfree (escapedmbox); no_log = 1; if (uri) { err = dirmngr_status_printf (ctrl, "SOURCE", "https://%s%s", domain, portstr); if (err) goto leave; } } } if (!uri) { err = gpg_error_from_syserror (); goto leave; } /* Setup an output stream and perform the get. */ { estream_t outfp; outfp = ctx? es_fopencookie (ctx, "w", data_line_cookie_functions) : NULL; if (!outfp && ctx) err = set_error (GPG_ERR_ASS_GENERAL, "error setting up a data stream"); else { if (ctrl->server_local) { if (no_log) ctrl->server_local->inhibit_data_logging = 1; ctrl->server_local->inhibit_data_logging_now = 0; ctrl->server_local->inhibit_data_logging_count = 0; } err = ks_action_fetch (ctrl, uri, outfp); es_fclose (outfp); if (ctrl->server_local) ctrl->server_local->inhibit_data_logging = 0; /* Register the result under the domain name of MBOX. */ switch (gpg_err_code (err)) { case 0: domaininfo_set_wkd_supported (domain_orig); break; case GPG_ERR_NO_NAME: /* There is no such domain. */ domaininfo_set_no_name (domain_orig); break; case GPG_ERR_NO_DATA: if (is_wkd_query && ctrl->server_local) { /* Mark that and schedule a check. */ domaininfo_set_wkd_not_found (domain_orig); workqueue_add_task (task_check_wkd_support, domain_orig, ctrl->server_local->session_id, 1); } else if (opt_policy_flags) /* No policy file - no support. */ domaininfo_set_wkd_not_supported (domain_orig); break; default: /* Don't register other errors. */ break; } } } leave: xfree (uri); xfree (encodedhash); xfree (mbox); xfree (domainbuf); return err; } static const char hlp_wkd_get[] = "WKD_GET [--submission-address|--policy-flags] \n" "\n" "Return the key or other info for \n" "from the Web Key Directory."; static gpg_error_t cmd_wkd_get (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err; err = proc_wkd_get (ctrl, ctx, line); return leave_cmd (ctx, err); } /* A task to check whether DOMAIN supports WKD. This is done by * checking whether the policy flags file can be read. */ static const char * task_check_wkd_support (ctrl_t ctrl, const char *domain) { char *string; if (!ctrl || !domain) return "check_wkd_support"; string = strconcat ("--policy-flags foo@", domain, NULL); if (!string) log_error ("%s: %s\n", __func__, gpg_strerror (gpg_error_from_syserror ())); else { proc_wkd_get (ctrl, NULL, string); xfree (string); } return NULL; } static const char hlp_ldapserver[] = "LDAPSERVER \n" "\n" "Add a new LDAP server to the list of configured LDAP servers.\n" "DATA is in the same format as expected in the configure file."; static gpg_error_t cmd_ldapserver (assuan_context_t ctx, char *line) { #if USE_LDAP ctrl_t ctrl = assuan_get_pointer (ctx); ldap_server_t server; ldap_server_t *last_next_p; while (spacep (line)) line++; if (*line == '\0') return leave_cmd (ctx, PARM_ERROR (_("ldapserver missing"))); server = ldapserver_parse_one (line, "", 0); if (! server) return leave_cmd (ctx, gpg_error (GPG_ERR_INV_ARG)); last_next_p = &ctrl->server_local->ldapservers; while (*last_next_p) last_next_p = &(*last_next_p)->next; *last_next_p = server; return leave_cmd (ctx, 0); #else (void)line; return leave_cmd (ctx, gpg_error (GPG_ERR_NOT_IMPLEMENTED)); #endif } static const char hlp_isvalid[] = "ISVALID [--only-ocsp] [--force-default-responder]" " []\n" "\n" "This command checks whether the certificate identified by the\n" "certificate_id is valid. This is done by consulting CRLs or\n" "whatever has been configured. Note, that the returned error codes\n" "are from gpg-error.h. The command may callback using the inquire\n" "function. See the manual for details.\n" "\n" "The CERTIFICATE_ID is a hex encoded string consisting of two parts,\n" "delimited by a single dot. The first part is the SHA-1 hash of the\n" "issuer name and the second part the serial number.\n" "\n" "If an OCSP check is desired CERTIFICATE_FPR with the hex encoded\n" "fingerprint of the certificate is required. In this case an OCSP\n" "request is done before consulting the CRL.\n" "\n" "If the option --only-ocsp is given, no fallback to a CRL check will\n" "be used.\n" "\n" "If the option --force-default-responder is given, only the default\n" "OCSP responder will be used and any other methods of obtaining an\n" "OCSP responder URL won't be used."; static gpg_error_t cmd_isvalid (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); char *issuerhash, *serialno, *fpr; gpg_error_t err; int did_inquire = 0; int ocsp_mode = 0; int only_ocsp; int force_default_responder; only_ocsp = has_option (line, "--only-ocsp"); force_default_responder = has_option (line, "--force-default-responder"); line = skip_options (line); /* We need to work on a copy of the line because that same Assuan * context may be used for an inquiry. That is because Assuan * reuses its line buffer. */ issuerhash = xstrdup (line); serialno = strchr (issuerhash, '.'); if (!serialno) { xfree (issuerhash); return leave_cmd (ctx, PARM_ERROR (_("serialno missing in cert ID"))); } *serialno++ = 0; if (strlen (issuerhash) != 40) { xfree (issuerhash); return leave_cmd (ctx, PARM_ERROR ("cert ID is too short")); } fpr = strchr (serialno, ' '); while (fpr && spacep (fpr)) fpr++; if (fpr && *fpr) { char *endp = strchr (fpr, ' '); if (endp) *endp = 0; if (strlen (fpr) != 40) { xfree (issuerhash); return leave_cmd (ctx, PARM_ERROR ("fingerprint too short")); } ocsp_mode = 1; } again: if (ocsp_mode) { /* Note, that we currently ignore the supplied fingerprint FPR; * instead ocsp_isvalid does an inquire to ask for the cert. * The fingerprint may eventually be used to lookup the * certificate in a local cache. */ if (!opt.allow_ocsp) err = gpg_error (GPG_ERR_NOT_SUPPORTED); else err = ocsp_isvalid (ctrl, NULL, NULL, force_default_responder); if (gpg_err_code (err) == GPG_ERR_CONFIGURATION && gpg_err_source (err) == GPG_ERR_SOURCE_DIRMNGR) { /* No default responder configured - fallback to CRL. */ if (!only_ocsp) log_info ("falling back to CRL check\n"); ocsp_mode = 0; goto again; } } else if (only_ocsp) err = gpg_error (GPG_ERR_NO_CRL_KNOWN); else { switch (crl_cache_isvalid (ctrl, issuerhash, serialno, ctrl->force_crl_refresh)) { case CRL_CACHE_VALID: err = 0; break; case CRL_CACHE_INVALID: err = gpg_error (GPG_ERR_CERT_REVOKED); break; case CRL_CACHE_DONTKNOW: if (did_inquire) err = gpg_error (GPG_ERR_NO_CRL_KNOWN); else if (!(err = inquire_cert_and_load_crl (ctx))) { did_inquire = 1; goto again; } break; case CRL_CACHE_CANTUSE: err = gpg_error (GPG_ERR_NO_CRL_KNOWN); break; default: log_fatal ("crl_cache_isvalid returned invalid code\n"); } } xfree (issuerhash); return leave_cmd (ctx, err); } /* If the line contains a SHA-1 fingerprint as the first argument, return the FPR vuffer on success. The function checks that the fingerprint consists of valid characters and prints and error message if it does not and returns NULL. Fingerprints are considered optional and thus no explicit error is returned. NULL is also returned if there is no fingerprint at all available. FPR must be a caller provided buffer of at least 20 bytes. Note that colons within the fingerprint are allowed to separate 2 hex digits; this allows for easier cutting and pasting using the usual fingerprint rendering. */ static unsigned char * get_fingerprint_from_line (const char *line, unsigned char *fpr) { const char *s; int i; for (s=line, i=0; *s && *s != ' '; s++ ) { if ( hexdigitp (s) && hexdigitp (s+1) ) { if ( i >= 20 ) return NULL; /* Fingerprint too long. */ fpr[i++] = xtoi_2 (s); s++; } else if ( *s != ':' ) return NULL; /* Invalid. */ } if ( i != 20 ) return NULL; /* Fingerprint to short. */ return fpr; } static const char hlp_checkcrl[] = "CHECKCRL []\n" "\n" "Check whether the certificate with FINGERPRINT (SHA-1 hash of the\n" "entire X.509 certificate blob) is valid or not by consulting the\n" "CRL responsible for this certificate. If the fingerprint has not\n" "been given or the certificate is not known, the function \n" "inquires the certificate using an\n" "\n" " INQUIRE TARGETCERT\n" "\n" "and the caller is expected to return the certificate for the\n" "request (which should match FINGERPRINT) as a binary blob.\n" "Processing then takes place without further interaction; in\n" "particular dirmngr tries to locate other required certificate by\n" "its own mechanism which includes a local certificate store as well\n" "as a list of trusted root certificates.\n" "\n" "The return value is the usual gpg-error code or 0 for ducesss;\n" "i.e. the certificate validity has been confirmed by a valid CRL."; static gpg_error_t cmd_checkcrl (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err; unsigned char fprbuffer[20], *fpr; ksba_cert_t cert; fpr = get_fingerprint_from_line (line, fprbuffer); cert = fpr? get_cert_byfpr (fpr) : NULL; if (!cert) { /* We do not have this certificate yet or the fingerprint has not been given. Inquire it from the client. */ unsigned char *value = NULL; size_t valuelen; err = assuan_inquire (ctrl->server_local->assuan_ctx, "TARGETCERT", &value, &valuelen, MAX_CERT_LENGTH); if (err) { log_error (_("assuan_inquire failed: %s\n"), gpg_strerror (err)); goto leave; } if (!valuelen) /* No data returned; return a comprehensible error. */ err = gpg_error (GPG_ERR_MISSING_CERT); else { err = ksba_cert_new (&cert); if (!err) err = ksba_cert_init_from_mem (cert, value, valuelen); } xfree (value); if(err) goto leave; } assert (cert); err = crl_cache_cert_isvalid (ctrl, cert, ctrl->force_crl_refresh); if (gpg_err_code (err) == GPG_ERR_NO_CRL_KNOWN) { err = crl_cache_reload_crl (ctrl, cert); if (!err) err = crl_cache_cert_isvalid (ctrl, cert, 0); } leave: ksba_cert_release (cert); return leave_cmd (ctx, err); } static const char hlp_checkocsp[] = "CHECKOCSP [--force-default-responder] []\n" "\n" "Check whether the certificate with FINGERPRINT (SHA-1 hash of the\n" "entire X.509 certificate blob) is valid or not by asking an OCSP\n" "responder responsible for this certificate. The optional\n" "fingerprint may be used for a quick check in case an OCSP check has\n" "been done for this certificate recently (we always cache OCSP\n" "responses for a couple of minutes). If the fingerprint has not been\n" "given or there is no cached result, the function inquires the\n" "certificate using an\n" "\n" " INQUIRE TARGETCERT\n" "\n" "and the caller is expected to return the certificate for the\n" "request (which should match FINGERPRINT) as a binary blob.\n" "Processing then takes place without further interaction; in\n" "particular dirmngr tries to locate other required certificates by\n" "its own mechanism which includes a local certificate store as well\n" "as a list of trusted root certificates.\n" "\n" "If the option --force-default-responder is given, only the default\n" "OCSP responder will be used and any other methods of obtaining an\n" "OCSP responder URL won't be used.\n" "\n" "The return value is the usual gpg-error code or 0 for ducesss;\n" "i.e. the certificate validity has been confirmed by a valid CRL."; static gpg_error_t cmd_checkocsp (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err; unsigned char fprbuffer[20], *fpr; ksba_cert_t cert; int force_default_responder; force_default_responder = has_option (line, "--force-default-responder"); line = skip_options (line); fpr = get_fingerprint_from_line (line, fprbuffer); cert = fpr? get_cert_byfpr (fpr) : NULL; if (!cert) { /* We do not have this certificate yet or the fingerprint has not been given. Inquire it from the client. */ unsigned char *value = NULL; size_t valuelen; err = assuan_inquire (ctrl->server_local->assuan_ctx, "TARGETCERT", &value, &valuelen, MAX_CERT_LENGTH); if (err) { log_error (_("assuan_inquire failed: %s\n"), gpg_strerror (err)); goto leave; } if (!valuelen) /* No data returned; return a comprehensible error. */ err = gpg_error (GPG_ERR_MISSING_CERT); else { err = ksba_cert_new (&cert); if (!err) err = ksba_cert_init_from_mem (cert, value, valuelen); } xfree (value); if(err) goto leave; } assert (cert); if (!opt.allow_ocsp) err = gpg_error (GPG_ERR_NOT_SUPPORTED); else err = ocsp_isvalid (ctrl, cert, NULL, force_default_responder); leave: ksba_cert_release (cert); return leave_cmd (ctx, err); } static int lookup_cert_by_url (assuan_context_t ctx, const char *url) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err = 0; unsigned char *value = NULL; size_t valuelen; /* Fetch single certificate given it's URL. */ err = fetch_cert_by_url (ctrl, url, &value, &valuelen); if (err) { log_error (_("fetch_cert_by_url failed: %s\n"), gpg_strerror (err)); goto leave; } /* Send the data, flush the buffer and then send an END. */ err = assuan_send_data (ctx, value, valuelen); if (!err) err = assuan_send_data (ctx, NULL, 0); if (!err) err = assuan_write_line (ctx, "END"); if (err) { log_error (_("error sending data: %s\n"), gpg_strerror (err)); goto leave; } leave: return err; } /* Send the certificate, flush the buffer and then send an END. */ static gpg_error_t return_one_cert (void *opaque, ksba_cert_t cert) { assuan_context_t ctx = opaque; gpg_error_t err; const unsigned char *der; size_t derlen; der = ksba_cert_get_image (cert, &derlen); if (!der) err = gpg_error (GPG_ERR_INV_CERT_OBJ); else { err = assuan_send_data (ctx, der, derlen); if (!err) err = assuan_send_data (ctx, NULL, 0); if (!err) err = assuan_write_line (ctx, "END"); } if (err) log_error (_("error sending data: %s\n"), gpg_strerror (err)); return err; } /* Lookup certificates from the internal cache or using the ldap servers. */ static int lookup_cert_by_pattern (assuan_context_t ctx, char *line, int single, int cache_only) { gpg_error_t err = 0; char *p; strlist_t sl, list = NULL; int truncated = 0, truncation_forced = 0; int count = 0; int local_count = 0; #if USE_LDAP ctrl_t ctrl = assuan_get_pointer (ctx); unsigned char *value = NULL; size_t valuelen; struct ldapserver_iter ldapserver_iter; cert_fetch_context_t fetch_context; #endif /*USE_LDAP*/ int any_no_data = 0; /* Break the line down into an STRLIST */ for (p=line; *p; line = p) { while (*p && *p != ' ') p++; if (*p) *p++ = 0; if (*line) { sl = xtrymalloc (sizeof *sl + strlen (line)); if (!sl) { err = gpg_error_from_errno (errno); goto leave; } memset (sl, 0, sizeof *sl); strcpy_escaped_plus (sl->d, line); sl->next = list; list = sl; } } /* First look through the internal cache. The certificates returned here are not counted towards the truncation limit. */ if (single && !cache_only) ; /* Do not read from the local cache in this case. */ else { for (sl=list; sl; sl = sl->next) { err = get_certs_bypattern (sl->d, return_one_cert, ctx); if (!err) local_count++; if (!err && single) goto ready; if (gpg_err_code (err) == GPG_ERR_NO_DATA) { err = 0; if (cache_only) any_no_data = 1; } else if (gpg_err_code (err) == GPG_ERR_INV_NAME && !cache_only) { /* No real fault because the internal pattern lookup can't yet cope with all types of pattern. */ err = 0; } if (err) goto ready; } } /* Loop over all configured servers unless we want only the certificates from the cache. */ #if USE_LDAP for (ldapserver_iter_begin (&ldapserver_iter, ctrl); !cache_only && !ldapserver_iter_end_p (&ldapserver_iter) && ldapserver_iter.server->host && !truncation_forced; ldapserver_iter_next (&ldapserver_iter)) { ldap_server_t ldapserver = ldapserver_iter.server; if (DBG_LOOKUP) log_debug ("cmd_lookup: trying %s:%d base=%s\n", ldapserver->host, ldapserver->port, ldapserver->base?ldapserver->base : "[default]"); /* Fetch certificates matching pattern */ err = start_cert_fetch (ctrl, &fetch_context, list, ldapserver); if ( gpg_err_code (err) == GPG_ERR_NO_DATA ) { if (DBG_LOOKUP) log_debug ("cmd_lookup: no data\n"); err = 0; any_no_data = 1; continue; } if (err) { log_error (_("start_cert_fetch failed: %s\n"), gpg_strerror (err)); goto leave; } /* Fetch the certificates for this query. */ while (!truncation_forced) { xfree (value); value = NULL; err = fetch_next_cert (fetch_context, &value, &valuelen); if (gpg_err_code (err) == GPG_ERR_NO_DATA ) { err = 0; any_no_data = 1; break; /* Ready. */ } if (gpg_err_code (err) == GPG_ERR_TRUNCATED) { truncated = 1; err = 0; break; /* Ready. */ } if (gpg_err_code (err) == GPG_ERR_EOF) { err = 0; break; /* Ready. */ } if (!err && !value) { err = gpg_error (GPG_ERR_BUG); goto leave; } if (err) { log_error (_("fetch_next_cert failed: %s\n"), gpg_strerror (err)); end_cert_fetch (fetch_context); goto leave; } if (DBG_LOOKUP) log_debug ("cmd_lookup: returning one cert%s\n", truncated? " (truncated)":""); /* Send the data, flush the buffer and then send an END line as a certificate delimiter. */ err = assuan_send_data (ctx, value, valuelen); if (!err) err = assuan_send_data (ctx, NULL, 0); if (!err) err = assuan_write_line (ctx, "END"); if (err) { log_error (_("error sending data: %s\n"), gpg_strerror (err)); end_cert_fetch (fetch_context); goto leave; } if (++count >= opt.max_replies ) { truncation_forced = 1; log_info (_("max_replies %d exceeded\n"), opt.max_replies ); } if (single) break; } end_cert_fetch (fetch_context); } #endif /*USE_LDAP*/ ready: if (truncated || truncation_forced) { char str[50]; sprintf (str, "%d", count); assuan_write_status (ctx, "TRUNCATED", str); } if (!err && !count && !local_count && any_no_data) err = gpg_error (GPG_ERR_NO_DATA); leave: free_strlist (list); return err; } static const char hlp_lookup[] = "LOOKUP [--url] [--single] [--cache-only] \n" "\n" "Lookup certificates matching PATTERN. With --url the pattern is\n" "expected to be one URL.\n" "\n" "If --url is not given: To allow for multiple patterns (which are ORed)\n" "quoting is required: Spaces are translated to \"+\" or \"%20\";\n" "obviously this requires that the usual escape quoting rules are applied.\n" "\n" "If --url is given no special escaping is required because URLs are\n" "already escaped this way.\n" "\n" "If --single is given the first and only the first match will be\n" "returned. If --cache-only is _not_ given, no local query will be\n" "done.\n" "\n" "If --cache-only is given no external lookup is done so that only\n" "certificates from the cache may get returned."; static gpg_error_t cmd_lookup (assuan_context_t ctx, char *line) { gpg_error_t err; int lookup_url, single, cache_only; lookup_url = has_leading_option (line, "--url"); single = has_leading_option (line, "--single"); cache_only = has_leading_option (line, "--cache-only"); line = skip_options (line); if (lookup_url && cache_only) err = gpg_error (GPG_ERR_NOT_FOUND); else if (lookup_url && single) err = gpg_error (GPG_ERR_NOT_IMPLEMENTED); else if (lookup_url) err = lookup_cert_by_url (ctx, line); else err = lookup_cert_by_pattern (ctx, line, single, cache_only); return leave_cmd (ctx, err); } static const char hlp_loadcrl[] = "LOADCRL [--url] \n" "\n" "Load the CRL in the file with name FILENAME into our cache. Note\n" "that FILENAME should be given with an absolute path because\n" "Dirmngrs cwd is not known. With --url the CRL is directly loaded\n" "from the given URL.\n" "\n" "This command is usually used by gpgsm using the invocation \"gpgsm\n" "--call-dirmngr loadcrl \". A direct invocation of Dirmngr\n" "is not useful because gpgsm might need to callback gpgsm to ask for\n" "the CA's certificate."; static gpg_error_t cmd_loadcrl (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err = 0; int use_url = has_leading_option (line, "--url"); line = skip_options (line); if (use_url) { ksba_reader_t reader; err = crl_fetch (ctrl, line, &reader); if (err) log_error (_("fetching CRL from '%s' failed: %s\n"), line, gpg_strerror (err)); else { err = crl_cache_insert (ctrl, line, reader); if (err) log_error (_("processing CRL from '%s' failed: %s\n"), line, gpg_strerror (err)); crl_close_reader (reader); } } else { char *buf; buf = xtrymalloc (strlen (line)+1); if (!buf) err = gpg_error_from_syserror (); else { strcpy_escaped_plus (buf, line); err = crl_cache_load (ctrl, buf); xfree (buf); } } return leave_cmd (ctx, err); } static const char hlp_listcrls[] = "LISTCRLS\n" "\n" "List the content of all CRLs in a readable format. This command is\n" "usually used by gpgsm using the invocation \"gpgsm --call-dirmngr\n" "listcrls\". It may also be used directly using \"dirmngr\n" "--list-crls\"."; static gpg_error_t cmd_listcrls (assuan_context_t ctx, char *line) { gpg_error_t err; estream_t fp; (void)line; fp = es_fopencookie (ctx, "w", data_line_cookie_functions); if (!fp) err = set_error (GPG_ERR_ASS_GENERAL, "error setting up a data stream"); else { err = crl_cache_list (fp); es_fclose (fp); } return leave_cmd (ctx, err); } static const char hlp_cachecert[] = "CACHECERT\n" "\n" "Put a certificate into the internal cache. This command might be\n" "useful if a client knows in advance certificates required for a\n" "test and wants to make sure they get added to the internal cache.\n" "It is also helpful for debugging. To get the actual certificate,\n" "this command immediately inquires it using\n" "\n" " INQUIRE TARGETCERT\n" "\n" "and the caller is expected to return the certificate for the\n" "request as a binary blob."; static gpg_error_t cmd_cachecert (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err; ksba_cert_t cert = NULL; unsigned char *value = NULL; size_t valuelen; (void)line; err = assuan_inquire (ctrl->server_local->assuan_ctx, "TARGETCERT", &value, &valuelen, MAX_CERT_LENGTH); if (err) { log_error (_("assuan_inquire failed: %s\n"), gpg_strerror (err)); goto leave; } if (!valuelen) /* No data returned; return a comprehensible error. */ err = gpg_error (GPG_ERR_MISSING_CERT); else { err = ksba_cert_new (&cert); if (!err) err = ksba_cert_init_from_mem (cert, value, valuelen); } xfree (value); if(err) goto leave; err = cache_cert (cert); leave: ksba_cert_release (cert); return leave_cmd (ctx, err); } static const char hlp_validate[] = "VALIDATE [--systrust] [--tls] [--no-crl]\n" "\n" "Validate a certificate using the certificate validation function\n" "used internally by dirmngr. This command is only useful for\n" "debugging. To get the actual certificate, this command immediately\n" "inquires it using\n" "\n" " INQUIRE TARGETCERT\n" "\n" "and the caller is expected to return the certificate for the\n" "request as a binary blob. The option --tls modifies this by asking\n" "for list of certificates with\n" "\n" " INQUIRE CERTLIST\n" "\n" "Here the first certificate is the target certificate, the remaining\n" "certificates are suggested intermediary certificates. All certificates\n" "need to be PEM encoded.\n" "\n" "The option --systrust changes the behaviour to include the system\n" "provided root certificates as trust anchors. The option --no-crl\n" "skips CRL checks"; static gpg_error_t cmd_validate (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err; ksba_cert_t cert = NULL; certlist_t certlist = NULL; unsigned char *value = NULL; size_t valuelen; int systrust_mode, tls_mode, no_crl; systrust_mode = has_option (line, "--systrust"); tls_mode = has_option (line, "--tls"); no_crl = has_option (line, "--no-crl"); line = skip_options (line); if (tls_mode) err = assuan_inquire (ctrl->server_local->assuan_ctx, "CERTLIST", &value, &valuelen, MAX_CERTLIST_LENGTH); else err = assuan_inquire (ctrl->server_local->assuan_ctx, "TARGETCERT", &value, &valuelen, MAX_CERT_LENGTH); if (err) { log_error (_("assuan_inquire failed: %s\n"), gpg_strerror (err)); goto leave; } if (!valuelen) /* No data returned; return a comprehensible error. */ err = gpg_error (GPG_ERR_MISSING_CERT); else if (tls_mode) { estream_t fp; fp = es_fopenmem_init (0, "rb", value, valuelen); if (!fp) err = gpg_error_from_syserror (); else { err = read_certlist_from_stream (&certlist, fp); es_fclose (fp); if (!err && !certlist) err = gpg_error (GPG_ERR_MISSING_CERT); if (!err) { /* Extract the first certificate from the list. */ cert = certlist->cert; ksba_cert_ref (cert); } } } else { err = ksba_cert_new (&cert); if (!err) err = ksba_cert_init_from_mem (cert, value, valuelen); } xfree (value); if(err) goto leave; if (!tls_mode) { /* If we have this certificate already in our cache, use the * cached version for validation because this will take care of * any cached results. We don't need to do this in tls mode * because this has already been done for certificate in a * certlist_t. */ unsigned char fpr[20]; ksba_cert_t tmpcert; cert_compute_fpr (cert, fpr); tmpcert = get_cert_byfpr (fpr); if (tmpcert) { ksba_cert_release (cert); cert = tmpcert; } } /* Quick hack to make verification work by inserting the supplied * certs into the cache. */ if (tls_mode && certlist) { certlist_t cl; for (cl = certlist->next; cl; cl = cl->next) cache_cert (cl->cert); } err = validate_cert_chain (ctrl, cert, NULL, (VALIDATE_FLAG_TRUST_CONFIG | (tls_mode ? VALIDATE_FLAG_TLS : 0) | (systrust_mode ? VALIDATE_FLAG_TRUST_SYSTEM : 0) | (no_crl ? VALIDATE_FLAG_NOCRLCHECK : 0)), NULL); leave: ksba_cert_release (cert); release_certlist (certlist); return leave_cmd (ctx, err); } /* Parse an keyserver URI and store it in a new uri item which is returned at R_ITEM. On error return an error code. */ static gpg_error_t make_keyserver_item (const char *uri, uri_item_t *r_item) { gpg_error_t err; uri_item_t item; *r_item = NULL; /* We used to have DNS CNAME redirection from the URLs below to * sks-keyserver. pools. The idea was to allow for a quick way to * switch to a different set of pools. The problem with that * approach is that TLS needs to verify the hostname and - because * DNS is not secured - it can only check the user supplied hostname * and not a hostname from a CNAME RR. Thus the final server all * need to have certificates with the actual pool name as well as * for keys.gnupg.net - that would render the advantage of * keys.gnupg.net useless and so we better give up on this. Because * the keys.gnupg.net URL are still in widespread use we do a static * mapping here. */ if (!strcmp (uri, "hkps://keys.gnupg.net") || !strcmp (uri, "keys.gnupg.net")) uri = "hkps://hkps.pool.sks-keyservers.net"; else if (!strcmp (uri, "https://keys.gnupg.net")) uri = "https://hkps.pool.sks-keyservers.net"; else if (!strcmp (uri, "hkp://keys.gnupg.net")) uri = "hkp://hkps.pool.sks-keyservers.net"; else if (!strcmp (uri, "http://keys.gnupg.net")) uri = "http://hkps.pool.sks-keyservers.net"; else if (!strcmp (uri, "hkps://http-keys.gnupg.net") || !strcmp (uri, "http-keys.gnupg.net")) uri = "hkps://ha.pool.sks-keyservers.net"; else if (!strcmp (uri, "https://http-keys.gnupg.net")) uri = "https://ha.pool.sks-keyservers.net"; else if (!strcmp (uri, "hkp://http-keys.gnupg.net")) uri = "hkp://ha.pool.sks-keyservers.net"; else if (!strcmp (uri, "http://http-keys.gnupg.net")) uri = "http://ha.pool.sks-keyservers.net"; item = xtrymalloc (sizeof *item + strlen (uri)); if (!item) return gpg_error_from_syserror (); item->next = NULL; item->parsed_uri = NULL; strcpy (item->uri, uri); #if USE_LDAP if (ldap_uri_p (item->uri)) err = ldap_parse_uri (&item->parsed_uri, uri); else #endif { - err = http_parse_uri (&item->parsed_uri, uri, 1); + err = http_parse_uri (&item->parsed_uri, uri, HTTP_PARSE_NO_SCHEME_CHECK); } if (err) xfree (item); else *r_item = item; return err; } /* If no keyserver is stored in CTRL but a global keyserver has been set, put that global keyserver into CTRL. We need use this function to help migrate from the old gpg based keyserver configuration to the new dirmngr based configuration. */ static gpg_error_t ensure_keyserver (ctrl_t ctrl) { gpg_error_t err; uri_item_t item; uri_item_t onion_items = NULL; uri_item_t plain_items = NULL; uri_item_t ui; strlist_t sl; if (ctrl->server_local->keyservers) return 0; /* Already set for this session. */ if (!opt.keyserver) { /* No global option set. Fall back to default: */ return make_keyserver_item (DIRMNGR_DEFAULT_KEYSERVER, &ctrl->server_local->keyservers); } for (sl = opt.keyserver; sl; sl = sl->next) { err = make_keyserver_item (sl->d, &item); if (err) goto leave; if (item->parsed_uri->onion) { item->next = onion_items; onion_items = item; } else { item->next = plain_items; plain_items = item; } } /* Decide which to use. Note that the session has no keyservers yet set. */ if (onion_items && !onion_items->next && plain_items && !plain_items->next) { /* If there is just one onion and one plain keyserver given, we take only one depending on whether Tor is running or not. */ if (is_tor_running (ctrl)) { ctrl->server_local->keyservers = onion_items; onion_items = NULL; } else { ctrl->server_local->keyservers = plain_items; plain_items = NULL; } } else if (!is_tor_running (ctrl)) { /* Tor is not running. It does not make sense to add Onion addresses. */ ctrl->server_local->keyservers = plain_items; plain_items = NULL; } else { /* In all other cases add all keyservers. */ ctrl->server_local->keyservers = onion_items; onion_items = NULL; for (ui = ctrl->server_local->keyservers; ui && ui->next; ui = ui->next) ; if (ui) ui->next = plain_items; else ctrl->server_local->keyservers = plain_items; plain_items = NULL; } leave: release_uri_item_list (onion_items); release_uri_item_list (plain_items); return err; } static const char hlp_keyserver[] = "KEYSERVER [] [|]\n" "Options are:\n" " --help\n" " --clear Remove all configured keyservers\n" " --resolve Resolve HKP host names and rotate\n" " --hosttable Print table of known hosts and pools\n" " --dead Mark as dead\n" " --alive Mark as alive\n" "\n" "If called without arguments list all configured keyserver URLs.\n" "If called with an URI add this as keyserver. Note that keyservers\n" "are configured on a per-session base. A default keyserver may already be\n" "present, thus the \"--clear\" option must be used to get full control.\n" "If \"--clear\" and an URI are used together the clear command is\n" "obviously executed first. A RESET command does not change the list\n" "of configured keyservers."; static gpg_error_t cmd_keyserver (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err = 0; int clear_flag, add_flag, help_flag, host_flag, resolve_flag; int dead_flag, alive_flag; uri_item_t item = NULL; /* gcc 4.4.5 is not able to detect that it is always initialized. */ clear_flag = has_option (line, "--clear"); help_flag = has_option (line, "--help"); resolve_flag = has_option (line, "--resolve"); host_flag = has_option (line, "--hosttable"); dead_flag = has_option (line, "--dead"); alive_flag = has_option (line, "--alive"); line = skip_options (line); add_flag = !!*line; if (help_flag) { err = ks_action_help (ctrl, line); goto leave; } if (resolve_flag) { err = ensure_keyserver (ctrl); if (err) { assuan_set_error (ctx, err, "Bad keyserver configuration in dirmngr.conf"); goto leave; } err = ks_action_resolve (ctrl, ctrl->server_local->keyservers); if (err) goto leave; } if (alive_flag && dead_flag) { err = set_error (GPG_ERR_ASS_PARAMETER, "no support for zombies"); goto leave; } if (dead_flag) { err = check_owner_permission (ctx, "no permission to use --dead"); if (err) goto leave; } if (alive_flag || dead_flag) { if (!*line) { err = set_error (GPG_ERR_ASS_PARAMETER, "name of host missing"); goto leave; } err = ks_hkp_mark_host (ctrl, line, alive_flag); if (err) goto leave; } if (host_flag) { err = ks_hkp_print_hosttable (ctrl); if (err) goto leave; } if (resolve_flag || host_flag || alive_flag || dead_flag) goto leave; if (add_flag) { err = make_keyserver_item (line, &item); if (err) goto leave; } if (clear_flag) release_ctrl_keyservers (ctrl); if (add_flag) { item->next = ctrl->server_local->keyservers; ctrl->server_local->keyservers = item; } if (!add_flag && !clear_flag && !help_flag) { /* List configured keyservers. However, we first add a global keyserver. */ uri_item_t u; err = ensure_keyserver (ctrl); if (err) { assuan_set_error (ctx, err, "Bad keyserver configuration in dirmngr.conf"); goto leave; } for (u=ctrl->server_local->keyservers; u; u = u->next) dirmngr_status (ctrl, "KEYSERVER", u->uri, NULL); } err = 0; leave: return leave_cmd (ctx, err); } static const char hlp_ks_search[] = "KS_SEARCH {}\n" "\n" "Search the configured OpenPGP keyservers (see command KEYSERVER)\n" "for keys matching PATTERN"; static gpg_error_t cmd_ks_search (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err; strlist_t list, sl; char *p; estream_t outfp; if (has_option (line, "--quick")) ctrl->timeout = opt.connect_quick_timeout; line = skip_options (line); /* Break the line down into an strlist. Each pattern is percent-plus escaped. */ list = NULL; for (p=line; *p; line = p) { while (*p && *p != ' ') p++; if (*p) *p++ = 0; if (*line) { sl = xtrymalloc (sizeof *sl + strlen (line)); if (!sl) { err = gpg_error_from_syserror (); goto leave; } sl->flags = 0; strcpy_escaped_plus (sl->d, line); sl->next = list; list = sl; } } err = ensure_keyserver (ctrl); if (err) goto leave; /* Setup an output stream and perform the search. */ outfp = es_fopencookie (ctx, "w", data_line_cookie_functions); if (!outfp) err = set_error (GPG_ERR_ASS_GENERAL, "error setting up a data stream"); else { err = ks_action_search (ctrl, ctrl->server_local->keyservers, list, outfp); es_fclose (outfp); } leave: free_strlist (list); return leave_cmd (ctx, err); } static const char hlp_ks_get[] = "KS_GET [--quick] [--ldap] {}\n" "\n" "Get the keys matching PATTERN from the configured OpenPGP keyservers\n" "(see command KEYSERVER). Each pattern should be a keyid, a fingerprint,\n" "or an exact name indicated by the '=' prefix. Option --quick uses a\n" "shorter timeout; --ldap will use only ldap servers"; static gpg_error_t cmd_ks_get (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err; strlist_t list, sl; char *p; estream_t outfp; int ldap_only; if (has_option (line, "--quick")) ctrl->timeout = opt.connect_quick_timeout; ldap_only = has_option (line, "--ldap"); line = skip_options (line); /* Break the line into a strlist. Each pattern is by definition percent-plus escaped. However we only support keyids and fingerprints and thus the client has no need to apply the escaping. */ list = NULL; for (p=line; *p; line = p) { while (*p && *p != ' ') p++; if (*p) *p++ = 0; if (*line) { sl = xtrymalloc (sizeof *sl + strlen (line)); if (!sl) { err = gpg_error_from_syserror (); goto leave; } sl->flags = 0; strcpy_escaped_plus (sl->d, line); sl->next = list; list = sl; } } err = ensure_keyserver (ctrl); if (err) goto leave; /* Setup an output stream and perform the get. */ outfp = es_fopencookie (ctx, "w", data_line_cookie_functions); if (!outfp) err = set_error (GPG_ERR_ASS_GENERAL, "error setting up a data stream"); else { ctrl->server_local->inhibit_data_logging = 1; ctrl->server_local->inhibit_data_logging_now = 0; ctrl->server_local->inhibit_data_logging_count = 0; err = ks_action_get (ctrl, ctrl->server_local->keyservers, list, ldap_only, outfp); es_fclose (outfp); ctrl->server_local->inhibit_data_logging = 0; } leave: free_strlist (list); return leave_cmd (ctx, err); } static const char hlp_ks_fetch[] = "KS_FETCH \n" "\n" "Get the key(s) from URL."; static gpg_error_t cmd_ks_fetch (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err; estream_t outfp; if (has_option (line, "--quick")) ctrl->timeout = opt.connect_quick_timeout; line = skip_options (line); err = ensure_keyserver (ctrl); /* FIXME: Why do we needs this here? */ if (err) goto leave; /* Setup an output stream and perform the get. */ outfp = es_fopencookie (ctx, "w", data_line_cookie_functions); if (!outfp) err = set_error (GPG_ERR_ASS_GENERAL, "error setting up a data stream"); else { ctrl->server_local->inhibit_data_logging = 1; ctrl->server_local->inhibit_data_logging_now = 0; ctrl->server_local->inhibit_data_logging_count = 0; err = ks_action_fetch (ctrl, line, outfp); es_fclose (outfp); ctrl->server_local->inhibit_data_logging = 0; } leave: return leave_cmd (ctx, err); } static const char hlp_ks_put[] = "KS_PUT\n" "\n" "Send a key to the configured OpenPGP keyservers. The actual key material\n" "is then requested by Dirmngr using\n" "\n" " INQUIRE KEYBLOCK\n" "\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 output\n" "of 'gpg --list-keys --with-colons KEYID').\n"; static gpg_error_t cmd_ks_put (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err; unsigned char *value = NULL; size_t valuelen; unsigned char *info = NULL; size_t infolen; /* No options for now. */ line = skip_options (line); err = ensure_keyserver (ctrl); if (err) goto leave; /* Ask for the key material. */ err = assuan_inquire (ctx, "KEYBLOCK", &value, &valuelen, MAX_KEYBLOCK_LENGTH); if (err) { log_error (_("assuan_inquire failed: %s\n"), gpg_strerror (err)); goto leave; } if (!valuelen) /* No data returned; return a comprehensible error. */ { err = gpg_error (GPG_ERR_MISSING_CERT); goto leave; } /* Ask for the key meta data. */ err = assuan_inquire (ctx, "KEYBLOCK_INFO", &info, &infolen, MAX_KEYBLOCK_LENGTH); if (err) { log_error (_("assuan_inquire failed: %s\n"), gpg_strerror (err)); goto leave; } /* Send the key. */ err = ks_action_put (ctrl, ctrl->server_local->keyservers, value, valuelen, info, infolen); leave: xfree (info); xfree (value); return leave_cmd (ctx, err); } static const char hlp_loadswdb[] = "LOADSWDB [--force]\n" "\n" "Load and verify the swdb.lst from the Net."; static gpg_error_t cmd_loadswdb (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err; err = dirmngr_load_swdb (ctrl, has_option (line, "--force")); return leave_cmd (ctx, err); } static const char hlp_getinfo[] = "GETINFO \n" "\n" "Multi purpose command to return certain information. \n" "Supported values of WHAT are:\n" "\n" "version - Return the version of the program.\n" "pid - Return the process id of the server.\n" "tor - Return OK if running in Tor mode\n" "dnsinfo - Return info about the DNS resolver\n" "socket_name - Return the name of the socket.\n" "session_id - Return the current session_id.\n" "workqueue - Inspect the work queue\n" "getenv NAME - Return value of envvar NAME\n"; static gpg_error_t cmd_getinfo (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); gpg_error_t err; char numbuf[50]; if (!strcmp (line, "version")) { const char *s = VERSION; err = assuan_send_data (ctx, s, strlen (s)); } else if (!strcmp (line, "pid")) { snprintf (numbuf, sizeof numbuf, "%lu", (unsigned long)getpid ()); err = assuan_send_data (ctx, numbuf, strlen (numbuf)); } else if (!strcmp (line, "socket_name")) { const char *s = dirmngr_get_current_socket_name (); err = assuan_send_data (ctx, s, strlen (s)); } else if (!strcmp (line, "session_id")) { snprintf (numbuf, sizeof numbuf, "%u", ctrl->server_local->session_id); err = assuan_send_data (ctx, numbuf, strlen (numbuf)); } else if (!strcmp (line, "tor")) { int use_tor; use_tor = dirmngr_use_tor (); if (use_tor) { if (!is_tor_running (ctrl)) err = assuan_write_status (ctx, "NO_TOR", "Tor not running"); else err = 0; if (!err) assuan_set_okay_line (ctx, use_tor == 1 ? "- Tor mode is enabled" /**/ : "- Tor mode is enforced"); } else err = set_error (GPG_ERR_FALSE, "Tor mode is NOT enabled"); } else if (!strcmp (line, "dnsinfo")) { if (standard_resolver_p ()) assuan_set_okay_line (ctx, "- Forced use of System resolver (w/o Tor support)"); else { #ifdef USE_LIBDNS assuan_set_okay_line (ctx, (recursive_resolver_p () ? "- Libdns recursive resolver" : "- Libdns stub resolver")); #else assuan_set_okay_line (ctx, "- System resolver (w/o Tor support)"); #endif } err = 0; } else if (!strcmp (line, "workqueue")) { workqueue_dump_queue (ctrl); err = 0; } else if (!strncmp (line, "getenv", 6) && (line[6] == ' ' || line[6] == '\t' || !line[6])) { line += 6; while (*line == ' ' || *line == '\t') line++; if (!*line) err = gpg_error (GPG_ERR_MISSING_VALUE); else { const char *s = getenv (line); if (!s) err = set_error (GPG_ERR_NOT_FOUND, "No such envvar"); else err = assuan_send_data (ctx, s, strlen (s)); } } else err = set_error (GPG_ERR_ASS_PARAMETER, "unknown value for WHAT"); return leave_cmd (ctx, err); } static const char hlp_killdirmngr[] = "KILLDIRMNGR\n" "\n" "This command allows a user - given sufficient permissions -\n" "to kill this dirmngr process.\n"; static gpg_error_t cmd_killdirmngr (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); (void)line; ctrl->server_local->stopme = 1; assuan_set_flag (ctx, ASSUAN_FORCE_CLOSE, 1); return 0; } static const char hlp_reloaddirmngr[] = "RELOADDIRMNGR\n" "\n" "This command is an alternative to SIGHUP\n" "to reload the configuration."; static gpg_error_t cmd_reloaddirmngr (assuan_context_t ctx, char *line) { (void)ctx; (void)line; dirmngr_sighup_action (); return 0; } static const char hlp_flushcrls[] = "FLUSHCRLS\n" "\n" "Remove all cached CRLs from memory and\n" "the file system."; static gpg_error_t cmd_flushcrls (assuan_context_t ctx, char *line) { (void)line; return leave_cmd (ctx, crl_cache_flush () ? GPG_ERR_GENERAL : 0); } /* Tell the assuan library about our commands. */ static int register_commands (assuan_context_t ctx) { static struct { const char *name; assuan_handler_t handler; const char * const help; } table[] = { { "DNS_CERT", cmd_dns_cert, hlp_dns_cert }, { "WKD_GET", cmd_wkd_get, hlp_wkd_get }, { "LDAPSERVER", cmd_ldapserver, hlp_ldapserver }, { "ISVALID", cmd_isvalid, hlp_isvalid }, { "CHECKCRL", cmd_checkcrl, hlp_checkcrl }, { "CHECKOCSP", cmd_checkocsp, hlp_checkocsp }, { "LOOKUP", cmd_lookup, hlp_lookup }, { "LOADCRL", cmd_loadcrl, hlp_loadcrl }, { "LISTCRLS", cmd_listcrls, hlp_listcrls }, { "CACHECERT", cmd_cachecert, hlp_cachecert }, { "VALIDATE", cmd_validate, hlp_validate }, { "KEYSERVER", cmd_keyserver, hlp_keyserver }, { "KS_SEARCH", cmd_ks_search, hlp_ks_search }, { "KS_GET", cmd_ks_get, hlp_ks_get }, { "KS_FETCH", cmd_ks_fetch, hlp_ks_fetch }, { "KS_PUT", cmd_ks_put, hlp_ks_put }, { "GETINFO", cmd_getinfo, hlp_getinfo }, { "LOADSWDB", cmd_loadswdb, hlp_loadswdb }, { "KILLDIRMNGR",cmd_killdirmngr,hlp_killdirmngr }, { "RELOADDIRMNGR",cmd_reloaddirmngr,hlp_reloaddirmngr }, { "FLUSHCRLS", cmd_flushcrls, hlp_flushcrls }, { NULL, NULL } }; int i, j, rc; for (i=j=0; table[i].name; i++) { rc = assuan_register_command (ctx, table[i].name, table[i].handler, table[i].help); if (rc) return rc; } return 0; } /* Note that we do not reset the list of configured keyservers. */ static gpg_error_t reset_notify (assuan_context_t ctx, char *line) { ctrl_t ctrl = assuan_get_pointer (ctx); (void)line; #if USE_LDAP ldapserver_list_free (ctrl->server_local->ldapservers); #endif /*USE_LDAP*/ ctrl->server_local->ldapservers = NULL; return 0; } /* This function is called by our assuan log handler to test whether a * log message shall really be printed. The function must return * false to inhibit the logging of MSG. CAT gives the requested log * category. MSG might be NULL. */ int dirmngr_assuan_log_monitor (assuan_context_t ctx, unsigned int cat, const char *msg) { ctrl_t ctrl = assuan_get_pointer (ctx); (void)cat; (void)msg; if (!ctrl || !ctrl->server_local) return 1; /* Can't decide - allow logging. */ if (!ctrl->server_local->inhibit_data_logging) return 1; /* Not requested - allow logging. */ /* Disallow logging if *_now is true. */ return !ctrl->server_local->inhibit_data_logging_now; } /* Startup the server and run the main command loop. With FD = -1, * use stdin/stdout. SESSION_ID is either 0 or a unique number * identifying a session. */ void start_command_handler (assuan_fd_t fd, unsigned int session_id) { static const char hello[] = "Dirmngr " VERSION " at your service"; static char *hello_line; int rc; assuan_context_t ctx; ctrl_t ctrl; ctrl = xtrycalloc (1, sizeof *ctrl); if (ctrl) ctrl->server_local = xtrycalloc (1, sizeof *ctrl->server_local); if (!ctrl || !ctrl->server_local) { log_error (_("can't allocate control structure: %s\n"), strerror (errno)); xfree (ctrl); return; } dirmngr_init_default_ctrl (ctrl); rc = assuan_new (&ctx); if (rc) { log_error (_("failed to allocate assuan context: %s\n"), gpg_strerror (rc)); dirmngr_exit (2); } if (fd == ASSUAN_INVALID_FD) { assuan_fd_t filedes[2]; filedes[0] = assuan_fdopen (0); filedes[1] = assuan_fdopen (1); rc = assuan_init_pipe_server (ctx, filedes); } else { rc = assuan_init_socket_server (ctx, fd, ASSUAN_SOCKET_SERVER_ACCEPTED); } if (rc) { assuan_release (ctx); log_error (_("failed to initialize the server: %s\n"), gpg_strerror(rc)); dirmngr_exit (2); } rc = register_commands (ctx); if (rc) { log_error (_("failed to the register commands with Assuan: %s\n"), gpg_strerror(rc)); dirmngr_exit (2); } if (!hello_line) { hello_line = xtryasprintf ("Home: %s\n" "Config: %s\n" "%s", gnupg_homedir (), opt.config_filename? opt.config_filename : "[none]", hello); } ctrl->server_local->assuan_ctx = ctx; assuan_set_pointer (ctx, ctrl); assuan_set_hello_line (ctx, hello_line); assuan_register_option_handler (ctx, option_handler); assuan_register_reset_notify (ctx, reset_notify); ctrl->server_local->session_id = session_id; for (;;) { rc = assuan_accept (ctx); if (rc == -1) break; if (rc) { log_info (_("Assuan accept problem: %s\n"), gpg_strerror (rc)); break; } #ifndef HAVE_W32_SYSTEM if (opt.verbose) { assuan_peercred_t peercred; if (!assuan_get_peercred (ctx, &peercred)) log_info ("connection from process %ld (%ld:%ld)\n", (long)peercred->pid, (long)peercred->uid, (long)peercred->gid); } #endif rc = assuan_process (ctx); if (rc) { log_info (_("Assuan processing failed: %s\n"), gpg_strerror (rc)); continue; } } #if USE_LDAP ldap_wrapper_connection_cleanup (ctrl); ldapserver_list_free (ctrl->server_local->ldapservers); #endif /*USE_LDAP*/ ctrl->server_local->ldapservers = NULL; release_ctrl_keyservers (ctrl); ctrl->server_local->assuan_ctx = NULL; assuan_release (ctx); if (ctrl->server_local->stopme) dirmngr_exit (0); if (ctrl->refcount) log_error ("oops: connection control structure still referenced (%d)\n", ctrl->refcount); else { release_ctrl_ocsp_certs (ctrl); xfree (ctrl->server_local); dirmngr_deinit_default_ctrl (ctrl); xfree (ctrl); } } /* Send a status line back to the client. KEYWORD is the status keyword, the optional string arguments are blank separated added to the line, the last argument must be a NULL. */ gpg_error_t dirmngr_status (ctrl_t ctrl, const char *keyword, ...) { gpg_error_t err = 0; va_list arg_ptr; assuan_context_t ctx; va_start (arg_ptr, keyword); if (ctrl->server_local && (ctx = ctrl->server_local->assuan_ctx)) { err = vprint_assuan_status_strings (ctx, keyword, arg_ptr); } va_end (arg_ptr); return err; } /* Print a help status line. The function splits text at LFs. */ gpg_error_t dirmngr_status_help (ctrl_t ctrl, const char *text) { gpg_error_t err = 0; assuan_context_t ctx; if (ctrl->server_local && (ctx = ctrl->server_local->assuan_ctx)) { char buf[950], *p; size_t n; do { p = buf; n = 0; for ( ; *text && *text != '\n' && n < DIM (buf)-2; n++) *p++ = *text++; if (*text == '\n') text++; *p = 0; err = assuan_write_status (ctx, "#", buf); } while (!err && *text); } return err; } /* Print a help status line using a printf like format. The function * splits text at LFs. */ gpg_error_t dirmngr_status_helpf (ctrl_t ctrl, const char *format, ...) { va_list arg_ptr; gpg_error_t err; char *buf; va_start (arg_ptr, format); buf = es_vbsprintf (format, arg_ptr); err = buf? 0 : gpg_error_from_syserror (); va_end (arg_ptr); if (!err) err = dirmngr_status_help (ctrl, buf); es_free (buf); return err; } /* This function is similar to print_assuan_status but takes a CTRL * arg instead of an assuan context as first argument. */ gpg_error_t dirmngr_status_printf (ctrl_t ctrl, const char *keyword, const char *format, ...) { gpg_error_t err; va_list arg_ptr; assuan_context_t ctx; if (!ctrl || !ctrl->server_local || !(ctx = ctrl->server_local->assuan_ctx)) return 0; va_start (arg_ptr, format); err = vprint_assuan_status (ctx, keyword, format, arg_ptr); va_end (arg_ptr); return err; } /* Send a tick progress indicator back. Fixme: This is only done for the currently active channel. */ gpg_error_t dirmngr_tick (ctrl_t ctrl) { static time_t next_tick = 0; gpg_error_t err = 0; time_t now = time (NULL); if (!next_tick) { next_tick = now + 1; } else if ( now > next_tick ) { if (ctrl) { err = dirmngr_status (ctrl, "PROGRESS", "tick", "? 0 0", NULL); if (err) { /* Take this as in indication for a cancel request. */ err = gpg_error (GPG_ERR_CANCELED); } now = time (NULL); } next_tick = now + 1; } return err; } diff --git a/dirmngr/t-http.c b/dirmngr/t-http.c index 8ad5e7a0f..7f3aa005d 100644 --- a/dirmngr/t-http.c +++ b/dirmngr/t-http.c @@ -1,498 +1,498 @@ /* t-http.c * Copyright (C) 1999, 2001, 2002, 2003, 2004, 2006, 2009, 2010, * 2011 Free Software Foundation, Inc. * Copyright (C) 2014 Werner Koch * * This file is part of GnuPG. * * This file is free software; you can redistribute it and/or modify * it under the terms of either * * - the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at * your option) any later version. * * or * * - the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at * your option) any later version. * * or both in parallel, as here. * * This file 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 . */ #include #include #include #include #include #include #include #include #include "../common/util.h" #include "../common/logging.h" #include "dns-stuff.h" #include "http.h" #include #if HTTP_USE_NTBTLS # include #elif HTTP_USE_GNUTLS # include /* For init, logging, and deinit. */ #endif /*HTTP_USE_GNUTLS*/ #define PGM "t-http" static int verbose; static int debug; static int no_verify; /* static void */ /* read_dh_params (const char *fname) */ /* { */ /* gpg_error_t err; */ /* int rc; */ /* FILE *fp; */ /* struct stat st; */ /* char *buf; */ /* size_t buflen; */ /* gnutls_datum_t datum; */ /* fp = fopen (fname, "rb"); */ /* if (!fp) */ /* { */ /* err = gpg_error_from_syserror (); */ /* log_fatal ("can't open '%s': %s\n", fname, gpg_strerror (err)); */ /* } */ /* if (fstat (fileno(fp), &st)) */ /* { */ /* err = gpg_error_from_syserror (); */ /* log_fatal ("can't stat '%s': %s\n", fname, gpg_strerror (err)); */ /* } */ /* buflen = st.st_size; */ /* buf = xmalloc (buflen+1); */ /* if (fread (buf, buflen, 1, fp) != 1) */ /* { */ /* err = gpg_error_from_syserror (); */ /* log_fatal ("error reading '%s': %s\n", fname, gpg_strerror (err)); */ /* } */ /* fclose (fp); */ /* datum.size = buflen; */ /* datum.data = buf; */ /* rc = gnutls_dh_params_import_pkcs3 (dh_params, &datum, GNUTLS_X509_FMT_PEM); */ /* if (rc < 0) */ /* log_fatal ("gnutls_dh_param_import failed: %s\n", gnutls_strerror (rc)); */ /* xfree (buf); */ /* } */ #if HTTP_USE_GNUTLS static gpg_error_t verify_callback (http_t hd, http_session_t session, int reserved) { (void)hd; (void)reserved; return no_verify? 0 : http_verify_server_credentials (session); } #endif #if HTTP_USE_GNUTLS static void my_gnutls_log (int level, const char *text) { fprintf (stderr, "gnutls:L%d: %s", level, text); } #endif #if HTTP_USE_NTBTLS static gpg_error_t my_http_tls_verify_cb (void *opaque, http_t http, http_session_t session, unsigned int http_flags, void *tls_context) { gpg_error_t err; int idx; ksba_cert_t cert; ksba_cert_t hostcert = NULL; (void)opaque; (void)http; (void)session; (void)http_flags; /* Get the peer's certs from ntbtls. */ for (idx = 0; (cert = ntbtls_x509_get_peer_cert (tls_context, idx)); idx++) { if (!idx) { log_info ("Received host certificate\n"); hostcert = cert; } else { log_info ("Received additional certificate\n"); ksba_cert_release (cert); } } if (!idx) { err = gpg_error (GPG_ERR_MISSING_CERT); goto leave; } err = 0; leave: ksba_cert_release (hostcert); log_info ("my_http_tls_verify_cb returns: %s\n", gpg_strerror (err)); return err; } #endif /*HTTP_USE_NTBTLS*/ /* Prepend FNAME with the srcdir environment variable's value and return an allocated filename. */ static char * prepend_srcdir (const char *fname) { static const char *srcdir; char *result; if (!srcdir && !(srcdir = getenv ("srcdir"))) srcdir = "."; result = xmalloc (strlen (srcdir) + 1 + strlen (fname) + 1); strcpy (result, srcdir); strcat (result, "/"); strcat (result, fname); return result; } int main (int argc, char **argv) { int last_argc = -1; gpg_error_t err; int rc; parsed_uri_t uri; uri_tuple_t r; http_t hd; int c; unsigned int my_http_flags = 0; int no_out = 0; int tls_dbg = 0; int no_crl = 0; const char *cafile = NULL; http_session_t session = NULL; unsigned int timeout = 0; gpgrt_init (); log_set_prefix (PGM, GPGRT_LOG_WITH_PREFIX | GPGRT_LOG_WITH_PID); if (argc) { argc--; argv++; } while (argc && last_argc != argc ) { last_argc = argc; if (!strcmp (*argv, "--")) { argc--; argv++; break; } else if (!strcmp (*argv, "--help")) { fputs ("usage: " PGM " URL\n" "Options:\n" " --verbose print timings etc.\n" " --debug flyswatter\n" " --tls-debug N use TLS debug level N\n" " --cacert FNAME expect CA certificate in file FNAME\n" " --timeout MS timeout for connect in MS\n" " --no-verify do not verify the certificate\n" " --force-tls use HTTP_FLAG_FORCE_TLS\n" " --force-tor use HTTP_FLAG_FORCE_TOR\n" " --no-out do not print the content\n" " --no-crl do not consuilt a CRL\n", stdout); exit (0); } else if (!strcmp (*argv, "--verbose")) { verbose++; argc--; argv++; } else if (!strcmp (*argv, "--debug")) { verbose += 2; debug++; argc--; argv++; } else if (!strcmp (*argv, "--tls-debug")) { argc--; argv++; if (argc) { tls_dbg = atoi (*argv); argc--; argv++; } } else if (!strcmp (*argv, "--cacert")) { argc--; argv++; if (argc) { cafile = *argv; argc--; argv++; } } else if (!strcmp (*argv, "--timeout")) { argc--; argv++; if (argc) { timeout = strtoul (*argv, NULL, 10); argc--; argv++; } } else if (!strcmp (*argv, "--no-verify")) { no_verify = 1; argc--; argv++; } else if (!strcmp (*argv, "--force-tls")) { my_http_flags |= HTTP_FLAG_FORCE_TLS; argc--; argv++; } else if (!strcmp (*argv, "--force-tor")) { my_http_flags |= HTTP_FLAG_FORCE_TOR; argc--; argv++; } else if (!strcmp (*argv, "--no-out")) { no_out = 1; argc--; argv++; } else if (!strcmp (*argv, "--no-crl")) { no_crl = 1; argc--; argv++; } else if (!strncmp (*argv, "--", 2)) { fprintf (stderr, PGM ": unknown option '%s'\n", *argv); exit (1); } } if (argc != 1) { fprintf (stderr, PGM ": no or too many URLS given\n"); exit (1); } if (!cafile) cafile = prepend_srcdir ("tls-ca.pem"); if (verbose) my_http_flags |= HTTP_FLAG_LOG_RESP; if (verbose || debug) http_set_verbose (verbose, debug); /* http.c makes use of the assuan socket wrapper. */ assuan_sock_init (); if ((my_http_flags & HTTP_FLAG_FORCE_TOR)) { enable_dns_tormode (1); if (assuan_sock_set_flag (ASSUAN_INVALID_FD, "tor-mode", 1)) { log_error ("error enabling Tor mode: %s\n", strerror (errno)); log_info ("(is your Libassuan recent enough?)\n"); } } #if HTTP_USE_NTBTLS log_info ("new session.\n"); err = http_session_new (&session, NULL, ((no_crl? HTTP_FLAG_NO_CRL : 0) | HTTP_FLAG_TRUST_DEF), my_http_tls_verify_cb, NULL); if (err) log_error ("http_session_new failed: %s\n", gpg_strerror (err)); ntbtls_set_debug (tls_dbg, NULL, NULL); #elif HTTP_USE_GNUTLS rc = gnutls_global_init (); if (rc) log_error ("gnutls_global_init failed: %s\n", gnutls_strerror (rc)); http_register_tls_callback (verify_callback); http_register_tls_ca (cafile); err = http_session_new (&session, NULL, ((no_crl? HTTP_FLAG_NO_CRL : 0) | HTTP_FLAG_TRUST_DEF), NULL, NULL); if (err) log_error ("http_session_new failed: %s\n", gpg_strerror (err)); /* rc = gnutls_dh_params_init(&dh_params); */ /* if (rc) */ /* log_error ("gnutls_dh_params_init failed: %s\n", gnutls_strerror (rc)); */ /* read_dh_params ("dh_param.pem"); */ /* rc = gnutls_certificate_set_x509_trust_file */ /* (certcred, "ca.pem", GNUTLS_X509_FMT_PEM); */ /* if (rc) */ /* log_error ("gnutls_certificate_set_x509_trust_file failed: %s\n", */ /* gnutls_strerror (rc)); */ /* gnutls_certificate_set_dh_params (certcred, dh_params); */ gnutls_global_set_log_function (my_gnutls_log); if (tls_dbg) gnutls_global_set_log_level (tls_dbg); #else (void)err; (void)tls_dbg; (void)no_crl; #endif /*HTTP_USE_GNUTLS*/ - rc = http_parse_uri (&uri, *argv, 1); + rc = http_parse_uri (&uri, *argv, HTTP_PARSE_NO_SCHEME_CHECK); if (rc) { log_error ("'%s': %s\n", *argv, gpg_strerror (rc)); return 1; } printf ("Scheme: %s\n", uri->scheme); if (uri->opaque) printf ("Value : %s\n", uri->path); else { printf ("Auth : %s\n", uri->auth? uri->auth:"[none]"); printf ("Host : %s (off=%hu)\n", uri->host, uri->off_host); printf ("Port : %u\n", uri->port); printf ("Path : %s (off=%hu)\n", uri->path, uri->off_path); for (r = uri->params; r; r = r->next) { printf ("Params: %s", r->name); if (!r->no_value) { printf ("=%s", r->value); if (strlen (r->value) != r->valuelen) printf (" [real length=%d]", (int) r->valuelen); } putchar ('\n'); } for (r = uri->query; r; r = r->next) { printf ("Query : %s", r->name); if (!r->no_value) { printf ("=%s", r->value); if (strlen (r->value) != r->valuelen) printf (" [real length=%d]", (int) r->valuelen); } putchar ('\n'); } printf ("Flags :%s%s%s%s%s\n", uri->is_http? " http":"", uri->is_ldap? " ldap":"", uri->opaque? " opaque":"", uri->v6lit? " v6lit":"", uri->onion? " onion":""); printf ("TLS : %s\n", uri->use_tls? "yes": (my_http_flags&HTTP_FLAG_FORCE_TLS)? "forced" : "no"); printf ("Tor : %s\n", (my_http_flags&HTTP_FLAG_FORCE_TOR)? "yes" : "no"); } fflush (stdout); http_release_parsed_uri (uri); uri = NULL; if (session) http_session_set_timeout (session, timeout); rc = http_open_document (NULL, &hd, *argv, NULL, my_http_flags, NULL, session, NULL, NULL); if (rc) { log_error ("can't get '%s': %s\n", *argv, gpg_strerror (rc)); return 1; } log_info ("open_http_document succeeded; status=%u\n", http_get_status_code (hd)); { const char **names; int i; names = http_get_header_names (hd); if (!names) log_fatal ("http_get_header_names failed: %s\n", gpg_strerror (gpg_error_from_syserror ())); for (i = 0; names[i]; i++) printf ("HDR: %s: %s\n", names[i], http_get_header (hd, names[i])); xfree (names); } fflush (stdout); switch (http_get_status_code (hd)) { case 200: case 400: case 401: case 403: case 404: { unsigned long count = 0; while ((c = es_getc (http_get_read_ptr (hd))) != EOF) { count++; if (!no_out) putchar (c); } log_info ("Received bytes: %lu\n", count); } break; case 301: case 302: case 307: log_info ("Redirected to: %s\n", http_get_header (hd, "Location")); break; } http_close (hd, 0); http_session_release (session); #ifdef HTTP_USE_GNUTLS gnutls_global_deinit (); #endif /*HTTP_USE_GNUTLS*/ return 0; }