Transfer from CVS at SourceForge
[doldaconnect.git] / daemon / auth-krb5.c
1 /*
2  *  Dolda Connect - Modular multiuser Direct Connect-style client
3  *  Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com)
4  *  
5  *  This program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2 of the License, or
8  *  (at your option) any later version.
9  *  
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *  
15  *  You should have received a copy of the GNU General Public License
16  *  along with this program; if not, write to the Free Software
17  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 */
19
20 #include <stdlib.h>
21 #include <unistd.h>
22 #include <string.h>
23 #include <errno.h>
24 #include <pwd.h>
25 #include <time.h>
26 #include <sys/stat.h>
27 #include <sys/param.h>
28
29 #ifdef HAVE_CONFIG_H
30 #include <config.h>
31 #endif
32 #include "auth.h"
33 #include "utils.h"
34 #include "conf.h"
35 #include "log.h"
36 #include "module.h"
37 #include "sysevents.h"
38
39 #ifdef HAVE_KRB5
40
41 #include <krb5.h>
42 #include <com_err.h>
43
44 struct krb5data
45 {
46     int state;
47     krb5_auth_context context;
48     krb5_ticket *ticket;
49     krb5_creds *creds;
50     krb5_ccache ccache;
51     int renew;
52     struct timer *renewtimer;
53     char *username, *cname;
54 };
55
56 static void setrenew(struct krb5data *data);
57
58 static krb5_context k5context;
59 static krb5_principal myprinc;
60 static krb5_keytab keytab;
61
62 static void releasekrb5(struct krb5data *data)
63 {
64     if(data->renewtimer != NULL)
65         canceltimer(data->renewtimer);
66     if(data->context != NULL)
67         krb5_auth_con_free(k5context, data->context);
68     if(data->ticket != NULL)
69         krb5_free_ticket(k5context, data->ticket);
70     if(data->creds != NULL)
71         krb5_free_creds(k5context, data->creds);
72     if(data->username != NULL)
73         free(data->username);
74     if(data->cname != NULL)
75         free(data->cname);
76     free(data);
77 }
78
79 static void release(struct authhandle *auth)
80 {
81     releasekrb5((struct krb5data *)auth->mechdata);
82 }
83
84 static struct krb5data *newkrb5data(void)
85 {
86     struct krb5data *new;
87     
88     new = smalloc(sizeof(*new));
89     memset(new, 0, sizeof(*new));
90     return(new);
91 }
92
93 static int inithandle(struct authhandle *auth, char *username)
94 {
95     int ret;
96     struct krb5data *data;
97     
98     data = newkrb5data();
99     if((ret = krb5_auth_con_init(k5context, &data->context)) != 0)
100     {
101         flog(LOG_ERR, "could initialize Kerberos auth context: %s", error_message(ret));
102         releasekrb5(data);
103         return(1);
104     }
105     krb5_auth_con_setflags(k5context, data->context, KRB5_AUTH_CONTEXT_DO_SEQUENCE);
106     data->username = sstrdup(username);
107     data->state = 0;
108     auth->mechdata = data;
109     return(0);
110 }
111
112 /* Copied from MIT Kerberos 5 1.3.3*/
113 static krb5_boolean my_krb5_kuserok(krb5_context context, krb5_principal principal, const char *luser, const char *loginfile, int authbydef)
114 {
115     struct stat sbuf;
116     struct passwd *pwd;
117     char pbuf[MAXPATHLEN];
118     krb5_boolean isok = FALSE;
119     FILE *fp;
120     char kuser[65];
121     char *princname;
122     char linebuf[BUFSIZ];
123     char *newline;
124     int gobble;
125
126     /* no account => no access */
127     if ((pwd = getpwnam(luser)) == NULL) {
128         return(FALSE);
129     }
130     (void) strncpy(pbuf, pwd->pw_dir, sizeof(pbuf) - 1);
131     pbuf[sizeof(pbuf) - 1] = '\0';
132     (void) strncat(pbuf, loginfile, sizeof(pbuf) - 1 - strlen(pbuf));
133
134     if (access(pbuf, F_OK)) {    /* not accessible */
135         /*
136          * if he's trying to log in as himself, and there is no .k5login file,
137          * let him.  To find out, call
138          * krb5_aname_to_localname to convert the principal to a name
139          * which we can string compare. 
140          */
141         if (authbydef) {
142             if (!(krb5_aname_to_localname(context, principal,
143                                           sizeof(kuser), kuser))
144                 && (strcmp(kuser, luser) == 0)) {
145                 return(TRUE);
146             }
147         } else {
148             return(FALSE);
149         }
150     }
151     if (krb5_unparse_name(context, principal, &princname))
152         return(FALSE);                  /* no hope of matching */
153
154     /* open ~/.k5login */
155     if ((fp = fopen(pbuf, "r")) == NULL) {
156         free(princname);
157         return(FALSE);
158     }
159     /*
160      * For security reasons, the .k5login file must be owned either by
161      * the user himself, or by root.  Otherwise, don't grant access.
162      */
163     if (fstat(fileno(fp), &sbuf)) {
164         fclose(fp);
165         free(princname);
166         return(FALSE);
167     }
168     if ((sbuf.st_uid != pwd->pw_uid) && sbuf.st_uid) {
169         fclose(fp);
170         free(princname);
171         return(FALSE);
172     }
173
174     /* check each line */
175     while (!isok && (fgets(linebuf, BUFSIZ, fp) != NULL)) {
176         /* null-terminate the input string */
177         linebuf[BUFSIZ-1] = '\0';
178         newline = NULL;
179         /* nuke the newline if it exists */
180         if ((newline = strchr(linebuf, '\n')))
181             *newline = '\0';
182         if (!strcmp(linebuf, princname)) {
183             isok = TRUE;
184             continue;
185         }
186         /* clean up the rest of the line if necessary */
187         if (!newline)
188             while (((gobble = getc(fp)) != EOF) && gobble != '\n');
189     }
190     free(princname);
191     fclose(fp);
192     return(isok);
193 }
194
195 static void renewcreds(int cancelled, struct krb5data *data)
196 {
197     int ret;
198     char ccnambuf[50];
199     krb5_ccache tmpcc;
200     krb5_creds newcreds;
201     static int ccserial = 0;
202     
203     data->renewtimer = NULL;
204     if(cancelled)
205         return;
206     memset(&newcreds, 0, sizeof(newcreds));
207     snprintf(ccnambuf, sizeof(ccnambuf), "MEMORY:%i", ccserial++);
208     if((ret = krb5_cc_resolve(k5context, ccnambuf, &tmpcc)) != 0)
209     {
210         flog(LOG_ERR, "could not resolve a temporary ccache `%s': %s", ccnambuf, error_message(ret));
211         data->renew = 0;
212         return;
213     }
214     if((ret = krb5_cc_initialize(k5context, tmpcc, data->ticket->enc_part2->client)) != 0)
215     {
216         flog(LOG_ERR, "could not initialize temporary ccache: %s", error_message(ret));
217         krb5_cc_destroy(k5context, tmpcc);
218         data->renew = 0;
219         return;
220     }
221     if((ret = krb5_cc_store_cred(k5context, tmpcc, data->creds)) != 0)
222     {
223         flog(LOG_ERR, "could not store creds into temporary ccache: %s", error_message(ret));
224         krb5_cc_destroy(k5context, tmpcc);
225         data->renew = 0;
226         return;
227     }
228     if((ret = krb5_get_renewed_creds(k5context, &newcreds, data->ticket->enc_part2->client, tmpcc, NULL)) != 0)
229     {
230         flog(LOG_ERR, "could not get renewed tickets for %s: %s", data->username, error_message(ret));
231         krb5_cc_destroy(k5context, tmpcc);
232         data->renew = 0;
233         return;
234     }
235     krb5_free_creds(k5context, data->creds);
236     data->creds = NULL;
237     if((ret = krb5_copy_creds(k5context, &newcreds, &data->creds)) != 0)
238     {
239         flog(LOG_ERR, "could not copy renewed creds: %s", error_message(ret));
240         krb5_cc_destroy(k5context, tmpcc);
241         data->renew = 0;
242         return;
243     }
244     krb5_free_cred_contents(k5context, &newcreds);
245     krb5_cc_destroy(k5context, tmpcc);
246     flog(LOG_ERR, "successfully renewed krb5 creds for %s", data->username);
247     setrenew(data);
248 }
249
250 static void setrenew(struct krb5data *data)
251 {
252     krb5_ticket_times times;
253     time_t now, good;
254     
255     times = data->creds->times;
256     if(!times.starttime)
257         times.starttime = times.authtime;
258     now = time(NULL);
259     if(times.endtime < now)
260     {
261         flog(LOG_DEBUG, "tickets already expired, cannot renew");
262         data->renew = 0;
263         return;
264     }
265     good = times.starttime + (((times.endtime - times.starttime) * 9) / 10);
266     data->renewtimer = timercallback(good, (void (*)(int, void *))renewcreds, data);
267 }
268
269 static int krbauth(struct authhandle *auth, char *passdata)
270 {
271     int ret;
272     struct krb5data *data;
273     char *msg;
274     size_t msglen;
275     int authorized;
276     krb5_data k5d;
277     krb5_flags apopt;
278     krb5_creds **fwdcreds;
279     
280     data = auth->mechdata;
281     if(passdata == NULL)
282     {
283         auth->prompt = AUTH_PR_AUTO;
284         if(auth->text != NULL)
285             free(auth->text);
286         auth->text = swcsdup(L"Send hex-encoded krb5 data");
287         data->state = 1;
288         return(AUTH_PASS);
289     } else {
290         if((msg = hexdecode(passdata, &msglen)) == NULL)
291         {
292             if(auth->text != NULL)
293                 free(auth->text);
294             auth->text = swcsdup(L"Invalid hex encoding");
295             return(AUTH_DENIED);
296         }
297         switch(data->state)
298         {
299         case 1:
300             k5d.length = msglen;
301             k5d.data = msg;
302             if((ret = krb5_rd_req(k5context, &data->context, &k5d, myprinc, keytab, &apopt, &data->ticket)) != 0)
303             {
304                 flog(LOG_INFO, "kerberos authentication failed for %s: %s", data->username, error_message(ret));
305                 if(auth->text != NULL)
306                     free(auth->text);
307                 auth->text = icmbstowcs((char *)error_message(ret), NULL);
308                 return(AUTH_DENIED);
309             }
310             free(msg);
311             if(apopt & AP_OPTS_MUTUAL_REQUIRED)
312             {
313                 if((ret = krb5_mk_rep(k5context, data->context, &k5d)) != 0)
314                 {
315                     flog(LOG_WARNING, "krb5_mk_rep returned an error: %s", error_message(ret));
316                     return(AUTH_ERR);
317                 }
318                 msg = hexencode(k5d.data, k5d.length);
319                 if(auth->text != NULL)
320                     free(auth->text);
321                 auth->text = icmbstowcs(msg, "us-ascii");
322                 free(msg);
323                 free(k5d.data);
324             } else {
325                 if(auth->text != NULL)
326                     free(auth->text);
327                 auth->text = swcsdup(L"");
328             }
329             data->state = 2;
330             return(AUTH_PASS);
331         case 2:
332             ret = atoi(msg);
333             free(msg);
334             if(ret == 1)
335             {
336                 /* That is, the client has accepted us as a valid
337                  * server.  Now check if the client is authorized. */
338                 if((ret = krb5_unparse_name(k5context, data->ticket->enc_part2->client, &data->cname)) != 0)
339                 {
340                     flog(LOG_ERR, "krb_unparse_name returned an error: %s", error_message(ret));
341                     return(AUTH_ERR);
342                 }
343                 authorized = 0;
344                 if(!authorized && my_krb5_kuserok(k5context, data->ticket->enc_part2->client, data->username, "/.k5login", 1))
345                     authorized = 1;
346                 /* Allow a seperate ACL for DC principals */
347                 if(!authorized && my_krb5_kuserok(k5context, data->ticket->enc_part2->client, data->username, "/.dc-k5login", 0))
348                     authorized = 1;
349                 if(authorized)
350                 {
351                     flog(LOG_INFO, "krb5 principal %s successfully authorized as %s", data->cname, data->username);
352                     return(AUTH_SUCCESS);
353                 } else {
354                     flog(LOG_INFO, "krb5 principal %s not authorized as %s", data->cname, data->username);
355                 }
356             }
357             if(ret == 2)
358             {
359                 if(auth->text != NULL)
360                     free(auth->text);
361                 auth->text = swcsdup(L"");
362                 data->state = 3;
363                 return(AUTH_PASS);
364             }
365             return(AUTH_DENIED);
366         case 3:
367             k5d.length = msglen;
368             k5d.data = msg;
369             if((ret = krb5_rd_cred(k5context, data->context, &k5d, &fwdcreds, NULL)) != 0)
370             {
371                 flog(LOG_ERR, "krb5_rd_cred returned an error: %s", error_message(ret));
372                 return(AUTH_ERR);
373             }
374             if(*fwdcreds == NULL)
375             {
376                 flog(LOG_ERR, "forwarded credentials array was empty (from %s)", data->username);
377                 krb5_free_tgt_creds(k5context, fwdcreds);
378                 return(AUTH_ERR);
379             }
380             flog(LOG_INFO, "received forwarded credentials for %s", data->username);
381             /* Copy only the first credential. (Change this if it becomes a problem) */
382             ret = krb5_copy_creds(k5context, *fwdcreds, &data->creds);
383             krb5_free_tgt_creds(k5context, fwdcreds);
384             if(ret != 0)
385             {
386                 flog(LOG_ERR, "could not copy forwarded credentials: %s", error_message(ret));
387                 return(AUTH_ERR);
388             }
389             if(confgetint("auth-krb5", "renewcreds"))
390             {
391                 data->renew = 1;
392                 setrenew(data);
393             }
394             if(auth->text != NULL)
395                 free(auth->text);
396             auth->text = swcsdup(L"");
397             data->state = 2;
398             return(AUTH_PASS);
399         default:
400             free(msg);
401             flog(LOG_ERR, "BUG? Invalid state encountered in krbauth: %i", data->state);
402             return(AUTH_ERR);
403         }
404     }
405 }
406
407 static int opensess(struct authhandle *auth)
408 {
409     int ret;
410     struct krb5data *data;
411     char *buf, *buf2;
412     int fd;
413     struct passwd *pwent;
414     
415     data = auth->mechdata;
416     if(data->creds != NULL)
417     {
418         if((pwent = getpwnam(data->username)) == NULL)
419         {
420             flog(LOG_ERR, "could not get passwd entry for forwarded tickets (user %s): %s", data->username, strerror(errno));
421             return(AUTH_ERR);
422         }
423         buf = sprintf2("/tmp/krb5cc_dc_%i_XXXXXX", pwent->pw_uid);
424         if((fd = mkstemp(buf)) < 0)
425         {
426             free(buf);
427             flog(LOG_ERR, "could not create temporary file for ccache: %s", strerror(errno));
428             return(AUTH_ERR);
429         }
430         close(fd);
431         buf2 = sprintf2("FILE:%s", buf);
432         if((ret = krb5_cc_resolve(k5context, buf2, &data->ccache)) != 0)
433         {
434             free(buf);
435             free(buf2);
436             flog(LOG_ERR, "could not resolve ccache name \"%s\": %s", buf2, error_message(ret));
437             return(AUTH_ERR);
438         }
439         setenv("KRB5CCNAME", buf2, 1);
440         free(buf2);
441         if((ret = krb5_cc_initialize(k5context, data->ccache, data->ticket->enc_part2->client)) != 0)
442         {
443             free(buf);
444             flog(LOG_ERR, "could not initialize ccache: %s", error_message(ret));
445             return(AUTH_ERR);
446         }
447         if((ret = krb5_cc_store_cred(k5context, data->ccache, data->creds)) != 0)
448         {
449             free(buf);
450             flog(LOG_ERR, "could not store forwarded TGT into ccache: %s", error_message(ret));
451             return(AUTH_ERR);
452         }
453         if(chown(buf, pwent->pw_uid, pwent->pw_gid))
454         {
455             free(buf);
456             flog(LOG_ERR, "could not chown new ccache to %i:%i: %s", pwent->pw_uid, pwent->pw_gid, strerror(errno));
457             return(AUTH_ERR);
458         }
459         free(buf);
460     }
461     return(AUTH_SUCCESS);
462 }
463
464 static int closesess(struct authhandle *auth)
465 {
466     struct krb5data *data;
467     
468     data = auth->mechdata;
469     if(data->ccache != NULL)
470     {
471         krb5_cc_destroy(k5context, data->ccache);
472         data->ccache = NULL;
473     }
474     return(AUTH_SUCCESS);
475 }
476
477 struct authmech authmech_krb5 =
478 {
479     .inithandle = inithandle,
480     .release = release,
481     .authenticate = krbauth,
482     .opensess = opensess,
483     .closesess = closesess,
484     .name = L"krb5",
485     .enabled = 1
486 };
487
488 static int init(int hup)
489 {
490     int ret;
491     char *buf;
492     krb5_principal newprinc;
493     
494     if(!hup)
495     {
496         regmech(&authmech_krb5);
497         if((ret = krb5_init_context(&k5context)))
498         {
499             flog(LOG_CRIT, "could not initialize Kerberos context: %s", error_message(ret));
500             return(1);
501         }
502         if((buf = icwcstombs(confgetstr("auth-krb5", "service"), NULL)) == NULL)
503         {
504             flog(LOG_CRIT, "could not convert service name (%ls) into local charset: %s", confgetstr("auth-krb5", "service"), strerror(errno));
505             return(1);
506         } else {
507             if((ret = krb5_sname_to_principal(k5context, NULL, buf, KRB5_NT_SRV_HST, &myprinc)) != 0)
508             {
509                 flog(LOG_CRIT, "could not get principal for service %s: %s", buf, error_message(ret));
510                 free(buf);
511                 return(1);
512             }
513             free(buf);
514         }
515         if((buf = icwcstombs(confgetstr("auth-krb5", "keytab"), NULL)) == NULL)
516         {
517             flog(LOG_ERR, "could not convert keytab name (%ls) into local charset: %s, using default keytab instead", confgetstr("auth-krb5", "keytab"), strerror(errno));
518             keytab = NULL;
519         } else {
520             if((ret = krb5_kt_resolve(k5context, buf, &keytab)) != 0)
521             {
522                 flog(LOG_ERR, "could not open keytab %s: %s, using default keytab instead", buf, error_message(ret));
523                 keytab = NULL;
524             }
525             free(buf);
526         }
527     }
528     if(hup)
529     {
530         if((buf = icwcstombs(confgetstr("auth-krb5", "service"), NULL)) == NULL)
531         {
532             flog(LOG_CRIT, "could not convert service name (%ls) into local charset: %s, not updating principal", confgetstr("auth-krb5", "service"), strerror(errno));
533         } else {
534             if((ret = krb5_sname_to_principal(k5context, NULL, buf, KRB5_NT_SRV_HST, &newprinc)) != 0)
535             {
536                 flog(LOG_CRIT, "could not get principal for service %s: %s, not updating principal", buf, error_message(ret));
537             } else {
538                 krb5_free_principal(k5context, myprinc);
539                 myprinc = newprinc;
540             }
541             free(buf);
542         }
543         if(keytab != NULL)
544             krb5_kt_close(k5context, keytab);
545         if((buf = icwcstombs(confgetstr("auth-krb5", "keytab"), NULL)) == NULL)
546         {
547             flog(LOG_ERR, "could not convert keytab name (%ls) into local charset: %s, using default keytab instead", confgetstr("auth-krb5", "keytab"), strerror(errno));
548             keytab = NULL;
549         } else {
550             if((ret = krb5_kt_resolve(k5context, buf, &keytab)) != 0)
551             {
552                 flog(LOG_ERR, "could not open keytab %s: %s, using default keytab instead", buf, error_message(ret));
553                 keytab = NULL;
554             }
555             free(buf);
556         }
557     }
558     return(0);
559 }
560
561 static void terminate(void)
562 {
563     if(keytab != NULL)
564         krb5_kt_close(k5context, keytab);
565     krb5_free_principal(k5context, myprinc);
566     krb5_free_context(k5context);
567 }
568
569 static struct configvar myvars[] =
570 {
571     {CONF_VAR_STRING, "service", {.str = L"doldacond"}},
572     {CONF_VAR_STRING, "keytab", {.str = L""}},
573     {CONF_VAR_BOOL, "renewcreds", {.num = 1}},
574     {CONF_VAR_END}
575 };
576
577 static struct module me =
578 {
579     .conf =
580     {
581         .vars = myvars
582     },
583     .init = init,
584     .terminate = terminate,
585     .name = "auth-krb5"
586 };
587
588 MODULE(me);
589
590 #endif /* HAVE_KRB5 */