]> arthur.barton.de Git - netatalk.git/blob - etc/uams/uams_gss.c
28ed7444f4ea3bef2ebab93b7d071bef95da8eb3
[netatalk.git] / etc / uams / uams_gss.c
1 /*
2  * Copyright (c) 1990,1993 Regents of The University of Michigan.
3  * Copyright (c) 1999 Adrian Sun (asun@u.washington.edu)
4  * Copyright (c) 2003 The Reed Institute
5  * Copyright (c) 2004 Bjoern Fernhomberg
6  * All Rights Reserved.  See COPYRIGHT.
7  */
8
9 #ifdef HAVE_CONFIG_H
10 #include "config.h"
11 #endif /* HAVE_CONFIG_H */
12
13 #include <stdbool.h>
14 #include <stdint.h>
15 #include <stdlib.h>
16 #include <string.h>
17 #include <arpa/inet.h>
18
19 #include <atalk/logger.h>
20 #include <atalk/afp.h>
21 #include <atalk/uam.h>
22 #include <atalk/util.h>
23 #include <atalk/compat.h>
24 #include <atalk/globals.h>
25
26 /* Kerberos includes */
27 #ifdef HAVE_GSSAPI_GSSAPI_H
28 #include <gssapi/gssapi.h>
29 #else
30 #include <gssapi.h>
31 #endif // HAVE_GSSAPI_GSSAPI_H
32
33 #ifdef HAVE_KERBEROS
34 #ifdef HAVE_KRB5_KRB5_H
35 #include <krb5/krb5.h>
36 #else
37 #include <krb5.h>
38 #endif /* HAVE_KRB5_KRB5_H */
39 #endif /* HAVE_KERBEROS */
40
41 #define LOG_UAMS(log_level, ...) \
42     LOG(log_level, logtype_uams, __VA_ARGS__)
43
44 #define LOG_LOGINCONT(log_level, ...) \
45     LOG_UAMS(log_level, "FPLoginCont: " __VA_ARGS__)
46
47 static void log_status(char *s,
48                        OM_uint32 major_status,
49                        OM_uint32 minor_status)
50 {
51     gss_buffer_desc msg = GSS_C_EMPTY_BUFFER;
52     OM_uint32 min_status, maj_status;
53     OM_uint32 maj_ctx = 0, min_ctx = 0;
54
55     while (1) {
56         maj_status = gss_display_status( &min_status, major_status,
57                                          GSS_C_GSS_CODE, GSS_C_NULL_OID,
58                                          &maj_ctx, &msg );
59         LOG_UAMS(log_error, "%s %.*s (error %s)",
60                  s, msg.length, msg.value, strerror(errno));
61         gss_release_buffer(&min_status, &msg);
62
63         if (!maj_ctx)
64             break;
65     }
66
67     while (1) {
68         maj_status = gss_display_status( &min_status, minor_status,
69                                          GSS_C_MECH_CODE, GSS_C_NULL_OID,
70                                          &min_ctx, &msg );
71         LOG_UAMS(log_error, "%s %.*s (error %s)",
72                  s, msg.length, msg.value, strerror(errno));
73         gss_release_buffer(&min_status, &msg);
74
75         if (!min_ctx)
76             break;
77     }
78 }
79
80 static void log_ctx_flags(OM_uint32 flags)
81 {
82     if (flags & GSS_C_DELEG_FLAG)
83         LOG_LOGINCONT(log_debug, "context flag: GSS_C_DELEG_FLAG");
84     if (flags & GSS_C_MUTUAL_FLAG)
85         LOG_LOGINCONT(log_debug, "context flag: GSS_C_MUTUAL_FLAG");
86     if (flags & GSS_C_REPLAY_FLAG)
87         LOG_LOGINCONT(log_debug, "context flag: GSS_C_REPLAY_FLAG");
88     if (flags & GSS_C_SEQUENCE_FLAG)
89         LOG_LOGINCONT(log_debug, "context flag: GSS_C_SEQUENCE_FLAG");
90     if (flags & GSS_C_CONF_FLAG)
91         LOG_LOGINCONT(log_debug, "context flag: GSS_C_CONF_FLAG");
92     if (flags & GSS_C_INTEG_FLAG)
93         LOG_LOGINCONT(log_debug, "context flag: GSS_C_INTEG_FLAG");
94 }
95
96 static void log_service_name(gss_ctx_id_t context)
97 {
98     OM_uint32 major_status = 0, minor_status = 0;
99     gss_name_t service_name;
100     gss_buffer_desc service_name_buffer;
101
102     major_status = gss_inquire_context(&minor_status,
103                                        context,
104                                        NULL,
105                                        &service_name,
106                                        NULL,
107                                        NULL,
108                                        NULL,
109                                        NULL,
110                                        NULL);
111     if (major_status != GSS_S_COMPLETE) {
112         log_status("gss_inquire_context", major_status, minor_status);
113         return;
114     }
115
116     major_status = gss_display_name(&minor_status,
117                                     service_name,
118                                     &service_name_buffer,
119                                     NULL);
120     if (major_status == GSS_S_COMPLETE) {
121         LOG_LOGINCONT(log_debug,
122                       "service principal is `%s'",
123                       service_name_buffer.value);
124
125         gss_release_buffer(&minor_status, &service_name_buffer);
126     } else
127         log_status("gss_display_name", major_status, minor_status);
128
129     gss_release_name(&minor_status, &service_name);
130 }
131
132 static int get_client_username(char *username,
133                                int ulen,
134                                gss_name_t *client_name)
135 {
136     OM_uint32 major_status = 0, minor_status = 0;
137     gss_buffer_desc client_name_buffer;
138     char *p;
139     int ret = 0;
140
141     /*
142      * To extract the unix username, use gss_display_name on client_name.
143      * We do rely on gss_display_name returning a zero terminated string.
144      * The username returned contains the realm and possibly an instance.
145      * We only want the username for afpd, so we have to strip those from
146      * the username before copying it to afpd's buffer.
147      */
148
149     major_status = gss_display_name(&minor_status,
150                                     *client_name,
151                                     &client_name_buffer,
152                                     NULL);
153     if (major_status != GSS_S_COMPLETE) {
154         log_status("gss_display_name", major_status, minor_status);
155         return 1;
156     }
157
158     LOG_LOGINCONT(log_debug,
159                   "user principal is `%s'",
160                   client_name_buffer.value);
161
162     /* chop off realm */
163     p = strchr(client_name_buffer.value, '@');
164     if (p)
165         *p = 0;
166     /* FIXME: chop off instance? */
167     p = strchr(client_name_buffer.value, '/');
168     if (p)
169         *p = 0;
170
171     /* check if this username fits into afpd's username buffer */
172     size_t cnblen = strlen(client_name_buffer.value);
173     if (cnblen >= ulen) {
174         /* The username is too long for afpd's buffer, bail out */
175         LOG_LOGINCONT(log_info,
176                       "username `%s' too long (%d)",
177                       client_name_buffer.value, cnblen);
178         ret = 1;
179     } else {
180         /* copy stripped username to afpd's buffer */
181         strlcpy(username, client_name_buffer.value, ulen);
182     }
183
184     gss_release_buffer(&minor_status, &client_name_buffer);
185
186     return ret;
187 }
188
189 /* wrap afpd's sessionkey */
190 static int wrap_sessionkey(gss_ctx_id_t context, struct session_info *sinfo)
191 {
192     OM_uint32 major_status = 0, minor_status = 0;
193     gss_buffer_desc sesskey_buff, wrap_buff;
194     int ret = 0;
195
196     /*
197      * gss_wrap afpd's session_key.
198      * This is needed fo OS X 10.3 clients. They request this information
199      * with type 8 (kGetKerberosSessionKey) on FPGetSession.
200      * See AFP 3.1 specs, page 77.
201      */
202     sesskey_buff.value = sinfo->sessionkey;
203     sesskey_buff.length = sinfo->sessionkey_len;
204
205     /* gss_wrap the session key with the default mechanism.
206        Require both confidentiality and integrity services */
207     major_status = gss_wrap(&minor_status,
208                             context,
209                             true,
210                             GSS_C_QOP_DEFAULT,
211                             &sesskey_buff,
212                             NULL,
213                             &wrap_buff);
214
215     if (major_status != GSS_S_COMPLETE) {
216         log_status("gss_wrap", major_status, minor_status);
217         return 1;
218     }
219
220     /* store the wrapped session key in afpd's session_info struct */
221     if (NULL == (sinfo->cryptedkey = malloc(wrap_buff.length))) {
222         LOG_UAMS(log_error,
223                  "wrap_sessionkey: out of memory tyring to allocate %u bytes",
224                  wrap_buff.length);
225         ret = 1;
226     } else {
227         /* cryptedkey is binary data */
228         memcpy(sinfo->cryptedkey, wrap_buff.value, wrap_buff.length);
229         sinfo->cryptedkey_len = wrap_buff.length;
230     }
231
232     /* we're done with buffer, release */
233     gss_release_buffer(&minor_status, &wrap_buff);
234
235     return ret;
236 }
237
238 static int accept_sec_context(gss_ctx_id_t *context,
239                               gss_buffer_desc *ticket_buffer,
240                               gss_name_t *client_name,
241                               gss_buffer_desc *authenticator_buff)
242 {
243     OM_uint32 major_status = 0, minor_status = 0, flags = 0;
244
245     /* Initialize autheticator buffer. */
246     authenticator_buff->length = 0;
247     authenticator_buff->value = NULL;
248
249     LOG_LOGINCONT(log_debug,
250                   "accepting context (ticketlen: %u)",
251                   ticket_buffer->length);
252
253     /*
254      * Try to accept the secondary context using the token in ticket_buffer.
255      * We don't care about the principals or mechanisms used, nor for the time.
256      * We don't act as a proxy either.
257      */
258     major_status = gss_accept_sec_context(&minor_status,
259                                           context,
260                                           GSS_C_NO_CREDENTIAL,
261                                           ticket_buffer,
262                                           GSS_C_NO_CHANNEL_BINDINGS,
263                                           client_name,
264                                           NULL,
265                                           authenticator_buff,
266                                           &flags,
267                                           NULL,
268                                           NULL);
269
270     if (major_status != GSS_S_COMPLETE) {
271         log_status("gss_accept_sec_context", major_status, minor_status);
272         return 1;
273     }
274
275     log_ctx_flags(flags);
276     return 0;
277 }
278
279 static int do_gss_auth(void *obj,
280                        char *ibuf, size_t ibuflen,
281                        char *rbuf, int *rbuflen,
282                        char *username, size_t ulen,
283                        struct session_info *sinfo )
284 {
285     OM_uint32 status = 0;
286     gss_name_t client_name;
287     gss_ctx_id_t context = GSS_C_NO_CONTEXT;
288     gss_buffer_desc ticket_buffer, authenticator_buff;
289     int ret = 0;
290
291     /*
292      * Try to accept the secondary context, using the ticket/token the
293      * client sent us. Ticket is stored at current ibuf position.
294      * Don't try to release ticket_buffer later, it points into ibuf!
295      */
296     ticket_buffer.length = ibuflen;
297     ticket_buffer.value = ibuf;
298
299     if ((ret = accept_sec_context(&context,
300                                   &ticket_buffer,
301                                   &client_name,
302                                   &authenticator_buff)))
303         return ret;
304     log_service_name(context);
305
306     /* We succesfully acquired the secondary context, now get the
307        username for afpd and gss_wrap the sessionkey */
308     if ((ret = get_client_username(username, ulen, &client_name)))
309         goto cleanup_client_name;
310
311     if ((ret = wrap_sessionkey(context, sinfo)))
312         goto cleanup_client_name;
313
314     /* Authenticated, construct the reply using:
315      * authenticator length (uint16_t)
316      * authenticator
317      */
318     /* copy the authenticator length into the reply buffer */
319     uint16_t auth_len = htons(authenticator_buff.length);
320     memcpy(rbuf, &auth_len, sizeof(auth_len));
321     *rbuflen += sizeof(auth_len);
322     rbuf += sizeof(auth_len);
323
324     /* copy the authenticator value into the reply buffer */
325     memcpy(rbuf, authenticator_buff.value, authenticator_buff.length);
326     *rbuflen += authenticator_buff.length;
327
328 cleanup_client_name:
329     gss_release_name(&status, &client_name);
330     gss_release_buffer(&status, &authenticator_buff);
331     gss_delete_sec_context(&status, &context, NULL);
332
333     return ret;
334 }
335
336 /* -------------------------- */
337
338 /*
339  * For the gss uam, this function only needs to return a two-byte
340  * login-session id. None of the data provided by the client up to this
341  * point is trustworthy as we'll have a signed ticket to parse in logincont.
342  */
343 static int gss_login(void *obj,
344                      struct passwd **uam_pwd,
345                      char *ibuf, size_t ibuflen,
346                      char *rbuf, size_t *rbuflen)
347 {
348     *rbuflen = 0;
349
350     /* The reply contains a two-byte ID value - note
351      * that Apple's implementation seems to always return 1 as well
352      */
353     uint16_t temp16 = htons(1);
354     memcpy(rbuf, &temp16, sizeof(temp16));
355     *rbuflen += sizeof(temp16);
356
357     return AFPERR_AUTHCONT;
358 }
359
360 static int gss_logincont(void *obj,
361                          struct passwd **uam_pwd,
362                          char *ibuf, size_t ibuflen,
363                          char *rbuf, size_t *rbuflen)
364 {
365     struct passwd *pwd = NULL;
366     uint16_t login_id;
367     char *username;
368     uint16_t ticket_len;
369     char *p;
370     int rblen;
371     size_t userlen;
372     struct session_info *sinfo;
373
374     /* Apple's AFP 3.1 documentation specifies that this command
375      * takes the following format:
376      * pad (byte)
377      * id returned in LoginExt response (uint16_t)
378      * username (format unspecified)
379      *   padded, when necessary, to end on an even boundary
380      * ticket length (uint16_t)
381      * ticket
382      */
383
384     /* Observation of AFP clients in the wild indicate that the actual
385      * format of this request is as follows:
386      * pad (byte) [consumed before login_ext is called]
387      * ?? (byte) - always observed to be 0
388      * id returned in LoginExt response (uint16_t)
389      * username, encoding unspecified, null terminated C string,
390      *   padded when the terminating null is an even numbered byte.
391      *   The packet is formated such that the username begins on an
392      *   odd numbered byte. Eg if the username is 3 characters and the
393      *   terminating null makes 4, expect to pad the the result.
394      *   The encoding of this string is unknown.
395      * ticket length (uint16_t)
396      * ticket
397      */
398
399     rblen = *rbuflen = 0;
400
401     if (ibuflen < 1 +sizeof(login_id)) {
402         LOG_LOGINCONT(log_info, "received incomplete packet");
403         return AFPERR_PARAM;
404     }
405     ibuf++, ibuflen--; /* ?? */
406
407     /* 2 byte ID from LoginExt -- always '00 01' in this implementation */
408     memcpy(&login_id, ibuf, sizeof(login_id));
409     ibuf += sizeof(login_id), ibuflen -= sizeof(login_id);
410     login_id = ntohs(login_id);
411
412     /* get the username buffer from apfd */
413     if (uam_afpserver_option(obj, UAM_OPTION_USERNAME, &username, &userlen) < 0)
414         return AFPERR_MISC;
415
416     /* get the session_info structure from afpd. We need the session key */
417     if (uam_afpserver_option(obj, UAM_OPTION_SESSIONINFO, &sinfo, NULL) < 0)
418         return AFPERR_MISC;
419
420     if (sinfo->sessionkey == NULL || sinfo->sessionkey_len == 0) {
421         /* Should never happen. Most likely way too old afpd version */
422         LOG_LOGINCONT(log_error, "internal error: afpd's sessionkey not set");
423         return AFPERR_MISC;
424     }
425
426     /* We skip past the 'username' parameter because all that matters is the ticket */
427     p = ibuf;
428     while ( *ibuf && ibuflen ) { ibuf++, ibuflen--; }
429     if (ibuflen < 4) {
430         LOG_LOGINCONT(log_info, "user is %s, no ticket", p);
431         return AFPERR_PARAM;
432     }
433
434     ibuf++, ibuflen--; /* null termination */
435
436     if ((ibuf - p + 1) % 2) ibuf++, ibuflen--; /* deal with potential padding */
437
438     LOG_LOGINCONT(log_debug, "client thinks user is %s", p);
439
440     /* get the length of the ticket the client sends us */
441     memcpy(&ticket_len, ibuf, sizeof(ticket_len));
442     ibuf += sizeof(ticket_len); ibuflen -= sizeof(ticket_len);
443     ticket_len = ntohs(ticket_len);
444
445     /* a little bounds checking */
446     if (ticket_len > ibuflen) {
447         LOG_LOGINCONT(log_info,
448                       "invalid ticket length (%u > %u)",
449                       ticket_len, ibuflen);
450         return AFPERR_PARAM;
451     }
452
453     /* now try to authenticate */
454     if (do_gss_auth(obj, ibuf, ticket_len, rbuf, &rblen, username, userlen, sinfo)) {
455         LOG_LOGINCONT(log_info, "do_gss_auth() failed" );
456         *rbuflen = 0;
457         return AFPERR_MISC;
458     }
459
460     /* We use the username we got back from the gssapi client_name.
461        Should we compare this to the username the client sent in the clear?
462        We know the character encoding of the cleartext username (UTF8), what
463        encoding is the gssapi name in? */
464     if ((pwd = uam_getname( obj, username, userlen )) == NULL) {
465         LOG_LOGINCONT(log_info, "uam_getname() failed for %s", username);
466         return AFPERR_NOTAUTH;
467     }
468     if (uam_checkuser(pwd) < 0) {
469         LOG_LOGINCONT(log_info, "`%s'' not a valid user", username);
470         return AFPERR_NOTAUTH;
471     }
472
473     *rbuflen = rblen;
474     *uam_pwd = pwd;
475     return AFP_OK;
476 }
477
478 /* logout */
479 static void gss_logout() {
480 }
481
482 static int gss_login_ext(void *obj,
483                          char *uname,
484                          struct passwd **uam_pwd,
485                          char *ibuf, size_t ibuflen,
486                          char *rbuf, size_t *rbuflen)
487 {
488     return gss_login(obj, uam_pwd, ibuf, ibuflen, rbuf, rbuflen);
489 }
490
491 static int set_principal(AFPObj *obj, char *principal)
492 {
493     size_t len = strlen(principal);
494
495     if (len > 255) {
496         LOG(log_error, logtype_afpd, "set_principal: principal '%s' too long (max=255)", principal, len);
497         return -1;
498     }
499
500     /* We store: 1 byte number principals (1) + 1 byte principal name length + zero terminated principal */
501     if ((obj->options.k5principal = malloc(len + 3)) == NULL) {
502         LOG(log_error, logtype_afpd, "set_principal: OOM");
503         return -1;
504     }
505
506     LOG(log_info, logtype_afpd, "Using AFP Kerberos service principal name: %s", principal);
507
508     obj->options.k5principal[0] = 1;
509     obj->options.k5principal[1] = (unsigned char)len;
510     obj->options.k5principal_buflen = len + 2;
511     strncpy(obj->options.k5principal + 2, principal, len);
512
513     return 0;
514 }
515
516 static int gss_create_principal(AFPObj *obj)
517 {
518     int rv = -1;
519 #ifdef HAVE_KERBEROS
520     krb5_context context;
521     krb5_error_code ret;
522     const char *error_msg;
523     krb5_keytab keytab;
524     krb5_keytab_entry entry;
525     krb5_principal service_principal;
526     char *principal;
527     krb5_kt_cursor cursor;
528
529     if (krb5_init_context(&context)) {
530         LOG(log_error, logtype_afpd, "gss_create_principal: failed to intialize a krb5_context");
531         goto exit;
532     }
533     if ((ret = krb5_kt_default(context, &keytab)))
534         goto krb5_error;
535
536     if (obj->options.k5service && obj->options.fqdn && obj->options.k5realm) {
537         LOG(log_debug, logtype_afpd, "gss_create_principal: using service principal specified in options");
538             
539         if ((ret = krb5_build_principal(context,
540                                         &service_principal,
541                                         strlen(obj->options.k5realm),
542                                         obj->options.k5realm,
543                                         obj->options.k5service,
544                                         obj->options.fqdn,
545                                         NULL)))
546             goto krb5_error;
547
548         if ((ret = krb5_kt_get_entry(context,
549                                      keytab,
550                                      service_principal,
551                                      0, // kvno - wildcard
552                                      0, // enctype - wildcard
553                                      &entry)) == KRB5_KT_NOTFOUND) {
554             krb5_unparse_name(context, service_principal, &principal);
555             LOG(log_error, logtype_afpd, "gss_create_principal: specified service principal '%s' not found in keytab", principal);
556 #ifdef HAVE_KRB5_FREE_UNPARSED_NAME
557             krb5_free_unparsed_name(context, principal);
558 #else
559             krb5_xfree(principal);
560 #endif
561             goto krb5_cleanup;
562         }
563         krb5_free_principal(context, service_principal);
564         if (ret)
565             goto krb5_error;
566     } else {
567         LOG(log_debug, logtype_afpd, "gss_create_principal: using first entry from keytab as service principal");
568         if ((ret = krb5_kt_start_seq_get(context, keytab, &cursor)))
569             goto krb5_error;
570         ret = krb5_kt_next_entry(context, keytab, &entry, &cursor);
571         krb5_kt_end_seq_get(context, keytab, &cursor);
572         if (ret)
573             goto krb5_error;
574     }
575
576     krb5_unparse_name(context, entry.principal, &principal);
577 #ifdef HAVE_KRB5_FREE_KEYTAB_ENTRY_CONTENTS
578     krb5_free_keytab_entry_contents(context, &entry);
579 #elif defined(HAVE_KRB5_KT_FREE_ENTRY)
580     krb5_kt_free_entry(context, &entry);
581 #endif
582     set_principal(obj, principal);
583     free(principal);
584     rv = 0;
585     goto krb5_cleanup;
586
587 krb5_error:
588     if (ret) {
589         error_msg = krb5_get_error_message(context, ret);
590         LOG(log_note, logtype_afpd, "Can't get principal from default keytab: %s",
591             (char *)error_msg);
592 #ifdef HAVE_KRB5_FREE_ERROR_MESSAGE
593         krb5_free_error_message(context, error_msg);
594 #else
595         krb5_xfree(error_msg);
596 #endif
597     }
598
599 krb5_cleanup:
600     krb5_kt_close(context, keytab);
601     krb5_free_context(context);
602
603 #else /* ! HAVE_KERBEROS */
604
605     if (!options->k5service || !options->fqdn || !options->k5realm)
606         goto exit;
607
608     char principal[255];
609     size_t len = snprintf(principal, sizeof(principal), "%s/%s@%s",
610                           options->k5service, options->fqdn, options->k5realm);
611     (void)set_principal(&obj, principal);
612     rv = 0;
613 #endif /* HAVE_KERBEROS */
614
615 exit:
616     return rv;
617 }
618
619 static int uam_setup(void *handle, const char *path)
620 {
621     AFPObj *obj = (AFPObj *)handle;
622     if (gss_create_principal(obj) != 0)
623         return -1;
624
625     return uam_register(UAM_SERVER_LOGIN_EXT, path, "Client Krb v2",
626                         gss_login, gss_logincont, gss_logout, gss_login_ext);
627 }
628
629 static void uam_cleanup(void)
630 {
631     uam_unregister(UAM_SERVER_LOGIN_EXT, "Client Krb v2");
632 }
633
634 UAM_MODULE_EXPORT struct uam_export uams_gss = {
635     UAM_MODULE_SERVER,
636     UAM_MODULE_VERSION,
637     uam_setup,
638     uam_cleanup
639 };