Fixed HTTP-client query-string handling bug.
[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, struct socket *sk, 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                 free(msg);
309                 return(AUTH_DENIED);
310             }
311             free(msg);
312             if(apopt & AP_OPTS_MUTUAL_REQUIRED)
313             {
314                 if((ret = krb5_mk_rep(k5context, data->context, &k5d)) != 0)
315                 {
316                     flog(LOG_WARNING, "krb5_mk_rep returned an error: %s", error_message(ret));
317                     return(AUTH_ERR);
318                 }
319                 msg = hexencode(k5d.data, k5d.length);
320                 if(auth->text != NULL)
321                     free(auth->text);
322                 auth->text = icmbstowcs(msg, "us-ascii");
323                 free(msg);
324                 free(k5d.data);
325             } else {
326                 if(auth->text != NULL)
327                     free(auth->text);
328                 auth->text = swcsdup(L"");
329             }
330             data->state = 2;
331             return(AUTH_PASS);
332         case 2:
333             ret = atoi(msg);
334             free(msg);
335             if(ret == 1)
336             {
337                 /* That is, the client has accepted us as a valid
338                  * server.  Now check if the client is authorized. */
339                 if((ret = krb5_unparse_name(k5context, data->ticket->enc_part2->client, &data->cname)) != 0)
340                 {
341                     flog(LOG_ERR, "krb_unparse_name returned an error: %s", error_message(ret));
342                     return(AUTH_ERR);
343                 }
344                 authorized = 0;
345                 if(!authorized && my_krb5_kuserok(k5context, data->ticket->enc_part2->client, data->username, "/.k5login", 1))
346                     authorized = 1;
347                 /* Allow a seperate ACL for DC principals */
348                 if(!authorized && my_krb5_kuserok(k5context, data->ticket->enc_part2->client, data->username, "/.dc-k5login", 0))
349                     authorized = 1;
350                 if(authorized)
351                 {
352                     flog(LOG_INFO, "krb5 principal %s successfully authorized as %s%s", data->cname, data->username, (data->creds == NULL)?"":" (with fwd creds)");
353                     return(AUTH_SUCCESS);
354                 } else {
355                     flog(LOG_INFO, "krb5 principal %s not authorized as %s", data->cname, data->username);
356                 }
357             }
358             if(ret == 2)
359             {
360                 if(auth->text != NULL)
361                     free(auth->text);
362                 auth->text = swcsdup(L"");
363                 data->state = 3;
364                 return(AUTH_PASS);
365             }
366             return(AUTH_DENIED);
367         case 3:
368             k5d.length = msglen;
369             k5d.data = msg;
370             if((ret = krb5_rd_cred(k5context, data->context, &k5d, &fwdcreds, NULL)) != 0)
371             {
372                 flog(LOG_ERR, "krb5_rd_cred returned an error: %s", error_message(ret));
373                 free(msg);
374                 return(AUTH_ERR);
375             }
376             free(msg);
377             if(*fwdcreds == NULL)
378             {
379                 flog(LOG_ERR, "forwarded credentials array was empty (from %s)", data->username);
380                 krb5_free_tgt_creds(k5context, fwdcreds);
381                 return(AUTH_ERR);
382             }
383             /* Copy only the first credential. (Change this if it becomes a problem) */
384             ret = krb5_copy_creds(k5context, *fwdcreds, &data->creds);
385             krb5_free_tgt_creds(k5context, fwdcreds);
386             if(ret != 0)
387             {
388                 flog(LOG_ERR, "could not copy forwarded credentials: %s", error_message(ret));
389                 return(AUTH_ERR);
390             }
391             if(confgetint("auth-krb5", "renewcreds"))
392             {
393                 data->renew = 1;
394                 setrenew(data);
395             }
396             if(auth->text != NULL)
397                 free(auth->text);
398             auth->text = swcsdup(L"");
399             data->state = 2;
400             return(AUTH_PASS);
401         default:
402             free(msg);
403             flog(LOG_ERR, "BUG? Invalid state encountered in krbauth: %i", data->state);
404             return(AUTH_ERR);
405         }
406     }
407 }
408
409 static int opensess(struct authhandle *auth)
410 {
411     int ret;
412     struct krb5data *data;
413     char *buf, *buf2;
414     int fd;
415     struct passwd *pwent;
416     
417     data = auth->mechdata;
418     if(data->creds != NULL)
419     {
420         if((pwent = getpwnam(data->username)) == NULL)
421         {
422             flog(LOG_ERR, "could not get passwd entry for forwarded tickets (user %s): %s", data->username, strerror(errno));
423             return(AUTH_ERR);
424         }
425         if(!confgetint("auth-krb5", "usedefcc"))
426         {
427             buf = sprintf2("/tmp/krb5cc_dc_%i_XXXXXX", pwent->pw_uid);
428             if((fd = mkstemp(buf)) < 0)
429             {
430                 free(buf);
431                 flog(LOG_ERR, "could not create temporary file for ccache: %s", strerror(errno));
432                 return(AUTH_ERR);
433             }
434             close(fd);
435             buf2 = sprintf2("FILE:%s", buf);
436             if((ret = krb5_cc_resolve(k5context, buf2, &data->ccache)) != 0)
437             {
438                 free(buf);
439                 free(buf2);
440                 flog(LOG_ERR, "could not resolve ccache name \"%s\": %s", buf2, error_message(ret));
441                 return(AUTH_ERR);
442             }
443             setenv("KRB5CCNAME", buf2, 1);
444             free(buf2);
445             if((ret = krb5_cc_initialize(k5context, data->ccache, data->ticket->enc_part2->client)) != 0)
446             {
447                 free(buf);
448                 flog(LOG_ERR, "could not initialize ccache: %s", error_message(ret));
449                 return(AUTH_ERR);
450             }
451             if((ret = krb5_cc_store_cred(k5context, data->ccache, data->creds)) != 0)
452             {
453                 free(buf);
454                 flog(LOG_ERR, "could not store forwarded TGT into ccache: %s", error_message(ret));
455                 return(AUTH_ERR);
456             }
457             if(chown(buf, pwent->pw_uid, pwent->pw_gid))
458             {
459                 free(buf);
460                 flog(LOG_ERR, "could not chown new ccache to %i:%i: %s", pwent->pw_uid, pwent->pw_gid, strerror(errno));
461                 return(AUTH_ERR);
462             }
463             free(buf);
464         } else {
465             if((buf = (char *)krb5_cc_default_name(k5context)) == NULL) {
466                 flog(LOG_ERR, "could not get default ccache name");
467                 return(AUTH_ERR);
468             }
469             if((ret = krb5_cc_resolve(k5context, buf, &data->ccache)) != 0)
470             {
471                 flog(LOG_ERR, "could not resolve ccache name \"%s\": %s", buf, error_message(ret));
472                 return(AUTH_ERR);
473             }
474             setenv("KRB5CCNAME", buf, 1);
475             if((ret = krb5_cc_initialize(k5context, data->ccache, data->ticket->enc_part2->client)) != 0)
476             {
477                 flog(LOG_ERR, "could not initialize ccache: %s", error_message(ret));
478                 return(AUTH_ERR);
479             }
480             if((ret = krb5_cc_store_cred(k5context, data->ccache, data->creds)) != 0)
481             {
482                 flog(LOG_ERR, "could not store forwarded TGT into ccache: %s", error_message(ret));
483                 return(AUTH_ERR);
484             }
485         }
486     }
487     return(AUTH_SUCCESS);
488 }
489
490 static int closesess(struct authhandle *auth)
491 {
492     struct krb5data *data;
493     
494     data = auth->mechdata;
495     if(data->ccache != NULL)
496     {
497         krb5_cc_destroy(k5context, data->ccache);
498         data->ccache = NULL;
499     }
500     return(AUTH_SUCCESS);
501 }
502
503 struct authmech authmech_krb5 =
504 {
505     .inithandle = inithandle,
506     .release = release,
507     .authenticate = krbauth,
508     .opensess = opensess,
509     .closesess = closesess,
510     .name = L"krb5",
511     .enabled = 1
512 };
513
514 static int init(int hup)
515 {
516     int ret;
517     char *buf;
518     krb5_principal newprinc;
519     
520     if(!hup)
521     {
522         regmech(&authmech_krb5);
523         if((ret = krb5_init_context(&k5context)))
524         {
525             flog(LOG_CRIT, "could not initialize Kerberos context: %s", error_message(ret));
526             return(1);
527         }
528         if((buf = icwcstombs(confgetstr("auth-krb5", "service"), NULL)) == NULL)
529         {
530             flog(LOG_CRIT, "could not convert service name (%ls) into local charset: %s", confgetstr("auth-krb5", "service"), strerror(errno));
531             return(1);
532         } else {
533             if((ret = krb5_sname_to_principal(k5context, NULL, buf, KRB5_NT_SRV_HST, &myprinc)) != 0)
534             {
535                 flog(LOG_CRIT, "could not get principal for service %s: %s", buf, error_message(ret));
536                 free(buf);
537                 return(1);
538             }
539             free(buf);
540         }
541         if((buf = icwcstombs(confgetstr("auth-krb5", "keytab"), NULL)) == NULL)
542         {
543             flog(LOG_ERR, "could not convert keytab name (%ls) into local charset: %s, using default keytab instead", confgetstr("auth-krb5", "keytab"), strerror(errno));
544             keytab = NULL;
545         } else {
546             if((ret = krb5_kt_resolve(k5context, buf, &keytab)) != 0)
547             {
548                 flog(LOG_ERR, "could not open keytab %s: %s, using default keytab instead", buf, error_message(ret));
549                 keytab = NULL;
550             }
551             free(buf);
552         }
553     }
554     if(hup)
555     {
556         if((buf = icwcstombs(confgetstr("auth-krb5", "service"), NULL)) == NULL)
557         {
558             flog(LOG_CRIT, "could not convert service name (%ls) into local charset: %s, not updating principal", confgetstr("auth-krb5", "service"), strerror(errno));
559         } else {
560             if((ret = krb5_sname_to_principal(k5context, NULL, buf, KRB5_NT_SRV_HST, &newprinc)) != 0)
561             {
562                 flog(LOG_CRIT, "could not get principal for service %s: %s, not updating principal", buf, error_message(ret));
563             } else {
564                 krb5_free_principal(k5context, myprinc);
565                 myprinc = newprinc;
566             }
567             free(buf);
568         }
569         if(keytab != NULL)
570             krb5_kt_close(k5context, keytab);
571         if((buf = icwcstombs(confgetstr("auth-krb5", "keytab"), NULL)) == NULL)
572         {
573             flog(LOG_ERR, "could not convert keytab name (%ls) into local charset: %s, using default keytab instead", confgetstr("auth-krb5", "keytab"), strerror(errno));
574             keytab = NULL;
575         } else {
576             if((ret = krb5_kt_resolve(k5context, buf, &keytab)) != 0)
577             {
578                 flog(LOG_ERR, "could not open keytab %s: %s, using default keytab instead", buf, error_message(ret));
579                 keytab = NULL;
580             }
581             free(buf);
582         }
583     }
584     return(0);
585 }
586
587 static void terminate(void)
588 {
589     if(keytab != NULL)
590         krb5_kt_close(k5context, keytab);
591     krb5_free_principal(k5context, myprinc);
592     krb5_free_context(k5context);
593 }
594
595 static struct configvar myvars[] =
596 {
597     /** The name of the service principal to use for Kerberos V
598      * authentication. */
599     {CONF_VAR_STRING, "service", {.str = L"doldacond"}},
600     /** The path to an alternative keytab file. If unspecified, the
601      * system default keytab will be used. */
602     {CONF_VAR_STRING, "keytab", {.str = L""}},
603     /** Whether to renew renewable credentials automatically before
604      * they expire. */
605     {CONF_VAR_BOOL, "renewcreds", {.num = 1}},
606     /** If true, the default credentials cache will be used, which is
607      * useful for e.g. Linux kernel key handling. If false, a file
608      * credentials cache will be created using mkstemp(3), using the
609      * pattern /tmp/krb5cc_dc_$UID_XXXXXX. */
610     {CONF_VAR_BOOL, "usedefcc", {.num = 0}},
611     {CONF_VAR_END}
612 };
613
614 static struct module me =
615 {
616     .conf =
617     {
618         .vars = myvars
619     },
620     .init = init,
621     .terminate = terminate,
622     .name = "auth-krb5"
623 };
624
625 MODULE(me);
626
627 #endif /* HAVE_KRB5 */