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