Index: include/svn_ra_svn.h =================================================================== --- include/svn_ra_svn.h (revision 7420) +++ include/svn_ra_svn.h (working copy) @@ -282,6 +282,18 @@ void *edit_baton, svn_boolean_t *aborted); +/** This function is only intended for use by svnserve. + * + * Perform CRAM-MD5 password authentication. On success, return + * SVN_NO_ERROR with *user set to the username and *success set to + * TRUE. On an error which can be reported to the client, report the + * error and return SVN_NO_ERROR with *success set to FALSE. On + * communications failure, return an error. + */ +svn_error_t *svn_ra_svn_cram_server(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + const char *pwdb, const char **user, + svn_boolean_t *success); + #ifdef __cplusplus } #endif /* __cplusplus */ Index: libsvn_ra_svn/client.c =================================================================== --- libsvn_ra_svn/client.c (revision 7420) +++ libsvn_ra_svn/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, "", 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, "", 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: libsvn_ra_svn/cram.c =================================================================== --- libsvn_ra_svn/cram.c (revision 0) +++ libsvn_ra_svn/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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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 NULL; + 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 NULL; +} + +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: libsvn_ra_svn/ra_svn.h =================================================================== --- libsvn_ra_svn/ra_svn.h (revision 7420) +++ libsvn_ra_svn/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 */ Index: svnserve/main.c =================================================================== --- svnserve/main.c (revision 7420) +++ svnserve/main.c (working copy) @@ -72,7 +72,7 @@ /* Option codes and descriptions for svnserve. * * This must not have more than SVN_OPT_MAX_OPTIONS entries; if you - * need more, increase that limit first. + * need more, increase that limit first. * * The entire list must be terminated with an entry of nulls. */ @@ -82,6 +82,7 @@ {"daemon", 'd', 0, "daemon mode"}, {"tunnel", 't', 0, "tunnel mode"}, {"listen-once", 'X', 0, "listen once (useful for debugging)"}, + {"password-file", 'p', 1, "password file"}, {"root", 'r', 1, "root of directory to serve"}, {"read-only", 'R', 0, "serve in read-only mode"}, #ifdef CONNECTION_HAVE_THREAD_OPTION @@ -147,6 +148,7 @@ svn_ra_svn_conn_t *conn; svn_boolean_t read_only; svn_boolean_t believe_username; + const char *pwfile; apr_pool_t *pool; }; @@ -155,8 +157,8 @@ { struct serve_thread_t *d = data; - svn_error_clear(serve(d->conn, d->root, FALSE, d->read_only, - d->believe_username, d->pool)); + svn_error_clear(serve(d->conn, d->root, FALSE, d->read_only, + d->believe_username, d->pwfile, d->pool)); svn_pool_destroy(d->pool); return NULL; @@ -176,7 +178,7 @@ apr_getopt_t *os; char errbuf[256]; int opt; - const char *arg, *root = "/"; + const char *arg, *root = "/", *pwfile = NULL; apr_status_t status; svn_ra_svn_conn_t *conn; apr_proc_t proc; @@ -221,6 +223,10 @@ listen_once = TRUE; break; + case 'p': + pwfile = arg; + break; + case 'r': SVN_INT_ERR(svn_utf_cstring_to_utf8(&root, arg, pool)); root = svn_path_internal_style(root, pool); @@ -251,7 +257,7 @@ apr_file_open_stdout(&out_file, pool); conn = svn_ra_svn_create_conn(NULL, in_file, out_file, pool); svn_error_clear(serve(conn, root, tunnel_mode, read_only, - believe_username, pool)); + believe_username, pwfile, pool)); exit(0); } @@ -322,7 +328,7 @@ if (listen_once) { - err = serve(conn, root, FALSE, read_only, believe_username, + err = serve(conn, root, FALSE, read_only, believe_username, pwfile, connection_pool); if (listen_once && err @@ -342,7 +348,8 @@ if (status == APR_INCHILD) { svn_error_clear(serve(conn, root, FALSE, read_only, - believe_username, connection_pool)); + believe_username, pwfile, + connection_pool)); apr_socket_close(usock); exit(0); } @@ -383,6 +390,7 @@ thread_data->root = root; thread_data->read_only = read_only; thread_data->believe_username = believe_username; + thread_data->pwfile = pwfile; thread_data->pool = connection_pool; status = apr_thread_create(&tid, tattr, serve_thread, thread_data, connection_pool); @@ -397,7 +405,8 @@ case connection_mode_single: /* Serve one connection at a time. */ - svn_error_clear(serve(conn, root, FALSE, read_only, believe_username, + svn_error_clear(serve(conn, root, FALSE, read_only, + believe_username, pwfile, connection_pool)); svn_pool_destroy(connection_pool); } Index: svnserve/serve.c =================================================================== --- svnserve/serve.c (revision 7420) +++ svnserve/serve.c (working copy) @@ -894,7 +894,8 @@ svn_error_t *serve(svn_ra_svn_conn_t *conn, const char *root, svn_boolean_t tunnel, svn_boolean_t read_only, - svn_boolean_t believe_username, apr_pool_t *pool) + svn_boolean_t believe_username, + const char *pwfile, apr_pool_t *pool) { svn_error_t *err, *io_err; apr_uint64_t ver; @@ -914,8 +915,10 @@ SVN_ERR(svn_ra_svn_write_number(conn, pool, 1)); SVN_ERR(svn_ra_svn_write_number(conn, pool, 1)); SVN_ERR(svn_ra_svn_start_list(conn, pool)); - /* We support anonymous and maybe external authentication. */ + /* We support ANONYMOUS, DIGEST-MD5, and maybe EXTERNAL authentication. */ SVN_ERR(svn_ra_svn_write_word(conn, pool, "ANONYMOUS")); + if (pwfile) + SVN_ERR(svn_ra_svn_write_word(conn, pool, "CRAM-MD5")); #if APR_HAS_USER if (tunnel) SVN_ERR(svn_ra_svn_write_word(conn, pool, "EXTERNAL")); @@ -954,6 +957,7 @@ "Requested username does not match")); return SVN_NO_ERROR; } + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w()", "success")); valid_mech = TRUE; } #endif @@ -962,15 +966,14 @@ { if (believe_username && mecharg && *mecharg) user = mecharg; - + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w()", "success")); valid_mech = TRUE; } - if (!valid_mech) /* Client gave us an unlisted mech. */ + else if (pwfile && strcmp(mech, "CRAM-MD5") == 0) + SVN_ERR(svn_ra_svn_cram_server(conn, pool, pwfile, &user, &valid_mech)); + if (!valid_mech) /* Client gave us an unlisted mech, or auth failed. */ return SVN_NO_ERROR; - /* Write back a success notification. */ - SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w()", "success")); - /* This is where the security layer would go into effect if we * supported security layers, which is a ways off. */ Index: svnserve/server.h =================================================================== --- svnserve/server.h (revision 7420) +++ svnserve/server.h (working copy) @@ -29,7 +29,8 @@ svn_error_t *serve(svn_ra_svn_conn_t *conn, const char *root, svn_boolean_t tunnel, svn_boolean_t read_only, - svn_boolean_t believe_username, apr_pool_t *pool); + svn_boolean_t believe_username, const char *pwfile, + apr_pool_t *pool); #ifdef __cplusplus }