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