1c0b08716b6aed2a2dd3ca61c548fcd25a649978
[freeradius.git] / src / modules / rlm_krb5 / rlm_krb5.c
1 /*
2  *   This program is free software; you can redistribute it and/or modify
3  *   it under the terms of the GNU General Public License as published by
4  *   the Free Software Foundation; either version 2 of the License, or
5  *   (at your option) any later version.
6  *
7  *   This program is distributed in the hope that it will be useful,
8  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
9  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10  *   GNU General Public License for more details.
11  *
12  *   You should have received a copy of the GNU General Public License
13  *   along with this program; if not, write to the Free Software
14  *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
15  */
16
17 /**
18  * $Id$
19  * @file rlm_krb5.c
20  * @brief Authenticate users, retrieving their TGT from a Kerberos V5 TDC.
21  *
22  * @copyright 2000,2006,2012-2013  The FreeRADIUS server project
23  * @copyright 2013  Arran Cudbard-Bell <a.cudbardb@freeradius.org>
24  * @copyright 2000  Nathan Neulinger <nneul@umr.edu>
25  * @copyright 2000  Alan DeKok <aland@ox.org>
26  */
27 RCSID("$Id$")
28
29 #include <freeradius-devel/radiusd.h>
30 #include <freeradius-devel/modules.h>
31 #include <freeradius-devel/rad_assert.h>
32 #include "krb5.h"
33
34 static const CONF_PARSER module_config[] = {
35         { "keytab", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_krb5_t, keytabname), NULL },
36         { "service_principal", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_krb5_t, service_princ), NULL },
37         { NULL, -1, 0, NULL, NULL }
38 };
39
40 static int mod_detach(void *instance)
41 {
42         rlm_krb5_t *inst = instance;
43
44 #ifndef HEIMDAL_KRB5
45         talloc_free(inst->vic_options);
46
47         if (inst->gic_options) krb5_get_init_creds_opt_free(inst->context, inst->gic_options);
48         if (inst->server) krb5_free_principal(inst->context, inst->server);
49 #endif
50
51         /* Don't free hostname, it's just a pointer into service_princ */
52         talloc_free(inst->service);
53
54         if (inst->context) krb5_free_context(inst->context);
55 #ifdef KRB5_IS_THREAD_SAFE
56         fr_connection_pool_free(inst->pool);
57 #endif
58
59         return 0;
60 }
61
62 static int mod_instantiate(CONF_SECTION *conf, void *instance)
63 {
64         rlm_krb5_t *inst = instance;
65         krb5_error_code ret;
66 #ifndef HEIMDAL_KRB5
67         krb5_keytab keytab;
68         char keytab_name[200];
69         char *princ_name;
70 #endif
71
72 #ifdef HEIMDAL_KRB5
73         DEBUG("Using Heimdal Kerberos library");
74 #else
75         DEBUG("Using MIT Kerberos library");
76 #endif
77
78         if (!krb5_is_thread_safe()) {
79 /*
80  *      rlm_krb5 was built as threadsafe
81  */
82 #ifdef KRB5_IS_THREAD_SAFE
83                 ERROR("Build time libkrb5 was threadsafe, but run time library claims not to be");
84                 ERROR("Modify runtime linker path (LD_LIBRARY_PATH on most systems), to prefer threadsafe libkrb5");
85                 return -1;
86 /*
87  *      rlm_krb5 was not built as threadsafe
88  */
89 #else
90                 radlog(L_WARN, "libkrb5 is not threadsafe, recompile it with thread support enabled ("
91 #  ifdef HEIMDAL_KRB5
92                        "--enable-pthread-support"
93 #  else
94                        "--disable-thread-support=no"
95 #  endif
96                        ")");
97                 WARN("rlm_krb5 will run in single threaded mode, performance may be degraded");
98         } else {
99                 WARN("Build time libkrb5 was not threadsafe, but run time library claims to be");
100                 WARN("Reconfigure and recompile rlm_krb5 to enable thread support");
101 #endif
102         }
103
104         inst->xlat_name = cf_section_name2(conf);
105         if (!inst->xlat_name) inst->xlat_name = cf_section_name1(conf);
106
107         ret = krb5_init_context(&inst->context);
108         if (ret) {
109                 ERROR("rlm_krb5 (%s): context initialisation failed: %s", inst->xlat_name,
110                       rlm_krb5_error(NULL, ret));
111
112                 return -1;
113         }
114
115         /*
116          *      Split service principal into service and host components
117          *      they're needed to build the server principal in MIT,
118          *      and to set the validation service in Heimdal.
119          */
120         if (inst->service_princ) {
121                 size_t len;
122                 /* Service principal appears to contain a host component */
123                 inst->hostname = strchr(inst->service_princ, '/');
124                 if (inst->hostname) {
125                         len = (inst->hostname - inst->service_princ);
126                         inst->hostname++;
127                 } else {
128                         len = strlen(inst->service_princ);
129                 }
130
131                 if (len) {
132                         inst->service = talloc_array(inst, char, (len + 1));
133                         strlcpy(inst->service, inst->service_princ, len + 1);
134                 }
135         }
136
137 #ifdef HEIMDAL_KRB5
138         if (inst->hostname) DEBUG("rlm_krb5 (%s): Ignoring hostname component of service principal \"%s\", not "
139                                   "needed/supported by Heimdal", inst->xlat_name, inst->hostname);
140 #else
141
142         /*
143          *      Convert the service principal string to a krb5 principal.
144          */
145         ret = krb5_sname_to_principal(inst->context, inst->hostname, inst->service, KRB5_NT_SRV_HST, &(inst->server));
146         if (ret) {
147                 ERROR("rlm_krb5 (%s): Failed parsing service principal: %s", inst->xlat_name,
148                       rlm_krb5_error(inst->context, ret));
149
150                 return -1;
151         }
152
153         ret = krb5_unparse_name(inst->context, inst->server, &princ_name);
154         if (ret) {
155                 /* Uh? */
156                 ERROR("rlm_krb5 (%s): Failed constructing service principal string: %s", inst->xlat_name,
157                       rlm_krb5_error(inst->context, ret));
158
159                 return -1;
160         }
161
162         /*
163          *      Not necessarily the same as the config item
164          */
165         DEBUG("rlm_krb5 (%s): Using service principal \"%s\"", inst->xlat_name, princ_name);
166         krb5_free_unparsed_name(inst->context, princ_name);
167
168         /*
169          *      Setup options for getting credentials and verifying them
170          */
171         ret = krb5_get_init_creds_opt_alloc(inst->context, &(inst->gic_options)); /* For some reason the 'init' version
172                                                                                     of this function is deprecated */
173         if (ret) {
174                 ERROR("rlm_krb5 (%s): Couldn't allocated inital credential options: %s", inst->xlat_name,
175                       rlm_krb5_error(inst->context, ret));
176
177                 return -1;
178         }
179
180         /*
181          *      Perform basic checks on the keytab
182          */
183         ret = inst->keytabname ?
184                 krb5_kt_resolve(inst->context, inst->keytabname, &keytab) :
185                 krb5_kt_default(inst->context, &keytab);
186         if (ret) {
187                 ERROR("rlm_krb5 (%s): Resolving keytab failed: %s", inst->xlat_name,
188                       rlm_krb5_error(inst->context, ret));
189
190                 return -1;
191         }
192
193         ret = krb5_kt_get_name(inst->context, keytab, keytab_name, sizeof(keytab_name));
194         krb5_kt_close(inst->context, keytab);
195         if (ret) {
196                 ERROR("rlm_krb5 (%s): Can't retrieve keytab name: %s", inst->xlat_name,
197                       rlm_krb5_error(inst->context, ret));
198
199                 return -1;
200         }
201
202         DEBUG("rlm_krb5 (%s): Using keytab \"%s\"", inst->xlat_name, keytab_name);
203
204         MEM(inst->vic_options = talloc_zero(inst, krb5_verify_init_creds_opt));
205         krb5_verify_init_creds_opt_init(inst->vic_options);
206 #endif
207
208 #ifdef KRB5_IS_THREAD_SAFE
209         /*
210          *      Initialize the socket pool.
211          */
212         inst->pool = fr_connection_pool_module_init(conf, inst, mod_conn_create, NULL, NULL);
213         if (!inst->pool) return -1;
214 #else
215         inst->conn = mod_conn_create(inst, inst);
216         if (!inst->conn) return -1;
217 #endif
218         return 0;
219 }
220
221 /** Common function for transforming a User-Name string into a principal.
222  *
223  * @param[out] client Where to write the client principal.
224  * @param[in] request Current request.
225  * @param[in] context Kerberos context.
226  */
227 static rlm_rcode_t krb5_parse_user(krb5_principal *client, REQUEST *request, krb5_context context)
228 {
229         krb5_error_code ret;
230         char *princ_name;
231
232         /*
233          *      We can only authenticate user requests which HAVE
234          *      a User-Name attribute.
235          */
236         if (!request->username) {
237                 REDEBUG("Attribute \"User-Name\" is required for authentication");
238
239                 return RLM_MODULE_INVALID;
240         }
241
242         /*
243          *      We can only authenticate user requests which HAVE
244          *      a User-Password attribute.
245          */
246         if (!request->password) {
247                 REDEBUG("Attribute \"User-Password\" is required for authentication");
248
249                 return RLM_MODULE_INVALID;
250         }
251
252         /*
253          *      Ensure that we're being passed a plain-text password,
254          *      and not anything else.
255          */
256         if (request->password->da->attr != PW_USER_PASSWORD) {
257                 REDEBUG("Attribute \"User-Password\" is required for authentication.  Cannot use \"%s\".",
258                         request->password->da->name);
259
260                 return RLM_MODULE_INVALID;
261         }
262
263         ret = krb5_parse_name(context, request->username->vp_strvalue, client);
264         if (ret) {
265                 REDEBUG("Failed parsing username as principal: %s", rlm_krb5_error(context, ret));
266
267                 return RLM_MODULE_FAIL;
268         }
269
270         krb5_unparse_name(context, *client, &princ_name);
271         RDEBUG("Using client principal \"%s\"", princ_name);
272 #ifdef HEIMDAL_KRB5
273         free(princ_name);
274 #else
275         krb5_free_unparsed_name(context, princ_name);
276 #endif
277         return RLM_MODULE_OK;
278 }
279
280 /** Log error message and return appropriate rcode
281  *
282  * Translate kerberos error codes into return codes.
283  * @param request Current request.
284  * @param ret code from kerberos.
285  * @param conn used in the last operation.
286  */
287 static rlm_rcode_t krb5_process_error(REQUEST *request, rlm_krb5_handle_t *conn, int ret)
288 {
289         rad_assert(ret != 0);
290         rad_assert(conn);       /* Silences warnings */
291
292         switch (ret) {
293         case KRB5_LIBOS_BADPWDMATCH:
294         case KRB5KRB_AP_ERR_BAD_INTEGRITY:
295                 REDEBUG("Provided password was incorrect (%i): %s", ret, rlm_krb5_error(conn->context, ret));
296                 return RLM_MODULE_REJECT;
297
298         case KRB5KDC_ERR_KEY_EXP:
299         case KRB5KDC_ERR_CLIENT_REVOKED:
300         case KRB5KDC_ERR_SERVICE_REVOKED:
301                 REDEBUG("Account has been locked out (%i): %s", ret, rlm_krb5_error(conn->context, ret));
302                 return RLM_MODULE_USERLOCK;
303
304         case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
305                 RDEBUG("User not found (%i): %s", ret, rlm_krb5_error(conn->context, ret));
306                 return RLM_MODULE_NOTFOUND;
307
308         default:
309                 REDEBUG("Error verifying credentials (%i): %s", ret, rlm_krb5_error(conn->context, ret));
310                 return RLM_MODULE_FAIL;
311         }
312 }
313
314 #ifdef HEIMDAL_KRB5
315
316 /*
317  *      Validate user/pass (Heimdal)
318  */
319 static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(void *instance, REQUEST *request)
320 {
321         rlm_krb5_t *inst = instance;
322         rlm_rcode_t rcode;
323         krb5_error_code ret;
324
325         rlm_krb5_handle_t *conn;
326
327         krb5_principal client;
328
329 #  ifdef KRB5_IS_THREAD_SAFE
330         conn = fr_connection_get(inst->pool);
331         if (!conn) return RLM_MODULE_FAIL;
332 #  else
333         conn = inst->conn;
334 #  endif
335
336         /*
337          *      Zero out local storage
338          */
339         memset(&client, 0, sizeof(client));
340
341         rcode = krb5_parse_user(&client, request, conn->context);
342         if (rcode != RLM_MODULE_OK) goto cleanup;
343
344         /*
345          *      Verify the user, using the options we set in instantiate
346          */
347         ret = krb5_verify_user_opt(conn->context, client, request->password->vp_strvalue, &conn->options);
348         if (ret) {
349                 rcode = krb5_process_error(request, conn, ret);
350                 goto cleanup;
351         }
352
353         /*
354          *      krb5_verify_user_opt adds the credentials to the ccache
355          *      we specified with krb5_verify_opt_set_ccache.
356          *
357          *      To make sure we don't accumulate thousands of sets of
358          *      credentials, remove them again here.
359          *
360          * @todo This should definitely be optional, which means writing code for the MIT
361          *       variant as well.
362          */
363         {
364                 krb5_cc_cursor cursor;
365                 krb5_creds cred;
366
367                 krb5_cc_start_seq_get(conn->context, conn->ccache, &cursor);
368                 for (ret = krb5_cc_next_cred(conn->context, conn->ccache, &cursor, &cred);
369                      ret == 0;
370                      ret = krb5_cc_next_cred(conn->context, conn->ccache, &cursor, &cred)) {
371                      krb5_cc_remove_cred(conn->context, conn->ccache, 0, &cred);
372                 }
373                 krb5_cc_end_seq_get(conn->context, conn->ccache, &cursor);
374         }
375
376 cleanup:
377         if (client) {
378                 krb5_free_principal(conn->context, client);
379         }
380
381 #  ifdef KRB5_IS_THREAD_SAFE
382         fr_connection_release(inst->pool, conn);
383 #  endif
384         return rcode;
385 }
386
387 #else  /* HEIMDAL_KRB5 */
388
389 /*
390  *  Validate userid/passwd (MIT)
391  */
392 static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(void *instance, REQUEST *request)
393 {
394         rlm_krb5_t *inst = instance;
395         rlm_rcode_t rcode;
396         krb5_error_code ret;
397
398         rlm_krb5_handle_t *conn;
399
400         krb5_principal client;
401         krb5_creds init_creds;
402         char *password;         /* compiler warnings */
403
404         rad_assert(inst->context);
405
406 #  ifdef KRB5_IS_THREAD_SAFE
407         conn = fr_connection_get(inst->pool);
408         if (!conn) return RLM_MODULE_FAIL;
409 #  else
410         conn = inst->conn;
411 #  endif
412
413         /*
414          *      Zero out local storage
415          */
416         memset(&client, 0, sizeof(client));
417         memset(&init_creds, 0, sizeof(init_creds));
418
419         /*
420          *      Check we have all the required VPs, and convert the username
421          *      into a principal.
422          */
423         rcode = krb5_parse_user(&client, request, conn->context);
424         if (rcode != RLM_MODULE_OK) goto cleanup;
425
426         /*
427          *      Retrieve the TGT from the TGS/KDC and check we can decrypt it.
428          */
429         memcpy(&password, &request->password->vp_strvalue, sizeof(password));
430         RDEBUG("Retrieving and decrypting TGT");
431         ret = krb5_get_init_creds_password(conn->context, &init_creds, client, password,
432                                            NULL, NULL, 0, NULL, inst->gic_options);
433         if (ret) {
434                 rcode = krb5_process_error(request, conn, ret);
435                 goto cleanup;
436         }
437
438         RDEBUG("Attempting to authenticate against service principal");
439         ret = krb5_verify_init_creds(conn->context, &init_creds, inst->server, conn->keytab, NULL, inst->vic_options);
440         if (ret) rcode = krb5_process_error(request, conn, ret);
441
442 cleanup:
443         if (client) krb5_free_principal(conn->context, client);
444         krb5_free_cred_contents(conn->context, &init_creds);
445
446 #  ifdef KRB5_IS_THREAD_SAFE
447         fr_connection_release(inst->pool, conn);
448 #  endif
449         return rcode;
450 }
451
452 #endif /* MIT_KRB5 */
453
454 extern module_t rlm_krb5;
455 module_t rlm_krb5 = {
456         .magic          = RLM_MODULE_INIT,
457         .name           = "krb5",
458         .type           = RLM_TYPE_HUP_SAFE
459 #ifdef KRB5_IS_THREAD_SAFE
460         | RLM_TYPE_THREAD_SAFE
461 #endif
462         ,
463         .inst_size      = sizeof(rlm_krb5_t),
464         .config         = module_config,
465         .instantiate    = mod_instantiate,
466         .detach         = mod_detach,
467         .methods = {
468                 [MOD_AUTHENTICATE]      = mod_authenticate
469         },
470 };