Merge branch 'sasl_fixes'
[users/heiko/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 */