cyrus_auth.c revision 299742
1/* 2 * sasl_auth.c : Functions for SASL-based authentication 3 * 4 * ==================================================================== 5 * Licensed to the Apache Software Foundation (ASF) under one 6 * or more contributor license agreements. See the NOTICE file 7 * distributed with this work for additional information 8 * regarding copyright ownership. The ASF licenses this file 9 * to you under the Apache License, Version 2.0 (the 10 * "License"); you may not use this file except in compliance 11 * with the License. You may obtain a copy of the License at 12 * 13 * http://www.apache.org/licenses/LICENSE-2.0 14 * 15 * Unless required by applicable law or agreed to in writing, 16 * software distributed under the License is distributed on an 17 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 * KIND, either express or implied. See the License for the 19 * specific language governing permissions and limitations 20 * under the License. 21 * ==================================================================== 22 */ 23 24#include "svn_private_config.h" 25#ifdef SVN_HAVE_SASL 26 27#define APR_WANT_STRFUNC 28#include <apr_want.h> 29#include <apr_general.h> 30#include <apr_strings.h> 31 32#include "svn_types.h" 33#include "svn_string.h" 34#include "svn_pools.h" 35#include "svn_error.h" 36#include "svn_ra_svn.h" 37#include "svn_base64.h" 38 39#include "private/svn_atomic.h" 40#include "private/ra_svn_sasl.h" 41#include "private/svn_ra_svn_private.h" 42 43#include "server.h" 44 45/* SASL calls this function before doing anything with a username, which gives 46 us an opportunity to do some sanity-checking. If the username contains 47 an '@', SASL interprets the part following the '@' as the name of the 48 authentication realm, and worst of all, this realm overrides the one that 49 we pass to sasl_server_new(). If we didn't check this, a user that could 50 successfully authenticate in one realm would be able to authenticate 51 in any other realm, simply by appending '@realm' to his username. 52 53 Note that the value returned in *OUT does not need to be 54 '\0'-terminated; we just need to set *OUT_LEN correctly. 55*/ 56static int canonicalize_username(sasl_conn_t *conn, 57 void *context, /* not used */ 58 const char *in, /* the username */ 59 unsigned inlen, /* its length */ 60 unsigned flags, /* not used */ 61 const char *user_realm, 62 char *out, /* the output buffer */ 63 unsigned out_max, unsigned *out_len) 64{ 65 size_t realm_len = strlen(user_realm); 66 char *pos; 67 68 *out_len = inlen; 69 70 /* If the username contains an '@', the part after the '@' is the realm 71 that the user wants to authenticate in. */ 72 pos = memchr(in, '@', inlen); 73 if (pos) 74 { 75 /* The only valid realm is user_realm (i.e. the repository's realm). 76 If the user gave us another realm, complain. */ 77 if (realm_len != inlen-(pos-in+1)) 78 return SASL_BADPROT; 79 if (strncmp(pos+1, user_realm, inlen-(pos-in+1)) != 0) 80 return SASL_BADPROT; 81 } 82 else 83 *out_len += realm_len + 1; 84 85 /* First, check that the output buffer is large enough. */ 86 if (*out_len > out_max) 87 return SASL_BADPROT; 88 89 /* Copy the username part. */ 90 strncpy(out, in, inlen); 91 92 /* If necessary, copy the realm part. */ 93 if (!pos) 94 { 95 out[inlen] = '@'; 96 strncpy(&out[inlen+1], user_realm, realm_len); 97 } 98 99 return SASL_OK; 100} 101 102static sasl_callback_t callbacks[] = 103{ 104 { SASL_CB_CANON_USER, (int (*)(void))canonicalize_username, NULL }, 105 { SASL_CB_LIST_END, NULL, NULL } 106}; 107 108static svn_error_t *initialize(void *baton, apr_pool_t *pool) 109{ 110 int result; 111 SVN_ERR(svn_ra_svn__sasl_common_init(pool)); 112 113 /* The second parameter tells SASL to look for a configuration file 114 named subversion.conf. */ 115 result = sasl_server_init(callbacks, SVN_RA_SVN_SASL_NAME); 116 if (result != SASL_OK) 117 { 118 svn_error_t *err = svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL, 119 sasl_errstring(result, NULL, NULL)); 120 return svn_error_quick_wrap(err, 121 _("Could not initialize the SASL library")); 122 } 123 return SVN_NO_ERROR; 124} 125 126svn_error_t *cyrus_init(apr_pool_t *pool) 127{ 128 SVN_ERR(svn_atomic__init_once(&svn_ra_svn__sasl_status, 129 initialize, NULL, pool)); 130 return SVN_NO_ERROR; 131} 132 133/* Tell the client the authentication failed. This is only used during 134 the authentication exchange (i.e. inside try_auth()). */ 135static svn_error_t * 136fail_auth(svn_ra_svn_conn_t *conn, apr_pool_t *pool, sasl_conn_t *sasl_ctx) 137{ 138 const char *msg = sasl_errdetail(sasl_ctx); 139 SVN_ERR(svn_ra_svn__write_tuple(conn, pool, "w(c)", "failure", msg)); 140 return svn_ra_svn__flush(conn, pool); 141} 142 143/* Like svn_ra_svn_write_cmd_failure, but also clears the given error 144 and sets it to SVN_NO_ERROR. */ 145static svn_error_t * 146write_failure(svn_ra_svn_conn_t *conn, apr_pool_t *pool, svn_error_t **err_p) 147{ 148 svn_error_t *write_err = svn_ra_svn__write_cmd_failure(conn, pool, *err_p); 149 svn_error_clear(*err_p); 150 *err_p = SVN_NO_ERROR; 151 return write_err; 152} 153 154/* Used if we run into a SASL error outside try_auth(). */ 155static svn_error_t * 156fail_cmd(svn_ra_svn_conn_t *conn, apr_pool_t *pool, sasl_conn_t *sasl_ctx) 157{ 158 svn_error_t *err = svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL, 159 sasl_errdetail(sasl_ctx)); 160 SVN_ERR(write_failure(conn, pool, &err)); 161 return svn_ra_svn__flush(conn, pool); 162} 163 164static svn_error_t *try_auth(svn_ra_svn_conn_t *conn, 165 sasl_conn_t *sasl_ctx, 166 apr_pool_t *pool, 167 server_baton_t *b, 168 svn_boolean_t *success) 169{ 170 const char *out, *mech; 171 const svn_string_t *arg = NULL, *in; 172 unsigned int outlen; 173 int result; 174 svn_boolean_t use_base64; 175 176 *success = FALSE; 177 178 /* Read the client's chosen mech and the initial token. */ 179 SVN_ERR(svn_ra_svn__read_tuple(conn, pool, "w(?s)", &mech, &in)); 180 181 if (strcmp(mech, "EXTERNAL") == 0 && !in) 182 in = svn_string_create(b->client_info->tunnel_user, pool); 183 else if (in) 184 in = svn_base64_decode_string(in, pool); 185 186 /* For CRAM-MD5, we don't base64-encode stuff. */ 187 use_base64 = (strcmp(mech, "CRAM-MD5") != 0); 188 189 /* sasl uses unsigned int for the length of strings, we use apr_size_t 190 * which may not be the same size. Deal with potential integer overflow */ 191 if (in && in->len > UINT_MAX) 192 return svn_error_createf(SVN_ERR_RA_NOT_AUTHORIZED, NULL, 193 _("Initial token is too long")); 194 195 result = sasl_server_start(sasl_ctx, mech, 196 in ? in->data : NULL, 197 in ? (unsigned int) in->len : 0, &out, &outlen); 198 199 if (result != SASL_OK && result != SASL_CONTINUE) 200 return fail_auth(conn, pool, sasl_ctx); 201 202 while (result == SASL_CONTINUE) 203 { 204 svn_ra_svn_item_t *item; 205 206 arg = svn_string_ncreate(out, outlen, pool); 207 /* Encode what we send to the client. */ 208 if (use_base64) 209 arg = svn_base64_encode_string2(arg, TRUE, pool); 210 211 SVN_ERR(svn_ra_svn__write_tuple(conn, pool, "w(s)", "step", arg)); 212 213 /* Read and decode the client response. */ 214 SVN_ERR(svn_ra_svn__read_item(conn, pool, &item)); 215 if (item->kind != SVN_RA_SVN_STRING) 216 return SVN_NO_ERROR; 217 218 in = item->u.string; 219 if (use_base64) 220 in = svn_base64_decode_string(in, pool); 221 222 if (in->len > UINT_MAX) 223 return svn_error_createf(SVN_ERR_RA_NOT_AUTHORIZED, NULL, 224 _("Step response is too long")); 225 226 result = sasl_server_step(sasl_ctx, in->data, (unsigned int) in->len, 227 &out, &outlen); 228 } 229 230 if (result != SASL_OK) 231 return fail_auth(conn, pool, sasl_ctx); 232 233 /* Send our last response, if necessary. */ 234 if (outlen) 235 arg = svn_base64_encode_string2(svn_string_ncreate(out, outlen, pool), TRUE, 236 pool); 237 else 238 arg = NULL; 239 240 *success = TRUE; 241 SVN_ERR(svn_ra_svn__write_tuple(conn, pool, "w(?s)", "success", arg)); 242 243 return SVN_NO_ERROR; 244} 245 246static apr_status_t sasl_dispose_cb(void *data) 247{ 248 sasl_conn_t *sasl_ctx = (sasl_conn_t*) data; 249 sasl_dispose(&sasl_ctx); 250 return APR_SUCCESS; 251} 252 253svn_error_t *cyrus_auth_request(svn_ra_svn_conn_t *conn, 254 apr_pool_t *pool, 255 server_baton_t *b, 256 enum access_type required, 257 svn_boolean_t needs_username) 258{ 259 sasl_conn_t *sasl_ctx; 260 apr_pool_t *subpool; 261 apr_status_t apr_err; 262 const char *localaddrport = NULL, *remoteaddrport = NULL; 263 const char *mechlist; 264 char hostname[APRMAXHOSTLEN + 1]; 265 sasl_security_properties_t secprops; 266 svn_boolean_t success, no_anonymous; 267 int mech_count, result = SASL_OK; 268 269 SVN_ERR(svn_ra_svn__get_addresses(&localaddrport, &remoteaddrport, 270 conn, pool)); 271 apr_err = apr_gethostname(hostname, sizeof(hostname), pool); 272 if (apr_err) 273 { 274 svn_error_t *err = svn_error_wrap_apr(apr_err, _("Can't get hostname")); 275 SVN_ERR(write_failure(conn, pool, &err)); 276 return svn_ra_svn__flush(conn, pool); 277 } 278 279 /* Create a SASL context. SASL_SUCCESS_DATA tells SASL that the protocol 280 supports sending data along with the final "success" message. */ 281 result = sasl_server_new(SVN_RA_SVN_SASL_NAME, 282 hostname, b->repository->realm, 283 localaddrport, remoteaddrport, 284 NULL, SASL_SUCCESS_DATA, 285 &sasl_ctx); 286 if (result != SASL_OK) 287 { 288 svn_error_t *err = svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL, 289 sasl_errstring(result, NULL, NULL)); 290 SVN_ERR(write_failure(conn, pool, &err)); 291 return svn_ra_svn__flush(conn, pool); 292 } 293 294 /* Make sure the context is always destroyed. */ 295 apr_pool_cleanup_register(b->pool, sasl_ctx, sasl_dispose_cb, 296 apr_pool_cleanup_null); 297 298 /* Initialize security properties. */ 299 svn_ra_svn__default_secprops(&secprops); 300 301 /* Don't allow ANONYMOUS if a username is required. */ 302 no_anonymous = needs_username || b->repository->anon_access < required; 303 if (no_anonymous) 304 secprops.security_flags |= SASL_SEC_NOANONYMOUS; 305 306 secprops.min_ssf = b->repository->min_ssf; 307 secprops.max_ssf = b->repository->max_ssf; 308 309 /* Set security properties. */ 310 result = sasl_setprop(sasl_ctx, SASL_SEC_PROPS, &secprops); 311 if (result != SASL_OK) 312 return fail_cmd(conn, pool, sasl_ctx); 313 314 /* SASL needs to know if we are externally authenticated. */ 315 if (b->client_info->tunnel_user) 316 result = sasl_setprop(sasl_ctx, SASL_AUTH_EXTERNAL, 317 b->client_info->tunnel_user); 318 if (result != SASL_OK) 319 return fail_cmd(conn, pool, sasl_ctx); 320 321 /* Get the list of mechanisms. */ 322 result = sasl_listmech(sasl_ctx, NULL, NULL, " ", NULL, 323 &mechlist, NULL, &mech_count); 324 325 if (result != SASL_OK) 326 return fail_cmd(conn, pool, sasl_ctx); 327 328 if (mech_count == 0) 329 { 330 svn_error_t *err = svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL, 331 _("Could not obtain the list" 332 " of SASL mechanisms")); 333 SVN_ERR(write_failure(conn, pool, &err)); 334 return svn_ra_svn__flush(conn, pool); 335 } 336 337 /* Send the list of mechanisms and the realm to the client. */ 338 SVN_ERR(svn_ra_svn__write_cmd_response(conn, pool, "(w)c", 339 mechlist, b->repository->realm)); 340 341 /* The main authentication loop. */ 342 subpool = svn_pool_create(pool); 343 do 344 { 345 svn_pool_clear(subpool); 346 SVN_ERR(try_auth(conn, sasl_ctx, subpool, b, &success)); 347 } 348 while (!success); 349 svn_pool_destroy(subpool); 350 351 SVN_ERR(svn_ra_svn__enable_sasl_encryption(conn, sasl_ctx, pool)); 352 353 if (no_anonymous) 354 { 355 char *p; 356 const void *user; 357 358 /* Get the authenticated username. */ 359 result = sasl_getprop(sasl_ctx, SASL_USERNAME, &user); 360 361 if (result != SASL_OK) 362 return fail_cmd(conn, pool, sasl_ctx); 363 364 if ((p = strchr(user, '@')) != NULL) 365 { 366 /* Drop the realm part. */ 367 b->client_info->user = apr_pstrndup(b->pool, user, 368 p - (const char *)user); 369 } 370 else 371 { 372 svn_error_t *err; 373 err = svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL, 374 _("Couldn't obtain the authenticated" 375 " username")); 376 SVN_ERR(write_failure(conn, pool, &err)); 377 return svn_ra_svn__flush(conn, pool); 378 } 379 } 380 381 return SVN_NO_ERROR; 382} 383 384#endif /* SVN_HAVE_SASL */ 385