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_realm", opt_stringptr,
64 (void *)(offsetof(auth_heimdal_gssapi_options_block, server_realm)) },
65 { "server_service", opt_stringptr,
66 (void *)(offsetof(auth_heimdal_gssapi_options_block, server_service)) }
69 int auth_heimdal_gssapi_options_count =
70 sizeof(auth_heimdal_gssapi_options)/sizeof(optionlist);
72 /* Defaults for the authenticator-specific options. */
73 auth_heimdal_gssapi_options_block auth_heimdal_gssapi_option_defaults = {
74 US"$primary_hostname", /* server_hostname */
75 NULL, /* server_keytab */
76 NULL, /* server_realm */
77 US"smtp", /* server_service */
80 /* "Globals" for managing the heimdal_gssapi interface. */
82 /* Utility functions */
84 exim_heimdal_error_debug(const char *, krb5_context, krb5_error_code);
86 exim_gssapi_error_defer(uschar *, OM_uint32, OM_uint32, const char *, ...)
87 PRINTF_FUNCTION(4, 5);
89 #define EmptyBuf(buf) do { buf.value = NULL; buf.length = 0; } while (0)
92 /*************************************************
93 * Initialization entry point *
94 *************************************************/
96 /* Called for each instance, after its options have been read, to
97 enable consistency checks to be done, or anything else that needs
100 /* Heimdal provides a GSSAPI extension method for setting the keytab;
101 in the init, we mostly just use raw krb5 methods so that we can report
102 the keytab contents, for -D+auth debugging. */
105 auth_heimdal_gssapi_init(auth_instance *ablock)
107 krb5_context context;
109 krb5_kt_cursor cursor;
110 krb5_keytab_entry entry;
112 char *principal, *enctype_s;
113 const char *k_keytab_typed_name = NULL;
114 auth_heimdal_gssapi_options_block *ob =
115 (auth_heimdal_gssapi_options_block *)(ablock->options_block);
117 ablock->server = FALSE;
118 ablock->client = FALSE;
120 if (!ob->server_service || !*ob->server_service) {
121 HDEBUG(D_auth) debug_printf("heimdal: missing server_service\n");
125 krc = krb5_init_context(&context);
128 HDEBUG(D_auth) debug_printf("heimdal: failed to initialise krb5 context: %s\n",
133 if (ob->server_keytab) {
134 k_keytab_typed_name = CCS string_sprintf("file:%s", expand_string(ob->server_keytab));
135 HDEBUG(D_auth) debug_printf("heimdal: using keytab %s\n", k_keytab_typed_name);
136 krc = krb5_kt_resolve(context, k_keytab_typed_name, &keytab);
138 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_resolve", context, krc);
142 HDEBUG(D_auth) debug_printf("heimdal: using system default keytab\n");
143 krc = krb5_kt_default(context, &keytab);
145 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_default", context, krc);
151 /* http://www.h5l.org/manual/HEAD/krb5/krb5_keytab_intro.html */
152 krc = krb5_kt_start_seq_get(context, keytab, &cursor);
154 exim_heimdal_error_debug("krb5_kt_start_seq_get", context, krc);
156 while ((krc = krb5_kt_next_entry(context, keytab, &entry, &cursor)) == 0) {
157 principal = enctype_s = NULL;
158 krb5_unparse_name(context, entry.principal, &principal);
159 krb5_enctype_to_string(context, entry.keyblock.keytype, &enctype_s);
160 debug_printf("heimdal: keytab principal: %s vno=%d type=%s\n",
161 principal ? principal : "??",
163 enctype_s ? enctype_s : "??");
166 krb5_kt_free_entry(context, &entry);
168 krc = krb5_kt_end_seq_get(context, keytab, &cursor);
170 exim_heimdal_error_debug("krb5_kt_end_seq_get", context, krc);
174 krc = krb5_kt_close(context, keytab);
176 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_close", context, krc);
178 krb5_free_context(context);
180 /* RFC 4121 section 5.2, SHOULD support 64K input buffers */
181 if (big_buffer_size < (64 * 1024)) {
183 big_buffer_size = 64 * 1024;
184 newbuf = store_malloc(big_buffer_size);
185 store_free(big_buffer);
189 ablock->server = TRUE;
194 exim_heimdal_error_debug(const char *label,
195 krb5_context context, krb5_error_code err)
198 kerrsc = krb5_get_error_message(context, err);
199 debug_printf("heimdal %s: %s\n", label, kerrsc ? kerrsc : "unknown error");
200 krb5_free_error_message(context, kerrsc);
203 /*************************************************
204 * Server entry point *
205 *************************************************/
207 /* For interface, see auths/README */
210 OM_uint32: portable type for unsigned int32
211 gss_buffer_desc / *gss_buffer_t: hold/point-to size_t .length & void *value
212 -- all strings/etc passed in should go through one of these
213 -- when allocated by gssapi, release with gss_release_buffer()
217 auth_heimdal_gssapi_server(auth_instance *ablock, uschar *initial_data)
219 gss_name_t gclient = GSS_C_NO_NAME;
220 gss_name_t gserver = GSS_C_NO_NAME;
221 gss_cred_id_t gcred = GSS_C_NO_CREDENTIAL;
222 gss_ctx_id_t gcontext = GSS_C_NO_CONTEXT;
223 uschar *ex_server_str;
224 gss_buffer_desc gbufdesc = GSS_C_EMPTY_BUFFER;
225 gss_buffer_desc gbufdesc_in = GSS_C_EMPTY_BUFFER;
226 gss_buffer_desc gbufdesc_out = GSS_C_EMPTY_BUFFER;
228 OM_uint32 maj_stat, min_stat;
229 int step, error_out, i;
230 uschar *tmp1, *tmp2, *from_client;
231 auth_heimdal_gssapi_options_block *ob =
232 (auth_heimdal_gssapi_options_block *)(ablock->options_block);
233 BOOL handled_empty_ir;
234 uschar *store_reset_point;
236 uschar sasl_config[4];
237 uschar requested_qop;
239 store_reset_point = store_get(0);
242 debug_printf("heimdal: initialising auth context for %s\n", ablock->name);
244 /* Construct our gss_name_t gserver describing ourselves */
245 tmp1 = expand_string(ob->server_service);
246 tmp2 = expand_string(ob->server_hostname);
247 ex_server_str = string_sprintf("%s@%s", tmp1, tmp2);
248 gbufdesc.value = (void *) ex_server_str;
249 gbufdesc.length = Ustrlen(ex_server_str);
250 maj_stat = gss_import_name(&min_stat,
251 &gbufdesc, GSS_C_NT_HOSTBASED_SERVICE, &gserver);
252 if (GSS_ERROR(maj_stat))
253 return exim_gssapi_error_defer(store_reset_point, maj_stat, min_stat,
254 "gss_import_name(%s)", CS gbufdesc.value);
256 /* Use a specific keytab, if specified */
257 if (ob->server_keytab) {
258 keytab = expand_string(ob->server_keytab);
259 maj_stat = gsskrb5_register_acceptor_identity(CCS keytab);
260 if (GSS_ERROR(maj_stat))
261 return exim_gssapi_error_defer(store_reset_point, maj_stat, min_stat,
262 "registering keytab \"%s\"", keytab);
264 debug_printf("heimdal: using keytab \"%s\"\n", keytab);
267 /* Acquire our credentials */
268 maj_stat = gss_acquire_cred(&min_stat,
269 gserver, /* desired name */
271 GSS_C_NULL_OID_SET, /* desired mechs */
272 GSS_C_ACCEPT, /* cred usage */
274 NULL /* actual mechs */,
275 NULL /* time rec */);
276 if (GSS_ERROR(maj_stat))
277 return exim_gssapi_error_defer(store_reset_point, maj_stat, min_stat,
278 "gss_acquire_cred(%s)", ex_server_str);
280 maj_stat = gss_release_name(&min_stat, &gserver);
282 HDEBUG(D_auth) debug_printf("heimdal: have server credentials.\n");
284 /* Loop talking to client */
286 from_client = initial_data;
287 handled_empty_ir = FALSE;
290 /* buffer sizes: auth_get_data() uses big_buffer, which we grow per
291 GSSAPI RFC in _init, if needed, to meet the SHOULD size of 64KB.
292 (big_buffer starts life at the MUST size of 16KB). */
295 0: getting initial data from client to feed into GSSAPI
296 1: iterating for as long as GSS_S_CONTINUE_NEEDED
297 2: GSS_S_COMPLETE, SASL wrapping for authz and qop to send to client
298 3: unpick final auth message from client
299 4: break/finish (non-step)
304 if (!from_client || *from_client == '\0') {
305 if (handled_empty_ir) {
306 HDEBUG(D_auth) debug_printf("gssapi: repeated empty input, grr.\n");
310 HDEBUG(D_auth) debug_printf("gssapi: missing initial response, nudging.\n");
311 error_out = auth_get_data(&from_client, US"", 0);
314 handled_empty_ir = TRUE;
318 /* We should now have the opening data from the client, base64-encoded. */
320 HDEBUG(D_auth) debug_printf("heimdal: have initial client data\n");
324 gbufdesc_in.length = auth_b64decode(from_client, USS &gbufdesc_in.value);
326 maj_stat = gss_release_name(&min_stat, &gclient);
327 gclient = GSS_C_NO_NAME;
329 maj_stat = gss_accept_sec_context(&min_stat,
330 &gcontext, /* context handle */
331 gcred, /* acceptor cred handle */
332 &gbufdesc_in, /* input from client */
333 GSS_C_NO_CHANNEL_BINDINGS, /* XXX fixme: use the channel bindings from GnuTLS */
334 &gclient, /* client identifier */
335 &mech_type, /* mechanism in use */
336 &gbufdesc_out, /* output to send to client */
337 NULL, /* return flags */
339 NULL /* delegated cred_handle */
341 if (GSS_ERROR(maj_stat)) {
342 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
343 "gss_accept_sec_context()");
347 if (&gbufdesc_out.length != 0) {
348 error_out = auth_get_data(&from_client,
349 gbufdesc_out.value, gbufdesc_out.length);
353 gss_release_buffer(&min_stat, &gbufdesc_out);
354 EmptyBuf(gbufdesc_out);
356 if (maj_stat == GSS_S_COMPLETE) {
358 HDEBUG(D_auth) debug_printf("heimdal: GSS complete\n");
360 HDEBUG(D_auth) debug_printf("heimdal: need more data\n");
365 memset(sasl_config, 0xFF, 4);
366 /* draft-ietf-sasl-gssapi-06.txt defines bitmasks for first octet
367 0x01 No security layer
368 0x02 Integrity protection
369 0x04 Confidentiality protection
371 The remaining three octets are the maximum buffer size for wrapped
373 sasl_config[0] = 0x01; /* Exim does not wrap/unwrap SASL layers after auth */
374 gbufdesc.value = (void *) sasl_config;
376 maj_stat = gss_wrap(&min_stat,
378 0, /* conf_req_flag: integrity only */
379 GSS_C_QOP_DEFAULT, /* qop requested */
380 &gbufdesc, /* message to protect */
381 NULL, /* conf_state: no confidentiality applied */
382 &gbufdesc_out /* output buffer */
384 if (GSS_ERROR(maj_stat)) {
385 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
386 "gss_wrap(SASL state after auth)");
391 HDEBUG(D_auth) debug_printf("heimdal SASL: requesting QOP with no security layers\n");
393 error_out = auth_get_data(&from_client,
394 gbufdesc_out.value, gbufdesc_out.length);
398 gss_release_buffer(&min_stat, &gbufdesc_out);
399 EmptyBuf(gbufdesc_out);
404 gbufdesc_in.length = auth_b64decode(from_client, USS &gbufdesc_in.value);
405 maj_stat = gss_unwrap(&min_stat,
407 &gbufdesc_in, /* data from client */
408 &gbufdesc_out, /* results */
409 NULL, /* conf state */
412 if (GSS_ERROR(maj_stat)) {
413 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
414 "gss_unwrap(final SASL message from client)");
418 if (gbufdesc_out.length < 5) {
420 debug_printf("gssapi: final message too short; "
421 "need flags, buf sizes and authzid\n");
426 requested_qop = (CS gbufdesc_out.value)[0];
427 if ((requested_qop & 0x01) == 0) {
429 debug_printf("gssapi: client requested security layers (%x)\n",
430 (unsigned int) requested_qop);
435 for (i = 0; i < AUTH_VARS; i++) auth_vars[i] = NULL;
439 The SASL provided identifier is an unverified authzid.
440 GSSAPI provides us with a verified identifier.
443 /* $auth2 is authzid requested at SASL layer */
444 expand_nlength[2] = gbufdesc_out.length - 4;
445 auth_vars[1] = expand_nstring[2] =
446 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);
471 debug_printf("heimdal SASL: happy with client request\n"
472 " auth1 (verified GSSAPI display-name): \"%s\"\n"
473 " auth2 (unverified SASL requested authzid): \"%s\"\n",
474 auth_vars[0], auth_vars[1]);
484 maj_stat = gss_release_cred(&min_stat, &gcred);
486 gss_release_name(&min_stat, &gclient);
487 gclient = GSS_C_NO_NAME;
489 if (gbufdesc_out.length) {
490 gss_release_buffer(&min_stat, &gbufdesc_out);
491 EmptyBuf(gbufdesc_out);
493 if (gcontext != GSS_C_NO_CONTEXT) {
494 gss_delete_sec_context(&min_stat, &gcontext, GSS_C_NO_BUFFER);
497 store_reset(store_reset_point);
502 /* Auth succeeded, check server_condition */
503 return auth_check_serv_cond(ablock);
508 exim_gssapi_error_defer(uschar *store_reset_point,
509 OM_uint32 major, OM_uint32 minor,
510 const char *format, ...)
513 uschar buffer[STRING_SPRINTF_BUFFER_SIZE];
514 OM_uint32 maj_stat, min_stat;
515 OM_uint32 msgcontext = 0;
516 gss_buffer_desc status_string;
518 va_start(ap, format);
519 if (!string_vformat(buffer, sizeof(buffer), format, ap))
520 log_write(0, LOG_MAIN|LOG_PANIC_DIE,
521 "exim_gssapi_error_defer expansion larger than %d",
525 auth_defer_msg = NULL;
528 maj_stat = gss_display_status(&min_stat,
529 major, GSS_C_GSS_CODE, GSS_C_NO_OID,
530 &msgcontext, &status_string);
532 if (auth_defer_msg == NULL) {
533 auth_defer_msg = string_copy(US status_string.value);
536 HDEBUG(D_auth) debug_printf("heimdal %s: %.*s\n",
537 buffer, (int)status_string.length, CS status_string.value);
538 gss_release_buffer(&min_stat, &status_string);
540 } while (msgcontext != 0);
542 if (store_reset_point)
543 store_reset(store_reset_point);
548 /*************************************************
549 * Client entry point *
550 *************************************************/
552 /* For interface, see auths/README */
555 auth_heimdal_gssapi_client(
556 auth_instance *ablock, /* authenticator block */
557 smtp_inblock *inblock, /* connection inblock */
558 smtp_outblock *outblock, /* connection outblock */
559 int timeout, /* command timeout */
560 uschar *buffer, /* buffer for reading response */
561 int buffsize) /* size of buffer */
564 debug_printf("Client side NOT IMPLEMENTED: you should not see this!\n");
565 /* NOT IMPLEMENTED */
569 /*************************************************
571 *************************************************/
574 auth_heimdal_gssapi_version_report(FILE *f)
576 /* No build-time constants available unless we link against libraries at
577 build-time and export the result as a string into a header ourselves. */
578 fprintf(f, "Library version: Heimdal: Runtime: %s\n"
580 heimdal_version, heimdal_long_version);
583 #endif /* AUTH_HEIMDAL_GSSAPI */
585 /* End of heimdal_gssapi.c */