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 = svn_sasl__server_init(callbacks, SVN_RA_SVN_SASL_NAME);
116  if (result != SASL_OK)
117    {
118      svn_error_t *err = svn_error_create(
119          SVN_ERR_RA_NOT_AUTHORIZED, NULL,
120          svn_sasl__errstring(result, NULL, NULL));
121      return svn_error_quick_wrap(err,
122                                  _("Could not initialize the SASL library"));
123    }
124  return SVN_NO_ERROR;
125}
126
127svn_error_t *cyrus_init(apr_pool_t *pool)
128{
129  SVN_ERR(svn_atomic__init_once(&svn_ra_svn__sasl_status,
130                                initialize, NULL, pool));
131  return SVN_NO_ERROR;
132}
133
134/* Tell the client the authentication failed. This is only used during
135   the authentication exchange (i.e. inside try_auth()). */
136static svn_error_t *
137fail_auth(svn_ra_svn_conn_t *conn, apr_pool_t *pool, sasl_conn_t *sasl_ctx)
138{
139  const char *msg = svn_sasl__errdetail(sasl_ctx);
140  SVN_ERR(svn_ra_svn__write_tuple(conn, pool, "w(c)", "failure", msg));
141  return svn_ra_svn__flush(conn, pool);
142}
143
144/* Like svn_ra_svn_write_cmd_failure, but also clears the given error
145   and sets it to SVN_NO_ERROR. */
146static svn_error_t *
147write_failure(svn_ra_svn_conn_t *conn, apr_pool_t *pool, svn_error_t **err_p)
148{
149  svn_error_t *write_err = svn_ra_svn__write_cmd_failure(conn, pool, *err_p);
150  svn_error_clear(*err_p);
151  *err_p = SVN_NO_ERROR;
152  return write_err;
153}
154
155/* Used if we run into a SASL error outside try_auth(). */
156static svn_error_t *
157fail_cmd(svn_ra_svn_conn_t *conn, apr_pool_t *pool, sasl_conn_t *sasl_ctx)
158{
159  svn_error_t *err = svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL,
160                                      svn_sasl__errdetail(sasl_ctx));
161  SVN_ERR(write_failure(conn, pool, &err));
162  return svn_ra_svn__flush(conn, pool);
163}
164
165static svn_error_t *try_auth(svn_ra_svn_conn_t *conn,
166                             sasl_conn_t *sasl_ctx,
167                             apr_pool_t *pool,
168                             server_baton_t *b,
169                             svn_boolean_t *success)
170{
171  const char *out, *mech;
172  const svn_string_t *arg = NULL, *in;
173  unsigned int outlen;
174  int result;
175  svn_boolean_t use_base64;
176
177  *success = FALSE;
178
179  /* Read the client's chosen mech and the initial token. */
180  SVN_ERR(svn_ra_svn__read_tuple(conn, pool, "w(?s)", &mech, &in));
181
182  if (strcmp(mech, "EXTERNAL") == 0 && !in)
183    in = svn_string_create(b->client_info->tunnel_user, pool);
184  else if (in)
185    in = svn_base64_decode_string(in, pool);
186
187  /* For CRAM-MD5, we don't base64-encode stuff. */
188  use_base64 = (strcmp(mech, "CRAM-MD5") != 0);
189
190  /* sasl uses unsigned int for the length of strings, we use apr_size_t
191   * which may not be the same size.  Deal with potential integer overflow */
192  if (in && in->len > UINT_MAX)
193    return svn_error_createf(SVN_ERR_RA_NOT_AUTHORIZED, NULL,
194                             _("Initial token is too long"));
195
196  result = svn_sasl__server_start(sasl_ctx, mech,
197                                  in ? in->data : NULL,
198                                  in ? (unsigned int) in->len : 0,
199                                  &out, &outlen);
200
201  if (result != SASL_OK && result != SASL_CONTINUE)
202    return fail_auth(conn, pool, sasl_ctx);
203
204  while (result == SASL_CONTINUE)
205    {
206      svn_ra_svn__item_t *item;
207
208      arg = svn_string_ncreate(out, outlen, pool);
209      /* Encode what we send to the client. */
210      if (use_base64)
211        arg = svn_base64_encode_string2(arg, TRUE, pool);
212
213      SVN_ERR(svn_ra_svn__write_tuple(conn, pool, "w(s)", "step", arg));
214
215      /* Read and decode the client response. */
216      SVN_ERR(svn_ra_svn__read_item(conn, pool, &item));
217      if (item->kind != SVN_RA_SVN_STRING)
218        return SVN_NO_ERROR;
219
220      in = &item->u.string;
221      if (use_base64)
222        in = svn_base64_decode_string(in, pool);
223
224      if (in->len > UINT_MAX)
225        return svn_error_createf(SVN_ERR_RA_NOT_AUTHORIZED, NULL,
226                                 _("Step response is too long"));
227
228      result = svn_sasl__server_step(sasl_ctx, in->data,
229                                     (unsigned int) in->len,
230                                     &out, &outlen);
231    }
232
233  if (result != SASL_OK)
234    return fail_auth(conn, pool, sasl_ctx);
235
236  /* Send our last response, if necessary. */
237  if (outlen)
238    arg = svn_base64_encode_string2(svn_string_ncreate(out, outlen, pool), TRUE,
239                                    pool);
240  else
241    arg = NULL;
242
243  *success = TRUE;
244  SVN_ERR(svn_ra_svn__write_tuple(conn, pool, "w(?s)", "success", arg));
245
246  return SVN_NO_ERROR;
247}
248
249static apr_status_t sasl_dispose_cb(void *data)
250{
251  sasl_conn_t *sasl_ctx = (sasl_conn_t*) data;
252  svn_sasl__dispose(&sasl_ctx);
253  return APR_SUCCESS;
254}
255
256svn_error_t *cyrus_auth_request(svn_ra_svn_conn_t *conn,
257                                apr_pool_t *pool,
258                                server_baton_t *b,
259                                enum access_type required,
260                                svn_boolean_t needs_username)
261{
262  sasl_conn_t *sasl_ctx;
263  apr_pool_t *subpool;
264  apr_status_t apr_err;
265  const char *localaddrport = NULL, *remoteaddrport = NULL;
266  const char *mechlist;
267  char hostname[APRMAXHOSTLEN + 1];
268  sasl_security_properties_t secprops;
269  svn_boolean_t success, no_anonymous;
270  int mech_count, result = SASL_OK;
271
272  SVN_ERR(svn_ra_svn__get_addresses(&localaddrport, &remoteaddrport,
273                                        conn, pool));
274  apr_err = apr_gethostname(hostname, sizeof(hostname), pool);
275  if (apr_err)
276    {
277      svn_error_t *err = svn_error_wrap_apr(apr_err, _("Can't get hostname"));
278      SVN_ERR(write_failure(conn, pool, &err));
279      return svn_ra_svn__flush(conn, pool);
280    }
281
282  /* Create a SASL context. SASL_SUCCESS_DATA tells SASL that the protocol
283     supports sending data along with the final "success" message. */
284  result = svn_sasl__server_new(SVN_RA_SVN_SASL_NAME,
285                                hostname, b->repository->realm,
286                                localaddrport, remoteaddrport,
287                                NULL, SASL_SUCCESS_DATA,
288                                &sasl_ctx);
289  if (result != SASL_OK)
290    {
291      svn_error_t *err = svn_error_create(
292          SVN_ERR_RA_NOT_AUTHORIZED, NULL,
293          svn_sasl__errstring(result, NULL, NULL));
294      SVN_ERR(write_failure(conn, pool, &err));
295      return svn_ra_svn__flush(conn, pool);
296    }
297
298  /* Make sure the context is always destroyed. */
299  apr_pool_cleanup_register(b->pool, sasl_ctx, sasl_dispose_cb,
300                            apr_pool_cleanup_null);
301
302  /* Initialize security properties. */
303  svn_ra_svn__default_secprops(&secprops);
304
305  /* Don't allow ANONYMOUS if a username is required. */
306  no_anonymous = needs_username || b->repository->anon_access < required;
307  if (no_anonymous)
308    secprops.security_flags |= SASL_SEC_NOANONYMOUS;
309
310  secprops.min_ssf = b->repository->min_ssf;
311  secprops.max_ssf = b->repository->max_ssf;
312
313  /* Set security properties. */
314  result = svn_sasl__setprop(sasl_ctx, SASL_SEC_PROPS, &secprops);
315  if (result != SASL_OK)
316    return fail_cmd(conn, pool, sasl_ctx);
317
318  /* SASL needs to know if we are externally authenticated. */
319  if (b->client_info->tunnel_user)
320    result = svn_sasl__setprop(sasl_ctx, SASL_AUTH_EXTERNAL,
321                               b->client_info->tunnel_user);
322  if (result != SASL_OK)
323    return fail_cmd(conn, pool, sasl_ctx);
324
325  /* Get the list of mechanisms. */
326  result = svn_sasl__listmech(sasl_ctx, NULL, NULL, " ", NULL,
327                              &mechlist, NULL, &mech_count);
328
329  if (result != SASL_OK)
330    return fail_cmd(conn, pool, sasl_ctx);
331
332  if (mech_count == 0)
333    {
334      svn_error_t *err = svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL,
335                                          _("Could not obtain the list"
336                                          " of SASL mechanisms"));
337      SVN_ERR(write_failure(conn, pool, &err));
338      return svn_ra_svn__flush(conn, pool);
339    }
340
341  /* Send the list of mechanisms and the realm to the client. */
342  SVN_ERR(svn_ra_svn__write_cmd_response(conn, pool, "(w)c",
343                                         mechlist, b->repository->realm));
344
345  /* The main authentication loop. */
346  subpool = svn_pool_create(pool);
347  do
348    {
349      svn_pool_clear(subpool);
350      SVN_ERR(try_auth(conn, sasl_ctx, subpool, b, &success));
351    }
352  while (!success);
353  svn_pool_destroy(subpool);
354
355  SVN_ERR(svn_ra_svn__enable_sasl_encryption(conn, sasl_ctx, pool));
356
357  if (no_anonymous)
358    {
359      char *p;
360      const void *user;
361
362      /* Get the authenticated username. */
363      result = svn_sasl__getprop(sasl_ctx, SASL_USERNAME, &user);
364
365      if (result != SASL_OK)
366        return fail_cmd(conn, pool, sasl_ctx);
367
368      if ((p = strchr(user, '@')) != NULL)
369        {
370          /* Drop the realm part. */
371          b->client_info->user = apr_pstrndup(b->pool, user,
372                                              p - (const char *)user);
373        }
374      else
375        {
376          svn_error_t *err;
377          err = svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL,
378                                 _("Couldn't obtain the authenticated"
379                                 " username"));
380          SVN_ERR(write_failure(conn, pool, &err));
381          return svn_ra_svn__flush(conn, pool);
382        }
383    }
384
385  return SVN_NO_ERROR;
386}
387
388#endif /* SVN_HAVE_SASL */
389