More translations.
[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%s", data->cname, data->username, (data->creds == NULL)?"":" (with fwd creds)");
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             /* Copy only the first credential. (Change this if it becomes a problem) */
381             ret = krb5_copy_creds(k5context, *fwdcreds, &data->creds);
382             krb5_free_tgt_creds(k5context, fwdcreds);
383             if(ret != 0)
384             {
385                 flog(LOG_ERR, "could not copy forwarded credentials: %s", error_message(ret));
386                 return(AUTH_ERR);
387             }
388             if(confgetint("auth-krb5", "renewcreds"))
389             {
390                 data->renew = 1;
391                 setrenew(data);
392             }
393             if(auth->text != NULL)
394                 free(auth->text);
395             auth->text = swcsdup(L"");
396             data->state = 2;
397             return(AUTH_PASS);
398         default:
399             free(msg);
400             flog(LOG_ERR, "BUG? Invalid state encountered in krbauth: %i", data->state);
401             return(AUTH_ERR);
402         }
403     }
404 }
405
406 static int opensess(struct authhandle *auth)
407 {
408     int ret;
409     struct krb5data *data;
410     char *buf, *buf2;
411     int fd;
412     struct passwd *pwent;
413     
414     data = auth->mechdata;
415     if(data->creds != NULL)
416     {
417         if((pwent = getpwnam(data->username)) == NULL)
418         {
419             flog(LOG_ERR, "could not get passwd entry for forwarded tickets (user %s): %s", data->username, strerror(errno));
420             return(AUTH_ERR);
421         }
422         if(confgetint("auth-krb5", "usedefcc"))
423         {
424             buf = sprintf2("/tmp/krb5cc_dc_%i_XXXXXX", pwent->pw_uid);
425             if((fd = mkstemp(buf)) < 0)
426             {
427                 free(buf);
428                 flog(LOG_ERR, "could not create temporary file for ccache: %s", strerror(errno));
429                 return(AUTH_ERR);
430             }
431             close(fd);
432             buf2 = sprintf2("FILE:%s", buf);
433             if((ret = krb5_cc_resolve(k5context, buf2, &data->ccache)) != 0)
434             {
435                 free(buf);
436                 free(buf2);
437                 flog(LOG_ERR, "could not resolve ccache name \"%s\": %s", buf2, error_message(ret));
438                 return(AUTH_ERR);
439             }
440             setenv("KRB5CCNAME", buf2, 1);
441             free(buf2);
442             if((ret = krb5_cc_initialize(k5context, data->ccache, data->ticket->enc_part2->client)) != 0)
443             {
444                 free(buf);
445                 flog(LOG_ERR, "could not initialize ccache: %s", error_message(ret));
446                 return(AUTH_ERR);
447             }
448             if((ret = krb5_cc_store_cred(k5context, data->ccache, data->creds)) != 0)
449             {
450                 free(buf);
451                 flog(LOG_ERR, "could not store forwarded TGT into ccache: %s", error_message(ret));
452                 return(AUTH_ERR);
453             }
454             if(chown(buf, pwent->pw_uid, pwent->pw_gid))
455             {
456                 free(buf);
457                 flog(LOG_ERR, "could not chown new ccache to %i:%i: %s", pwent->pw_uid, pwent->pw_gid, strerror(errno));
458                 return(AUTH_ERR);
459             }
460             free(buf);
461         } else {
462             if((buf = (char *)krb5_cc_default_name(k5context)) == NULL) {
463                 flog(LOG_ERR, "could not get default ccache name");
464                 return(AUTH_ERR);
465             }
466             if((ret = krb5_cc_resolve(k5context, buf, &data->ccache)) != 0)
467             {
468                 flog(LOG_ERR, "could not resolve ccache name \"%s\": %s", buf2, error_message(ret));
469                 return(AUTH_ERR);
470             }
471             setenv("KRB5CCNAME", buf, 1);
472             if((ret = krb5_cc_initialize(k5context, data->ccache, data->ticket->enc_part2->client)) != 0)
473             {
474                 flog(LOG_ERR, "could not initialize ccache: %s", error_message(ret));
475                 return(AUTH_ERR);
476             }
477             if((ret = krb5_cc_store_cred(k5context, data->ccache, data->creds)) != 0)
478             {
479                 flog(LOG_ERR, "could not store forwarded TGT into ccache: %s", error_message(ret));
480                 return(AUTH_ERR);
481             }
482         }
483     }
484     return(AUTH_SUCCESS);
485 }
486
487 static int closesess(struct authhandle *auth)
488 {
489     struct krb5data *data;
490     
491     data = auth->mechdata;
492     if(data->ccache != NULL)
493     {
494         krb5_cc_destroy(k5context, data->ccache);
495         data->ccache = NULL;
496     }
497     return(AUTH_SUCCESS);
498 }
499
500 struct authmech authmech_krb5 =
501 {
502     .inithandle = inithandle,
503     .release = release,
504     .authenticate = krbauth,
505     .opensess = opensess,
506     .closesess = closesess,
507     .name = L"krb5",
508     .enabled = 1
509 };
510
511 static int init(int hup)
512 {
513     int ret;
514     char *buf;
515     krb5_principal newprinc;
516     
517     if(!hup)
518     {
519         regmech(&authmech_krb5);
520         if((ret = krb5_init_context(&k5context)))
521         {
522             flog(LOG_CRIT, "could not initialize Kerberos context: %s", error_message(ret));
523             return(1);
524         }
525         if((buf = icwcstombs(confgetstr("auth-krb5", "service"), NULL)) == NULL)
526         {
527             flog(LOG_CRIT, "could not convert service name (%ls) into local charset: %s", confgetstr("auth-krb5", "service"), strerror(errno));
528             return(1);
529         } else {
530             if((ret = krb5_sname_to_principal(k5context, NULL, buf, KRB5_NT_SRV_HST, &myprinc)) != 0)
531             {
532                 flog(LOG_CRIT, "could not get principal for service %s: %s", buf, error_message(ret));
533                 free(buf);
534                 return(1);
535             }
536             free(buf);
537         }
538         if((buf = icwcstombs(confgetstr("auth-krb5", "keytab"), NULL)) == NULL)
539         {
540             flog(LOG_ERR, "could not convert keytab name (%ls) into local charset: %s, using default keytab instead", confgetstr("auth-krb5", "keytab"), strerror(errno));
541             keytab = NULL;
542         } else {
543             if((ret = krb5_kt_resolve(k5context, buf, &keytab)) != 0)
544             {
545                 flog(LOG_ERR, "could not open keytab %s: %s, using default keytab instead", buf, error_message(ret));
546                 keytab = NULL;
547             }
548             free(buf);
549         }
550     }
551     if(hup)
552     {
553         if((buf = icwcstombs(confgetstr("auth-krb5", "service"), NULL)) == NULL)
554         {
555             flog(LOG_CRIT, "could not convert service name (%ls) into local charset: %s, not updating principal", confgetstr("auth-krb5", "service"), strerror(errno));
556         } else {
557             if((ret = krb5_sname_to_principal(k5context, NULL, buf, KRB5_NT_SRV_HST, &newprinc)) != 0)
558             {
559                 flog(LOG_CRIT, "could not get principal for service %s: %s, not updating principal", buf, error_message(ret));
560             } else {
561                 krb5_free_principal(k5context, myprinc);
562                 myprinc = newprinc;
563             }
564             free(buf);
565         }
566         if(keytab != NULL)
567             krb5_kt_close(k5context, keytab);
568         if((buf = icwcstombs(confgetstr("auth-krb5", "keytab"), NULL)) == NULL)
569         {
570             flog(LOG_ERR, "could not convert keytab name (%ls) into local charset: %s, using default keytab instead", confgetstr("auth-krb5", "keytab"), strerror(errno));
571             keytab = NULL;
572         } else {
573             if((ret = krb5_kt_resolve(k5context, buf, &keytab)) != 0)
574             {
575                 flog(LOG_ERR, "could not open keytab %s: %s, using default keytab instead", buf, error_message(ret));
576                 keytab = NULL;
577             }
578             free(buf);
579         }
580     }
581     return(0);
582 }
583
584 static void terminate(void)
585 {
586     if(keytab != NULL)
587         krb5_kt_close(k5context, keytab);
588     krb5_free_principal(k5context, myprinc);
589     krb5_free_context(k5context);
590 }
591
592 static struct configvar myvars[] =
593 {
594     {CONF_VAR_STRING, "service", {.str = L"doldacond"}},
595     {CONF_VAR_STRING, "keytab", {.str = L""}},
596     {CONF_VAR_BOOL, "renewcreds", {.num = 1}},
597     {CONF_VAR_BOOL, "usedefcc", {.num = 0}},
598     {CONF_VAR_END}
599 };
600
601 static struct module me =
602 {
603     .conf =
604     {
605         .vars = myvars
606     },
607     .init = init,
608     .terminate = terminate,
609     .name = "auth-krb5"
610 };
611
612 MODULE(me);
613
614 #endif /* HAVE_KRB5 */