Sometimes KRB5_IS_THREAD_SAFE isn't defined
[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
33 /* krb5 includes */
34 #include <krb5.h>
35
36 #ifdef ET_COMM_ERR
37 #  include <et/com_err.h>
38 #else
39 #  include <com_err.h>
40 #endif
41
42 /*
43  *  Work around bug in krb5_copy_context which attempts to copy the list
44  *  of tgs_kytpes and tkt_ktypes associated with a context... except by
45  *  default the pointers to those lists are NULL, and so it SEGVs
46  *
47  *  The functions sigs below are not provided by krb5.h, but are available
48  *  in the library.
49  */
50 #if !defined(HEIMDAL_KRB5) && defined(KRB5_IS_THREAD_SAFE)
51 krb5_error_code
52 krb5_set_default_in_tkt_ktypes(krb5_context context, const krb5_enctype *etypes);
53
54 krb5_error_code
55 krb5_get_default_in_tkt_ktypes(krb5_context context, krb5_enctype **ktypes);
56
57 krb5_error_code
58 krb5_get_tgs_ktypes(krb5_context context, krb5_const_principal princ, krb5_enctype **ktypes);
59
60 krb5_error_code
61 krb5_set_default_tgs_ktypes(krb5_context context, const krb5_enctype *etypes);
62
63 void
64 krb5_free_ktypes(krb5_context context, krb5_enctype *val);
65 #endif
66
67 /** Instance configuration for rlm_krb5
68  *
69  * Holds the configuration and preparsed data for a instance of rlm_krb5.
70  */
71 typedef struct rlm_krb5_t {
72         char const      *xlat_name;     //!< This module's instance name.
73         char const      *keytabname;    //!< The keytab to resolve the service in.
74         char const      *service_princ; //!< The service name provided by the
75                                         //!< config parser.
76
77         char            *hostname;      //!< The hostname component of
78                                         //!< service_princ, or NULL.
79         char            *service;       //!< The service component of service_princ, or NULL.
80
81         krb5_context context;           //!< The kerberos context (cloned once per request).
82
83 #ifndef HEIMDAL_KRB5
84         krb5_get_init_creds_opt         *gic_options;   //!< Options to pass to the get_initial_credentials
85                                                         //!< function.
86         krb5_verify_init_creds_opt      *vic_options;   //!< Options to pass to the validate_initial_creds
87                                                         //!< function.
88
89         krb5_principal server;          //!< A structure representing the parsed
90                                         //!< service_princ.
91 #endif
92
93 } rlm_krb5_t;
94
95 static const CONF_PARSER module_config[] = {
96         { "keytab", PW_TYPE_STRING_PTR, offsetof(rlm_krb5_t, keytabname), NULL, NULL },
97         { "service_principal", PW_TYPE_STRING_PTR, offsetof(rlm_krb5_t,service_princ), NULL, NULL },
98         { NULL, -1, 0, NULL, NULL }
99 };
100
101 static int krb5_detach(void *instance)
102 {
103         rlm_krb5_t *inst = instance;
104
105 #ifndef HEIMDAL_KRB5
106         talloc_free(inst->vic_options);
107
108         if (inst->gic_options) {
109                 krb5_get_init_creds_opt_free(inst->context, inst->gic_options);
110         }
111
112         if (inst->server) {
113                 krb5_free_principal(inst->context, inst->server);
114         }
115 #endif
116
117         /* Don't free hostname, it's just a pointer into service_princ */
118         talloc_free(inst->service);
119
120         if (inst->context) {
121                 krb5_free_context(inst->context);
122         }
123
124         return 0;
125 }
126
127 static int krb5_instantiate(CONF_SECTION *conf, void *instance)
128 {
129         rlm_krb5_t *inst = instance;
130         krb5_error_code ret;
131 #ifndef HEIMDAL_KRB5
132         krb5_keytab keytab;
133         char keytab_name[200];
134         char *princ_name;
135 #endif
136
137 #ifdef HEIMDAL_KRB5
138         DEBUG("Using Heimdal Kerberos library");
139 #else
140         DEBUG("Using MIT Kerberos library");
141 #endif
142
143 #ifndef KRB5_IS_THREAD_SAFE
144         if (!krb5_is_thread_safe()) {
145                 DEBUGI("libkrb5 is not threadsafe, recompile it with thread support enabled");
146                 WDEBUG("rlm_krb5 will run in single threaded mode, performance may be degraded");
147         } else {
148                 WDEBUG("Build time libkrb5 was not threadsafe, but run time library claims to be");
149                 WDEBUG("Reconfigure and recompile rlm_krb5 to enable thread support");
150         }
151 #endif
152
153         inst->xlat_name = cf_section_name2(conf);
154         if (!inst->xlat_name) {
155                 inst->xlat_name = cf_section_name1(conf);
156         }
157
158         ret = krb5_init_context(&inst->context);
159         if (ret) {
160                 EDEBUG("rlm_krb5 (%s): Context initialisation failed: %s", inst->xlat_name, error_message(ret));
161
162                 return -1;
163         }
164
165         DEBUG("rlm_krb5 (%s): Context initialised successfully", inst->xlat_name);
166
167         /*
168          *      Split service principal into service and host components
169          *      they're needed to build the server principal in MIT,
170          *      and to set the validation service in Heimdal.
171          */
172         if (inst->service_princ) {
173                 size_t len;
174                 /* Service principal appears to contain a host component */
175                 inst->hostname = strchr(inst->service_princ, '/');
176                 if (inst->hostname) {
177                         len = (inst->hostname - inst->service_princ);
178                         inst->hostname++;
179                 } else {
180                         len = strlen(inst->service_princ);
181                 }
182
183                 if (len) {
184                         inst->service = talloc_array(inst, char, (len + 1));
185                         strlcpy(inst->service, inst->service_princ, len + 1);
186                 }
187         }
188
189 #ifdef HEIMDAL_KRB5
190         if (inst->hostname) {
191                 DEBUG("rlm_krb5 (%s): Ignoring hostname component of service principal \"%s\", not "
192                        "needed/supported by Heimdal", inst->xlat_name, inst->hostname);
193         }
194 #else
195
196         /*
197          *      Convert the service principal string to a krb5 principal.
198          */
199         ret = krb5_sname_to_principal(inst->context, inst->hostname, inst->service, KRB5_NT_SRV_HST, &(inst->server));
200         if (ret) {
201                 EDEBUG("rlm_krb5 (%s): Failed parsing service principal: %s", inst->xlat_name, error_message(ret));
202
203                 return -1;
204         }
205
206         ret = krb5_unparse_name(inst->context, inst->server, &princ_name);
207         if (ret) {
208                 /* Uh? */
209                 EDEBUG("rlm_krb5 (%s): Failed constructing service principal string: %s", inst->xlat_name,
210                        error_message(ret));
211
212                 return -1;
213         }
214
215         /*
216          *      Not necessarily the same as the config item
217          */
218         DEBUG("rlm_krb5 (%s): Using service principal \"%s\"", inst->xlat_name, princ_name);
219
220         krb5_free_unparsed_name(inst->context, princ_name);
221
222         /*
223          *      Setup options for getting credentials and verifying them
224          */
225
226         /* For some reason the 'init' version of this function is deprecated */
227         ret = krb5_get_init_creds_opt_alloc(inst->context, &(inst->gic_options));
228         if (ret) {
229                 EDEBUG("rlm_krb5 (%s): Couldn't allocated inital credential options: %s", inst->xlat_name,
230                        error_message(ret));
231
232                 return -1;
233         }
234
235         /*
236          *      Perform basic checks on the keytab
237          */
238         ret = inst->keytabname ?
239                 krb5_kt_resolve(inst->context, inst->keytabname, &keytab) :
240                 krb5_kt_default(inst->context, &keytab);
241         if (ret) {
242                 EDEBUG("rlm_krb5 (%s): Resolving keytab failed: %s", inst->xlat_name, error_message(ret));
243
244                 return -1;
245         }
246
247         ret = krb5_kt_get_name(inst->context, keytab, keytab_name, sizeof(keytab_name));
248         krb5_kt_close(inst->context, keytab);
249         if (ret) {
250                 EDEBUG("rlm_krb5 (%s): Can't retrieve keytab name: %s", inst->xlat_name, error_message(ret));
251
252                 return -1;
253         }
254
255         DEBUG("rlm_krb5 (%s): Using keytab \"%s\"", inst->xlat_name, keytab_name);
256
257         MEM(inst->vic_options = talloc_zero(inst, krb5_verify_init_creds_opt));
258
259         krb5_verify_init_creds_opt_init(inst->vic_options);
260         krb5_verify_init_creds_opt_set_ap_req_nofail(inst->vic_options, true);
261
262 #  ifdef KRB5_IS_THREAD_SAFE
263         /*
264          *      Explicitly set enctypes to work around bug in krb5_copy_context
265          */
266         {
267                 krb5_enctype *enctypes;
268
269                 krb5_get_default_in_tkt_ktypes(inst->context, &enctypes);
270                 krb5_set_default_in_tkt_ktypes(inst->context, enctypes);
271                 krb5_free_ktypes(inst->context, enctypes);
272
273                 krb5_get_tgs_ktypes(inst->context, inst->server, &enctypes);
274                 krb5_set_default_tgs_ktypes(inst->context, enctypes);
275                 krb5_free_ktypes(inst->context, enctypes);
276         }
277 #  endif
278 #endif
279
280         return 0;
281 }
282
283 static rlm_rcode_t krb5_parse_user(REQUEST *request, krb5_context context, krb5_principal *client)
284 {
285         krb5_error_code ret;
286         char *princ_name;
287
288         /*
289          *      We can only authenticate user requests which HAVE
290          *      a User-Name attribute.
291          */
292         if (!request->username) {
293                 REDEBUG("Attribute \"User-Name\" is required for authentication");
294
295                 return RLM_MODULE_INVALID;
296         }
297
298         /*
299          *      We can only authenticate user requests which HAVE
300          *      a User-Password attribute.
301          */
302         if (!request->password) {
303                 REDEBUG("Attribute \"User-Password\" is required for authentication");
304
305                 return RLM_MODULE_INVALID;
306         }
307
308         /*
309          *      Ensure that we're being passed a plain-text password,
310          *      and not anything else.
311          */
312         if (request->password->da->attr != PW_USER_PASSWORD) {
313                 REDEBUG("Attribute \"User-Password\" is required for authentication.  Cannot use \"%s\".",
314                         request->password->da->name);
315
316                 return RLM_MODULE_INVALID;
317         }
318
319         ret = krb5_parse_name(context, request->username->vp_strvalue, client);
320         if (ret) {
321                 REDEBUG("Failed parsing username as principal: %s", error_message(ret));
322
323                 return RLM_MODULE_FAIL;
324         }
325
326         krb5_unparse_name(context, *client, &princ_name);
327         RDEBUG("Using client principal \"%s\"", princ_name);
328 #ifdef HEIMDAL_KRB5
329         free(princ_name);
330 #else
331         krb5_free_unparsed_name(context, princ_name);
332 #endif
333         return RLM_MODULE_OK;
334 }
335
336 #ifdef HEIMDAL_KRB5
337
338 /*
339  *      Validate user/pass (Heimdal)
340  */
341 static rlm_rcode_t krb5_auth(void *instance, REQUEST *request)
342 {
343         rlm_krb5_t *inst = instance;
344         rlm_rcode_t rcode;
345
346         krb5_error_code ret;
347
348         krb5_principal client;
349         krb5_ccache ccache;
350         krb5_keytab keytab;
351         krb5_verify_opt options;
352         krb5_context context;
353
354         rad_assert(inst->context);
355
356 #ifdef KRB5_IS_THREAD_SAFE
357         /*
358          *      See above in MIT krb5_auth
359          */
360         ret = krb5_copy_context(inst->context, &context);
361         if (ret) {
362                 REDEBUG("Error cloning krb5 context: %s", error_message(ret));
363
364                 return RLM_MODULE_FAIL;
365         }
366 #else
367         context = inst->context;
368 #endif
369
370         /*
371          *      Zero out local storage
372          */
373         memset(&keytab, 0, sizeof(keytab));
374         memset(&client, 0, sizeof(client));
375
376         /*
377          *      Setup krb5_verify_user options
378          *
379          *      Not entirely sure this is necessary, but as we use context
380          *      to get the cache handle, we probably do have to do this with
381          *      the cloned context.
382          */
383         krb5_cc_default(context, &ccache);
384
385         krb5_verify_opt_init(&options);
386         krb5_verify_opt_set_ccache(&options, ccache);
387         ret = inst->keytabname ?
388                 krb5_kt_resolve(context, inst->keytabname, &keytab) :
389                 krb5_kt_default(context, &keytab);
390         if (ret) {
391                 REDEBUG("Resolving keytab failed: %s", error_message(ret));
392                 rcode = RLM_MODULE_FAIL;
393
394                 goto cleanup;
395         }
396
397         krb5_verify_opt_set_keytab(&options, keytab);
398         krb5_verify_opt_set_secure(&options, true);
399
400         if (inst->service) {
401                 krb5_verify_opt_set_service(&options, inst->service);
402         }
403
404         rcode = krb5_parse_user(request, context, &client);
405         if (rcode != RLM_MODULE_OK) goto cleanup;
406
407         /*
408          *      Verify the user, using the options we set in instantiate
409          */
410         ret = krb5_verify_user_opt(context, client, request->password->vp_strvalue, &options);
411         if (ret) {
412                 switch (ret) {
413                 case KRB5_LIBOS_BADPWDMATCH:
414                 case KRB5KRB_AP_ERR_BAD_INTEGRITY:
415                         REDEBUG("Provided password was incorrect (%i): %s", ret, error_message(ret));
416                         rcode = RLM_MODULE_REJECT;
417
418                         break;
419                 case KRB5KDC_ERR_KEY_EXP:
420                 case KRB5KDC_ERR_CLIENT_REVOKED:
421                 case KRB5KDC_ERR_SERVICE_REVOKED:
422                         REDEBUG("Account has been locked out (%i): %s", ret, error_message(ret));
423                         rcode = RLM_MODULE_USERLOCK;
424
425                         break;
426                 case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
427                         RDEBUG("User not found: %s (%i)", ret, error_message(ret));
428                         rcode = RLM_MODULE_NOTFOUND;
429
430                 default:
431                         REDEBUG("Error verifying credentials (%i): %s", ret, error_message(ret));
432                         rcode = RLM_MODULE_FAIL;
433
434                         break;
435                 }
436
437                 goto cleanup;
438         }
439
440         cleanup:
441         if (client) {
442                 krb5_free_principal(context, client);
443         }
444         if (keytab) {
445                 krb5_kt_close(context, keytab);
446         }
447 #ifdef KRB5_IS_THREAD_SAFE
448         krb5_free_context(context);
449 #endif
450         return rcode;
451 }
452
453 #else  /* HEIMDAL_KRB5 */
454
455 /*
456  *  Validate userid/passwd (MIT)
457  */
458 static rlm_rcode_t krb5_auth(void *instance, REQUEST *request)
459 {
460         rlm_krb5_t *inst = instance;
461         rlm_rcode_t rcode;
462         krb5_error_code ret;
463
464         krb5_principal client;
465         krb5_creds init_creds;
466         krb5_keytab keytab;     /* ktid */
467         krb5_context context;
468         char *password;         /* compiler warnings */
469
470         rad_assert(inst->context);
471
472 #ifdef KRB5_IS_THREAD_SAFE
473         /*
474          *      All the snippets on threadsafety say that individual threads
475          *      must each use their own copy of context.
476          *
477          *      As we don't have any per thread instantiation, we either have
478          *      to clone inst->context on every request, or use the connection
479          *      API.
480          *
481          *      @todo Use the connection API (3.0 only).
482          */
483         ret = krb5_copy_context(inst->context, &context);
484         if (ret) {
485                 REDEBUG("Error cloning krb5 context: %s", error_message(ret));
486
487                 return RLM_MODULE_FAIL;
488         }
489         rad_assert(context != NULL); /* tell coverity copy context copies it */
490 #else
491         context = inst->context;
492 #endif
493
494         /*
495          *      Zero out local storage
496          */
497         memset(&keytab, 0, sizeof(keytab));
498         memset(&client, 0, sizeof(client));
499         memset(&init_creds, 0, sizeof(init_creds));
500
501         /*
502          *      Check we have all the required VPs, and convert the username
503          *      into a principal.
504          */
505         rcode = krb5_parse_user(request, context, &client);
506         if (rcode != RLM_MODULE_OK) goto cleanup;
507
508         /*
509          *      Setup the keytab
510          */
511         ret = inst->keytabname ?
512                 krb5_kt_resolve(context, inst->keytabname, &keytab) :
513                 krb5_kt_default(context, &keytab);
514         if (ret) {
515                 REDEBUG("Resolving keytab failed: %s", error_message(ret));
516
517                 goto cleanup;
518         }
519
520         /*
521          *      Retrieve the TGT from the TGS/KDC and check we can decrypt it.
522          */
523         memcpy(&password, &request->password->vp_strvalue, sizeof(password));
524         ret = krb5_get_init_creds_password(context, &init_creds, client, password,
525                                            NULL, NULL, 0, NULL, inst->gic_options);
526         password = NULL;
527         if (ret) {
528                 error:
529                 switch (ret) {
530                 case KRB5_LIBOS_BADPWDMATCH:
531                 case KRB5KRB_AP_ERR_BAD_INTEGRITY:
532                         REDEBUG("Provided password was incorrect (%i): %s", ret, error_message(ret));
533                         rcode = RLM_MODULE_REJECT;
534                         break;
535
536                 case KRB5KDC_ERR_KEY_EXP:
537                 case KRB5KDC_ERR_CLIENT_REVOKED:
538                 case KRB5KDC_ERR_SERVICE_REVOKED:
539                         REDEBUG("Account has been locked out (%i): %s", ret, error_message(ret));
540                         rcode = RLM_MODULE_USERLOCK;
541                         break;
542
543                 case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
544                         REDEBUG("User not found (%i): %s", ret,  error_message(ret));
545                         rcode = RLM_MODULE_NOTFOUND;
546                         break;
547
548                 default:
549                         REDEBUG("Error retrieving or verifying credentials (%i): %s", ret, error_message(ret));
550                         rcode = RLM_MODULE_FAIL;
551                         break;
552                 }
553
554                 goto cleanup;
555         }
556
557         RDEBUG("Successfully retrieved and decrypted TGT");
558
559         ret = krb5_verify_init_creds(context, &init_creds, inst->server, keytab, NULL, inst->vic_options);
560         if (ret) goto error;
561
562         cleanup:
563
564         if (context) {
565                 if (client) {
566                         krb5_free_principal(context, client);
567                 }
568                 if (keytab) {
569                         krb5_kt_close(context, keytab);
570                 }
571
572                 krb5_free_cred_contents(context, &init_creds);
573
574 #ifdef KRB5_IS_THREAD_SAFE
575                 krb5_free_context(context);
576 #endif
577         }
578
579         return rcode;
580 }
581
582 #endif /* MIT_KRB5 */
583
584 module_t rlm_krb5 = {
585         RLM_MODULE_INIT,
586         "krb5",
587         RLM_TYPE_CHECK_CONFIG_SAFE | RLM_TYPE_HUP_SAFE
588 #ifdef KRB5_IS_THREAD_SAFE
589         | RLM_TYPE_THREAD_SAFE
590 #endif
591         ,
592         sizeof(rlm_krb5_t),
593         module_config,
594         krb5_instantiate,               /* instantiation */
595         krb5_detach,                    /* detach */
596         {
597                 krb5_auth,              /* authenticate */
598                 NULL,                   /* authorize */
599                 NULL,                   /* pre-accounting */
600                 NULL,                   /* accounting */
601                 NULL,                   /* checksimul */
602                 NULL,                   /* pre-proxy */
603                 NULL,                   /* post-proxy */
604                 NULL                    /* post-auth */
605         },
606 };