1 /*************************************************
2 * Exim - an Internet mail transport agent *
3 *************************************************/
5 /* Copyright (c) University of Cambridge 1995 - 2012 */
6 /* See the file NOTICE for conditions of use and distribution. */
8 /* Copyright (c) Twitter Inc 2012
9 Author: Phil Pennock <pdp@exim.org> */
10 /* Copyright (c) Phil Pennock 2012 */
12 /* Interface to Heimdal library for GSSAPI authentication. */
14 /* Naming and rationale
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
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.
27 Without rename, we could add an option for GS2 support in the future.
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().
44 #ifndef AUTH_HEIMDAL_GSSAPI
45 /* dummy function to satisfy compilers when we link in an "empty" file. */
46 static void dummy(int x) { dummy(x-1); }
49 #include <gssapi/gssapi.h>
50 #include <gssapi/gssapi_krb5.h>
52 /* for the _init debugging */
55 #include "heimdal_gssapi.h"
57 /* Authenticator-specific options. */
58 optionlist auth_heimdal_gssapi_options[] = {
59 { "server_hostname", opt_stringptr,
60 (void *)(offsetof(auth_heimdal_gssapi_options_block, server_hostname)) },
61 { "server_keytab", opt_stringptr,
62 (void *)(offsetof(auth_heimdal_gssapi_options_block, server_keytab)) },
63 { "server_service", opt_stringptr,
64 (void *)(offsetof(auth_heimdal_gssapi_options_block, server_service)) }
67 int auth_heimdal_gssapi_options_count =
68 sizeof(auth_heimdal_gssapi_options)/sizeof(optionlist);
70 /* Defaults for the authenticator-specific options. */
71 auth_heimdal_gssapi_options_block auth_heimdal_gssapi_option_defaults = {
72 US"$primary_hostname", /* server_hostname */
73 NULL, /* server_keytab */
74 US"smtp", /* server_service */
77 /* "Globals" for managing the heimdal_gssapi interface. */
79 /* Utility functions */
81 exim_heimdal_error_debug(const char *, krb5_context, krb5_error_code);
83 exim_gssapi_error_defer(uschar *, OM_uint32, OM_uint32, const char *, ...)
84 PRINTF_FUNCTION(4, 5);
86 #define EmptyBuf(buf) do { buf.value = NULL; buf.length = 0; } while (0)
89 /*************************************************
90 * Initialization entry point *
91 *************************************************/
93 /* Called for each instance, after its options have been read, to
94 enable consistency checks to be done, or anything else that needs
97 /* Heimdal provides a GSSAPI extension method for setting the keytab;
98 in the init, we mostly just use raw krb5 methods so that we can report
99 the keytab contents, for -D+auth debugging. */
102 auth_heimdal_gssapi_init(auth_instance *ablock)
104 krb5_context context;
106 krb5_kt_cursor cursor;
107 krb5_keytab_entry entry;
109 char *principal, *enctype_s;
110 const char *k_keytab_typed_name = NULL;
111 auth_heimdal_gssapi_options_block *ob =
112 (auth_heimdal_gssapi_options_block *)(ablock->options_block);
114 ablock->server = FALSE;
115 ablock->client = FALSE;
117 if (!ob->server_service || !*ob->server_service) {
118 HDEBUG(D_auth) debug_printf("heimdal: missing server_service\n");
122 krc = krb5_init_context(&context);
125 HDEBUG(D_auth) debug_printf("heimdal: failed to initialise krb5 context: %s\n",
130 if (ob->server_keytab) {
131 k_keytab_typed_name = CCS string_sprintf("file:%s", expand_string(ob->server_keytab));
132 HDEBUG(D_auth) debug_printf("heimdal: using keytab %s\n", k_keytab_typed_name);
133 krc = krb5_kt_resolve(context, k_keytab_typed_name, &keytab);
135 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_resolve", context, krc);
139 HDEBUG(D_auth) debug_printf("heimdal: using system default keytab\n");
140 krc = krb5_kt_default(context, &keytab);
142 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_default", context, krc);
148 /* http://www.h5l.org/manual/HEAD/krb5/krb5_keytab_intro.html */
149 krc = krb5_kt_start_seq_get(context, keytab, &cursor);
151 exim_heimdal_error_debug("krb5_kt_start_seq_get", context, krc);
153 while ((krc = krb5_kt_next_entry(context, keytab, &entry, &cursor)) == 0) {
154 principal = enctype_s = NULL;
155 krb5_unparse_name(context, entry.principal, &principal);
156 krb5_enctype_to_string(context, entry.keyblock.keytype, &enctype_s);
157 debug_printf("heimdal: keytab principal: %s vno=%d type=%s\n",
158 principal ? principal : "??",
160 enctype_s ? enctype_s : "??");
163 krb5_kt_free_entry(context, &entry);
165 krc = krb5_kt_end_seq_get(context, keytab, &cursor);
167 exim_heimdal_error_debug("krb5_kt_end_seq_get", context, krc);
171 krc = krb5_kt_close(context, keytab);
173 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_close", context, krc);
175 krb5_free_context(context);
177 /* RFC 4121 section 5.2, SHOULD support 64K input buffers */
178 if (big_buffer_size < (64 * 1024)) {
180 big_buffer_size = 64 * 1024;
181 newbuf = store_malloc(big_buffer_size);
182 store_free(big_buffer);
186 ablock->server = TRUE;
191 exim_heimdal_error_debug(const char *label,
192 krb5_context context, krb5_error_code err)
195 kerrsc = krb5_get_error_message(context, err);
196 debug_printf("heimdal %s: %s\n", label, kerrsc ? kerrsc : "unknown error");
197 krb5_free_error_message(context, kerrsc);
200 /*************************************************
201 * Server entry point *
202 *************************************************/
204 /* For interface, see auths/README */
207 OM_uint32: portable type for unsigned int32
208 gss_buffer_desc / *gss_buffer_t: hold/point-to size_t .length & void *value
209 -- all strings/etc passed in should go through one of these
210 -- when allocated by gssapi, release with gss_release_buffer()
214 auth_heimdal_gssapi_server(auth_instance *ablock, uschar *initial_data)
216 gss_name_t gclient = GSS_C_NO_NAME;
217 gss_name_t gserver = GSS_C_NO_NAME;
218 gss_cred_id_t gcred = GSS_C_NO_CREDENTIAL;
219 gss_ctx_id_t gcontext = GSS_C_NO_CONTEXT;
220 uschar *ex_server_str;
221 gss_buffer_desc gbufdesc = GSS_C_EMPTY_BUFFER;
222 gss_buffer_desc gbufdesc_in = GSS_C_EMPTY_BUFFER;
223 gss_buffer_desc gbufdesc_out = GSS_C_EMPTY_BUFFER;
225 OM_uint32 maj_stat, min_stat;
226 int step, error_out, i;
227 uschar *tmp1, *tmp2, *from_client;
228 auth_heimdal_gssapi_options_block *ob =
229 (auth_heimdal_gssapi_options_block *)(ablock->options_block);
230 BOOL handled_empty_ir;
231 uschar *store_reset_point;
233 uschar sasl_config[4];
234 uschar requested_qop;
236 store_reset_point = store_get(0);
239 debug_printf("heimdal: initialising auth context for %s\n", ablock->name);
241 /* Construct our gss_name_t gserver describing ourselves */
242 tmp1 = expand_string(ob->server_service);
243 tmp2 = expand_string(ob->server_hostname);
244 ex_server_str = string_sprintf("%s@%s", tmp1, tmp2);
245 gbufdesc.value = (void *) ex_server_str;
246 gbufdesc.length = Ustrlen(ex_server_str);
247 maj_stat = gss_import_name(&min_stat,
248 &gbufdesc, GSS_C_NT_HOSTBASED_SERVICE, &gserver);
249 if (GSS_ERROR(maj_stat))
250 return exim_gssapi_error_defer(store_reset_point, maj_stat, min_stat,
251 "gss_import_name(%s)", CS gbufdesc.value);
253 /* Use a specific keytab, if specified */
254 if (ob->server_keytab) {
255 keytab = expand_string(ob->server_keytab);
256 maj_stat = gsskrb5_register_acceptor_identity(CCS keytab);
257 if (GSS_ERROR(maj_stat))
258 return exim_gssapi_error_defer(store_reset_point, maj_stat, min_stat,
259 "registering keytab \"%s\"", keytab);
261 debug_printf("heimdal: using keytab \"%s\"\n", keytab);
264 /* Acquire our credentials */
265 maj_stat = gss_acquire_cred(&min_stat,
266 gserver, /* desired name */
268 GSS_C_NULL_OID_SET, /* desired mechs */
269 GSS_C_ACCEPT, /* cred usage */
271 NULL /* actual mechs */,
272 NULL /* time rec */);
273 if (GSS_ERROR(maj_stat))
274 return exim_gssapi_error_defer(store_reset_point, maj_stat, min_stat,
275 "gss_acquire_cred(%s)", ex_server_str);
277 maj_stat = gss_release_name(&min_stat, &gserver);
279 HDEBUG(D_auth) debug_printf("heimdal: have server credentials.\n");
281 /* Loop talking to client */
283 from_client = initial_data;
284 handled_empty_ir = FALSE;
287 /* buffer sizes: auth_get_data() uses big_buffer, which we grow per
288 GSSAPI RFC in _init, if needed, to meet the SHOULD size of 64KB.
289 (big_buffer starts life at the MUST size of 16KB). */
292 0: getting initial data from client to feed into GSSAPI
293 1: iterating for as long as GSS_S_CONTINUE_NEEDED
294 2: GSS_S_COMPLETE, SASL wrapping for authz and qop to send to client
295 3: unpick final auth message from client
296 4: break/finish (non-step)
301 if (!from_client || *from_client == '\0') {
302 if (handled_empty_ir) {
303 HDEBUG(D_auth) debug_printf("gssapi: repeated empty input, grr.\n");
307 HDEBUG(D_auth) debug_printf("gssapi: missing initial response, nudging.\n");
308 error_out = auth_get_data(&from_client, US"", 0);
311 handled_empty_ir = TRUE;
315 /* We should now have the opening data from the client, base64-encoded. */
317 HDEBUG(D_auth) debug_printf("heimdal: have initial client data\n");
321 gbufdesc_in.length = auth_b64decode(from_client, USS &gbufdesc_in.value);
323 maj_stat = gss_release_name(&min_stat, &gclient);
324 gclient = GSS_C_NO_NAME;
326 maj_stat = gss_accept_sec_context(&min_stat,
327 &gcontext, /* context handle */
328 gcred, /* acceptor cred handle */
329 &gbufdesc_in, /* input from client */
330 GSS_C_NO_CHANNEL_BINDINGS, /* XXX fixme: use the channel bindings from GnuTLS */
331 &gclient, /* client identifier */
332 &mech_type, /* mechanism in use */
333 &gbufdesc_out, /* output to send to client */
334 NULL, /* return flags */
336 NULL /* delegated cred_handle */
338 if (GSS_ERROR(maj_stat)) {
339 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
340 "gss_accept_sec_context()");
344 if (&gbufdesc_out.length != 0) {
345 error_out = auth_get_data(&from_client,
346 gbufdesc_out.value, gbufdesc_out.length);
350 gss_release_buffer(&min_stat, &gbufdesc_out);
351 EmptyBuf(gbufdesc_out);
353 if (maj_stat == GSS_S_COMPLETE) {
355 HDEBUG(D_auth) debug_printf("heimdal: GSS complete\n");
357 HDEBUG(D_auth) debug_printf("heimdal: need more data\n");
362 memset(sasl_config, 0xFF, 4);
363 /* draft-ietf-sasl-gssapi-06.txt defines bitmasks for first octet
364 0x01 No security layer
365 0x02 Integrity protection
366 0x04 Confidentiality protection
368 The remaining three octets are the maximum buffer size for wrapped
370 sasl_config[0] = 0x01; /* Exim does not wrap/unwrap SASL layers after auth */
371 gbufdesc.value = (void *) sasl_config;
373 maj_stat = gss_wrap(&min_stat,
375 0, /* conf_req_flag: integrity only */
376 GSS_C_QOP_DEFAULT, /* qop requested */
377 &gbufdesc, /* message to protect */
378 NULL, /* conf_state: no confidentiality applied */
379 &gbufdesc_out /* output buffer */
381 if (GSS_ERROR(maj_stat)) {
382 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
383 "gss_wrap(SASL state after auth)");
388 HDEBUG(D_auth) debug_printf("heimdal SASL: requesting QOP with no security layers\n");
390 error_out = auth_get_data(&from_client,
391 gbufdesc_out.value, gbufdesc_out.length);
395 gss_release_buffer(&min_stat, &gbufdesc_out);
396 EmptyBuf(gbufdesc_out);
401 gbufdesc_in.length = auth_b64decode(from_client, USS &gbufdesc_in.value);
402 maj_stat = gss_unwrap(&min_stat,
404 &gbufdesc_in, /* data from client */
405 &gbufdesc_out, /* results */
406 NULL, /* conf state */
409 if (GSS_ERROR(maj_stat)) {
410 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
411 "gss_unwrap(final SASL message from client)");
415 if (gbufdesc_out.length < 4) {
417 debug_printf("gssapi: final message too short; "
418 "need flags, buf sizes and optional authzid\n");
423 requested_qop = (CS gbufdesc_out.value)[0];
424 if ((requested_qop & 0x01) == 0) {
426 debug_printf("gssapi: client requested security layers (%x)\n",
427 (unsigned int) requested_qop);
432 for (i = 0; i < AUTH_VARS; i++) auth_vars[i] = NULL;
436 The SASL provided identifier is an unverified authzid.
437 GSSAPI provides us with a verified identifier, but it might be empty
441 /* $auth2 is authzid requested at SASL layer */
442 if (gbufdesc_out.length > 4) {
443 expand_nlength[2] = gbufdesc_out.length - 4;
444 auth_vars[1] = expand_nstring[2] =
445 string_copyn((US gbufdesc_out.value) + 4, expand_nlength[2]);
449 gss_release_buffer(&min_stat, &gbufdesc_out);
450 EmptyBuf(gbufdesc_out);
452 /* $auth1 is GSSAPI display name */
453 maj_stat = gss_display_name(&min_stat,
457 if (GSS_ERROR(maj_stat)) {
458 auth_vars[1] = expand_nstring[2] = NULL;
460 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
461 "gss_display_name(client identifier)");
466 expand_nlength[1] = gbufdesc_out.length;
467 auth_vars[0] = expand_nstring[1] =
468 string_copyn(gbufdesc_out.value, gbufdesc_out.length);
470 if (expand_nmax == 0) { /* should be: authzid was empty */
472 expand_nlength[2] = expand_nlength[1];
473 auth_vars[1] = expand_nstring[2] = string_copyn(expand_nstring[1], expand_nlength[1]);
475 debug_printf("heimdal SASL: empty authzid, set to dup of GSSAPI display name\n");
479 debug_printf("heimdal SASL: happy with client request\n"
480 " auth1 (verified GSSAPI display-name): \"%s\"\n"
481 " auth2 (unverified SASL requested authzid): \"%s\"\n",
482 auth_vars[0], auth_vars[1]);
492 maj_stat = gss_release_cred(&min_stat, &gcred);
494 gss_release_name(&min_stat, &gclient);
495 gclient = GSS_C_NO_NAME;
497 if (gbufdesc_out.length) {
498 gss_release_buffer(&min_stat, &gbufdesc_out);
499 EmptyBuf(gbufdesc_out);
501 if (gcontext != GSS_C_NO_CONTEXT) {
502 gss_delete_sec_context(&min_stat, &gcontext, GSS_C_NO_BUFFER);
505 store_reset(store_reset_point);
510 /* Auth succeeded, check server_condition */
511 return auth_check_serv_cond(ablock);
516 exim_gssapi_error_defer(uschar *store_reset_point,
517 OM_uint32 major, OM_uint32 minor,
518 const char *format, ...)
521 uschar buffer[STRING_SPRINTF_BUFFER_SIZE];
522 OM_uint32 maj_stat, min_stat;
523 OM_uint32 msgcontext = 0;
524 gss_buffer_desc status_string;
526 va_start(ap, format);
527 if (!string_vformat(buffer, sizeof(buffer), format, ap))
528 log_write(0, LOG_MAIN|LOG_PANIC_DIE,
529 "exim_gssapi_error_defer expansion larger than %d",
533 auth_defer_msg = NULL;
536 maj_stat = gss_display_status(&min_stat,
537 major, GSS_C_GSS_CODE, GSS_C_NO_OID,
538 &msgcontext, &status_string);
540 if (auth_defer_msg == NULL) {
541 auth_defer_msg = string_copy(US status_string.value);
544 HDEBUG(D_auth) debug_printf("heimdal %s: %.*s\n",
545 buffer, (int)status_string.length, CS status_string.value);
546 gss_release_buffer(&min_stat, &status_string);
548 } while (msgcontext != 0);
550 if (store_reset_point)
551 store_reset(store_reset_point);
556 /*************************************************
557 * Client entry point *
558 *************************************************/
560 /* For interface, see auths/README */
563 auth_heimdal_gssapi_client(
564 auth_instance *ablock, /* authenticator block */
565 smtp_inblock *inblock, /* connection inblock */
566 smtp_outblock *outblock, /* connection outblock */
567 int timeout, /* command timeout */
568 uschar *buffer, /* buffer for reading response */
569 int buffsize) /* size of buffer */
572 debug_printf("Client side NOT IMPLEMENTED: you should not see this!\n");
573 /* NOT IMPLEMENTED */
577 /*************************************************
579 *************************************************/
582 auth_heimdal_gssapi_version_report(FILE *f)
584 /* No build-time constants available unless we link against libraries at
585 build-time and export the result as a string into a header ourselves. */
586 fprintf(f, "Library version: Heimdal: Runtime: %s\n"
588 heimdal_version, heimdal_long_version);
591 #endif /* AUTH_HEIMDAL_GSSAPI */
593 /* End of heimdal_gssapi.c */