[svn.haxx.se] · SVN Dev · SVN Users · SVN Org · TSVN Dev · TSVN Users · Subclipse Dev · Subclipse Users · this month's index

Awful first cut of ra_svn password authentication

From: Greg Hudson <ghudson_at_MIT.EDU>
Date: 2003-10-14 21:19:37 CEST

There have been a lot of calls for ra_svn authentication in daemon
mode. I have some technical issues with the Cyrus library, so I
decided that since we're nearing 1.0, I should cut bait and make a
simple implementation of one of the shared-secret SASL mechanisms.

I have working code, but here are some notes:

  * I am a bit lost on how to integrate this on the client side. Our
    current auth framework is based on an HTTP model where you try all
    operations anonymously and retry them with authentication (in a
    specific realm) if the server asks for it. (Corollary: if a
    server allows anonymous commits, but you want to authenticate
    anyway to get your username recorded, you can't do that.) I
    didn't design the ra_svn protocol and implementation with the HTTP
    model in mind, so I'm a little stuck. The client integration in
    this patch is inflexible and broken; it's just enough to let me
    exercise the code.

  * I implemented CRAM-MD5, which is a trivial challenge-response
    method. It doesn't authenticate the server and it has no
    provision for stream protection (not that I would have implemented
    that right now anyway), but it does protect the password from
    eavesdroppers and it's dirt simple to implement.

    Also, I figure if we ever do integrate the Cyrus library, there's
    no reason to use it with CRAM-MD5 (DIGEST-MD5 is better in every
    way except implementation complexity), so there's no upgrade
    issue.

  * On the server, the natural place to put the password database is
    inside the repository. But you can't, because authentication
    happens before a repository is selected. (Perhaps that's a
    mistake.) So instead, this patch has you put the password
    database wherever you want and tell svnserve where that is with
    the -p flag.

  * The IETF is moving towards requiring "stringprep" (RFC 3454) to
    handle internationalized usernames and passwords; I believe the
    idea is that if you type a username and password which look like
    the real username and password, it should work, even if it uses
    different Unicode characters. SASL is moving there relatively
    quickly; HTTP is likely to move there eventually. I have no
    interest in implementing RFC 3454 right now, but we might want to
    be on the lookout for an implementation (ours or someone else's)
    to solve this problem in the long term.

Comments appreciated.

Index: client.c
===================================================================
--- client.c (revision 7420)
+++ client.c (working copy)
@@ -204,7 +204,7 @@
   SVN_ERR(svn_ra_svn_write_cmd(b->conn, pool, "delete-path", "c", path));
   return SVN_NO_ERROR;
 }
-
+
 static svn_error_t *ra_svn_link_path(void *baton, const char *path,
                                      const char *url,
                                      svn_revnum_t rev,
@@ -371,6 +371,7 @@
   svn_ra_svn_conn_t *conn;
   apr_socket_t *sock;
   const char *hostname, *user, *status, *tunnel, *realmstring, **args;
+ const char *password;
   unsigned short port;
   apr_uint64_t minver, maxver;
   apr_array_header_t *mechlist, *caplist, *status_param;
@@ -440,8 +441,25 @@
     return svn_error_createf(SVN_ERR_RA_SVN_BAD_VERSION, NULL,
                              "Server requires minimum version %d",
                              (int) minver);
- if (tunnel && find_mech(mechlist, "EXTERNAL"))
+ if (find_mech(mechlist, "CRAM-MD5"))
     {
+ realmstring = apr_psprintf(pool, "<svn://%s:%d>", hostname, port);
+ err = svn_auth_first_credentials(&creds, &iterstate,
+ SVN_AUTH_CRED_SIMPLE, realmstring,
+ callbacks->auth_baton, pool);
+ if (err)
+ svn_error_clear(err);
+ else if (creds)
+ {
+ user = ((svn_auth_cred_simple_t *) creds)->username;
+ password = ((svn_auth_cred_simple_t *) creds)->password;
+ SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "nw(c)()",
+ (apr_uint64_t) 1, "CRAM-MD5", ""));
+ SVN_ERR(svn_ra_svn__cram_client(conn, pool, user, password));
+ }
+ }
+ else if (tunnel && find_mech(mechlist, "EXTERNAL"))
+ {
       /* Ask the server to use the ssh connection environment (on
        * Unix, that means uid) to determine the authentication name. */
       SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "nw(c)()", (apr_uint64_t) 1,
@@ -453,8 +471,8 @@
         {
           realmstring = apr_psprintf(pool, "<svn://%s:%d>", hostname, port);
 
- err = svn_auth_first_credentials(&creds, &iterstate,
- SVN_AUTH_CRED_USERNAME, realmstring,
+ err = svn_auth_first_credentials(&creds, &iterstate,
+ SVN_AUTH_CRED_USERNAME, realmstring,
                                            callbacks->auth_baton, pool);
           if (err)
             svn_error_clear(err);
@@ -462,8 +480,8 @@
             user = ((svn_auth_cred_username_t *) creds)->username;
         }
 
- /* We send along whatever username we've got as the mechanism argument,
- * and if the server wants, it can make use of that when committing
+ /* We send along whatever username we've got as the mechanism argument,
+ * and if the server wants, it can make use of that when committing
        * changes. */
       SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "nw(c)()", (apr_uint64_t) 1,
                                      "ANONYMOUS", user ? user : ""));
