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