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