Merge branch 'dbmjz'
[exim.git] / src / src / auths / heimdal_gssapi.c
1 /*************************************************
2 *     Exim - an Internet mail transport agent    *
3 *************************************************/
4
5 /* Copyright (c) University of Cambridge 1995 - 2012 */
6 /* See the file NOTICE for conditions of use and distribution. */
7
8 /* Copyright (c) Twitter Inc 2012
9    Author: Phil Pennock <pdp@exim.org> */
10 /* Copyright (c) Phil Pennock 2012 */
11
12 /* Interface to Heimdal library for GSSAPI authentication. */
13
14 /* Naming and rationale
15
16 Sensibly, this integration would be deferred to a SASL library, but none
17 of them appear to offer keytab file selection interfaces in their APIs.  It
18 might be that this driver only requires minor modification to work with MIT
19 Kerberos.
20
21 Heimdal provides a number of interfaces for various forms of authentication.
22 As GS2 does not appear to provide keytab control interfaces either, we may
23 end up supporting that too.  It's possible that we could trivially expand to
24 support NTLM support via Heimdal, etc.  Rather than try to be too generic
25 immediately, this driver is directly only supporting GSSAPI.
26
27 Without rename, we could add an option for GS2 support in the future.
28 */
29
30 /* Sources
31
32 * mailcheck-imap (Perl, client-side, written by me years ago)
33 * gsasl driver (GPL, server-side)
34 * heimdal sources and man-pages, plus http://www.h5l.org/manual/
35 * FreeBSD man-pages (very informative!)
36 * http://www.ggf.org/documents/GFD.24.pdf confirming GSS_KRB5_REGISTER_ACCEPTOR_IDENTITY_X
37   semantics, that found by browsing Heimdal source to find how to set the keytab; however,
38   after multiple attempts I failed to get that to work and instead switched to
39   gsskrb5_register_acceptor_identity().
40 */
41
42 #include "../exim.h"
43
44 #ifndef AUTH_HEIMDAL_GSSAPI
45 /* dummy function to satisfy compilers when we link in an "empty" file. */
46 static void dummy(int x) { dummy(x-1); }
47 #else
48
49 #include <gssapi/gssapi.h>
50 #include <gssapi/gssapi_krb5.h>
51
52 /* for the _init debugging */
53 #include <krb5.h>
54
55 #include "heimdal_gssapi.h"
56
57 /* Authenticator-specific options. */
58 optionlist auth_heimdal_gssapi_options[] = {
59   { "server_hostname",      opt_stringptr,
60       (void *)(offsetof(auth_heimdal_gssapi_options_block, server_hostname)) },
61   { "server_keytab",        opt_stringptr,
62       (void *)(offsetof(auth_heimdal_gssapi_options_block, server_keytab)) },
63   { "server_service",       opt_stringptr,
64       (void *)(offsetof(auth_heimdal_gssapi_options_block, server_service)) }
65 };
66
67 int auth_heimdal_gssapi_options_count =
68   sizeof(auth_heimdal_gssapi_options)/sizeof(optionlist);
69
70 /* Defaults for the authenticator-specific options. */
71 auth_heimdal_gssapi_options_block auth_heimdal_gssapi_option_defaults = {
72   US"$primary_hostname",    /* server_hostname */
73   NULL,                     /* server_keytab */
74   US"smtp",                 /* server_service */
75 };
76
77 /* "Globals" for managing the heimdal_gssapi interface. */
78
79 /* Utility functions */
80 static void
81   exim_heimdal_error_debug(const char *, krb5_context, krb5_error_code);
82 static int
83   exim_gssapi_error_defer(uschar *, OM_uint32, OM_uint32, const char *, ...)
84     PRINTF_FUNCTION(4, 5);
85
86 #define EmptyBuf(buf) do { buf.value = NULL; buf.length = 0; } while (0)
87
88
89 /*************************************************
90 *          Initialization entry point            *
91 *************************************************/
92
93 /* Called for each instance, after its options have been read, to
94 enable consistency checks to be done, or anything else that needs
95 to be set up. */
96
97 /* Heimdal provides a GSSAPI extension method for setting the keytab;
98 in the init, we mostly just use raw krb5 methods so that we can report
99 the keytab contents, for -D+auth debugging. */
100
101 void
102 auth_heimdal_gssapi_init(auth_instance *ablock)
103 {
104   krb5_context context;
105   krb5_keytab keytab;
106   krb5_kt_cursor cursor;
107   krb5_keytab_entry entry;
108   krb5_error_code krc;
109   char *principal, *enctype_s;
110   const char *k_keytab_typed_name = NULL;
111   auth_heimdal_gssapi_options_block *ob =
112     (auth_heimdal_gssapi_options_block *)(ablock->options_block);
113
114   ablock->server = FALSE;
115   ablock->client = FALSE;
116
117   if (!ob->server_service || !*ob->server_service) {
118     HDEBUG(D_auth) debug_printf("heimdal: missing server_service\n");
119     return;
120   }
121
122   krc = krb5_init_context(&context);
123   if (krc != 0) {
124     int kerr = errno;
125     HDEBUG(D_auth) debug_printf("heimdal: failed to initialise krb5 context: %s\n",
126         strerror(kerr));
127     return;
128   }
129
130   if (ob->server_keytab) {
131     k_keytab_typed_name = CCS string_sprintf("file:%s", expand_string(ob->server_keytab));
132     HDEBUG(D_auth) debug_printf("heimdal: using keytab %s\n", k_keytab_typed_name);
133     krc = krb5_kt_resolve(context, k_keytab_typed_name, &keytab);
134     if (krc) {
135       HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_resolve", context, krc);
136       return;
137     }
138   } else {
139     HDEBUG(D_auth) debug_printf("heimdal: using system default keytab\n");
140     krc = krb5_kt_default(context, &keytab);
141     if (krc) {
142       HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_default", context, krc);
143       return;
144     }
145   }
146
147   HDEBUG(D_auth) {
148     /* http://www.h5l.org/manual/HEAD/krb5/krb5_keytab_intro.html */
149     krc = krb5_kt_start_seq_get(context, keytab, &cursor);
150     if (krc)
151       exim_heimdal_error_debug("krb5_kt_start_seq_get", context, krc);
152     else {
153       while ((krc = krb5_kt_next_entry(context, keytab, &entry, &cursor)) == 0) {
154         principal = enctype_s = NULL;
155         krb5_unparse_name(context, entry.principal, &principal);
156         krb5_enctype_to_string(context, entry.keyblock.keytype, &enctype_s);
157         debug_printf("heimdal: keytab principal: %s  vno=%d  type=%s\n",
158             principal ? principal : "??",
159             entry.vno,
160             enctype_s ? enctype_s : "??");
161         free(principal);
162         free(enctype_s);
163         krb5_kt_free_entry(context, &entry);
164       }
165       krc = krb5_kt_end_seq_get(context, keytab, &cursor);
166       if (krc)
167         exim_heimdal_error_debug("krb5_kt_end_seq_get", context, krc);
168     }
169   }
170
171   krc = krb5_kt_close(context, keytab);
172   if (krc)
173     HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_close", context, krc);
174
175   krb5_free_context(context);
176
177   /* RFC 4121 section 5.2, SHOULD support 64K input buffers */
178   if (big_buffer_size < (64 * 1024)) {
179     uschar *newbuf;
180     big_buffer_size = 64 * 1024;
181     newbuf = store_malloc(big_buffer_size);
182     store_free(big_buffer);
183     big_buffer = newbuf;
184   }
185
186   ablock->server = TRUE;
187 }
188
189
190 static void
191 exim_heimdal_error_debug(const char *label,
192     krb5_context context, krb5_error_code err)
193 {
194   const char *kerrsc;
195   kerrsc = krb5_get_error_message(context, err);
196   debug_printf("heimdal %s: %s\n", label, kerrsc ? kerrsc : "unknown error");
197   krb5_free_error_message(context, kerrsc);
198 }
199
200 /*************************************************
201 *             Server entry point                 *
202 *************************************************/
203
204 /* For interface, see auths/README */
205
206 /* GSSAPI notes:
207 OM_uint32: portable type for unsigned int32
208 gss_buffer_desc / *gss_buffer_t: hold/point-to size_t .length & void *value
209   -- all strings/etc passed in should go through one of these
210   -- when allocated by gssapi, release with gss_release_buffer()
211 */
212
213 int
214 auth_heimdal_gssapi_server(auth_instance *ablock, uschar *initial_data)
215 {
216   gss_name_t gclient = GSS_C_NO_NAME;
217   gss_name_t gserver = GSS_C_NO_NAME;
218   gss_cred_id_t gcred = GSS_C_NO_CREDENTIAL;
219   gss_ctx_id_t gcontext = GSS_C_NO_CONTEXT;
220   uschar *ex_server_str;
221   gss_buffer_desc gbufdesc = GSS_C_EMPTY_BUFFER;
222   gss_buffer_desc gbufdesc_in = GSS_C_EMPTY_BUFFER;
223   gss_buffer_desc gbufdesc_out = GSS_C_EMPTY_BUFFER;
224   gss_OID mech_type;
225   OM_uint32 maj_stat, min_stat;
226   int step, error_out, i;
227   uschar *tmp1, *tmp2, *from_client;
228   auth_heimdal_gssapi_options_block *ob =
229     (auth_heimdal_gssapi_options_block *)(ablock->options_block);
230   BOOL handled_empty_ir;
231   uschar *store_reset_point;
232   uschar *keytab;
233   uschar sasl_config[4];
234   uschar requested_qop;
235
236   store_reset_point = store_get(0);
237
238   HDEBUG(D_auth)
239     debug_printf("heimdal: initialising auth context for %s\n", ablock->name);
240
241   /* Construct our gss_name_t gserver describing ourselves */
242   tmp1 = expand_string(ob->server_service);
243   tmp2 = expand_string(ob->server_hostname);
244   ex_server_str = string_sprintf("%s@%s", tmp1, tmp2);
245   gbufdesc.value = (void *) ex_server_str;
246   gbufdesc.length = Ustrlen(ex_server_str);
247   maj_stat = gss_import_name(&min_stat,
248       &gbufdesc, GSS_C_NT_HOSTBASED_SERVICE, &gserver);
249   if (GSS_ERROR(maj_stat))
250     return exim_gssapi_error_defer(store_reset_point, maj_stat, min_stat,
251         "gss_import_name(%s)", CS gbufdesc.value);
252
253   /* Use a specific keytab, if specified */
254   if (ob->server_keytab) {
255     keytab = expand_string(ob->server_keytab);
256     maj_stat = gsskrb5_register_acceptor_identity(CCS keytab);
257     if (GSS_ERROR(maj_stat))
258       return exim_gssapi_error_defer(store_reset_point, maj_stat, min_stat,
259           "registering keytab \"%s\"", keytab);
260     HDEBUG(D_auth)
261       debug_printf("heimdal: using keytab \"%s\"\n", keytab);
262   }
263
264   /* Acquire our credentials */
265   maj_stat = gss_acquire_cred(&min_stat,
266       gserver,             /* desired name */
267       0,                   /* time */
268       GSS_C_NULL_OID_SET,  /* desired mechs */
269       GSS_C_ACCEPT,        /* cred usage */
270       &gcred,              /* handle */
271       NULL                 /* actual mechs */,
272       NULL                 /* time rec */);
273   if (GSS_ERROR(maj_stat))
274     return exim_gssapi_error_defer(store_reset_point, maj_stat, min_stat,
275         "gss_acquire_cred(%s)", ex_server_str);
276
277   maj_stat = gss_release_name(&min_stat, &gserver);
278
279   HDEBUG(D_auth) debug_printf("heimdal: have server credentials.\n");
280
281   /* Loop talking to client */
282   step = 0;
283   from_client = initial_data;
284   handled_empty_ir = FALSE;
285   error_out = OK;
286
287   /* buffer sizes: auth_get_data() uses big_buffer, which we grow per
288   GSSAPI RFC in _init, if needed, to meet the SHOULD size of 64KB.
289   (big_buffer starts life at the MUST size of 16KB). */
290
291   /* step values
292   0: getting initial data from client to feed into GSSAPI
293   1: iterating for as long as GSS_S_CONTINUE_NEEDED
294   2: GSS_S_COMPLETE, SASL wrapping for authz and qop to send to client
295   3: unpick final auth message from client
296   4: break/finish (non-step)
297   */
298   while (step < 4) {
299     switch (step) {
300       case 0:
301         if (!from_client || *from_client == '\0') {
302           if (handled_empty_ir) {
303             HDEBUG(D_auth) debug_printf("gssapi: repeated empty input, grr.\n");
304             error_out = BAD64;
305             goto ERROR_OUT;
306           } else {
307             HDEBUG(D_auth) debug_printf("gssapi: missing initial response, nudging.\n");
308             error_out = auth_get_data(&from_client, US"", 0);
309             if (error_out != OK)
310               goto ERROR_OUT;
311             handled_empty_ir = TRUE;
312             continue;
313           }
314         }
315         /* We should now have the opening data from the client, base64-encoded. */
316         step += 1;
317         HDEBUG(D_auth) debug_printf("heimdal: have initial client data\n");
318         break;
319
320       case 1:
321         gbufdesc_in.length = auth_b64decode(from_client, USS &gbufdesc_in.value);
322         if (gclient) {
323           maj_stat = gss_release_name(&min_stat, &gclient);
324           gclient = GSS_C_NO_NAME;
325         }
326         maj_stat = gss_accept_sec_context(&min_stat,
327             &gcontext,          /* context handle */
328             gcred,              /* acceptor cred handle */
329             &gbufdesc_in,       /* input from client */
330             GSS_C_NO_CHANNEL_BINDINGS,  /* XXX fixme: use the channel bindings from GnuTLS */
331             &gclient,           /* client identifier */
332             &mech_type,         /* mechanism in use */
333             &gbufdesc_out,      /* output to send to client */
334             NULL,               /* return flags */
335             NULL,               /* time rec */
336             NULL                /* delegated cred_handle */
337             );
338         if (GSS_ERROR(maj_stat)) {
339           exim_gssapi_error_defer(NULL, maj_stat, min_stat,
340               "gss_accept_sec_context()");
341           error_out = FAIL;
342           goto ERROR_OUT;
343         }
344         if (&gbufdesc_out.length != 0) {
345           error_out = auth_get_data(&from_client,
346               gbufdesc_out.value, gbufdesc_out.length);
347           if (error_out != OK)
348             goto ERROR_OUT;
349
350           gss_release_buffer(&min_stat, &gbufdesc_out);
351           EmptyBuf(gbufdesc_out);
352         }
353         if (maj_stat == GSS_S_COMPLETE) {
354           step += 1;
355           HDEBUG(D_auth) debug_printf("heimdal: GSS complete\n");
356         } else {
357           HDEBUG(D_auth) debug_printf("heimdal: need more data\n");
358         }
359         break;
360
361       case 2:
362         memset(sasl_config, 0xFF, 4);
363         /* draft-ietf-sasl-gssapi-06.txt defines bitmasks for first octet
364         0x01 No security layer
365         0x02 Integrity protection
366         0x04 Confidentiality protection
367
368         The remaining three octets are the maximum buffer size for wrapped
369         content. */
370         sasl_config[0] = 0x01;  /* Exim does not wrap/unwrap SASL layers after auth */
371         gbufdesc.value = (void *) sasl_config;
372         gbufdesc.length = 4;
373         maj_stat = gss_wrap(&min_stat,
374             gcontext,
375             0,                    /* conf_req_flag: integrity only */
376             GSS_C_QOP_DEFAULT,    /* qop requested */
377             &gbufdesc,            /* message to protect */
378             NULL,                 /* conf_state: no confidentiality applied */
379             &gbufdesc_out         /* output buffer */
380             );
381         if (GSS_ERROR(maj_stat)) {
382           exim_gssapi_error_defer(NULL, maj_stat, min_stat,
383               "gss_wrap(SASL state after auth)");
384           error_out = FAIL;
385           goto ERROR_OUT;
386         }
387
388         HDEBUG(D_auth) debug_printf("heimdal SASL: requesting QOP with no security layers\n");
389
390         error_out = auth_get_data(&from_client,
391             gbufdesc_out.value, gbufdesc_out.length);
392         if (error_out != OK)
393           goto ERROR_OUT;
394
395         gss_release_buffer(&min_stat, &gbufdesc_out);
396         EmptyBuf(gbufdesc_out);
397         step += 1;
398         break;
399
400       case 3:
401         gbufdesc_in.length = auth_b64decode(from_client, USS &gbufdesc_in.value);
402         maj_stat = gss_unwrap(&min_stat,
403             gcontext,
404             &gbufdesc_in,       /* data from client */
405             &gbufdesc_out,      /* results */
406             NULL,               /* conf state */
407             NULL                /* qop state */
408             );
409         if (GSS_ERROR(maj_stat)) {
410           exim_gssapi_error_defer(NULL, maj_stat, min_stat,
411               "gss_unwrap(final SASL message from client)");
412           error_out = FAIL;
413           goto ERROR_OUT;
414         }
415         if (gbufdesc_out.length < 4) {
416           HDEBUG(D_auth)
417             debug_printf("gssapi: final message too short; "
418                 "need flags, buf sizes and optional authzid\n");
419           error_out = FAIL;
420           goto ERROR_OUT;
421         }
422
423         requested_qop = (CS gbufdesc_out.value)[0];
424         if ((requested_qop & 0x01) == 0) {
425           HDEBUG(D_auth)
426             debug_printf("gssapi: client requested security layers (%x)\n",
427                 (unsigned int) requested_qop);
428           error_out = FAIL;
429           goto ERROR_OUT;
430         }
431
432         for (i = 0; i < AUTH_VARS; i++) auth_vars[i] = NULL;
433         expand_nmax = 0;
434
435         /* Identifiers:
436         The SASL provided identifier is an unverified authzid.
437         GSSAPI provides us with a verified identifier, but it might be empty
438         for some clients.
439         */
440
441         /* $auth2 is authzid requested at SASL layer */
442         if (gbufdesc_out.length > 4) {
443           expand_nlength[2] = gbufdesc_out.length - 4;
444           auth_vars[1] = expand_nstring[2] =
445             string_copyn((US gbufdesc_out.value) + 4, expand_nlength[2]);
446           expand_nmax = 2;
447         }
448
449         gss_release_buffer(&min_stat, &gbufdesc_out);
450         EmptyBuf(gbufdesc_out);
451
452         /* $auth1 is GSSAPI display name */
453         maj_stat = gss_display_name(&min_stat,
454             gclient,
455             &gbufdesc_out,
456             &mech_type);
457         if (GSS_ERROR(maj_stat)) {
458           auth_vars[1] = expand_nstring[2] = NULL;
459           expand_nmax = 0;
460           exim_gssapi_error_defer(NULL, maj_stat, min_stat,
461               "gss_display_name(client identifier)");
462           error_out = FAIL;
463           goto ERROR_OUT;
464         }
465
466         expand_nlength[1] = gbufdesc_out.length;
467         auth_vars[0] = expand_nstring[1] =
468           string_copyn(gbufdesc_out.value, gbufdesc_out.length);
469
470         if (expand_nmax == 0) { /* should be: authzid was empty */
471           expand_nmax = 2;
472           expand_nlength[2] = expand_nlength[1];
473           auth_vars[1] = expand_nstring[2] = string_copyn(expand_nstring[1], expand_nlength[1]);
474           HDEBUG(D_auth)
475             debug_printf("heimdal SASL: empty authzid, set to dup of GSSAPI display name\n");
476         }
477
478         HDEBUG(D_auth)
479           debug_printf("heimdal SASL: happy with client request\n"
480              "  auth1 (verified GSSAPI display-name): \"%s\"\n"
481              "  auth2 (unverified SASL requested authzid): \"%s\"\n",
482              auth_vars[0], auth_vars[1]);
483
484         step += 1;
485         break;
486
487     } /* switch */
488   } /* while step */
489
490
491 ERROR_OUT:
492   maj_stat = gss_release_cred(&min_stat, &gcred);
493   if (gclient) {
494     gss_release_name(&min_stat, &gclient);
495     gclient = GSS_C_NO_NAME;
496   }
497   if (gbufdesc_out.length) {
498     gss_release_buffer(&min_stat, &gbufdesc_out);
499     EmptyBuf(gbufdesc_out);
500   }
501   if (gcontext != GSS_C_NO_CONTEXT) {
502     gss_delete_sec_context(&min_stat, &gcontext, GSS_C_NO_BUFFER);
503   }
504
505   store_reset(store_reset_point);
506
507   if (error_out != OK)
508     return error_out;
509
510   /* Auth succeeded, check server_condition */
511   return auth_check_serv_cond(ablock);
512 }
513
514
515 static int
516 exim_gssapi_error_defer(uschar *store_reset_point,
517     OM_uint32 major, OM_uint32 minor,
518     const char *format, ...)
519 {
520   va_list ap;
521   uschar buffer[STRING_SPRINTF_BUFFER_SIZE];
522   OM_uint32 maj_stat, min_stat;
523   OM_uint32 msgcontext = 0;
524   gss_buffer_desc status_string;
525
526   va_start(ap, format);
527   if (!string_vformat(buffer, sizeof(buffer), format, ap))
528     log_write(0, LOG_MAIN|LOG_PANIC_DIE,
529         "exim_gssapi_error_defer expansion larger than %d",
530         sizeof(buffer));
531   va_end(ap);
532
533   auth_defer_msg = NULL;
534
535   do {
536     maj_stat = gss_display_status(&min_stat,
537         major, GSS_C_GSS_CODE, GSS_C_NO_OID,
538         &msgcontext, &status_string);
539
540     if (auth_defer_msg == NULL) {
541       auth_defer_msg = string_copy(US status_string.value);
542     }
543
544     HDEBUG(D_auth) debug_printf("heimdal %s: %.*s\n",
545         buffer, (int)status_string.length, CS status_string.value);
546     gss_release_buffer(&min_stat, &status_string);
547
548   } while (msgcontext != 0);
549
550   if (store_reset_point)
551     store_reset(store_reset_point);
552   return DEFER;
553 }
554
555
556 /*************************************************
557 *              Client entry point                *
558 *************************************************/
559
560 /* For interface, see auths/README */
561
562 int
563 auth_heimdal_gssapi_client(
564   auth_instance *ablock,                 /* authenticator block */
565   smtp_inblock *inblock,                 /* connection inblock */
566   smtp_outblock *outblock,               /* connection outblock */
567   int timeout,                           /* command timeout */
568   uschar *buffer,                        /* buffer for reading response */
569   int buffsize)                          /* size of buffer */
570 {
571   HDEBUG(D_auth)
572     debug_printf("Client side NOT IMPLEMENTED: you should not see this!\n");
573   /* NOT IMPLEMENTED */
574   return FAIL;
575 }
576
577 /*************************************************
578 *                Diagnostic API                  *
579 *************************************************/
580
581 void
582 auth_heimdal_gssapi_version_report(FILE *f)
583 {
584   /* No build-time constants available unless we link against libraries at
585   build-time and export the result as a string into a header ourselves. */
586   fprintf(f, "Library version: Heimdal: Runtime: %s\n"
587              " Build Info: %s\n",
588           heimdal_version, heimdal_long_version);
589 }
590
591 #endif  /* AUTH_HEIMDAL_GSSAPI */
592
593 /* End of heimdal_gssapi.c */