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