Minor cleanups.
[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 */