@@ -789,7 +807,7 @@
     target = "";
 
   /* Tell the server we want to start a status operation. */
- SVN_ERR(svn_ra_svn_write_cmd(conn, pool, "status", "cb(?r)",
+ SVN_ERR(svn_ra_svn_write_cmd(conn, pool, "status", "cb(?r)",
                                target, recurse, rev));
 
   /* Fetch a reporter for the caller to drive. The reporter will drive
Index: cram.c
===================================================================
--- cram.c (revision 0)
+++ cram.c (revision 0)
@@ -0,0 +1,221 @@
+/*
+ * cram.c : Minimal standalone CRAM-MD5 implementation
+ *
+ * ====================================================================
+ * Copyright (c) 2000-2003 CollabNet. All rights reserved.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at http://subversion.tigris.org/license-1.html.
+ * If newer versions of this license are posted there, you may use a
+ * newer version instead, at your option.
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals. For exact contribution history, see the revision
+ * history and logs, available at http://subversion.tigris.org/.
+ * ====================================================================
+ */
+
+
+
+#define APR_WANT_STRFUNC
+#define APR_WANT_STDIO
+#include <apr_want.h>
+#include <apr_general.h>
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_network_io.h>
+#include <apr_time.h>
+#include <apr_md5.h>
+
+#include <svn_types.h>
+#include <svn_string.h>
+#include <svn_pools.h>
+#include <svn_error.h>
+#include <svn_ra_svn.h>
+
+#include "ra_svn.h"
+
+static int hex_to_int(char c)
+{
+ return (c >= '0' && c <= '9') ? c - '0'
+ : (c >= 'a' && c <= 'f') ? c - 'a' + 10
+ : -1;
+}
+
+static char int_to_hex(int v)
+{
+ return (v < 10) ? '0' + v : 'a' + (v - 10);
+}
+
+static svn_boolean_t hex_decode(char *hashval, const char *hexval)
+{
+ int i, h1, h2;
+
+ for (i = 0; i < APR_MD5_DIGESTSIZE; i++)
+ {
+ h1 = hex_to_int(hexval[2 * i]);
+ h2 = hex_to_int(hexval[2 * i + 1]);
+ if (h1 == -1 || h2 == -1)
+ return FALSE;
+ hashval[i] = (h1 << 4) | h2;
+ }
+ return TRUE;
+}
+
+static void hex_encode(char *hexval, const char *hashval)
+{
+ int i;
+
+ for (i = 0; i < APR_MD5_DIGESTSIZE; i++)
+ {
+ hexval[2 * i] = int_to_hex((hashval[i] >> 4) & 0xf);
+ hexval[2 * i + 1] = int_to_hex(hashval[i] & 0xf);
+ }
+}
+
+static const char *lookup_password(const char *pwfile, const char *user,
+ apr_pool_t *pool)
+{
+ FILE *fp;
+ char line[2048];
+ const char *sep;
+ int ulen = strlen(user), len;
+
+ fp = fopen(pwfile, "r");
+ if (!fp)
+ return FALSE;
+ while (fgets(line, sizeof(line), fp) != NULL)
+ {
+ len = strlen(line);
+ if (len > 0 && line[len - 1] == '\n')
+ line[--len] = '\0';
+ sep = strchr(line, ':');
+ if (sep && sep - line == ulen && memcmp(line, user, ulen) == 0)
+ {
+ fclose(fp);
+ return apr_pstrdup(pool, sep + 1);
+ }
+ }
+ fclose(fp);
+ return FALSE;
+}
+
+static void compute_digest(char *digest, const char *challenge,
+ const char *password)
+{
+ char secret[64];
+ int len = strlen(password), i;
+ apr_md5_ctx_t ctx;
+
+ /* Munge the password into a 64-byte secret. */
+ memset(secret, 0, sizeof(secret));
+ if (len <= sizeof(secret))
+ memcpy(secret, password, len);
+ else
+ apr_md5(secret, password, len);
+
+ /* Compute MD5(secret XOR opad, MD5(secret XOR ipad, challenge)),
+ * where ipad is a string of 0x36 and opad is a string of 0x5c. */
+ for (i = 0; i < sizeof(secret); i++)
+ secret[i] ^= 0x36;
+ apr_md5_init(&ctx);
+ apr_md5_update(&ctx, secret, sizeof(secret));
+ apr_md5_update(&ctx, challenge, strlen(challenge));
+ apr_md5_final(digest, &ctx);
+ for (i = 0; i < sizeof(secret); i++)
+ secret[i] ^= (0x36 ^ 0x5c);
+ apr_md5_init(&ctx);
+ apr_md5_update(&ctx, secret, sizeof(secret));
+ apr_md5_update(&ctx, digest, APR_MD5_DIGESTSIZE);
+ apr_md5_final(digest, &ctx);
+}
+
+/* Fail the authentication, from the server's perspective. */
+static svn_error_t *fail(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
+ const char *msg)
+{
+ SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w(c)", "failure", msg));
+ return svn_ra_svn_flush(conn, pool);
+}
+
+svn_error_t *svn_ra_svn_cram_server(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
+ const char *pwfile, const char **user,
+ svn_boolean_t *success)
+{
+ apr_status_t status;
+ apr_uint64_t nonce;
+ char hostbuf[APRMAXHOSTLEN + 1];
+ char cdigest[APR_MD5_DIGESTSIZE], sdigest[APR_MD5_DIGESTSIZE];
+ const char *challenge, *sep, *password;
+ svn_ra_svn_item_t *item;
+ svn_string_t *resp;
+
+ *success = FALSE;
+
+ /* Send a challenge. */
+ status = apr_generate_random_bytes((unsigned char *) &nonce, sizeof(nonce));
+ if (APR_STATUS_IS_SUCCESS(status))
+ status = apr_gethostname(hostbuf, sizeof(hostbuf), pool);
+ if (!APR_STATUS_IS_SUCCESS(status))
+ return fail(conn, pool, "Internal server error in authentication");
+ challenge = apr_psprintf(pool,
+ "<%" APR_UINT64_T_FMT ".%" APR_TIME_T_FMT "@%s>",
+ nonce, apr_time_now(), hostbuf);
+ SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w(c)", "step", challenge));
+
+ /* Read the client's response and decode it into *user and cdigest. */
+ SVN_ERR(svn_ra_svn_read_item(conn, pool, &item));
+ if (item->kind != SVN_RA_SVN_STRING) /* Very wrong; don't report failure */
+ return SVN_NO_ERROR;
+ resp = item->u.string;
+ sep = strchr(resp->data, ' ');
+ if (!sep || resp->len - (sep + 1 - resp->data) != APR_MD5_DIGESTSIZE * 2
+ || !hex_decode(cdigest, sep + 1))
+ return fail(conn, pool, "Malformed client response in authentication");
+ *user = apr_pstrmemdup(pool, resp->data, sep - resp->data);
+
+ /* Verify the digest against the password in pwfile. */
+ password = lookup_password(pwfile, *user, pool);
+ if (!password)
+ return fail(conn, pool, "Username not found");
+ compute_digest(sdigest, challenge, password);
+ if (memcmp(cdigest, sdigest, sizeof(sdigest)) != 0)
+ return fail(conn, pool, "Password incorrect");
+
+ *success = TRUE;
+ return svn_ra_svn_write_tuple(conn, pool, "w()", "success");
+}
+
+static svn_error_t *check_failure(const char *word, const char *str,
+ svn_boolean_t final)
+{
+ if (strcmp(word, "failure") == 0)
+ return svn_error_createf(SVN_ERR_RA_NOT_AUTHORIZED, NULL,
+ "Authentication error: %s", str);
+ else if ((final && (strcmp(word, "success") != 0 || str))
+ || (!final && (strcmp(word, "step") != 0 || !str)))
+ return svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL,
+ "Unexpected server response to authentication");
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *svn_ra_svn__cram_client(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
+ const char *user, const char *password)
+{
+ const char *word, *str, *reply;
+ char digest[APR_MD5_DIGESTSIZE], hex[2 * APR_MD5_DIGESTSIZE + 1];
+
+ SVN_ERR(svn_ra_svn_read_tuple(conn, pool, "w(?c)", &word, &str));
+ SVN_ERR(check_failure(word, str, FALSE));
+ compute_digest(digest, str, password);
+ hex_encode(hex, digest);
+ hex[sizeof(hex) - 1] = '\0';
+ reply = apr_psprintf(pool, "%s %s", user, hex);
+ SVN_ERR(svn_ra_svn_write_cstring(conn, pool, reply));
+#if 0
+ SVN_ERR(svn_ra_svn_read_tuple(conn, pool, "w(?c)", &word, &str));
+ SVN_ERR(check_failure(word, str, TRUE));
+#endif
+ return SVN_NO_ERROR;
+}
Index: ra_svn.h
===================================================================
--- ra_svn.h (revision 7420)
+++ ra_svn.h (working copy)
@@ -44,6 +44,9 @@
   const char *uuid;
 };
 
+svn_error_t *svn_ra_svn__cram_client(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
+ const char *user, const char *password);
+
 #ifdef __cplusplus
 }
 #endif /* __cplusplus */

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@subversion.tigris.org
For additional commands, e-mail: dev-help@subversion.tigris.org
Received on Tue Oct 14 21:25:02 2003

This is an archived mail posted to the Subversion Dev mailing list.

This site is subject to the Apache Privacy Policy and the Apache Public Forum Archive Policy.