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