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