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