Add S4U2Proxy support
[mod_auth_gssapi.git] / src / mod_auth_gssapi.c
1 /*
2    MOD AUTH GSSAPI
3
4    Copyright (C) 2014 Simo Sorce <simo@redhat.com>
5
6    Permission is hereby granted, free of charge, to any person obtaining a
7    copy of this software and associated documentation files (the "Software"),
8    to deal in the Software without restriction, including without limitation
9    the rights to use, copy, modify, merge, publish, distribute, sublicense,
10    and/or sell copies of the Software, and to permit persons to whom the
11    Software is furnished to do so, subject to the following conditions:
12
13    The above copyright notice and this permission notice shall be included in
14    all copies or substantial portions of the Software.
15
16    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
19    THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22    DEALINGS IN THE SOFTWARE.
23 */
24
25 #include "mod_auth_gssapi.h"
26
27
28 module AP_MODULE_DECLARE_DATA auth_gssapi_module;
29
30 APR_DECLARE_OPTIONAL_FN(int, ssl_is_https, (conn_rec *));
31
32 static char *mag_status(request_rec *req, int type, uint32_t err)
33 {
34     uint32_t maj_ret, min_ret;
35     gss_buffer_desc text;
36     uint32_t msg_ctx;
37     char *msg_ret;
38     int len;
39
40     msg_ret = NULL;
41     msg_ctx = 0;
42     do {
43         maj_ret = gss_display_status(&min_ret, err, type,
44                                      GSS_C_NO_OID, &msg_ctx, &text);
45         if (maj_ret != GSS_S_COMPLETE) {
46             return msg_ret;
47         }
48
49         len = text.length;
50         if (msg_ret) {
51             msg_ret = apr_psprintf(req->pool, "%s, %*s",
52                                    msg_ret, len, (char *)text.value);
53         } else {
54             msg_ret = apr_psprintf(req->pool, "%*s", len, (char *)text.value);
55         }
56         gss_release_buffer(&min_ret, &text);
57     } while (msg_ctx != 0);
58
59     return msg_ret;
60 }
61
62 static char *mag_error(request_rec *req, const char *msg,
63                        uint32_t maj, uint32_t min)
64 {
65     char *msg_maj;
66     char *msg_min;
67
68     msg_maj = mag_status(req, GSS_C_GSS_CODE, maj);
69     msg_min = mag_status(req, GSS_C_MECH_CODE, min);
70     return apr_psprintf(req->pool, "%s: [%s (%s)]", msg, msg_maj, msg_min);
71 }
72
73 static APR_OPTIONAL_FN_TYPE(ssl_is_https) *mag_is_https = NULL;
74
75 static int mag_post_config(apr_pool_t *cfgpool, apr_pool_t *log,
76                            apr_pool_t *temp, server_rec *s)
77 {
78     /* FIXME: create mutex to deal with connections and contexts ? */
79     mag_is_https = APR_RETRIEVE_OPTIONAL_FN(ssl_is_https);
80     mag_post_config_session();
81
82     return OK;
83 }
84
85 static int mag_pre_connection(conn_rec *c, void *csd)
86 {
87     struct mag_conn *mc;
88
89     mc = apr_pcalloc(c->pool, sizeof(struct mag_conn));
90     if (!mc) return DECLINED;
91
92     mc->parent = c->pool;
93     ap_set_module_config(c->conn_config, &auth_gssapi_module, (void*)mc);
94     return OK;
95 }
96
97 static apr_status_t mag_conn_destroy(void *ptr)
98 {
99     struct mag_conn *mc = (struct mag_conn *)ptr;
100     uint32_t min;
101
102     if (mc->ctx) {
103         (void)gss_delete_sec_context(&min, &mc->ctx, GSS_C_NO_BUFFER);
104         mc->established = false;
105     }
106     return APR_SUCCESS;
107 }
108
109 static bool mag_conn_is_https(conn_rec *c)
110 {
111     if (mag_is_https) {
112         if (mag_is_https(c)) return true;
113     }
114
115     return false;
116 }
117
118 static void mag_store_deleg_creds(request_rec *req,
119                                   char *dir, char *clientname,
120                                   gss_cred_id_t delegated_cred,
121                                   char **ccachefile)
122 {
123     gss_key_value_element_desc element;
124     gss_key_value_set_desc store;
125     char *value;
126     uint32_t maj, min;
127
128     value = apr_psprintf(req->pool, "FILE:%s/%s", dir, clientname);
129     if (!value) {
130         ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, NULL,
131                      "OOM storing delegated credentials");
132         return;
133     }
134
135     element.key = "ccache";
136     element.value = value;
137     store.elements = &element;
138     store.count = 1;
139
140     maj = gss_store_cred_into(&min, delegated_cred, GSS_C_INITIATE,
141                               GSS_C_NULL_OID, 1, 1, &store, NULL, NULL);
142     if (GSS_ERROR(maj)) {
143         ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, req,
144                       mag_error(req, "failed to store delegated creds",
145                                 maj, min));
146     }
147
148     *ccachefile = value;
149 }
150
151 static int mag_auth(request_rec *req)
152 {
153     const char *type;
154     struct mag_config *cfg;
155     const char *auth_header;
156     char *auth_header_type;
157     char *auth_header_value;
158     int ret = HTTP_UNAUTHORIZED;
159     gss_ctx_id_t ctx = GSS_C_NO_CONTEXT;
160     gss_ctx_id_t *pctx;
161     gss_buffer_desc input = GSS_C_EMPTY_BUFFER;
162     gss_buffer_desc output = GSS_C_EMPTY_BUFFER;
163     gss_buffer_desc name = GSS_C_EMPTY_BUFFER;
164     gss_name_t client = GSS_C_NO_NAME;
165     gss_cred_id_t acquired_cred = GSS_C_NO_CREDENTIAL;
166     gss_cred_id_t delegated_cred = GSS_C_NO_CREDENTIAL;
167     uint32_t flags;
168     uint32_t vtime;
169     uint32_t maj, min;
170     char *reply;
171     size_t replen;
172     char *clientname;
173     gss_OID mech_type = GSS_C_NO_OID;
174     gss_buffer_desc lname = GSS_C_EMPTY_BUFFER;
175     struct mag_conn *mc = NULL;
176
177     type = ap_auth_type(req);
178     if ((type == NULL) || (strcasecmp(type, "GSSAPI") != 0)) {
179         return DECLINED;
180     }
181
182     /* ignore auth for subrequests */
183     if (!ap_is_initial_req(req)) {
184         return OK;
185     }
186
187     cfg = ap_get_module_config(req->per_dir_config, &auth_gssapi_module);
188
189     if (cfg->ssl_only) {
190         if (!mag_conn_is_https(req->connection)) {
191             ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, req,
192                           "Not a TLS connection, refusing to authenticate!");
193             goto done;
194         }
195     }
196
197     if (cfg->gss_conn_ctx) {
198         mc = (struct mag_conn *)ap_get_module_config(
199                                                 req->connection->conn_config,
200                                                 &auth_gssapi_module);
201         if (!mc) {
202             ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, 0, req,
203                           "Failed to retrieve connection context!");
204             goto done;
205         }
206     }
207
208     /* if available, session always supersedes connection bound data */
209     mag_check_session(req, cfg, &mc);
210
211     if (mc) {
212         /* register the context in the memory pool, so it can be freed
213          * when the connection/request is terminated */
214         apr_pool_userdata_set(mc, "mag_conn_ptr",
215                               mag_conn_destroy, mc->parent);
216
217         if (mc->established) {
218             ap_log_rerror(APLOG_MARK, APLOG_DEBUG|APLOG_NOERRNO, 0, req,
219                           "Already established context found!");
220             apr_table_set(req->subprocess_env, "GSS_NAME", mc->gss_name);
221             req->ap_auth_type = apr_pstrdup(req->pool, "Negotiate");
222             req->user = apr_pstrdup(req->pool, mc->user_name);
223             ret = OK;
224             goto done;
225         }
226         pctx = &mc->ctx;
227     } else {
228         pctx = &ctx;
229     }
230
231     auth_header = apr_table_get(req->headers_in, "Authorization");
232     if (!auth_header) goto done;
233
234     auth_header_type = ap_getword_white(req->pool, &auth_header);
235     if (!auth_header_type) goto done;
236
237     if (strcasecmp(auth_header_type, "Negotiate") != 0) goto done;
238
239     auth_header_value = ap_getword_white(req->pool, &auth_header);
240     if (!auth_header_value) goto done;
241     input.length = apr_base64_decode_len(auth_header_value) + 1;
242     input.value = apr_pcalloc(req->pool, input.length);
243     if (!input.value) goto done;
244     input.length = apr_base64_decode(input.value, auth_header_value);
245
246 #ifdef HAVE_GSS_ACQUIRE_CRED_FROM
247     if (cfg->use_s4u2proxy) {
248         maj = gss_acquire_cred_from(&min, GSS_C_NO_NAME, 0,
249                                     GSS_C_NO_OID_SET, GSS_C_BOTH,
250                                     cfg->cred_store, &acquired_cred,
251                                     NULL, NULL);
252         if (GSS_ERROR(maj)) {
253             ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, req,
254                           mag_error(req, "gss_acquire_cred_from() failed",
255                                     maj, min));
256             goto done;
257         }
258     }
259 #endif
260
261     maj = gss_accept_sec_context(&min, pctx, acquired_cred,
262                                  &input, GSS_C_NO_CHANNEL_BINDINGS,
263                                  &client, &mech_type, &output, &flags, &vtime,
264                                  &delegated_cred);
265     if (GSS_ERROR(maj)) {
266         ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, req,
267                       mag_error(req, "gss_accept_sec_context() failed",
268                                 maj, min));
269         goto done;
270     }
271
272     if (maj == GSS_S_CONTINUE_NEEDED) {
273         if (!mc) {
274             ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, req,
275                           "Mechanism needs continuation but neither "
276                           "GssapiConnectionBound nor "
277                           "GssapiUseSessions are available");
278             gss_delete_sec_context(&min, pctx, GSS_C_NO_BUFFER);
279             gss_release_buffer(&min, &output);
280             output.length = 0;
281         }
282         /* auth not complete send token and wait next packet */
283         goto done;
284     }
285
286     req->ap_auth_type = apr_pstrdup(req->pool, "Negotiate");
287
288     /* Always set the GSS name in an env var */
289     maj = gss_display_name(&min, client, &name, NULL);
290     if (GSS_ERROR(maj)) {
291         ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, req,
292                       mag_error(req, "gss_accept_sec_context() failed",
293                                 maj, min));
294         goto done;
295     }
296     clientname = apr_pstrndup(req->pool, name.value, name.length);
297     apr_table_set(req->subprocess_env, "GSS_NAME", clientname);
298
299 #ifdef HAVE_GSS_STORE_CRED_INTO
300     if (cfg->deleg_ccache_dir && delegated_cred != GSS_C_NO_CREDENTIAL) {
301         char *ccachefile = NULL;
302
303         mag_store_deleg_creds(req, cfg->deleg_ccache_dir, clientname,
304                               delegated_cred, &ccachefile);
305
306         if (ccachefile) {
307             apr_table_set(req->subprocess_env, "KRB5CCNAME", ccachefile);
308         }
309     }
310 #endif
311
312     if (cfg->map_to_local) {
313         maj = gss_localname(&min, client, mech_type, &lname);
314         if (maj != GSS_S_COMPLETE) {
315             ap_log_rerror(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, req,
316                           mag_error(req, "gss_localname() failed", maj, min));
317             goto done;
318         }
319         req->user = apr_pstrndup(req->pool, lname.value, lname.length);
320     } else {
321         req->user = clientname;
322     }
323
324     if (mc) {
325         mc->user_name = apr_pstrdup(mc->parent, req->user);
326         mc->gss_name = apr_pstrdup(mc->parent, clientname);
327         mc->established = true;
328         if (vtime == GSS_C_INDEFINITE || vtime < MIN_SESS_EXP_TIME) {
329             vtime = MIN_SESS_EXP_TIME;
330         }
331         mc->expiration = time(NULL) + vtime;
332         mag_attempt_session(req, cfg, mc);
333     }
334
335     ret = OK;
336
337 done:
338     if (ret == HTTP_UNAUTHORIZED) {
339         if (output.length != 0) {
340             replen = apr_base64_encode_len(output.length) + 1;
341             reply = apr_pcalloc(req->pool, 10 + replen);
342             if (reply) {
343                 memcpy(reply, "Negotiate ", 10);
344                 apr_base64_encode(&reply[10], output.value, output.length);
345                 apr_table_add(req->err_headers_out,
346                               "WWW-Authenticate", reply);
347             }
348         } else {
349             apr_table_add(req->err_headers_out,
350                           "WWW-Authenticate", "Negotiate");
351         }
352     }
353     gss_release_cred(&min, &delegated_cred);
354     gss_release_buffer(&min, &output);
355     gss_release_name(&min, &client);
356     gss_release_buffer(&min, &name);
357     gss_release_buffer(&min, &lname);
358     return ret;
359 }
360
361
362 static void *mag_create_dir_config(apr_pool_t *p, char *dir)
363 {
364     struct mag_config *cfg;
365
366     cfg = (struct mag_config *)apr_pcalloc(p, sizeof(struct mag_config));
367     if (!cfg) return NULL;
368     cfg->pool = p;
369
370     return cfg;
371 }
372
373 static const char *mag_ssl_only(cmd_parms *parms, void *mconfig, int on)
374 {
375     struct mag_config *cfg = (struct mag_config *)mconfig;
376     cfg->ssl_only = on ? true : false;
377     return NULL;
378 }
379
380 static const char *mag_map_to_local(cmd_parms *parms, void *mconfig, int on)
381 {
382     struct mag_config *cfg = (struct mag_config *)mconfig;
383     cfg->map_to_local = on ? true : false;
384     return NULL;
385 }
386
387 static const char *mag_conn_ctx(cmd_parms *parms, void *mconfig, int on)
388 {
389     struct mag_config *cfg = (struct mag_config *)mconfig;
390     cfg->gss_conn_ctx = on ? true : false;
391     return NULL;
392 }
393
394 static const char *mag_use_sess(cmd_parms *parms, void *mconfig, int on)
395 {
396     struct mag_config *cfg = (struct mag_config *)mconfig;
397     cfg->use_sessions = on ? true : false;
398     return NULL;
399 }
400
401 static const char *mag_use_s4u2p(cmd_parms *parms, void *mconfig, int on)
402 {
403     struct mag_config *cfg = (struct mag_config *)mconfig;
404     cfg->use_s4u2proxy = on ? true : false;
405
406     if (cfg->deleg_ccache_dir == NULL) {
407         cfg->deleg_ccache_dir = apr_pstrdup(parms->pool, "/tmp");
408         if (!cfg->deleg_ccache_dir) {
409             ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0,
410                          parms->server, "%s", "OOM setting deleg_ccache_dir.");
411         }
412     }
413     return NULL;
414 }
415
416 static const char *mag_sess_key(cmd_parms *parms, void *mconfig, const char *w)
417 {
418     struct mag_config *cfg = (struct mag_config *)mconfig;
419     struct databuf keys;
420     unsigned char *val;
421     apr_status_t rc;
422     const char *k;
423     int l;
424
425     if (strncmp(w, "key:", 4) != 0) {
426         ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, parms->server,
427                      "Invalid key format, expected prefix 'key:'");
428         return NULL;
429     }
430     k = w + 4;
431
432     l = apr_base64_decode_len(k);
433     val = apr_palloc(parms->temp_pool, l);
434     if (!val) {
435         ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, parms->server,
436                      "Failed to get memory to decode key");
437         return NULL;
438     }
439
440     keys.length = (int)apr_base64_decode_binary(val, k);
441     keys.value = (unsigned char *)val;
442
443     if (keys.length != 32) {
444         ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, parms->server,
445                      "Invalid key lenght, expected 32 got %d", keys.length);
446         return NULL;
447     }
448
449     rc = SEAL_KEY_CREATE(cfg->pool, &cfg->mag_skey, &keys);
450     if (rc != OK) {
451         ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, parms->server,
452                      "Failed to import sealing key!");
453     }
454     return NULL;
455 }
456
457 #define MAX_CRED_OPTIONS 10
458
459 static const char *mag_cred_store(cmd_parms *parms, void *mconfig,
460                                   const char *w)
461 {
462     struct mag_config *cfg = (struct mag_config *)mconfig;
463     gss_key_value_element_desc *elements;
464     uint32_t count;
465     size_t size;
466     const char *p;
467     char *value;
468     char *key;
469
470     p = strchr(w, ':');
471     if (!p) {
472         ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, parms->server,
473                      "%s [%s]", "Invalid syntax for GssapiCredStore option", w);
474         return NULL;
475     }
476
477     key = apr_pstrndup(parms->pool, w, (p-w));
478     value = apr_pstrdup(parms->pool, p + 1);
479     if (!key || !value) {
480         ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, parms->server,
481                      "%s", "OOM handling GssapiCredStore option");
482         return NULL;
483     }
484
485     if (!cfg->cred_store) {
486         cfg->cred_store = apr_pcalloc(parms->pool,
487                                       sizeof(gss_key_value_set_desc));
488         if (!cfg->cred_store) {
489             ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, parms->server,
490                          "%s", "OOM handling GssapiCredStore option");
491             return NULL;
492         }
493         size = sizeof(gss_key_value_element_desc) * MAX_CRED_OPTIONS;
494         cfg->cred_store->elements = apr_palloc(parms->pool, size);
495         if (!cfg->cred_store->elements) {
496             ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, parms->server,
497                          "%s", "OOM handling GssapiCredStore option");
498         }
499     }
500
501     elements = cfg->cred_store->elements;
502     count = cfg->cred_store->count;
503
504     if (count >= MAX_CRED_OPTIONS) {
505         ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, parms->server,
506                      "Too many GssapiCredStore options (MAX: %d)",
507                      MAX_CRED_OPTIONS);
508         return NULL;
509     }
510     cfg->cred_store->count++;
511
512     elements[count].key = key;
513     elements[count].value = value;
514
515     return NULL;
516 }
517
518 static const char *mag_deleg_ccache_dir(cmd_parms *parms, void *mconfig,
519                                         const char *value)
520 {
521     struct mag_config *cfg = (struct mag_config *)mconfig;
522
523     cfg->deleg_ccache_dir = apr_pstrdup(parms->pool, value);
524     if (!cfg->deleg_ccache_dir) {
525         ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, parms->server,
526                      "%s", "OOM handling GssapiDelegCcacheDir option");
527     }
528
529     return NULL;
530 }
531
532 static const command_rec mag_commands[] = {
533     AP_INIT_FLAG("GssapiSSLonly", mag_ssl_only, NULL, OR_AUTHCFG,
534                   "Work only if connection is SSL Secured"),
535     AP_INIT_FLAG("GssapiLocalName", mag_map_to_local, NULL, OR_AUTHCFG,
536                   "Work only if connection is SSL Secured"),
537     AP_INIT_FLAG("GssapiConnectionBound", mag_conn_ctx, NULL, OR_AUTHCFG,
538                   "Authentication is bound to the TCP connection"),
539     AP_INIT_FLAG("GssapiUseSessions", mag_use_sess, NULL, OR_AUTHCFG,
540                   "Authentication uses mod_sessions to hold status"),
541     AP_INIT_RAW_ARGS("GssapiSessionKey", mag_sess_key, NULL, OR_AUTHCFG,
542                      "Key Used to seal session data."),
543 #ifdef HAVE_GSS_ACQUIRE_CRED_FROM
544     AP_INIT_FLAG("GssapiUseS4U2Proxy", mag_use_s4u2p, NULL, OR_AUTHCFG,
545                   "Initializes credentials for s4u2proxy usage"),
546 #endif
547 #ifdef HAVE_GSS_STORE_CRED_INTO
548     AP_INIT_ITERATE("GssapiCredStore", mag_cred_store, NULL, OR_AUTHCFG,
549                     "Credential Store"),
550     AP_INIT_RAW_ARGS("GssapiDelegCcacheDir", mag_deleg_ccache_dir, NULL,
551                      OR_AUTHCFG, "Directory to store delegated credentials"),
552 #endif
553     { NULL }
554 };
555
556 static void
557 mag_register_hooks(apr_pool_t *p)
558 {
559     ap_hook_check_user_id(mag_auth, NULL, NULL, APR_HOOK_MIDDLE);
560     ap_hook_post_config(mag_post_config, NULL, NULL, APR_HOOK_MIDDLE);
561     ap_hook_pre_connection(mag_pre_connection, NULL, NULL, APR_HOOK_MIDDLE);
562 }
563
564 module AP_MODULE_DECLARE_DATA auth_gssapi_module =
565 {
566     STANDARD20_MODULE_STUFF,
567     mag_create_dir_config,
568     NULL,
569     NULL,
570     NULL,
571     mag_commands,
572     mag_register_hooks
573 };