Merge branch 'master' of git://git.exim.org/exim
[exim.git] / src / src / auths / heimdal_gssapi.c
1 /*************************************************
2 * Exim - an Internet mail transport agent *
3 *************************************************/
4
5 /* Copyright (c) University of Cambridge 1995 - 2012 */
6 /* See the file NOTICE for conditions of use and distribution. */
7
8 /* Copyright (c) Twitter Inc 2012
9 Author: Phil Pennock <pdp@exim.org> */
10 /* Copyright (c) Phil Pennock 2012 */
11
12 /* Interface to Heimdal library for GSSAPI authentication. */
13
14 /* Naming and rationale
15
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
19 Kerberos.
20
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.
26
27 Without rename, we could add an option for GS2 support in the future.
28 */
29
30 /* Sources
31
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().
40 */
41
42 #include "../exim.h"
43
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); }
47 #else
48
49 #include <gssapi/gssapi.h>
50 #include <gssapi/gssapi_krb5.h>
51
52 /* for the _init debugging */
53 #include <krb5.h>
54
55 #include "heimdal_gssapi.h"
56
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)) }
65 };
66
67 int auth_heimdal_gssapi_options_count =
68 sizeof(auth_heimdal_gssapi_options)/sizeof(optionlist);
69
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 */
75 };
76
77 /* "Globals" for managing the heimdal_gssapi interface. */
78
79 /* Utility functions */
80 static void
81 exim_heimdal_error_debug(const char *, krb5_context, krb5_error_code);
82 static int
83 exim_gssapi_error_defer(uschar *, OM_uint32, OM_uint32, const char *, ...)
84 PRINTF_FUNCTION(4, 5);
85
86 #define EmptyBuf(buf) do { buf.value = NULL; buf.length = 0; } while (0)
87
88
89 /*************************************************
90 * Initialization entry point *
91 *************************************************/
92
93 /* Called for each instance, after its options have been read, to
94 enable consistency checks to be done, or anything else that needs
95 to be set up. */
96
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. */
100
101 void
102 auth_heimdal_gssapi_init(auth_instance *ablock)
103 {
104 krb5_context context;
105 krb5_keytab keytab;
106 krb5_kt_cursor cursor;
107 krb5_keytab_entry entry;
108 krb5_error_code krc;
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);
113
114 ablock->server = FALSE;
115 ablock->client = FALSE;
116
117 if (!ob->server_service || !*ob->server_service) {
118 HDEBUG(D_auth) debug_printf("heimdal: missing server_service\n");
119 return;
120 }
121
122 krc = krb5_init_context(&context);
123 if (krc != 0) {
124 int kerr = errno;
125 HDEBUG(D_auth) debug_printf("heimdal: failed to initialise krb5 context: %s\n",
126 strerror(kerr));
127 return;
128 }
129
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);
134 if (krc) {
135 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_resolve", context, krc);
136 return;
137 }
138 } else {
139 HDEBUG(D_auth) debug_printf("heimdal: using system default keytab\n");
140 krc = krb5_kt_default(context, &keytab);
141 if (krc) {
142 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_default", context, krc);
143 return;
144 }
145 }
146
147 HDEBUG(D_auth) {
148 /* http://www.h5l.org/manual/HEAD/krb5/krb5_keytab_intro.html */
149 krc = krb5_kt_start_seq_get(context, keytab, &cursor);
150 if (krc)
151 exim_heimdal_error_debug("krb5_kt_start_seq_get", context, krc);
152 else {
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 : "??",
159 entry.vno,
160 enctype_s ? enctype_s : "??");
161 free(principal);
162 free(enctype_s);
163 krb5_kt_free_entry(context, &entry);
164 }
165 krc = krb5_kt_end_seq_get(context, keytab, &cursor);
166 if (krc)
167 exim_heimdal_error_debug("krb5_kt_end_seq_get", context, krc);
168 }
169 }
170
171 krc = krb5_kt_close(context, keytab);
172 if (krc)
173 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_close", context, krc);
174
175 krb5_free_context(context);
176
177 /* RFC 4121 section 5.2, SHOULD support 64K input buffers */
178 if (big_buffer_size < (64 * 1024)) {
179 uschar *newbuf;
180 big_buffer_size = 64 * 1024;
181 newbuf = store_malloc(big_buffer_size);
182 store_free(big_buffer);
183 big_buffer = newbuf;
184 }
185
186 ablock->server = TRUE;
187 }
188
189
190 static void
191 exim_heimdal_error_debug(const char *label,
192 krb5_context context, krb5_error_code err)
193 {
194 const char *kerrsc;
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);
198 }
199
200 /*************************************************
201 * Server entry point *
202 *************************************************/
203
204 /* For interface, see auths/README */
205
206 /* GSSAPI notes:
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()
211 */
212
213 int
214 auth_heimdal_gssapi_server(auth_instance *ablock, uschar *initial_data)
215 {
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;
224 gss_OID mech_type;
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;
232 uschar *keytab;
233 uschar sasl_config[4];
234 uschar requested_qop;
235
236 store_reset_point = store_get(0);
237
238 HDEBUG(D_auth)
239 debug_printf("heimdal: initialising auth context for %s\n", ablock->name);
240
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);
252
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);
260 HDEBUG(D_auth)
261 debug_printf("heimdal: using keytab \"%s\"\n", keytab);
262 }
263
264 /* Acquire our credentials */
265 maj_stat = gss_acquire_cred(&min_stat,
266 gserver, /* desired name */
267 0, /* time */
268 GSS_C_NULL_OID_SET, /* desired mechs */
269 GSS_C_ACCEPT, /* cred usage */
270 &gcred, /* handle */
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);
276
277 maj_stat = gss_release_name(&min_stat, &gserver);
278
279 HDEBUG(D_auth) debug_printf("heimdal: have server credentials.\n");
280
281 /* Loop talking to client */
282 step = 0;
283 from_client = initial_data;
284 handled_empty_ir = FALSE;
285 error_out = OK;
286
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). */
290
291 /* step values
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)
297 */
298 while (step < 4) {
299 switch (step) {
300 case 0:
301 if (!from_client || *from_client == '\0') {
302 if (handled_empty_ir) {
303 HDEBUG(D_auth) debug_printf("gssapi: repeated empty input, grr.\n");
304 error_out = BAD64;
305 goto ERROR_OUT;
306 } else {
307 HDEBUG(D_auth) debug_printf("gssapi: missing initial response, nudging.\n");
308 error_out = auth_get_data(&from_client, US"", 0);
309 if (error_out != OK)
310 goto ERROR_OUT;
311 handled_empty_ir = TRUE;
312 continue;
313 }
314 }
315 /* We should now have the opening data from the client, base64-encoded. */
316 step += 1;
317 HDEBUG(D_auth) debug_printf("heimdal: have initial client data\n");
318 break;
319
320 case 1:
321 gbufdesc_in.length = auth_b64decode(from_client, USS &gbufdesc_in.value);
322 if (gclient) {
323 maj_stat = gss_release_name(&min_stat, &gclient);
324 gclient = GSS_C_NO_NAME;
325 }
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 */
335 NULL, /* time rec */
336 NULL /* delegated cred_handle */
337 );
338 if (GSS_ERROR(maj_stat)) {
339 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
340 "gss_accept_sec_context()");
341 error_out = FAIL;
342 goto ERROR_OUT;
343 }
344 if (&gbufdesc_out.length != 0) {
345 error_out = auth_get_data(&from_client,
346 gbufdesc_out.value, gbufdesc_out.length);
347 if (error_out != OK)
348 goto ERROR_OUT;
349
350 gss_release_buffer(&min_stat, &gbufdesc_out);
351 EmptyBuf(gbufdesc_out);
352 }
353 if (maj_stat == GSS_S_COMPLETE) {
354 step += 1;
355 HDEBUG(D_auth) debug_printf("heimdal: GSS complete\n");
356 } else {
357 HDEBUG(D_auth) debug_printf("heimdal: need more data\n");
358 }
359 break;
360
361 case 2:
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
367
368 The remaining three octets are the maximum buffer size for wrapped
369 content. */
370 sasl_config[0] = 0x01; /* Exim does not wrap/unwrap SASL layers after auth */
371 gbufdesc.value = (void *) sasl_config;
372 gbufdesc.length = 4;
373 maj_stat = gss_wrap(&min_stat,
374 gcontext,
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 */
380 );
381 if (GSS_ERROR(maj_stat)) {
382 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
383 "gss_wrap(SASL state after auth)");
384 error_out = FAIL;
385 goto ERROR_OUT;
386 }
387
388 HDEBUG(D_auth) debug_printf("heimdal SASL: requesting QOP with no security layers\n");
389
390 error_out = auth_get_data(&from_client,
391 gbufdesc_out.value, gbufdesc_out.length);
392 if (error_out != OK)
393 goto ERROR_OUT;
394
395 gss_release_buffer(&min_stat, &gbufdesc_out);
396 EmptyBuf(gbufdesc_out);
397 step += 1;
398 break;
399
400 case 3:
401 gbufdesc_in.length = auth_b64decode(from_client, USS &gbufdesc_in.value);
402 maj_stat = gss_unwrap(&min_stat,
403 gcontext,
404 &gbufdesc_in, /* data from client */
405 &gbufdesc_out, /* results */
406 NULL, /* conf state */
407 NULL /* qop state */
408 );
409 if (GSS_ERROR(maj_stat)) {
410 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
411 "gss_unwrap(final SASL message from client)");
412 error_out = FAIL;
413 goto ERROR_OUT;
414 }
415 if (gbufdesc_out.length < 4) {
416 HDEBUG(D_auth)
417 debug_printf("gssapi: final message too short; "
418 "need flags, buf sizes and optional authzid\n");
419 error_out = FAIL;
420 goto ERROR_OUT;
421 }
422
423 requested_qop = (CS gbufdesc_out.value)[0];
424 if ((requested_qop & 0x01) == 0) {
425 HDEBUG(D_auth)
426 debug_printf("gssapi: client requested security layers (%x)\n",
427 (unsigned int) requested_qop);
428 error_out = FAIL;
429 goto ERROR_OUT;
430 }
431
432 for (i = 0; i < AUTH_VARS; i++) auth_vars[i] = NULL;
433 expand_nmax = 0;
434
435 /* Identifiers:
436 The SASL provided identifier is an unverified authzid.
437 GSSAPI provides us with a verified identifier, but it might be empty
438 for some clients.
439 */
440
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]);
446 expand_nmax = 2;
447 }
448
449 gss_release_buffer(&min_stat, &gbufdesc_out);
450 EmptyBuf(gbufdesc_out);
451
452 /* $auth1 is GSSAPI display name */
453 maj_stat = gss_display_name(&min_stat,
454 gclient,
455 &gbufdesc_out,
456 &mech_type);
457 if (GSS_ERROR(maj_stat)) {
458 auth_vars[1] = expand_nstring[2] = NULL;
459 expand_nmax = 0;
460 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
461 "gss_display_name(client identifier)");
462 error_out = FAIL;
463 goto ERROR_OUT;
464 }
465
466 expand_nlength[1] = gbufdesc_out.length;
467 auth_vars[0] = expand_nstring[1] =
468 string_copyn(gbufdesc_out.value, gbufdesc_out.length);
469
470 if (expand_nmax == 0) { /* should be: authzid was empty */
471 expand_nmax = 2;
472 expand_nlength[2] = expand_nlength[1];
473 auth_vars[1] = expand_nstring[2] = string_copyn(expand_nstring[1], expand_nlength[1]);
474 HDEBUG(D_auth)
475 debug_printf("heimdal SASL: empty authzid, set to dup of GSSAPI display name\n");
476 }
477
478 HDEBUG(D_auth)
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]);
483
484 step += 1;
485 break;
486
487 } /* switch */
488 } /* while step */
489
490
491 ERROR_OUT:
492 maj_stat = gss_release_cred(&min_stat, &gcred);
493 if (gclient) {
494 gss_release_name(&min_stat, &gclient);
495 gclient = GSS_C_NO_NAME;
496 }
497 if (gbufdesc_out.length) {
498 gss_release_buffer(&min_stat, &gbufdesc_out);
499 EmptyBuf(gbufdesc_out);
500 }
501 if (gcontext != GSS_C_NO_CONTEXT) {
502 gss_delete_sec_context(&min_stat, &gcontext, GSS_C_NO_BUFFER);
503 }
504
505 store_reset(store_reset_point);
506
507 if (error_out != OK)
508 return error_out;
509
510 /* Auth succeeded, check server_condition */
511 return auth_check_serv_cond(ablock);
512 }
513
514
515 static int
516 exim_gssapi_error_defer(uschar *store_reset_point,
517 OM_uint32 major, OM_uint32 minor,
518 const char *format, ...)
519 {
520 va_list ap;
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;
525
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 %lu",
530 sizeof(buffer));
531 va_end(ap);
532
533 auth_defer_msg = NULL;
534
535 do {
536 maj_stat = gss_display_status(&min_stat,
537 major, GSS_C_GSS_CODE, GSS_C_NO_OID,
538 &msgcontext, &status_string);
539
540 if (auth_defer_msg == NULL) {
541 auth_defer_msg = string_copy(US status_string.value);
542 }
543
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);
547
548 } while (msgcontext != 0);
549
550 if (store_reset_point)
551 store_reset(store_reset_point);
552 return DEFER;
553 }
554
555
556 /*************************************************
557 * Client entry point *
558 *************************************************/
559
560 /* For interface, see auths/README */
561
562 int
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 */
570 {
571 HDEBUG(D_auth)
572 debug_printf("Client side NOT IMPLEMENTED: you should not see this!\n");
573 /* NOT IMPLEMENTED */
574 return FAIL;
575 }
576
577 /*************************************************
578 * Diagnostic API *
579 *************************************************/
580
581 void
582 auth_heimdal_gssapi_version_report(FILE *f)
583 {
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"
587 " Build Info: %s\n",
588 heimdal_version, heimdal_long_version);
589 }
590
591 #endif /* AUTH_HEIMDAL_GSSAPI */
592
593 /* End of heimdal_gssapi.c */