More compiler quietening.
[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);
47 static void dummy2(int x) { dummy(x-1); }
48 static void dummy(int x) { dummy2(x-1); }
49 #else
50
51 #include <gssapi/gssapi.h>
52 #include <gssapi/gssapi_krb5.h>
53
54 /* for the _init debugging */
55 #include <krb5.h>
56
57 #include "heimdal_gssapi.h"
58
59 /* Authenticator-specific options. */
60 optionlist auth_heimdal_gssapi_options[] = {
61 { "server_hostname", opt_stringptr,
62 (void *)(offsetof(auth_heimdal_gssapi_options_block, server_hostname)) },
63 { "server_keytab", opt_stringptr,
64 (void *)(offsetof(auth_heimdal_gssapi_options_block, server_keytab)) },
65 { "server_service", opt_stringptr,
66 (void *)(offsetof(auth_heimdal_gssapi_options_block, server_service)) }
67 };
68
69 int auth_heimdal_gssapi_options_count =
70 sizeof(auth_heimdal_gssapi_options)/sizeof(optionlist);
71
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 US"smtp", /* server_service */
77 };
78
79 /* "Globals" for managing the heimdal_gssapi interface. */
80
81 /* Utility functions */
82 static void
83 exim_heimdal_error_debug(const char *, krb5_context, krb5_error_code);
84 static int
85 exim_gssapi_error_defer(uschar *, OM_uint32, OM_uint32, const char *, ...)
86 PRINTF_FUNCTION(4, 5);
87
88 #define EmptyBuf(buf) do { buf.value = NULL; buf.length = 0; } while (0)
89
90
91 /*************************************************
92 * Initialization entry point *
93 *************************************************/
94
95 /* Called for each instance, after its options have been read, to
96 enable consistency checks to be done, or anything else that needs
97 to be set up. */
98
99 /* Heimdal provides a GSSAPI extension method for setting the keytab;
100 in the init, we mostly just use raw krb5 methods so that we can report
101 the keytab contents, for -D+auth debugging. */
102
103 void
104 auth_heimdal_gssapi_init(auth_instance *ablock)
105 {
106 krb5_context context;
107 krb5_keytab keytab;
108 krb5_kt_cursor cursor;
109 krb5_keytab_entry entry;
110 krb5_error_code krc;
111 char *principal, *enctype_s;
112 const char *k_keytab_typed_name = NULL;
113 auth_heimdal_gssapi_options_block *ob =
114 (auth_heimdal_gssapi_options_block *)(ablock->options_block);
115
116 ablock->server = FALSE;
117 ablock->client = FALSE;
118
119 if (!ob->server_service || !*ob->server_service) {
120 HDEBUG(D_auth) debug_printf("heimdal: missing server_service\n");
121 return;
122 }
123
124 krc = krb5_init_context(&context);
125 if (krc != 0) {
126 int kerr = errno;
127 HDEBUG(D_auth) debug_printf("heimdal: failed to initialise krb5 context: %s\n",
128 strerror(kerr));
129 return;
130 }
131
132 if (ob->server_keytab) {
133 k_keytab_typed_name = CCS string_sprintf("file:%s", expand_string(ob->server_keytab));
134 HDEBUG(D_auth) debug_printf("heimdal: using keytab %s\n", k_keytab_typed_name);
135 krc = krb5_kt_resolve(context, k_keytab_typed_name, &keytab);
136 if (krc) {
137 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_resolve", context, krc);
138 return;
139 }
140 } else {
141 HDEBUG(D_auth) debug_printf("heimdal: using system default keytab\n");
142 krc = krb5_kt_default(context, &keytab);
143 if (krc) {
144 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_default", context, krc);
145 return;
146 }
147 }
148
149 HDEBUG(D_auth) {
150 /* http://www.h5l.org/manual/HEAD/krb5/krb5_keytab_intro.html */
151 krc = krb5_kt_start_seq_get(context, keytab, &cursor);
152 if (krc)
153 exim_heimdal_error_debug("krb5_kt_start_seq_get", context, krc);
154 else {
155 while ((krc = krb5_kt_next_entry(context, keytab, &entry, &cursor)) == 0) {
156 principal = enctype_s = NULL;
157 krb5_unparse_name(context, entry.principal, &principal);
158 krb5_enctype_to_string(context, entry.keyblock.keytype, &enctype_s);
159 debug_printf("heimdal: keytab principal: %s vno=%d type=%s\n",
160 principal ? principal : "??",
161 entry.vno,
162 enctype_s ? enctype_s : "??");
163 free(principal);
164 free(enctype_s);
165 krb5_kt_free_entry(context, &entry);
166 }
167 krc = krb5_kt_end_seq_get(context, keytab, &cursor);
168 if (krc)
169 exim_heimdal_error_debug("krb5_kt_end_seq_get", context, krc);
170 }
171 }
172
173 krc = krb5_kt_close(context, keytab);
174 if (krc)
175 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_close", context, krc);
176
177 krb5_free_context(context);
178
179 /* RFC 4121 section 5.2, SHOULD support 64K input buffers */
180 if (big_buffer_size < (64 * 1024)) {
181 uschar *newbuf;
182 big_buffer_size = 64 * 1024;
183 newbuf = store_malloc(big_buffer_size);
184 store_free(big_buffer);
185 big_buffer = newbuf;
186 }
187
188 ablock->server = TRUE;
189 }
190
191
192 static void
193 exim_heimdal_error_debug(const char *label,
194 krb5_context context, krb5_error_code err)
195 {
196 const char *kerrsc;
197 kerrsc = krb5_get_error_message(context, err);
198 debug_printf("heimdal %s: %s\n", label, kerrsc ? kerrsc : "unknown error");
199 krb5_free_error_message(context, kerrsc);
200 }
201
202 /*************************************************
203 * Server entry point *
204 *************************************************/
205
206 /* For interface, see auths/README */
207
208 /* GSSAPI notes:
209 OM_uint32: portable type for unsigned int32
210 gss_buffer_desc / *gss_buffer_t: hold/point-to size_t .length & void *value
211 -- all strings/etc passed in should go through one of these
212 -- when allocated by gssapi, release with gss_release_buffer()
213 */
214
215 int
216 auth_heimdal_gssapi_server(auth_instance *ablock, uschar *initial_data)
217 {
218 gss_name_t gclient = GSS_C_NO_NAME;
219 gss_name_t gserver = GSS_C_NO_NAME;
220 gss_cred_id_t gcred = GSS_C_NO_CREDENTIAL;
221 gss_ctx_id_t gcontext = GSS_C_NO_CONTEXT;
222 uschar *ex_server_str;
223 gss_buffer_desc gbufdesc = GSS_C_EMPTY_BUFFER;
224 gss_buffer_desc gbufdesc_in = GSS_C_EMPTY_BUFFER;
225 gss_buffer_desc gbufdesc_out = GSS_C_EMPTY_BUFFER;
226 gss_OID mech_type;
227 OM_uint32 maj_stat, min_stat;
228 int step, error_out, i;
229 uschar *tmp1, *tmp2, *from_client;
230 auth_heimdal_gssapi_options_block *ob =
231 (auth_heimdal_gssapi_options_block *)(ablock->options_block);
232 BOOL handled_empty_ir;
233 uschar *store_reset_point;
234 uschar *keytab;
235 uschar sasl_config[4];
236 uschar requested_qop;
237
238 store_reset_point = store_get(0);
239
240 HDEBUG(D_auth)
241 debug_printf("heimdal: initialising auth context for %s\n", ablock->name);
242
243 /* Construct our gss_name_t gserver describing ourselves */
244 tmp1 = expand_string(ob->server_service);
245 tmp2 = expand_string(ob->server_hostname);
246 ex_server_str = string_sprintf("%s@%s", tmp1, tmp2);
247 gbufdesc.value = (void *) ex_server_str;
248 gbufdesc.length = Ustrlen(ex_server_str);
249 maj_stat = gss_import_name(&min_stat,
250 &gbufdesc, GSS_C_NT_HOSTBASED_SERVICE, &gserver);
251 if (GSS_ERROR(maj_stat))
252 return exim_gssapi_error_defer(store_reset_point, maj_stat, min_stat,
253 "gss_import_name(%s)", CS gbufdesc.value);
254
255 /* Use a specific keytab, if specified */
256 if (ob->server_keytab) {
257 keytab = expand_string(ob->server_keytab);
258 maj_stat = gsskrb5_register_acceptor_identity(CCS keytab);
259 if (GSS_ERROR(maj_stat))
260 return exim_gssapi_error_defer(store_reset_point, maj_stat, min_stat,
261 "registering keytab \"%s\"", keytab);
262 HDEBUG(D_auth)
263 debug_printf("heimdal: using keytab \"%s\"\n", keytab);
264 }
265
266 /* Acquire our credentials */
267 maj_stat = gss_acquire_cred(&min_stat,
268 gserver, /* desired name */
269 0, /* time */
270 GSS_C_NULL_OID_SET, /* desired mechs */
271 GSS_C_ACCEPT, /* cred usage */
272 &gcred, /* handle */
273 NULL /* actual mechs */,
274 NULL /* time rec */);
275 if (GSS_ERROR(maj_stat))
276 return exim_gssapi_error_defer(store_reset_point, maj_stat, min_stat,
277 "gss_acquire_cred(%s)", ex_server_str);
278
279 maj_stat = gss_release_name(&min_stat, &gserver);
280
281 HDEBUG(D_auth) debug_printf("heimdal: have server credentials.\n");
282
283 /* Loop talking to client */
284 step = 0;
285 from_client = initial_data;
286 handled_empty_ir = FALSE;
287 error_out = OK;
288
289 /* buffer sizes: auth_get_data() uses big_buffer, which we grow per
290 GSSAPI RFC in _init, if needed, to meet the SHOULD size of 64KB.
291 (big_buffer starts life at the MUST size of 16KB). */
292
293 /* step values
294 0: getting initial data from client to feed into GSSAPI
295 1: iterating for as long as GSS_S_CONTINUE_NEEDED
296 2: GSS_S_COMPLETE, SASL wrapping for authz and qop to send to client
297 3: unpick final auth message from client
298 4: break/finish (non-step)
299 */
300 while (step < 4) {
301 switch (step) {
302 case 0:
303 if (!from_client || *from_client == '\0') {
304 if (handled_empty_ir) {
305 HDEBUG(D_auth) debug_printf("gssapi: repeated empty input, grr.\n");
306 error_out = BAD64;
307 goto ERROR_OUT;
308 } else {
309 HDEBUG(D_auth) debug_printf("gssapi: missing initial response, nudging.\n");
310 error_out = auth_get_data(&from_client, US"", 0);
311 if (error_out != OK)
312 goto ERROR_OUT;
313 handled_empty_ir = TRUE;
314 continue;
315 }
316 }
317 /* We should now have the opening data from the client, base64-encoded. */
318 step += 1;
319 HDEBUG(D_auth) debug_printf("heimdal: have initial client data\n");
320 break;
321
322 case 1:
323 gbufdesc_in.length = auth_b64decode(from_client, USS &gbufdesc_in.value);
324 if (gclient) {
325 maj_stat = gss_release_name(&min_stat, &gclient);
326 gclient = GSS_C_NO_NAME;
327 }
328 maj_stat = gss_accept_sec_context(&min_stat,
329 &gcontext, /* context handle */
330 gcred, /* acceptor cred handle */
331 &gbufdesc_in, /* input from client */
332 GSS_C_NO_CHANNEL_BINDINGS, /* XXX fixme: use the channel bindings from GnuTLS */
333 &gclient, /* client identifier */
334 &mech_type, /* mechanism in use */
335 &gbufdesc_out, /* output to send to client */
336 NULL, /* return flags */
337 NULL, /* time rec */
338 NULL /* delegated cred_handle */
339 );
340 if (GSS_ERROR(maj_stat)) {
341 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
342 "gss_accept_sec_context()");
343 error_out = FAIL;
344 goto ERROR_OUT;
345 }
346 if (&gbufdesc_out.length != 0) {
347 error_out = auth_get_data(&from_client,
348 gbufdesc_out.value, gbufdesc_out.length);
349 if (error_out != OK)
350 goto ERROR_OUT;
351
352 gss_release_buffer(&min_stat, &gbufdesc_out);
353 EmptyBuf(gbufdesc_out);
354 }
355 if (maj_stat == GSS_S_COMPLETE) {
356 step += 1;
357 HDEBUG(D_auth) debug_printf("heimdal: GSS complete\n");
358 } else {
359 HDEBUG(D_auth) debug_printf("heimdal: need more data\n");
360 }
361 break;
362
363 case 2:
364 memset(sasl_config, 0xFF, 4);
365 /* draft-ietf-sasl-gssapi-06.txt defines bitmasks for first octet
366 0x01 No security layer
367 0x02 Integrity protection
368 0x04 Confidentiality protection
369
370 The remaining three octets are the maximum buffer size for wrapped
371 content. */
372 sasl_config[0] = 0x01; /* Exim does not wrap/unwrap SASL layers after auth */
373 gbufdesc.value = (void *) sasl_config;
374 gbufdesc.length = 4;
375 maj_stat = gss_wrap(&min_stat,
376 gcontext,
377 0, /* conf_req_flag: integrity only */
378 GSS_C_QOP_DEFAULT, /* qop requested */
379 &gbufdesc, /* message to protect */
380 NULL, /* conf_state: no confidentiality applied */
381 &gbufdesc_out /* output buffer */
382 );
383 if (GSS_ERROR(maj_stat)) {
384 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
385 "gss_wrap(SASL state after auth)");
386 error_out = FAIL;
387 goto ERROR_OUT;
388 }
389
390 HDEBUG(D_auth) debug_printf("heimdal SASL: requesting QOP with no security layers\n");
391
392 error_out = auth_get_data(&from_client,
393 gbufdesc_out.value, gbufdesc_out.length);
394 if (error_out != OK)
395 goto ERROR_OUT;
396
397 gss_release_buffer(&min_stat, &gbufdesc_out);
398 EmptyBuf(gbufdesc_out);
399 step += 1;
400 break;
401
402 case 3:
403 gbufdesc_in.length = auth_b64decode(from_client, USS &gbufdesc_in.value);
404 maj_stat = gss_unwrap(&min_stat,
405 gcontext,
406 &gbufdesc_in, /* data from client */
407 &gbufdesc_out, /* results */
408 NULL, /* conf state */
409 NULL /* qop state */
410 );
411 if (GSS_ERROR(maj_stat)) {
412 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
413 "gss_unwrap(final SASL message from client)");
414 error_out = FAIL;
415 goto ERROR_OUT;
416 }
417 if (gbufdesc_out.length < 4) {
418 HDEBUG(D_auth)
419 debug_printf("gssapi: final message too short; "
420 "need flags, buf sizes and optional authzid\n");
421 error_out = FAIL;
422 goto ERROR_OUT;
423 }
424
425 requested_qop = (CS gbufdesc_out.value)[0];
426 if ((requested_qop & 0x01) == 0) {
427 HDEBUG(D_auth)
428 debug_printf("gssapi: client requested security layers (%x)\n",
429 (unsigned int) requested_qop);
430 error_out = FAIL;
431 goto ERROR_OUT;
432 }
433
434 for (i = 0; i < AUTH_VARS; i++) auth_vars[i] = NULL;
435 expand_nmax = 0;
436
437 /* Identifiers:
438 The SASL provided identifier is an unverified authzid.
439 GSSAPI provides us with a verified identifier, but it might be empty
440 for some clients.
441 */
442
443 /* $auth2 is authzid requested at SASL layer */
444 if (gbufdesc_out.length > 4) {
445 expand_nlength[2] = gbufdesc_out.length - 4;
446 auth_vars[1] = expand_nstring[2] =
447 string_copyn((US gbufdesc_out.value) + 4, expand_nlength[2]);
448 expand_nmax = 2;
449 }
450
451 gss_release_buffer(&min_stat, &gbufdesc_out);
452 EmptyBuf(gbufdesc_out);
453
454 /* $auth1 is GSSAPI display name */
455 maj_stat = gss_display_name(&min_stat,
456 gclient,
457 &gbufdesc_out,
458 &mech_type);
459 if (GSS_ERROR(maj_stat)) {
460 auth_vars[1] = expand_nstring[2] = NULL;
461 expand_nmax = 0;
462 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
463 "gss_display_name(client identifier)");
464 error_out = FAIL;
465 goto ERROR_OUT;
466 }
467
468 expand_nlength[1] = gbufdesc_out.length;
469 auth_vars[0] = expand_nstring[1] =
470 string_copyn(gbufdesc_out.value, gbufdesc_out.length);
471
472 if (expand_nmax == 0) { /* should be: authzid was empty */
473 expand_nmax = 2;
474 expand_nlength[2] = expand_nlength[1];
475 auth_vars[1] = expand_nstring[2] = string_copyn(expand_nstring[1], expand_nlength[1]);
476 HDEBUG(D_auth)
477 debug_printf("heimdal SASL: empty authzid, set to dup of GSSAPI display name\n");
478 }
479
480 HDEBUG(D_auth)
481 debug_printf("heimdal SASL: happy with client request\n"
482 " auth1 (verified GSSAPI display-name): \"%s\"\n"
483 " auth2 (unverified SASL requested authzid): \"%s\"\n",
484 auth_vars[0], auth_vars[1]);
485
486 step += 1;
487 break;
488
489 } /* switch */
490 } /* while step */
491
492
493 ERROR_OUT:
494 maj_stat = gss_release_cred(&min_stat, &gcred);
495 if (gclient) {
496 gss_release_name(&min_stat, &gclient);
497 gclient = GSS_C_NO_NAME;
498 }
499 if (gbufdesc_out.length) {
500 gss_release_buffer(&min_stat, &gbufdesc_out);
501 EmptyBuf(gbufdesc_out);
502 }
503 if (gcontext != GSS_C_NO_CONTEXT) {
504 gss_delete_sec_context(&min_stat, &gcontext, GSS_C_NO_BUFFER);
505 }
506
507 store_reset(store_reset_point);
508
509 if (error_out != OK)
510 return error_out;
511
512 /* Auth succeeded, check server_condition */
513 return auth_check_serv_cond(ablock);
514 }
515
516
517 static int
518 exim_gssapi_error_defer(uschar *store_reset_point,
519 OM_uint32 major, OM_uint32 minor,
520 const char *format, ...)
521 {
522 va_list ap;
523 uschar buffer[STRING_SPRINTF_BUFFER_SIZE];
524 OM_uint32 maj_stat, min_stat;
525 OM_uint32 msgcontext = 0;
526 gss_buffer_desc status_string;
527
528 va_start(ap, format);
529 if (!string_vformat(buffer, sizeof(buffer), format, ap))
530 log_write(0, LOG_MAIN|LOG_PANIC_DIE,
531 "exim_gssapi_error_defer expansion larger than %lu",
532 sizeof(buffer));
533 va_end(ap);
534
535 auth_defer_msg = NULL;
536
537 do {
538 maj_stat = gss_display_status(&min_stat,
539 major, GSS_C_GSS_CODE, GSS_C_NO_OID,
540 &msgcontext, &status_string);
541
542 if (auth_defer_msg == NULL) {
543 auth_defer_msg = string_copy(US status_string.value);
544 }
545
546 HDEBUG(D_auth) debug_printf("heimdal %s: %.*s\n",
547 buffer, (int)status_string.length, CS status_string.value);
548 gss_release_buffer(&min_stat, &status_string);
549
550 } while (msgcontext != 0);
551
552 if (store_reset_point)
553 store_reset(store_reset_point);
554 return DEFER;
555 }
556
557
558 /*************************************************
559 * Client entry point *
560 *************************************************/
561
562 /* For interface, see auths/README */
563
564 int
565 auth_heimdal_gssapi_client(
566 auth_instance *ablock, /* authenticator block */
567 smtp_inblock *inblock, /* connection inblock */
568 smtp_outblock *outblock, /* connection outblock */
569 int timeout, /* command timeout */
570 uschar *buffer, /* buffer for reading response */
571 int buffsize) /* size of buffer */
572 {
573 HDEBUG(D_auth)
574 debug_printf("Client side NOT IMPLEMENTED: you should not see this!\n");
575 /* NOT IMPLEMENTED */
576 return FAIL;
577 }
578
579 /*************************************************
580 * Diagnostic API *
581 *************************************************/
582
583 void
584 auth_heimdal_gssapi_version_report(FILE *f)
585 {
586 /* No build-time constants available unless we link against libraries at
587 build-time and export the result as a string into a header ourselves. */
588 fprintf(f, "Library version: Heimdal: Runtime: %s\n"
589 " Build Info: %s\n",
590 heimdal_version, heimdal_long_version);
591 }
592
593 #endif /* AUTH_HEIMDAL_GSSAPI */
594
595 /* End of heimdal_gssapi.c */