summaryrefslogtreecommitdiff
path: root/subversion/libsvn_subr/gpg_agent.c
diff options
context:
space:
mode:
Diffstat (limited to 'subversion/libsvn_subr/gpg_agent.c')
-rw-r--r--subversion/libsvn_subr/gpg_agent.c642
1 files changed, 642 insertions, 0 deletions
diff --git a/subversion/libsvn_subr/gpg_agent.c b/subversion/libsvn_subr/gpg_agent.c
new file mode 100644
index 0000000..4dbf118
--- /dev/null
+++ b/subversion/libsvn_subr/gpg_agent.c
@@ -0,0 +1,642 @@
+/*
+ * gpg_agent.c: GPG Agent provider for SVN_AUTH_CRED_*
+ *
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ */
+
+/* ==================================================================== */
+
+/* This auth provider stores a plaintext password in memory managed by
+ * a running gpg-agent. In contrast to other password store providers
+ * it does not save the password to disk.
+ *
+ * Prompting is performed by the gpg-agent using a "pinentry" program
+ * which needs to be installed separately. There are several pinentry
+ * implementations with different front-ends (e.g. qt, gtk, ncurses).
+ *
+ * The gpg-agent will let the password time out after a while,
+ * or immediately when it receives the SIGHUP signal.
+ * When the password has timed out it will automatically prompt the
+ * user for the password again. This is transparent to Subversion.
+ *
+ * SECURITY CONSIDERATIONS:
+ *
+ * Communication to the agent happens over a UNIX socket, which is located
+ * in a directory which only the user running Subversion can access.
+ * However, any program the user runs could access this socket and get
+ * the Subversion password if the program knows the "cache ID" Subversion
+ * uses for the password.
+ * The cache ID is very easy to obtain for programs running as the same user.
+ * Subversion uses the MD5 of the realmstring as cache ID, and these checksums
+ * are also used as filenames within ~/.subversion/auth/svn.simple.
+ * Unlike GNOME Keyring or KDE Wallet, the user is not prompted for
+ * permission if another program attempts to access the password.
+ *
+ * Therefore, while the gpg-agent is running and has the password cached,
+ * this provider is no more secure than a file storing the password in
+ * plaintext.
+ */
+
+
+/*** Includes. ***/
+
+#ifndef WIN32
+
+#include <unistd.h>
+
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include <apr_pools.h>
+#include "svn_auth.h"
+#include "svn_config.h"
+#include "svn_error.h"
+#include "svn_pools.h"
+#include "svn_cmdline.h"
+#include "svn_checksum.h"
+#include "svn_string.h"
+#include "svn_hash.h"
+#include "svn_user.h"
+#include "svn_dirent_uri.h"
+
+#include "private/svn_auth_private.h"
+
+#include "svn_private_config.h"
+
+#ifdef SVN_HAVE_GPG_AGENT
+
+#define BUFFER_SIZE 1024
+#define ATTEMPT_PARAMETER "svn.simple.gpg_agent.attempt"
+
+/* Modify STR in-place such that blanks are escaped as required by the
+ * gpg-agent protocol. Return a pointer to STR. */
+static char *
+escape_blanks(char *str)
+{
+ char *s = str;
+
+ while (*s)
+ {
+ if (*s == ' ')
+ *s = '+';
+ s++;
+ }
+
+ return str;
+}
+
+/* Generate the string CACHE_ID_P based on the REALMSTRING allocated in
+ * RESULT_POOL using SCRATCH_POOL for temporary allocations. This is similar
+ * to other password caching mechanisms. */
+static svn_error_t *
+get_cache_id(const char **cache_id_p, const char *realmstring,
+ apr_pool_t *scratch_pool, apr_pool_t *result_pool)
+{
+ const char *cache_id = NULL;
+ svn_checksum_t *digest = NULL;
+
+ SVN_ERR(svn_checksum(&digest, svn_checksum_md5, realmstring,
+ strlen(realmstring), scratch_pool));
+ cache_id = svn_checksum_to_cstring(digest, result_pool);
+ *cache_id_p = cache_id;
+
+ return SVN_NO_ERROR;
+}
+
+/* Attempt to read a gpg-agent response message from the socket SD into
+ * buffer BUF. Buf is assumed to be N bytes large. Return TRUE if a response
+ * message could be read that fits into the buffer. Else return FALSE.
+ * If a message could be read it will always be NUL-terminated and the
+ * trailing newline is retained. */
+static svn_boolean_t
+receive_from_gpg_agent(int sd, char *buf, size_t n)
+{
+ int i = 0;
+ size_t recvd;
+ char c;
+
+ /* Clear existing buffer content before reading response. */
+ if (n > 0)
+ *buf = '\0';
+
+ /* Require the message to fit into the buffer and be terminated
+ * with a newline. */
+ while (i < n)
+ {
+ recvd = read(sd, &c, 1);
+ if (recvd == -1)
+ return FALSE;
+ buf[i] = c;
+ i++;
+ if (i < n && c == '\n')
+ {
+ buf[i] = '\0';
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+/* Using socket SD, send the option OPTION with the specified VALUE
+ * to the gpg agent. Store the response in BUF, assumed to be N bytes
+ * in size, and evaluate the response. Return TRUE if the agent liked
+ * the smell of the option, if there is such a thing, and doesn't feel
+ * saturated by it. Else return FALSE.
+ * Do temporary allocations in scratch_pool. */
+static svn_boolean_t
+send_option(int sd, char *buf, size_t n, const char *option, const char *value,
+ apr_pool_t *scratch_pool)
+{
+ const char *request;
+
+ request = apr_psprintf(scratch_pool, "OPTION %s=%s\n", option, value);
+
+ if (write(sd, request, strlen(request)) == -1)
+ return FALSE;
+
+ if (!receive_from_gpg_agent(sd, buf, n))
+ return FALSE;
+
+ return (strncmp(buf, "OK", 2) == 0);
+}
+
+/* Send the BYE command and disconnect from the gpg-agent. Doing this avoids
+ * gpg-agent emitting a "Connection reset by peer" log message with some
+ * versions of gpg-agent. */
+static void
+bye_gpg_agent(int sd)
+{
+ /* don't bother to check the result of the write, it either worked or it
+ * didn't, but either way we're closing. */
+ write(sd, "BYE\n", 4);
+ close(sd);
+}
+
+/* Locate a running GPG Agent, and return an open file descriptor
+ * for communication with the agent in *NEW_SD. If no running agent
+ * can be found, set *NEW_SD to -1. */
+static svn_error_t *
+find_running_gpg_agent(int *new_sd, apr_pool_t *pool)
+{
+ char *buffer;
+ char *gpg_agent_info = NULL;
+ const char *socket_name = NULL;
+ const char *request = NULL;
+ const char *p = NULL;
+ char *ep = NULL;
+ int sd;
+
+ *new_sd = -1;
+
+ /* This implements the method of finding the socket as described in
+ * the gpg-agent man page under the --use-standard-socket option.
+ * The manage page misleadingly says the standard socket is
+ * "named 'S.gpg-agent' located in the home directory." The standard
+ * socket path is actually in the .gnupg directory in the home directory,
+ * i.e. ~/.gnupg/S.gpg-agent */
+ gpg_agent_info = getenv("GPG_AGENT_INFO");
+ if (gpg_agent_info != NULL)
+ {
+ apr_array_header_t *socket_details;
+
+ /* For reference GPG_AGENT_INFO consists of 3 : separated fields.
+ * The path to the socket, the pid of the gpg-agent process and
+ * finally the version of the protocol the agent talks. */
+ socket_details = svn_cstring_split(gpg_agent_info, ":", TRUE,
+ pool);
+ socket_name = APR_ARRAY_IDX(socket_details, 0, const char *);
+ }
+ else
+ {
+ const char *homedir = svn_user_get_homedir(pool);
+
+ if (!homedir)
+ return SVN_NO_ERROR;
+
+ socket_name = svn_dirent_join_many(pool, homedir, ".gnupg",
+ "S.gpg-agent", NULL);
+ }
+
+ if (socket_name != NULL)
+ {
+ struct sockaddr_un addr;
+
+ addr.sun_family = AF_UNIX;
+ strncpy(addr.sun_path, socket_name, sizeof(addr.sun_path) - 1);
+ addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
+
+ sd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (sd == -1)
+ return SVN_NO_ERROR;
+
+ if (connect(sd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
+ {
+ close(sd);
+ return SVN_NO_ERROR;
+ }
+ }
+ else
+ return SVN_NO_ERROR;
+
+ /* Receive the connection status from the gpg-agent daemon. */
+ buffer = apr_palloc(pool, BUFFER_SIZE);
+ if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE))
+ {
+ bye_gpg_agent(sd);
+ return SVN_NO_ERROR;
+ }
+
+ if (strncmp(buffer, "OK", 2) != 0)
+ {
+ bye_gpg_agent(sd);
+ return SVN_NO_ERROR;
+ }
+
+ /* The GPG-Agent documentation says:
+ * "Clients should deny to access an agent with a socket name which does
+ * not match its own configuration". */
+ request = "GETINFO socket_name\n";
+ if (write(sd, request, strlen(request)) == -1)
+ {
+ bye_gpg_agent(sd);
+ return SVN_NO_ERROR;
+ }
+ if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE))
+ {
+ bye_gpg_agent(sd);
+ return SVN_NO_ERROR;
+ }
+ if (strncmp(buffer, "D", 1) == 0)
+ p = &buffer[2];
+ if (!p)
+ {
+ bye_gpg_agent(sd);
+ return SVN_NO_ERROR;
+ }
+ ep = strchr(p, '\n');
+ if (ep != NULL)
+ *ep = '\0';
+ if (strcmp(socket_name, p) != 0)
+ {
+ bye_gpg_agent(sd);
+ return SVN_NO_ERROR;
+ }
+ /* The agent will terminate its response with "OK". */
+ if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE))
+ {
+ bye_gpg_agent(sd);
+ return SVN_NO_ERROR;
+ }
+ if (strncmp(buffer, "OK", 2) != 0)
+ {
+ bye_gpg_agent(sd);
+ return SVN_NO_ERROR;
+ }
+
+ *new_sd = sd;
+ return SVN_NO_ERROR;
+}
+
+static svn_boolean_t
+send_options(int sd, char *buf, size_t n, apr_pool_t *scratch_pool)
+{
+ const char *tty_name;
+ const char *tty_type;
+ const char *lc_ctype;
+ const char *display;
+
+ /* Send TTY_NAME to the gpg-agent daemon. */
+ tty_name = getenv("GPG_TTY");
+ if (tty_name != NULL)
+ {
+ if (!send_option(sd, buf, n, "ttyname", tty_name, scratch_pool))
+ return FALSE;
+ }
+
+ /* Send TTY_TYPE to the gpg-agent daemon. */
+ tty_type = getenv("TERM");
+ if (tty_type != NULL)
+ {
+ if (!send_option(sd, buf, n, "ttytype", tty_type, scratch_pool))
+ return FALSE;
+ }
+
+ /* Compute LC_CTYPE. */
+ lc_ctype = getenv("LC_ALL");
+ if (lc_ctype == NULL)
+ lc_ctype = getenv("LC_CTYPE");
+ if (lc_ctype == NULL)
+ lc_ctype = getenv("LANG");
+
+ /* Send LC_CTYPE to the gpg-agent daemon. */
+ if (lc_ctype != NULL)
+ {
+ if (!send_option(sd, buf, n, "lc-ctype", lc_ctype, scratch_pool))
+ return FALSE;
+ }
+
+ /* Send DISPLAY to the gpg-agent daemon. */
+ display = getenv("DISPLAY");
+ if (display != NULL)
+ {
+ if (!send_option(sd, buf, n, "display", display, scratch_pool))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/* Implementation of svn_auth__password_get_t that retrieves the password
+ from gpg-agent */
+static svn_error_t *
+password_get_gpg_agent(svn_boolean_t *done,
+ const char **password,
+ apr_hash_t *creds,
+ const char *realmstring,
+ const char *username,
+ apr_hash_t *parameters,
+ svn_boolean_t non_interactive,
+ apr_pool_t *pool)
+{
+ int sd;
+ const char *p = NULL;
+ char *ep = NULL;
+ char *buffer;
+ const char *request = NULL;
+ const char *cache_id = NULL;
+ char *password_prompt;
+ char *realm_prompt;
+ char *error_prompt;
+ int *attempt;
+
+ *done = FALSE;
+
+ attempt = svn_hash_gets(parameters, ATTEMPT_PARAMETER);
+
+ SVN_ERR(find_running_gpg_agent(&sd, pool));
+ if (sd == -1)
+ return SVN_NO_ERROR;
+
+ buffer = apr_palloc(pool, BUFFER_SIZE);
+
+ if (!send_options(sd, buffer, BUFFER_SIZE, pool))
+ {
+ bye_gpg_agent(sd);
+ return SVN_NO_ERROR;
+ }
+
+ SVN_ERR(get_cache_id(&cache_id, realmstring, pool, pool));
+
+ password_prompt = apr_psprintf(pool, _("Password for '%s': "), username);
+ realm_prompt = apr_psprintf(pool, _("Enter your Subversion password for %s"),
+ realmstring);
+ if (*attempt == 1)
+ /* X means no error to the gpg-agent protocol */
+ error_prompt = apr_pstrdup(pool, "X");
+ else
+ error_prompt = apr_pstrdup(pool, _("Authentication failed"));
+
+ request = apr_psprintf(pool,
+ "GET_PASSPHRASE --data %s"
+ "%s %s %s %s\n",
+ non_interactive ? "--no-ask " : "",
+ cache_id,
+ escape_blanks(error_prompt),
+ escape_blanks(password_prompt),
+ escape_blanks(realm_prompt));
+
+ if (write(sd, request, strlen(request)) == -1)
+ {
+ bye_gpg_agent(sd);
+ return SVN_NO_ERROR;
+ }
+ if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE))
+ {
+ bye_gpg_agent(sd);
+ return SVN_NO_ERROR;
+ }
+
+ bye_gpg_agent(sd);
+
+ if (strncmp(buffer, "ERR", 3) == 0)
+ return SVN_NO_ERROR;
+
+ p = NULL;
+ if (strncmp(buffer, "D", 1) == 0)
+ p = &buffer[2];
+
+ if (!p)
+ return SVN_NO_ERROR;
+
+ ep = strchr(p, '\n');
+ if (ep != NULL)
+ *ep = '\0';
+
+ *password = p;
+
+ *done = TRUE;
+ return SVN_NO_ERROR;
+}
+
+
+/* Implementation of svn_auth__password_set_t that would store the
+ password in GPG Agent if that's how this particular integration
+ worked. But it isn't. GPG Agent stores the password provided by
+ the user via the pinentry program immediately upon its provision
+ (and regardless of its accuracy as passwords go), so we just need
+ to check if a running GPG Agent exists. */
+static svn_error_t *
+password_set_gpg_agent(svn_boolean_t *done,
+ apr_hash_t *creds,
+ const char *realmstring,
+ const char *username,
+ const char *password,
+ apr_hash_t *parameters,
+ svn_boolean_t non_interactive,
+ apr_pool_t *pool)
+{
+ int sd;
+
+ *done = FALSE;
+
+ SVN_ERR(find_running_gpg_agent(&sd, pool));
+ if (sd == -1)
+ return SVN_NO_ERROR;
+
+ bye_gpg_agent(sd);
+ *done = TRUE;
+
+ return SVN_NO_ERROR;
+}
+
+
+/* An implementation of svn_auth_provider_t::first_credentials() */
+static svn_error_t *
+simple_gpg_agent_first_creds(void **credentials,
+ void **iter_baton,
+ void *provider_baton,
+ apr_hash_t *parameters,
+ const char *realmstring,
+ apr_pool_t *pool)
+{
+ svn_error_t *err;
+ int *attempt = apr_palloc(pool, sizeof(*attempt));
+
+ *attempt = 1;
+ svn_hash_sets(parameters, ATTEMPT_PARAMETER, attempt);
+ err = svn_auth__simple_creds_cache_get(credentials, iter_baton,
+ provider_baton, parameters,
+ realmstring, password_get_gpg_agent,
+ SVN_AUTH__GPG_AGENT_PASSWORD_TYPE,
+ pool);
+ *iter_baton = attempt;
+
+ return err;
+}
+
+/* An implementation of svn_auth_provider_t::next_credentials() */
+static svn_error_t *
+simple_gpg_agent_next_creds(void **credentials,
+ void *iter_baton,
+ void *provider_baton,
+ apr_hash_t *parameters,
+ const char *realmstring,
+ apr_pool_t *pool)
+{
+ int *attempt = (int *)iter_baton;
+ int sd;
+ char *buffer;
+ const char *cache_id = NULL;
+ const char *request = NULL;
+
+ *credentials = NULL;
+
+ /* The users previous credentials failed so first remove the cached entry,
+ * before trying to retrieve them again. Because gpg-agent stores cached
+ * credentials immediately upon retrieving them, this gives us the
+ * opportunity to remove the invalid credentials and prompt the
+ * user again. While it's possible that server side issues could trigger
+ * this, this cache is ephemeral so at worst we're just speeding up
+ * when the user would need to re-enter their password. */
+
+ if (svn_hash_gets(parameters, SVN_AUTH_PARAM_NON_INTERACTIVE))
+ {
+ /* In this case since we're running non-interactively we do not
+ * want to clear the cache since the user was never prompted by
+ * gpg-agent to set a password. */
+ return SVN_NO_ERROR;
+ }
+
+ *attempt = *attempt + 1;
+
+ SVN_ERR(find_running_gpg_agent(&sd, pool));
+ if (sd == -1)
+ return SVN_NO_ERROR;
+
+ buffer = apr_palloc(pool, BUFFER_SIZE);
+
+ if (!send_options(sd, buffer, BUFFER_SIZE, pool))
+ {
+ bye_gpg_agent(sd);
+ return SVN_NO_ERROR;
+ }
+
+ SVN_ERR(get_cache_id(&cache_id, realmstring, pool, pool));
+
+ request = apr_psprintf(pool, "CLEAR_PASSPHRASE %s\n", cache_id);
+
+ if (write(sd, request, strlen(request)) == -1)
+ {
+ bye_gpg_agent(sd);
+ return SVN_NO_ERROR;
+ }
+
+ if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE))
+ {
+ bye_gpg_agent(sd);
+ return SVN_NO_ERROR;
+ }
+
+ if (strncmp(buffer, "OK\n", 3) != 0)
+ {
+ bye_gpg_agent(sd);
+ return SVN_NO_ERROR;
+ }
+
+ /* TODO: This attempt limit hard codes it at 3 attempts (or 2 retries)
+ * which matches svn command line client's retry_limit as set in
+ * svn_cmdline_create_auth_baton(). It would be nice to have that
+ * limit reflected here but that violates the boundry between the
+ * prompt provider and the cache provider. gpg-agent is acting as
+ * both here due to the peculiarties of their design so we'll have to
+ * live with this for now. Note that when these failures get exceeded
+ * it'll eventually fall back on the retry limits of whatever prompt
+ * provider is in effect, so this effectively doubles the limit. */
+ if (*attempt < 4)
+ return svn_auth__simple_creds_cache_get(credentials, &iter_baton,
+ provider_baton, parameters,
+ realmstring,
+ password_get_gpg_agent,
+ SVN_AUTH__GPG_AGENT_PASSWORD_TYPE,
+ pool);
+
+ return SVN_NO_ERROR;
+}
+
+
+/* An implementation of svn_auth_provider_t::save_credentials() */
+static svn_error_t *
+simple_gpg_agent_save_creds(svn_boolean_t *saved,
+ void *credentials,
+ void *provider_baton,
+ apr_hash_t *parameters,
+ const char *realmstring,
+ apr_pool_t *pool)
+{
+ return svn_auth__simple_creds_cache_set(saved, credentials,
+ provider_baton, parameters,
+ realmstring, password_set_gpg_agent,
+ SVN_AUTH__GPG_AGENT_PASSWORD_TYPE,
+ pool);
+}
+
+
+static const svn_auth_provider_t gpg_agent_simple_provider = {
+ SVN_AUTH_CRED_SIMPLE,
+ simple_gpg_agent_first_creds,
+ simple_gpg_agent_next_creds,
+ simple_gpg_agent_save_creds
+};
+
+
+/* Public API */
+void
+svn_auth_get_gpg_agent_simple_provider(svn_auth_provider_object_t **provider,
+ apr_pool_t *pool)
+{
+ svn_auth_provider_object_t *po = apr_pcalloc(pool, sizeof(*po));
+
+ po->vtable = &gpg_agent_simple_provider;
+ *provider = po;
+}
+
+#endif /* SVN_HAVE_GPG_AGENT */
+#endif /* !WIN32 */