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