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