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 < 5) {
417 debug_printf("gssapi: final message too short; "
418 "need flags, buf sizes and 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.
440 /* $auth2 is authzid requested at SASL layer */
441 expand_nlength[2] = gbufdesc_out.length - 4;
442 auth_vars[1] = expand_nstring[2] =
443 string_copyn((US gbufdesc_out.value) + 4, expand_nlength[2]);
446 gss_release_buffer(&min_stat, &gbufdesc_out);
447 EmptyBuf(gbufdesc_out);
449 /* $auth1 is GSSAPI display name */
450 maj_stat = gss_display_name(&min_stat,
454 if (GSS_ERROR(maj_stat)) {
455 auth_vars[1] = expand_nstring[2] = NULL;
457 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
458 "gss_display_name(client identifier)");
463 expand_nlength[1] = gbufdesc_out.length;
464 auth_vars[0] = expand_nstring[1] =
465 string_copyn(gbufdesc_out.value, gbufdesc_out.length);
468 debug_printf("heimdal SASL: happy with client request\n"
469 " auth1 (verified GSSAPI display-name): \"%s\"\n"
470 " auth2 (unverified SASL requested authzid): \"%s\"\n",
471 auth_vars[0], auth_vars[1]);
481 maj_stat = gss_release_cred(&min_stat, &gcred);
483 gss_release_name(&min_stat, &gclient);
484 gclient = GSS_C_NO_NAME;
486 if (gbufdesc_out.length) {
487 gss_release_buffer(&min_stat, &gbufdesc_out);
488 EmptyBuf(gbufdesc_out);
490 if (gcontext != GSS_C_NO_CONTEXT) {
491 gss_delete_sec_context(&min_stat, &gcontext, GSS_C_NO_BUFFER);
494 store_reset(store_reset_point);
499 /* Auth succeeded, check server_condition */
500 return auth_check_serv_cond(ablock);
505 exim_gssapi_error_defer(uschar *store_reset_point,
506 OM_uint32 major, OM_uint32 minor,
507 const char *format, ...)
510 uschar buffer[STRING_SPRINTF_BUFFER_SIZE];
511 OM_uint32 maj_stat, min_stat;
512 OM_uint32 msgcontext = 0;
513 gss_buffer_desc status_string;
515 va_start(ap, format);
516 if (!string_vformat(buffer, sizeof(buffer), format, ap))
517 log_write(0, LOG_MAIN|LOG_PANIC_DIE,
518 "exim_gssapi_error_defer expansion larger than %d",
522 auth_defer_msg = NULL;
525 maj_stat = gss_display_status(&min_stat,
526 major, GSS_C_GSS_CODE, GSS_C_NO_OID,
527 &msgcontext, &status_string);
529 if (auth_defer_msg == NULL) {
530 auth_defer_msg = string_copy(US status_string.value);
533 HDEBUG(D_auth) debug_printf("heimdal %s: %.*s\n",
534 buffer, (int)status_string.length, CS status_string.value);
535 gss_release_buffer(&min_stat, &status_string);
537 } while (msgcontext != 0);
539 if (store_reset_point)
540 store_reset(store_reset_point);
545 /*************************************************
546 * Client entry point *
547 *************************************************/
549 /* For interface, see auths/README */
552 auth_heimdal_gssapi_client(
553 auth_instance *ablock, /* authenticator block */
554 smtp_inblock *inblock, /* connection inblock */
555 smtp_outblock *outblock, /* connection outblock */
556 int timeout, /* command timeout */
557 uschar *buffer, /* buffer for reading response */
558 int buffsize) /* size of buffer */
561 debug_printf("Client side NOT IMPLEMENTED: you should not see this!\n");
562 /* NOT IMPLEMENTED */
566 /*************************************************
568 *************************************************/
571 auth_heimdal_gssapi_version_report(FILE *f)
573 /* No build-time constants available unless we link against libraries at
574 build-time and export the result as a string into a header ourselves. */
575 fprintf(f, "Library version: Heimdal: Runtime: %s\n"
577 heimdal_version, heimdal_long_version);
580 #endif /* AUTH_HEIMDAL_GSSAPI */
582 /* End of heimdal_gssapi.c */