5f3b7ecb42f91ec0863f8934afd1da20a0af34e2
[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_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)) }
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 NULL, /* server_realm */
77 US"smtp", /* server_service */
78 };
79
80 /* "Globals" for managing the heimdal_gssapi interface. */
81
82 /* Utility functions */
83 static void
84 exim_heimdal_error_debug(const char *, krb5_context, krb5_error_code);
85 static int
86 exim_gssapi_error_defer(uschar *, OM_uint32, OM_uint32, const char *, ...)
87 PRINTF_FUNCTION(4, 5);
88
89 #define EmptyBuf(buf) do { buf.value = NULL; buf.length = 0; } while (0)
90
91
92 /*************************************************
93 * Initialization entry point *
94 *************************************************/
95
96 /* Called for each instance, after its options have been read, to
97 enable consistency checks to be done, or anything else that needs
98 to be set up. */
99
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. */
103
104 void
105 auth_heimdal_gssapi_init(auth_instance *ablock)
106 {
107 krb5_context context;
108 krb5_keytab keytab;
109 krb5_kt_cursor cursor;
110 krb5_keytab_entry entry;
111 krb5_error_code krc;
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);
116
117 ablock->server = FALSE;
118 ablock->client = FALSE;
119
120 if (!ob->server_service || !*ob->server_service) {
121 HDEBUG(D_auth) debug_printf("heimdal: missing server_service\n");
122 return;
123 }
124
125 krc = krb5_init_context(&context);
126 if (krc != 0) {
127 int kerr = errno;
128 HDEBUG(D_auth) debug_printf("heimdal: failed to initialise krb5 context: %s\n",
129 strerror(kerr));
130 return;
131 }
132
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);
137 if (krc) {
138 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_resolve", context, krc);
139 return;
140 }
141 } else {
142 HDEBUG(D_auth) debug_printf("heimdal: using system default keytab\n");
143 krc = krb5_kt_default(context, &keytab);
144 if (krc) {
145 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_default", context, krc);
146 return;
147 }
148 }
149
150 HDEBUG(D_auth) {
151 /* http://www.h5l.org/manual/HEAD/krb5/krb5_keytab_intro.html */
152 krc = krb5_kt_start_seq_get(context, keytab, &cursor);
153 if (krc)
154 exim_heimdal_error_debug("krb5_kt_start_seq_get", context, krc);
155 else {
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 : "??",
162 entry.vno,
163 enctype_s ? enctype_s : "??");
164 free(principal);
165 free(enctype_s);
166 krb5_kt_free_entry(context, &entry);
167 }
168 krc = krb5_kt_end_seq_get(context, keytab, &cursor);
169 if (krc)
170 exim_heimdal_error_debug("krb5_kt_end_seq_get", context, krc);
171 }
172 }
173
174 krc = krb5_kt_close(context, keytab);
175 if (krc)
176 HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_close", context, krc);
177
178 krb5_free_context(context);
179
180 /* RFC 4121 section 5.2, SHOULD support 64K input buffers */
181 if (big_buffer_size < (64 * 1024)) {
182 uschar *newbuf;
183 big_buffer_size = 64 * 1024;
184 newbuf = store_malloc(big_buffer_size);
185 store_free(big_buffer);
186 big_buffer = newbuf;
187 }
188
189 ablock->server = TRUE;
190 }
191
192
193 static void
194 exim_heimdal_error_debug(const char *label,
195 krb5_context context, krb5_error_code err)
196 {
197 const char *kerrsc;
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);
201 }
202
203 /*************************************************
204 * Server entry point *
205 *************************************************/
206
207 /* For interface, see auths/README */
208
209 /* GSSAPI notes:
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()
214 */
215
216 int
217 auth_heimdal_gssapi_server(auth_instance *ablock, uschar *initial_data)
218 {
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;
227 gss_OID mech_type;
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;
235 uschar *keytab;
236 uschar sasl_config[4];
237 uschar requested_qop;
238
239 store_reset_point = store_get(0);
240
241 HDEBUG(D_auth)
242 debug_printf("heimdal: initialising auth context for %s\n", ablock->name);
243
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);
255
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);
263 HDEBUG(D_auth)
264 debug_printf("heimdal: using keytab \"%s\"\n", keytab);
265 }
266
267 /* Acquire our credentials */
268 maj_stat = gss_acquire_cred(&min_stat,
269 gserver, /* desired name */
270 0, /* time */
271 GSS_C_NULL_OID_SET, /* desired mechs */
272 GSS_C_ACCEPT, /* cred usage */
273 &gcred, /* handle */
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);
279
280 maj_stat = gss_release_name(&min_stat, &gserver);
281
282 HDEBUG(D_auth) debug_printf("heimdal: have server credentials.\n");
283
284 /* Loop talking to client */
285 step = 0;
286 from_client = initial_data;
287 handled_empty_ir = FALSE;
288 error_out = OK;
289
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). */
293
294 /* step values
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)
300 */
301 while (step < 4) {
302 switch (step) {
303 case 0:
304 if (!from_client || *from_client == '\0') {
305 if (handled_empty_ir) {
306 HDEBUG(D_auth) debug_printf("gssapi: repeated empty input, grr.\n");
307 error_out = BAD64;
308 goto ERROR_OUT;
309 } else {
310 HDEBUG(D_auth) debug_printf("gssapi: missing initial response, nudging.\n");
311 error_out = auth_get_data(&from_client, US"", 0);
312 if (error_out != OK)
313 goto ERROR_OUT;
314 handled_empty_ir = TRUE;
315 continue;
316 }
317 }
318 /* We should now have the opening data from the client, base64-encoded. */
319 step += 1;
320 HDEBUG(D_auth) debug_printf("heimdal: have initial client data\n");
321 break;
322
323 case 1:
324 gbufdesc_in.length = auth_b64decode(from_client, USS &gbufdesc_in.value);
325 if (gclient) {
326 maj_stat = gss_release_name(&min_stat, &gclient);
327 gclient = GSS_C_NO_NAME;
328 }
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 */
338 NULL, /* time rec */
339 NULL /* delegated cred_handle */
340 );
341 if (GSS_ERROR(maj_stat)) {
342 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
343 "gss_accept_sec_context()");
344 error_out = FAIL;
345 goto ERROR_OUT;
346 }
347 if (&gbufdesc_out.length != 0) {
348 error_out = auth_get_data(&from_client,
349 gbufdesc_out.value, gbufdesc_out.length);
350 if (error_out != OK)
351 goto ERROR_OUT;
352
353 gss_release_buffer(&min_stat, &gbufdesc_out);
354 EmptyBuf(gbufdesc_out);
355 }
356 if (maj_stat == GSS_S_COMPLETE) {
357 step += 1;
358 HDEBUG(D_auth) debug_printf("heimdal: GSS complete\n");
359 } else {
360 HDEBUG(D_auth) debug_printf("heimdal: need more data\n");
361 }
362 break;
363
364 case 2:
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
370
371 The remaining three octets are the maximum buffer size for wrapped
372 content. */
373 sasl_config[0] = 0x01; /* Exim does not wrap/unwrap SASL layers after auth */
374 gbufdesc.value = (void *) sasl_config;
375 gbufdesc.length = 4;
376 maj_stat = gss_wrap(&min_stat,
377 gcontext,
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 */
383 );
384 if (GSS_ERROR(maj_stat)) {
385 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
386 "gss_wrap(SASL state after auth)");
387 error_out = FAIL;
388 goto ERROR_OUT;
389 }
390
391 HDEBUG(D_auth) debug_printf("heimdal SASL: requesting QOP with no security layers\n");
392
393 error_out = auth_get_data(&from_client,
394 gbufdesc_out.value, gbufdesc_out.length);
395 if (error_out != OK)
396 goto ERROR_OUT;
397
398 gss_release_buffer(&min_stat, &gbufdesc_out);
399 EmptyBuf(gbufdesc_out);
400 step += 1;
401 break;
402
403 case 3:
404 gbufdesc_in.length = auth_b64decode(from_client, USS &gbufdesc_in.value);
405 maj_stat = gss_unwrap(&min_stat,
406 gcontext,
407 &gbufdesc_in, /* data from client */
408 &gbufdesc_out, /* results */
409 NULL, /* conf state */
410 NULL /* qop state */
411 );
412 if (GSS_ERROR(maj_stat)) {
413 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
414 "gss_unwrap(final SASL message from client)");
415 error_out = FAIL;
416 goto ERROR_OUT;
417 }
418 if (gbufdesc_out.length < 5) {
419 HDEBUG(D_auth)
420 debug_printf("gssapi: final message too short; "
421 "need flags, buf sizes and authzid\n");
422 error_out = FAIL;
423 goto ERROR_OUT;
424 }
425
426 requested_qop = (CS gbufdesc_out.value)[0];
427 if ((requested_qop & 0x01) == 0) {
428 HDEBUG(D_auth)
429 debug_printf("gssapi: client requested security layers (%x)\n",
430 (unsigned int) requested_qop);
431 error_out = FAIL;
432 goto ERROR_OUT;
433 }
434
435 for (i = 0; i < AUTH_VARS; i++) auth_vars[i] = NULL;
436 expand_nmax = 0;
437
438 /* Identifiers:
439 The SASL provided identifier is an unverified authzid.
440 GSSAPI provides us with a verified identifier.
441 */
442
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]);
447 expand_nmax = 2;
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 HDEBUG(D_auth)
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]);
475
476 step += 1;
477 break;
478
479 } /* switch */
480 } /* while step */
481
482
483 ERROR_OUT:
484 maj_stat = gss_release_cred(&min_stat, &gcred);
485 if (gclient) {
486 gss_release_name(&min_stat, &gclient);
487 gclient = GSS_C_NO_NAME;
488 }
489 if (gbufdesc_out.length) {
490 gss_release_buffer(&min_stat, &gbufdesc_out);
491 EmptyBuf(gbufdesc_out);
492 }
493 if (gcontext != GSS_C_NO_CONTEXT) {
494 gss_delete_sec_context(&min_stat, &gcontext, GSS_C_NO_BUFFER);
495 }
496
497 store_reset(store_reset_point);
498
499 if (error_out != OK)
500 return error_out;
501
502 /* Auth succeeded, check server_condition */
503 return auth_check_serv_cond(ablock);
504 }
505
506
507 static int
508 exim_gssapi_error_defer(uschar *store_reset_point,
509 OM_uint32 major, OM_uint32 minor,
510 const char *format, ...)
511 {
512 va_list ap;
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;
517
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",
522 sizeof(buffer));
523 va_end(ap);
524
525 auth_defer_msg = NULL;
526
527 do {
528 maj_stat = gss_display_status(&min_stat,
529 major, GSS_C_GSS_CODE, GSS_C_NO_OID,
530 &msgcontext, &status_string);
531
532 if (auth_defer_msg == NULL) {
533 auth_defer_msg = string_copy(US status_string.value);
534 }
535
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);
539
540 } while (msgcontext != 0);
541
542 if (store_reset_point)
543 store_reset(store_reset_point);
544 return DEFER;
545 }
546
547
548 /*************************************************
549 * Client entry point *
550 *************************************************/
551
552 /* For interface, see auths/README */
553
554 int
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 */
562 {
563 HDEBUG(D_auth)
564 debug_printf("Client side NOT IMPLEMENTED: you should not see this!\n");
565 /* NOT IMPLEMENTED */
566 return FAIL;
567 }
568
569 /*************************************************
570 * Diagnostic API *
571 *************************************************/
572
573 void
574 auth_heimdal_gssapi_version_report(FILE *f)
575 {
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"
579 " Build Info: %s\n",
580 heimdal_version, heimdal_long_version);
581 }
582
583 #endif /* AUTH_HEIMDAL_GSSAPI */
584
585 /* End of heimdal_gssapi.c */