1 /*************************************************
2 * Exim - an Internet mail transport agent *
3 *************************************************/
5 /* Copyright (c) The Exim Maintainers 2020 - 2023 */
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 */
10 /* Copyright (c) Twitter Inc 2012
11 Author: Phil Pennock <pdp@exim.org> */
12 /* Copyright (c) Phil Pennock 2012 */
14 /* Interface to Heimdal library for GSSAPI authentication. */
16 /* Naming and rationale
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
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.
29 Without rename, we could add an option for GS2 support in the future.
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().
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); }
53 #include <gssapi/gssapi.h>
54 #include <gssapi/gssapi_krb5.h>
56 /* for the _init debugging */
59 #include "heimdal_gssapi.h"
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) }
71 int auth_heimdal_gssapi_options_count =
72 sizeof(auth_heimdal_gssapi_options)/sizeof(optionlist);
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 */
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) {}
91 #else /*!MACRO_PREDEF*/
95 /* "Globals" for managing the heimdal_gssapi interface. */
97 /* Utility functions */
99 exim_heimdal_error_debug(const char *, krb5_context, krb5_error_code);
101 exim_gssapi_error_defer(rmark, OM_uint32, OM_uint32, const char *, ...)
102 PRINTF_FUNCTION(4, 5);
104 #define EmptyBuf(buf) do { buf.value = NULL; buf.length = 0; } while (0)
107 /*************************************************
108 * Initialization entry point *
109 *************************************************/
111 /* Called for each instance, after its options have been read, to
112 enable consistency checks to be done, or anything else that needs
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. */
120 auth_heimdal_gssapi_init(auth_instance *ablock)
122 krb5_context context;
124 krb5_kt_cursor cursor;
125 krb5_keytab_entry entry;
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);
132 ablock->server = FALSE;
133 ablock->client = FALSE;
135 if (!ob->server_service || !*ob->server_service)
137 HDEBUG(D_auth) debug_printf("heimdal: missing server_service\n");
141 if ((krc = krb5_init_context(&context)))
144 HDEBUG(D_auth) debug_printf("heimdal: failed to initialise krb5 context: %s\n",
149 if (ob->server_keytab)
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)))
155 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_resolve", context, krc);
161 HDEBUG(D_auth) debug_printf("heimdal: using system default keytab\n");
162 if ((krc = krb5_kt_default(context, &keytab)))
164 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_default", context, krc);
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);
176 while (!(krc = krb5_kt_next_entry(context, keytab, &entry, &cursor)))
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 : "??",
184 enctype_s ? enctype_s : "??");
187 krb5_kt_free_entry(context, &entry);
189 if ((krc = krb5_kt_end_seq_get(context, keytab, &cursor)))
190 exim_heimdal_error_debug("krb5_kt_end_seq_get", context, krc);
194 if ((krc = krb5_kt_close(context, keytab)))
195 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_close", context, krc);
197 krb5_free_context(context);
199 ablock->server = TRUE;
204 exim_heimdal_error_debug(const char *label,
205 krb5_context context, krb5_error_code err)
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);
213 /*************************************************
214 * Server entry point *
215 *************************************************/
217 /* For interface, see auths/README */
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()
227 auth_heimdal_gssapi_server(auth_instance *ablock, uschar *initial_data)
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;
238 OM_uint32 maj_stat, min_stat;
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;
246 uschar sasl_config[4];
247 uschar requested_qop;
249 store_reset_point = store_mark();
252 debug_printf("heimdal: initialising auth context for %s\n", ablock->name);
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);
266 /* Use a specific keytab, if specified */
267 if (ob->server_keytab)
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);
275 debug_printf("heimdal: using keytab \"%s\"\n", keytab);
278 /* Acquire our credentials */
279 maj_stat = gss_acquire_cred(&min_stat,
280 gserver, /* desired name */
282 GSS_C_NULL_OID_SET, /* desired mechs */
283 GSS_C_ACCEPT, /* cred usage */
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);
291 maj_stat = gss_release_name(&min_stat, &gserver);
293 HDEBUG(D_auth) debug_printf("heimdal: have server credentials.\n");
295 /* Loop talking to client */
297 from_client = initial_data;
298 handled_empty_ir = FALSE;
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). */
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)
316 if (!from_client || !*from_client)
318 if (handled_empty_ir)
320 HDEBUG(D_auth) debug_printf("gssapi: repeated empty input, grr.\n");
325 HDEBUG(D_auth) debug_printf("gssapi: missing initial response, nudging.\n");
326 if ((error_out = auth_get_data(&from_client, US"", 0)) != OK)
328 handled_empty_ir = TRUE;
331 /* We should now have the opening data from the client, base64-encoded. */
333 HDEBUG(D_auth) debug_printf("heimdal: have initial client data\n");
337 gbufdesc_in.length = b64decode(from_client, USS &gbufdesc_in.value, GET_TAINTED);
340 maj_stat = gss_release_name(&min_stat, &gclient);
341 gclient = GSS_C_NO_NAME;
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 */
353 NULL /* delegated cred_handle */
355 if (GSS_ERROR(maj_stat))
357 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
358 "gss_accept_sec_context()");
362 if (gbufdesc_out.length != 0)
364 error_out = auth_get_data(&from_client,
365 gbufdesc_out.value, gbufdesc_out.length);
369 gss_release_buffer(&min_stat, &gbufdesc_out);
370 EmptyBuf(gbufdesc_out);
372 if (maj_stat == GSS_S_COMPLETE)
375 HDEBUG(D_auth) debug_printf("heimdal: GSS complete\n");
378 HDEBUG(D_auth) debug_printf("heimdal: need more data\n");
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
388 The remaining three octets are the maximum buffer size for wrapped
390 sasl_config[0] = 0x01; /* Exim does not wrap/unwrap SASL layers after auth */
391 gbufdesc.value = (void *) sasl_config;
393 maj_stat = gss_wrap(&min_stat,
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 */
401 if (GSS_ERROR(maj_stat))
403 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
404 "gss_wrap(SASL state after auth)");
409 HDEBUG(D_auth) debug_printf("heimdal SASL: requesting QOP with no security layers\n");
411 error_out = auth_get_data(&from_client,
412 gbufdesc_out.value, gbufdesc_out.length);
416 gss_release_buffer(&min_stat, &gbufdesc_out);
417 EmptyBuf(gbufdesc_out);
422 gbufdesc_in.length = b64decode(from_client, USS &gbufdesc_in.value, GET_TAINTED);
423 maj_stat = gss_unwrap(&min_stat,
425 &gbufdesc_in, /* data from client */
426 &gbufdesc_out, /* results */
427 NULL, /* conf state */
430 if (GSS_ERROR(maj_stat))
432 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
433 "gss_unwrap(final SASL message from client)");
437 if (gbufdesc_out.length < 4)
440 debug_printf("gssapi: final message too short; "
441 "need flags, buf sizes and optional authzid\n");
446 requested_qop = (CS gbufdesc_out.value)[0];
447 if (!(requested_qop & 0x01))
450 debug_printf("gssapi: client requested security layers (%x)\n",
451 (unsigned int) requested_qop);
456 for (int i = 0; i < AUTH_VARS; i++) auth_vars[i] = NULL;
460 The SASL provided identifier is an unverified authzid.
461 GSSAPI provides us with a verified identifier, but it might be empty
465 /* $auth2 is authzid requested at SASL layer */
466 if (gbufdesc_out.length > 4)
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]);
474 gss_release_buffer(&min_stat, &gbufdesc_out);
475 EmptyBuf(gbufdesc_out);
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))
482 auth_vars[1] = expand_nstring[2] = NULL;
484 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
485 "gss_display_name(client identifier)");
490 expand_nlength[1] = gbufdesc_out.length;
491 auth_vars[0] = expand_nstring[1] =
492 string_copyn(gbufdesc_out.value, gbufdesc_out.length);
494 if (expand_nmax == 0) /* should be: authzid was empty */
497 expand_nlength[2] = expand_nlength[1];
498 auth_vars[1] = expand_nstring[2] = string_copyn(expand_nstring[1], expand_nlength[1]);
500 debug_printf("heimdal SASL: empty authzid, set to dup of GSSAPI display name\n");
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]);
517 maj_stat = gss_release_cred(&min_stat, &gcred);
520 gss_release_name(&min_stat, &gclient);
521 gclient = GSS_C_NO_NAME;
523 if (gbufdesc_out.length)
525 gss_release_buffer(&min_stat, &gbufdesc_out);
526 EmptyBuf(gbufdesc_out);
528 if (gcontext != GSS_C_NO_CONTEXT)
529 gss_delete_sec_context(&min_stat, &gcontext, GSS_C_NO_BUFFER);
531 store_reset(store_reset_point);
536 /* Auth succeeded, check server_condition */
537 return auth_check_serv_cond(ablock);
542 exim_gssapi_error_defer(rmark store_reset_point,
543 OM_uint32 major, OM_uint32 minor,
544 const char *format, ...)
547 OM_uint32 maj_stat, min_stat;
548 OM_uint32 msgcontext = 0;
549 gss_buffer_desc status_string;
554 va_start(ap, format);
555 g = string_vformat(NULL, SVFMT_EXTEND|SVFMT_REBUFFER, format, ap);
559 auth_defer_msg = NULL;
562 maj_stat = gss_display_status(&min_stat,
563 major, GSS_C_GSS_CODE, GSS_C_NO_OID, &msgcontext, &status_string);
566 auth_defer_msg = string_copy(US status_string.value);
568 HDEBUG(D_auth) debug_printf("heimdal %Y: %.*s\n",
569 g, (int)status_string.length, CS status_string.value);
570 gss_release_buffer(&min_stat, &status_string);
572 } while (msgcontext != 0);
574 if (store_reset_point)
575 store_reset(store_reset_point);
580 /*************************************************
581 * Client entry point *
582 *************************************************/
584 /* For interface, see auths/README */
587 auth_heimdal_gssapi_client(
588 auth_instance *ablock, /* authenticator block */
589 void * sx, /* connection */
590 int timeout, /* command timeout */
591 uschar *buffer, /* buffer for reading response */
592 int buffsize) /* size of buffer */
595 debug_printf("Client side NOT IMPLEMENTED: you should not see this!\n");
596 /* NOT IMPLEMENTED */
600 /*************************************************
602 *************************************************/
605 auth_heimdal_gssapi_version_report(gstring * g)
607 /* No build-time constants available unless we link against libraries at
608 build-time and export the result as a string into a header ourselves. */
610 return string_fmt_append(g, "Library version: Heimdal: Runtime: %s\n"
612 heimdal_version, heimdal_long_version));
615 #endif /*!MACRO_PREDEF*/
616 #endif /* AUTH_HEIMDAL_GSSAPI */
618 /* End of heimdal_gssapi.c */