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