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