Drop server_realm from heimdal_gssapi
[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 < 5) {
416 HDEBUG(D_auth)
417 debug_printf("gssapi: final message too short; "
418 "need flags, buf sizes and 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.
438 */
439
440 /* $auth2 is authzid requested at SASL layer */
441 expand_nlength[2] = gbufdesc_out.length - 4;
442 auth_vars[1] = expand_nstring[2] =
443 string_copyn((US gbufdesc_out.value) + 4, expand_nlength[2]);
444 expand_nmax = 2;
445
446 gss_release_buffer(&min_stat, &gbufdesc_out);
447 EmptyBuf(gbufdesc_out);
448
449 /* $auth1 is GSSAPI display name */
450 maj_stat = gss_display_name(&min_stat,
451 gclient,
452 &gbufdesc_out,
453 &mech_type);
454 if (GSS_ERROR(maj_stat)) {
455 auth_vars[1] = expand_nstring[2] = NULL;
456 expand_nmax = 0;
457 exim_gssapi_error_defer(NULL, maj_stat, min_stat,
458 "gss_display_name(client identifier)");
459 error_out = FAIL;
460 goto ERROR_OUT;
461 }
462
463 expand_nlength[1] = gbufdesc_out.length;
464 auth_vars[0] = expand_nstring[1] =
465 string_copyn(gbufdesc_out.value, gbufdesc_out.length);
466
467 HDEBUG(D_auth)
468 debug_printf("heimdal SASL: happy with client request\n"
469 " auth1 (verified GSSAPI display-name): \"%s\"\n"
470 " auth2 (unverified SASL requested authzid): \"%s\"\n",
471 auth_vars[0], auth_vars[1]);
472
473 step += 1;
474 break;
475
476 } /* switch */
477 } /* while step */
478
479
480 ERROR_OUT:
481 maj_stat = gss_release_cred(&min_stat, &gcred);
482 if (gclient) {
483 gss_release_name(&min_stat, &gclient);
484 gclient = GSS_C_NO_NAME;
485 }
486 if (gbufdesc_out.length) {
487 gss_release_buffer(&min_stat, &gbufdesc_out);
488 EmptyBuf(gbufdesc_out);
489 }
490 if (gcontext != GSS_C_NO_CONTEXT) {
491 gss_delete_sec_context(&min_stat, &gcontext, GSS_C_NO_BUFFER);
492 }
493
494 store_reset(store_reset_point);
495
496 if (error_out != OK)
497 return error_out;
498
499 /* Auth succeeded, check server_condition */
500 return auth_check_serv_cond(ablock);
501 }
502
503
504 static int
505 exim_gssapi_error_defer(uschar *store_reset_point,
506 OM_uint32 major, OM_uint32 minor,
507 const char *format, ...)
508 {
509 va_list ap;
510 uschar buffer[STRING_SPRINTF_BUFFER_SIZE];
511 OM_uint32 maj_stat, min_stat;
512 OM_uint32 msgcontext = 0;
513 gss_buffer_desc status_string;
514
515 va_start(ap, format);
516 if (!string_vformat(buffer, sizeof(buffer), format, ap))
517 log_write(0, LOG_MAIN|LOG_PANIC_DIE,
518 "exim_gssapi_error_defer expansion larger than %d",
519 sizeof(buffer));
520 va_end(ap);
521
522 auth_defer_msg = NULL;
523
524 do {
525 maj_stat = gss_display_status(&min_stat,
526 major, GSS_C_GSS_CODE, GSS_C_NO_OID,
527 &msgcontext, &status_string);
528
529 if (auth_defer_msg == NULL) {
530 auth_defer_msg = string_copy(US status_string.value);
531 }
532
533 HDEBUG(D_auth) debug_printf("heimdal %s: %.*s\n",
534 buffer, (int)status_string.length, CS status_string.value);
535 gss_release_buffer(&min_stat, &status_string);
536
537 } while (msgcontext != 0);
538
539 if (store_reset_point)
540 store_reset(store_reset_point);
541 return DEFER;
542 }
543
544
545 /*************************************************
546 * Client entry point *
547 *************************************************/
548
549 /* For interface, see auths/README */
550
551 int
552 auth_heimdal_gssapi_client(
553 auth_instance *ablock, /* authenticator block */
554 smtp_inblock *inblock, /* connection inblock */
555 smtp_outblock *outblock, /* connection outblock */
556 int timeout, /* command timeout */
557 uschar *buffer, /* buffer for reading response */
558 int buffsize) /* size of buffer */
559 {
560 HDEBUG(D_auth)
561 debug_printf("Client side NOT IMPLEMENTED: you should not see this!\n");
562 /* NOT IMPLEMENTED */
563 return FAIL;
564 }
565
566 /*************************************************
567 * Diagnostic API *
568 *************************************************/
569
570 void
571 auth_heimdal_gssapi_version_report(FILE *f)
572 {
573 /* No build-time constants available unless we link against libraries at
574 build-time and export the result as a string into a header ourselves. */
575 fprintf(f, "Library version: Heimdal: Runtime: %s\n"
576 " Build Info: %s\n",
577 heimdal_version, heimdal_long_version);
578 }
579
580 #endif /* AUTH_HEIMDAL_GSSAPI */
581
582 /* End of heimdal_gssapi.c */