aboutsummaryrefslogtreecommitdiff
path: root/contrib/subversion/subversion/libsvn_subr/gpg_agent.c
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/subversion/subversion/libsvn_subr/gpg_agent.c')
-rw-r--r--contrib/subversion/subversion/libsvn_subr/gpg_agent.c302
1 files changed, 228 insertions, 74 deletions
diff --git a/contrib/subversion/subversion/libsvn_subr/gpg_agent.c b/contrib/subversion/subversion/libsvn_subr/gpg_agent.c
index e23339074a40..4dbf1184e4e5 100644
--- a/contrib/subversion/subversion/libsvn_subr/gpg_agent.c
+++ b/contrib/subversion/subversion/libsvn_subr/gpg_agent.c
@@ -72,6 +72,9 @@
#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"
@@ -80,6 +83,7 @@
#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. */
@@ -98,6 +102,24 @@ escape_blanks(char *str)
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.
@@ -156,6 +178,17 @@ send_option(int sd, char *buf, size_t n, const char *option, const char *value,
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
@@ -173,17 +206,34 @@ find_running_gpg_agent(int *new_sd, apr_pool_t *pool)
*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
- return SVN_NO_ERROR;
+ {
+ 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)
{
@@ -210,13 +260,13 @@ find_running_gpg_agent(int *new_sd, apr_pool_t *pool)
buffer = apr_palloc(pool, BUFFER_SIZE);
if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE))
{
- close(sd);
+ bye_gpg_agent(sd);
return SVN_NO_ERROR;
}
if (strncmp(buffer, "OK", 2) != 0)
{
- close(sd);
+ bye_gpg_agent(sd);
return SVN_NO_ERROR;
}
@@ -226,19 +276,19 @@ find_running_gpg_agent(int *new_sd, apr_pool_t *pool)
request = "GETINFO socket_name\n";
if (write(sd, request, strlen(request)) == -1)
{
- close(sd);
+ bye_gpg_agent(sd);
return SVN_NO_ERROR;
}
if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE))
{
- close(sd);
+ bye_gpg_agent(sd);
return SVN_NO_ERROR;
}
if (strncmp(buffer, "D", 1) == 0)
p = &buffer[2];
if (!p)
{
- close(sd);
+ bye_gpg_agent(sd);
return SVN_NO_ERROR;
}
ep = strchr(p, '\n');
@@ -246,18 +296,18 @@ find_running_gpg_agent(int *new_sd, apr_pool_t *pool)
*ep = '\0';
if (strcmp(socket_name, p) != 0)
{
- close(sd);
+ 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))
{
- close(sd);
+ bye_gpg_agent(sd);
return SVN_NO_ERROR;
}
if (strncmp(buffer, "OK", 2) != 0)
{
- close(sd);
+ bye_gpg_agent(sd);
return SVN_NO_ERROR;
}
@@ -265,60 +315,28 @@ find_running_gpg_agent(int *new_sd, apr_pool_t *pool)
return SVN_NO_ERROR;
}
-/* 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)
+static svn_boolean_t
+send_options(int sd, char *buf, size_t n, apr_pool_t *scratch_pool)
{
- int sd;
- const char *p = NULL;
- char *ep = NULL;
- char *buffer;
- const char *request = NULL;
- const char *cache_id = NULL;
const char *tty_name;
const char *tty_type;
const char *lc_ctype;
const char *display;
- svn_checksum_t *digest = NULL;
- char *password_prompt;
- char *realm_prompt;
-
- *done = FALSE;
-
- SVN_ERR(find_running_gpg_agent(&sd, pool));
- if (sd == -1)
- return SVN_NO_ERROR;
-
- buffer = apr_palloc(pool, BUFFER_SIZE);
/* Send TTY_NAME to the gpg-agent daemon. */
tty_name = getenv("GPG_TTY");
if (tty_name != NULL)
{
- if (!send_option(sd, buffer, BUFFER_SIZE, "ttyname", tty_name, pool))
- {
- close(sd);
- return SVN_NO_ERROR;
- }
+ 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, buffer, BUFFER_SIZE, "ttytype", tty_type, pool))
- {
- close(sd);
- return SVN_NO_ERROR;
- }
+ if (!send_option(sd, buf, n, "ttytype", tty_type, scratch_pool))
+ return FALSE;
}
/* Compute LC_CTYPE. */
@@ -331,53 +349,92 @@ password_get_gpg_agent(svn_boolean_t *done,
/* Send LC_CTYPE to the gpg-agent daemon. */
if (lc_ctype != NULL)
{
- if (!send_option(sd, buffer, BUFFER_SIZE, "lc-ctype", lc_ctype, pool))
- {
- close(sd);
- return SVN_NO_ERROR;
- }
+ 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, buffer, BUFFER_SIZE, "display", display, pool))
- {
- close(sd);
- return SVN_NO_ERROR;
- }
+ if (!send_option(sd, buf, n, "display", display, scratch_pool))
+ return FALSE;
}
- /* Create the CACHE_ID which will be generated based on REALMSTRING similar
- to other password caching mechanisms. */
- SVN_ERR(svn_checksum(&digest, svn_checksum_md5, realmstring,
- strlen(realmstring), pool));
- cache_id = svn_checksum_to_cstring(digest, pool);
+ 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--repeat=1 "
- "%s X %s %s\n",
+ "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)
{
- close(sd);
+ bye_gpg_agent(sd);
return SVN_NO_ERROR;
}
if (!receive_from_gpg_agent(sd, buffer, BUFFER_SIZE))
{
- close(sd);
+ bye_gpg_agent(sd);
return SVN_NO_ERROR;
}
- close(sd);
+ bye_gpg_agent(sd);
if (strncmp(buffer, "ERR", 3) == 0)
return SVN_NO_ERROR;
@@ -424,7 +481,7 @@ password_set_gpg_agent(svn_boolean_t *done,
if (sd == -1)
return SVN_NO_ERROR;
- close(sd);
+ bye_gpg_agent(sd);
*done = TRUE;
return SVN_NO_ERROR;
@@ -440,11 +497,108 @@ simple_gpg_agent_first_creds(void **credentials,
const char *realmstring,
apr_pool_t *pool)
{
- 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);
+ 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;
}
@@ -468,7 +622,7 @@ simple_gpg_agent_save_creds(svn_boolean_t *saved,
static const svn_auth_provider_t gpg_agent_simple_provider = {
SVN_AUTH_CRED_SIMPLE,
simple_gpg_agent_first_creds,
- NULL,
+ simple_gpg_agent_next_creds,
simple_gpg_agent_save_creds
};