Merge pull request #215 from alanbuxey/master
[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         rad_assert(context != NULL); /* tell coverity copy context copies it */
331
332         /*
333          *      Check we have all the required VPs, and convert the username
334          *      into a principal.
335          */
336         rcode = krb5_parse_user(inst, request, &client);
337         if (rcode != RLM_MODULE_OK) goto cleanup;
338
339         /*
340          *      Retrieve the TGT from the TGS/KDC and check we can decrypt it.
341          */
342         memset(&init_creds, 0, sizeof(init_creds));
343         ret = krb5_get_init_creds_password(*context, &init_creds, client,
344                                            request->password->vp_strvalue,
345                                            NULL, NULL, 0, NULL,
346                                            inst->gic_options);
347         if (ret) {
348                 error:
349                 switch (ret) {
350                 case KRB5_LIBOS_BADPWDMATCH:
351                 case KRB5KRB_AP_ERR_BAD_INTEGRITY:
352                         RDEBUG("Provided password was incorrect: %s",
353                                error_message(ret));
354                         rcode = RLM_MODULE_REJECT;
355                         break;
356                         
357                 case KRB5KDC_ERR_KEY_EXP:
358                 case KRB5KDC_ERR_CLIENT_REVOKED:
359                 case KRB5KDC_ERR_SERVICE_REVOKED:
360                         RDEBUG("Account has been locked out: %s",
361                                error_message(ret));
362                         rcode = RLM_MODULE_USERLOCK;
363                         break;
364                         
365                 case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
366                         RDEBUG("User not found: %s", error_message(ret));
367                         rcode = RLM_MODULE_NOTFOUND;
368                         break;
369                         
370                 default:
371                         radlog(L_ERR, "rlm_krb5 (%s): Failed getting/verifying "
372                                "credentials: %s", inst->xlat_name,
373                                error_message(ret));
374                         rcode = RLM_MODULE_FAIL;
375                         break;
376                 }
377
378                 goto cleanup;
379         }
380         
381         RDEBUG("Successfully retrieved and decrypted TGT");
382         
383         memset(&keytab, 0, sizeof(keytab));
384         ret = inst->keytabname ? 
385                 krb5_kt_resolve(*context, inst->keytabname, &keytab) :
386                 krb5_kt_default(*context, &keytab);
387         if (ret) {
388                 radlog(L_ERR, "rlm_krb5 (%s): Resolving keytab failed: %s",
389                        inst->xlat_name, error_message(ret));
390                 
391                 goto cleanup;
392         }
393         
394         ret = krb5_verify_init_creds(*context, &init_creds, inst->server,
395                                      keytab, NULL, inst->vic_options);
396         if (ret) goto error;
397  
398         cleanup:
399
400         if (context) {
401                 krb5_free_cred_contents(*context, &init_creds);
402                 krb5_free_context(*context);
403                 krb5_kt_close(*context, keytab);
404         }
405         
406         return rcode;
407 }
408
409 #else
410
411 /*
412  *      Validate user/pass (Heimdal)
413  */
414 static rlm_rcode_t krb5_auth(void *instance, REQUEST *request)
415 {
416         rlm_krb5_t *inst = instance;
417         rlm_rcode_t rcode;
418         
419         krb5_error_code ret;
420         
421         krb5_principal client;
422         krb5_ccache ccache;
423         krb5_keytab keytab;
424         krb5_verify_opt options;
425         krb5_context *context = NULL;
426         
427         /*
428          *      See above in MIT krb5_auth
429          */
430         ret = krb5_copy_context(*(inst->context), context);
431         if (ret) {
432                 radlog(L_ERR, "rlm_krb5 (%s): Error cloning krb5 context: %s",
433                        inst->xlat_name, error_message(ret));
434                 
435                 return RLM_MODULE_FAIL;
436         }
437         
438         /*
439          *      Setup krb5_verify_user options
440          *
441          *      Not entirely sure this is necessary, but as we use context 
442          *      to get the cache handle, we probably do have to do this with
443          *      the cloned context.
444          */
445         krb5_cc_default(*context, &ccache);
446         
447         krb5_verify_opt_init(&options);
448         krb5_verify_opt_set_ccache(&options, ccache);
449         
450         memset(&keytab, 0, sizeof(keytab));
451         ret = inst->keytabname ? 
452                 krb5_kt_resolve(*context, inst->keytabname, &keytab) :
453                 krb5_kt_default(*context, &keytab);
454         if (ret) {
455                 radlog(L_ERR, "rlm_krb5 (%s): Resolving keytab failed: %s",
456                        inst->xlat_name, error_message(ret));
457                 
458                 goto cleanup;
459         }
460         
461         krb5_verify_opt_set_keytab(&options, keytab);
462         krb5_verify_opt_set_secure(&options, TRUE);
463
464         if (inst->service) {
465                 krb5_verify_opt_set_service(&options, inst->service);
466         }
467         
468         rcode = krb5_parse_user(inst, request, &client);
469         if (rcode != RLM_MODULE_OK) goto cleanup;
470
471         /* 
472          *      Verify the user, using the options we set in instantiate
473          */
474         ret = krb5_verify_user_opt(*context, client,
475                                    request->password->vp_strvalue,
476                                    &options);
477         if (ret) {
478                 switch (ret) {
479                 case KRB5_LIBOS_BADPWDMATCH:
480                 case KRB5KRB_AP_ERR_BAD_INTEGRITY:
481                         RDEBUG("Provided password was incorrect: %s",
482                                error_message(ret));
483                         rcode = RLM_MODULE_REJECT;
484                 
485                         break;
486                 case KRB5KDC_ERR_KEY_EXP:
487                 case KRB5KDC_ERR_CLIENT_REVOKED:
488                 case KRB5KDC_ERR_SERVICE_REVOKED:
489                         RDEBUG("Account has been locked out: %s",
490                                error_message(ret));
491                         rcode = RLM_MODULE_USERLOCK;
492                 
493                         break;
494                 case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
495                         RDEBUG("User not found: %s", error_message(ret));
496                         rcode = RLM_MODULE_NOTFOUND;
497                                         
498                 default:
499                         radlog(L_ERR, "rlm_krb5 (%s): Verifying user failed: "
500                                "%s", inst->xlat_name, error_message(ret));
501                         rcode = RLM_MODULE_FAIL;
502                 
503                         break;
504                 }
505
506                 goto cleanup;
507         }
508         
509         cleanup:
510         
511         krb5_free_context(*context);
512         krb5_kt_close(*context, keytab);
513         
514         return rcode;
515 }
516
517 #endif /* HEIMDAL_KRB5 */
518
519 module_t rlm_krb5 = {
520         RLM_MODULE_INIT,
521         "Kerberos",
522         RLM_TYPE_THREAD_SAFE | RLM_TYPE_CHECK_CONFIG_SAFE | RLM_TYPE_HUP_SAFE,
523         krb5_instantiate,               /* instantiation */
524         krb5_detach,                    /* detach */
525         {
526                 krb5_auth,              /* authenticate */
527                 NULL,                   /* authorize */
528                 NULL,                   /* pre-accounting */
529                 NULL,                   /* accounting */
530                 NULL,                   /* checksimul */
531                 NULL,                   /* pre-proxy */
532                 NULL,                   /* post-proxy */
533                 NULL                    /* post-auth */
534         },
535 };