kdc: _kdc_find_etype if is_preauth must use long term keys
[heimdal.git] / kdc / httpkadmind.c
blob0e31b4044c3ca2577a28904b5e558cc4dbf1e7ef
1 /*
2 * Copyright (c) 2020 Kungliga Tekniska Högskolan
3 * (Royal Institute of Technology, Stockholm, Sweden).
4 * All rights reserved.
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in the
15 * documentation and/or other materials provided with the distribution.
17 * 3. Neither the name of the Institute nor the names of its contributors
18 * may be used to endorse or promote products derived from this software
19 * without specific prior written permission.
21 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
22 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
25 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
27 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
30 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
31 * SUCH DAMAGE.
37 #define _XOPEN_SOURCE_EXTENDED 1
38 #define _DEFAULT_SOURCE 1
39 #define _BSD_SOURCE 1
40 #define _GNU_SOURCE 1
42 #include <sys/socket.h>
43 #include <sys/types.h>
44 #include <sys/stat.h>
45 #include <sys/time.h>
46 #include <ctype.h>
47 #include <dlfcn.h>
48 #include <errno.h>
49 #include <fcntl.h>
50 #include <pthread.h>
51 #include <signal.h>
52 #include <stdarg.h>
53 #include <stddef.h>
54 #include <stdint.h>
55 #include <stdio.h>
56 #include <stdlib.h>
57 #include <string.h>
58 #include <time.h>
59 #include <unistd.h>
60 #include <netdb.h>
61 #include <netinet/in.h>
62 #include <netinet/ip.h>
64 #include <microhttpd.h>
65 #include "kdc_locl.h"
66 #include "token_validator_plugin.h"
67 #include <getarg.h>
68 #include <roken.h>
69 #include <krb5.h>
70 #include <gssapi/gssapi.h>
71 #include <gssapi/gssapi_krb5.h>
72 #include <hx509.h>
73 #include "../lib/hx509/hx_locl.h"
74 #include <hx509-private.h>
75 #include <kadm5/admin.h>
76 #include <kadm5/private.h>
77 #include <kadm5/kadm5_err.h>
79 #define heim_pcontext krb5_context
80 #define heim_pconfig krb5_context
81 #include <heimbase-svc.h>
83 #if MHD_VERSION < 0x00097002 || defined(MHD_YES)
84 /* libmicrohttpd changed these from int valued macros to an enum in 0.9.71 */
85 #ifdef MHD_YES
86 #undef MHD_YES
87 #undef MHD_NO
88 #endif
89 enum MHD_Result { MHD_NO = 0, MHD_YES = 1 };
90 #define MHD_YES 1
91 #define MHD_NO 0
92 typedef int heim_mhd_result;
93 #else
94 typedef enum MHD_Result heim_mhd_result;
95 #endif
97 #define BODYLEN_IS_STRLEN (~0)
100 * Libmicrohttpd is not the easiest API to use. It's got issues.
102 * One of the issues is how responses are handled, and the return value of the
103 * resource handler (MHD_NO -> close the connection, MHD_YES -> send response).
104 * Note that the handler could return MHD_YES without having set an HTTP
105 * response.
107 * There's memory management issues as well.
109 * Here we have to be careful about return values.
111 * Some of the functions defined here return just a krb5_error_code without
112 * having set an HTTP response on error.
113 * Others do set an HTTP response on error.
114 * The convention is to either set an HTTP response on error, or not at all,
115 * but not a mix of errors where for some the function will set a response and
116 * for others it won't.
118 * We do use some system error codes to stand in for errors here.
119 * Specifically:
121 * - EACCES -> authorization failed
122 * - EINVAL -> bad API usage
123 * - ENOSYS -> missing CSRF token but CSRF token required
125 * FIXME: We should rely only on krb5_set_error_message() and friends and make
126 * error responses only in route(), mapping krb5_error_code values to
127 * HTTP status codes. This would simplify the error handling convention
128 * here.
131 /* Our request description structure */
132 typedef struct kadmin_request_desc {
133 HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS;
135 struct MHD_Connection *connection;
136 krb5_times token_times;
138 * FIXME
140 * Currently we re-use the authz framework from bx509d, using an
141 * `hx509_request' instance (an abstraction for CSRs) to represent the
142 * request because that is what the authz plugin uses that implements the
143 * policy we want checked here.
145 * This is inappropriate in the long-term in two ways:
147 * - the policy for certificates deals in SANs and EKUs, whereas the
148 * policy for ext_keytab deals in host-based service principal names,
149 * and there is not a one-to-one mapping of service names to EKUs;
151 * - using a type from libhx509 for representing requests for things that
152 * aren't certificates is really not appropriate no matter how similar
153 * the use cases for this all might be.
155 * What we need to do is develop a library that can represent requests for
156 * credentials via naming attributes like SANs and Kerberos principal
157 * names, but more arbitrary still than what `hx509_request' supports, and
158 * then invokes a plugin.
160 * Also, we might want to develop an in-tree authorization solution that is
161 * richer than what kadmin.acl supports now, storing grants in HDB entries
162 * and/or similar places.
164 * For expediency we use `hx509_request' here for now, impedance mismatches
165 * be damned.
167 hx509_request req; /* For authz only */
168 heim_array_t service_names;
169 heim_array_t hostnames;
170 heim_array_t spns;
171 krb5_principal cprinc;
172 krb5_keytab keytab;
173 krb5_storage *sp;
174 void *kadm_handle;
175 char *realm;
176 char *keytab_name;
177 char *freeme1;
178 char *enctypes;
179 const char *method;
180 unsigned int response_set:1;
181 unsigned int materialize:1;
182 unsigned int rotate_now:1;
183 unsigned int rotate:1;
184 unsigned int revoke:1;
185 unsigned int create:1;
186 unsigned int ro:1;
187 unsigned int is_self:1;
188 char frombuf[128];
189 } *kadmin_request_desc;
191 static void
192 audit_trail(kadmin_request_desc r, krb5_error_code ret)
194 const char *retname = NULL;
197 * Get a symbolic name for some error codes.
199 * Really, libcom_err should have a primitive for this, and ours could, but
200 * we can't use a system libcom_err if we extend ours.
202 #define CASE(x) case x : retname = #x; break
203 switch (ret) {
204 case ENOSYS: retname = "ECSRFTOKENREQD"; break;
205 CASE(EINVAL);
206 CASE(ENOMEM);
207 CASE(EACCES);
208 CASE(HDB_ERR_NOT_FOUND_HERE);
209 CASE(HDB_ERR_WRONG_REALM);
210 CASE(HDB_ERR_EXISTS);
211 CASE(HDB_ERR_KVNO_NOT_FOUND);
212 CASE(HDB_ERR_NOENTRY);
213 CASE(HDB_ERR_NO_MKEY);
214 CASE(KRB5_KDC_UNREACH);
215 CASE(KADM5_FAILURE);
216 CASE(KADM5_AUTH_GET);
217 CASE(KADM5_AUTH_ADD);
218 CASE(KADM5_AUTH_MODIFY);
219 CASE(KADM5_AUTH_DELETE);
220 CASE(KADM5_AUTH_INSUFFICIENT);
221 CASE(KADM5_BAD_DB);
222 CASE(KADM5_DUP);
223 CASE(KADM5_RPC_ERROR);
224 CASE(KADM5_NO_SRV);
225 CASE(KADM5_BAD_HIST_KEY);
226 CASE(KADM5_NOT_INIT);
227 CASE(KADM5_UNK_PRINC);
228 CASE(KADM5_UNK_POLICY);
229 CASE(KADM5_BAD_MASK);
230 CASE(KADM5_BAD_CLASS);
231 CASE(KADM5_BAD_LENGTH);
232 CASE(KADM5_BAD_POLICY);
233 CASE(KADM5_BAD_PRINCIPAL);
234 CASE(KADM5_BAD_AUX_ATTR);
235 CASE(KADM5_BAD_HISTORY);
236 CASE(KADM5_BAD_MIN_PASS_LIFE);
237 CASE(KADM5_PASS_Q_TOOSHORT);
238 CASE(KADM5_PASS_Q_CLASS);
239 CASE(KADM5_PASS_Q_DICT);
240 CASE(KADM5_PASS_Q_GENERIC);
241 CASE(KADM5_PASS_REUSE);
242 CASE(KADM5_PASS_TOOSOON);
243 CASE(KADM5_POLICY_REF);
244 CASE(KADM5_INIT);
245 CASE(KADM5_BAD_PASSWORD);
246 CASE(KADM5_PROTECT_PRINCIPAL);
247 CASE(KADM5_BAD_SERVER_HANDLE);
248 CASE(KADM5_BAD_STRUCT_VERSION);
249 CASE(KADM5_OLD_STRUCT_VERSION);
250 CASE(KADM5_NEW_STRUCT_VERSION);
251 CASE(KADM5_BAD_API_VERSION);
252 CASE(KADM5_OLD_LIB_API_VERSION);
253 CASE(KADM5_OLD_SERVER_API_VERSION);
254 CASE(KADM5_NEW_LIB_API_VERSION);
255 CASE(KADM5_NEW_SERVER_API_VERSION);
256 CASE(KADM5_SECURE_PRINC_MISSING);
257 CASE(KADM5_NO_RENAME_SALT);
258 CASE(KADM5_BAD_CLIENT_PARAMS);
259 CASE(KADM5_BAD_SERVER_PARAMS);
260 CASE(KADM5_AUTH_LIST);
261 CASE(KADM5_AUTH_CHANGEPW);
262 CASE(KADM5_BAD_TL_TYPE);
263 CASE(KADM5_MISSING_CONF_PARAMS);
264 CASE(KADM5_BAD_SERVER_NAME);
265 CASE(KADM5_KS_TUPLE_NOSUPP);
266 CASE(KADM5_SETKEY3_ETYPE_MISMATCH);
267 CASE(KADM5_DECRYPT_USAGE_NOSUPP);
268 CASE(KADM5_POLICY_OP_NOSUPP);
269 CASE(KADM5_KEEPOLD_NOSUPP);
270 CASE(KADM5_AUTH_GET_KEYS);
271 CASE(KADM5_ALREADY_LOCKED);
272 CASE(KADM5_NOT_LOCKED);
273 CASE(KADM5_LOG_CORRUPT);
274 CASE(KADM5_LOG_NEEDS_UPGRADE);
275 CASE(KADM5_BAD_SERVER_HOOK);
276 CASE(KADM5_SERVER_HOOK_NOT_FOUND);
277 CASE(KADM5_OLD_SERVER_HOOK_VERSION);
278 CASE(KADM5_NEW_SERVER_HOOK_VERSION);
279 CASE(KADM5_READ_ONLY);
280 case 0:
281 retname = "SUCCESS";
282 break;
283 default:
284 retname = NULL;
285 break;
287 heim_audit_trail((heim_svc_req_desc)r, ret, retname);
290 static krb5_log_facility *logfac;
291 static pthread_key_t k5ctx;
293 static krb5_error_code
294 get_krb5_context(krb5_context *contextp)
296 krb5_error_code ret;
298 if ((*contextp = pthread_getspecific(k5ctx)))
299 return 0;
301 ret = krb5_init_context(contextp);
302 /* XXX krb5_set_log_dest(), warn_dest, debug_dest */
303 if (ret == 0)
304 (void) pthread_setspecific(k5ctx, *contextp);
305 return ret;
308 static int port = -1;
309 static int help_flag;
310 static int daemonize;
311 static int daemon_child_fd = -1;
312 static int local_hdb;
313 static int local_hdb_read_only;
314 static int read_only;
315 static int verbose_counter;
316 static int version_flag;
317 static int reverse_proxied_flag;
318 static int thread_per_client_flag;
319 struct getarg_strings audiences;
320 static const char *cert_file;
321 static const char *priv_key_file;
322 static const char *cache_dir;
323 static const char *realm;
324 static const char *hdb;
325 static const char *primary_server_URI;
326 static const char *kadmin_server;
327 static const char *writable_kadmin_server;
328 static const char *stash_file;
329 static const char *kadmin_client_name = "httpkadmind/admin";
330 static const char *kadmin_client_keytab;
331 static struct getarg_strings auth_types;
333 #define set_conf(c, f, v, b) \
334 if (v) { \
335 if (((c).f = strdup(v)) == NULL) \
336 goto enomem; \
337 conf.mask |= b; \
341 * Does NOT set an HTTP response, naturally, as it doesn't even have access to
342 * the connection.
344 static krb5_error_code
345 get_kadm_handle(krb5_context context,
346 const char *want_realm,
347 int want_write,
348 void **kadm_handle)
350 kadm5_config_params conf;
351 krb5_error_code ret;
354 * If the caller wants to write and we are configured to redirect in that
355 * case, then trigger a redirect by returning KADM5_READ_ONLY.
357 if (want_write && local_hdb_read_only && primary_server_URI)
358 return KADM5_READ_ONLY;
359 if (want_write && read_only)
360 return KADM5_READ_ONLY;
363 * Configure kadm5 connection.
365 * Note that all of these are optional, and will be found in krb5.conf or,
366 * in some cases, in DNS, as needed.
368 memset(&conf, 0, sizeof(conf));
369 conf.realm = NULL;
370 conf.dbname = NULL;
371 conf.stash_file = NULL;
372 conf.admin_server = NULL;
373 conf.readonly_admin_server = NULL;
374 set_conf(conf, realm, want_realm, KADM5_CONFIG_REALM);
375 set_conf(conf, dbname, hdb, KADM5_CONFIG_DBNAME);
376 set_conf(conf, stash_file, stash_file, KADM5_CONFIG_STASH_FILE);
377 set_conf(conf, admin_server, writable_kadmin_server, KADM5_CONFIG_ADMIN_SERVER);
378 set_conf(conf, readonly_admin_server, kadmin_server,
379 KADM5_CONFIG_READONLY_ADMIN_SERVER);
382 * If we have a local HDB we'll use it if we can. If the local HDB is
383 * read-only and the caller wants to write, then we won't use the local
384 * HDB, naturally.
386 if (local_hdb && (!local_hdb_read_only || !want_write)) {
387 ret = kadm5_s_init_with_password_ctx(context,
388 kadmin_client_name,
389 NULL, /* password */
390 NULL, /* service_name */
391 &conf,
392 0, /* struct_version */
393 0, /* api_version */
394 kadm_handle);
395 goto out;
399 * Remote connection. This will connect to a read-only kadmind if
400 * possible, and if so, reconnect to a writable kadmind as needed.
402 * Note that kadmin_client_keytab can be an HDB: or HDBGET: keytab.
404 ret = kadm5_c_init_with_skey_ctx(context,
405 kadmin_client_name,
406 kadmin_client_keytab,
407 KADM5_ADMIN_SERVICE,
408 &conf,
409 0, /* struct_version */
410 0, /* api_version */
411 kadm_handle);
412 goto out;
414 enomem:
415 ret = krb5_enomem(context);
417 out:
418 free(conf.readonly_admin_server);
419 free(conf.admin_server);
420 free(conf.stash_file);
421 free(conf.dbname);
422 free(conf.realm);
423 return ret;
426 static krb5_error_code resp(kadmin_request_desc, int, krb5_error_code,
427 enum MHD_ResponseMemoryMode, const char *,
428 const void *, size_t, const char *, const char *);
429 static krb5_error_code bad_req(kadmin_request_desc, krb5_error_code, int,
430 const char *, ...)
431 HEIMDAL_PRINTF_ATTRIBUTE((__printf__, 4, 5));
433 static krb5_error_code bad_enomem(kadmin_request_desc, krb5_error_code);
434 static krb5_error_code bad_400(kadmin_request_desc, krb5_error_code, const char *);
435 static krb5_error_code bad_401(kadmin_request_desc, const char *);
436 static krb5_error_code bad_403(kadmin_request_desc, krb5_error_code, const char *);
437 static krb5_error_code bad_404(kadmin_request_desc, const char *);
438 static krb5_error_code bad_405(kadmin_request_desc, const char *);
439 /*static krb5_error_code bad_500(kadmin_request_desc, krb5_error_code, const char *);*/
440 static krb5_error_code bad_503(kadmin_request_desc, krb5_error_code, const char *);
442 static int
443 validate_token(kadmin_request_desc r)
445 krb5_error_code ret;
446 const char *token;
447 const char *host;
448 char token_type[64]; /* Plenty */
449 char *p;
450 krb5_data tok;
451 size_t host_len, brk, i;
453 memset(&r->token_times, 0, sizeof(r->token_times));
454 host = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
455 MHD_HTTP_HEADER_HOST);
456 if (host == NULL)
457 return bad_400(r, EINVAL, "Host header is missing");
459 /* Exclude port number here (IPv6-safe because of the below) */
460 host_len = ((p = strchr(host, ':'))) ? p - host : strlen(host);
462 token = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
463 MHD_HTTP_HEADER_AUTHORIZATION);
464 if (token == NULL)
465 return bad_401(r, "Authorization token is missing");
466 brk = strcspn(token, " \t");
467 if (token[brk] == '\0' || brk > sizeof(token_type) - 1)
468 return bad_401(r, "Authorization token is missing");
469 memcpy(token_type, token, brk);
470 token_type[brk] = '\0';
471 token += brk + 1;
472 tok.length = strlen(token);
473 tok.data = (void *)(uintptr_t)token;
475 for (i = 0; i < audiences.num_strings; i++)
476 if (strncasecmp(host, audiences.strings[i], host_len) == 0 &&
477 audiences.strings[i][host_len] == '\0')
478 break;
479 if (i == audiences.num_strings)
480 return bad_403(r, EINVAL, "Host: value is not accepted here");
482 r->sname = strdup(host); /* No need to check for ENOMEM here */
484 ret = kdc_validate_token(r->context, NULL /* realm */, token_type, &tok,
485 (const char **)&audiences.strings[i], 1,
486 &r->cprinc, &r->token_times);
487 if (ret)
488 return bad_403(r, ret, "Token validation failed");
489 if (r->cprinc == NULL)
490 return bad_403(r, ret,
491 "Could not extract a principal name from token");
492 ret = krb5_unparse_name(r->context, r->cprinc, &r->cname);
493 if (ret)
494 return bad_503(r, ret,
495 "Could not extract a principal name from token");
496 return 0;
499 static void
500 k5_free_context(void *ctx)
502 krb5_free_context(ctx);
505 #ifndef HAVE_UNLINKAT
506 static int
507 unlink1file(const char *dname, const char *name)
509 char p[PATH_MAX];
511 if (strlcpy(p, dname, sizeof(p)) < sizeof(p) &&
512 strlcat(p, "/", sizeof(p)) < sizeof(p) &&
513 strlcat(p, name, sizeof(p)) < sizeof(p))
514 return unlink(p);
515 return ERANGE;
517 #endif
519 static void
520 rm_cache_dir(void)
522 struct dirent *e;
523 DIR *d;
526 * This works, but not on Win32:
528 * (void) simple_execlp("rm", "rm", "-rf", cache_dir, NULL);
530 * We make no directories in `cache_dir', so we need not recurse.
532 if ((d = opendir(cache_dir)) == NULL)
533 return;
535 while ((e = readdir(d))) {
536 #ifdef HAVE_UNLINKAT
538 * Because unlinkat() takes a directory FD, implementing one for
539 * libroken is tricky at best. Instead we might want to implement an
540 * rm_dash_rf() function in lib/roken.
542 (void) unlinkat(dirfd(d), e->d_name, 0);
543 #else
544 (void) unlink1file(cache_dir, e->d_name);
545 #endif
547 (void) closedir(d);
548 (void) rmdir(cache_dir);
552 * Work around older libmicrohttpd not strduping response header values when
553 * set.
555 static HEIMDAL_THREAD_LOCAL struct redirect_uri {
556 char uri[4096];
557 size_t len;
558 size_t first_param;
559 int valid;
560 } redirect_uri;
562 static void
563 redirect_uri_appends(struct redirect_uri *redirect,
564 const char *s)
566 size_t sz, len;
567 char *p;
569 if (!redirect->valid || redirect->len >= sizeof(redirect->uri) - 1) {
570 redirect->valid = 0;
571 return;
573 /* Optimize strlcpy by using redirect->uri + redirect->len */
574 p = redirect->uri + redirect->len;
575 sz = sizeof(redirect->uri) - redirect->len;
576 if ((len = strlcpy(p, s, sz)) >= sz)
577 redirect->valid = 0;
578 else
579 redirect->len += len;
582 static heim_mhd_result
583 make_redirect_uri_param_cb(void *d,
584 enum MHD_ValueKind kind,
585 const char *key,
586 const char *val)
588 struct redirect_uri *redirect = d;
590 redirect_uri_appends(redirect, redirect->first_param ? "?" : "&");
591 redirect_uri_appends(redirect, key);
592 if (val) {
593 redirect_uri_appends(redirect, "=");
594 redirect_uri_appends(redirect, val);
596 redirect->first_param = 0;
597 return MHD_YES;
600 static const char *
601 make_redirect_uri(kadmin_request_desc r, const char *base)
603 redirect_uri.len = 0;
604 redirect_uri.uri[0] = '\0';
605 redirect_uri.valid = 1;
606 redirect_uri.first_param = 1;
608 redirect_uri_appends(&redirect_uri, base); /* Redirect to primary URI base */
609 redirect_uri_appends(&redirect_uri, r->reqtype); /* URI local-part */
610 (void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
611 make_redirect_uri_param_cb,
612 &redirect_uri);
613 return redirect_uri.valid ? redirect_uri.uri : NULL;
618 * XXX Shouldn't be a body, but a status message. The body should be
619 * configurable to be from a file. MHD doesn't give us a way to set the
620 * response status message though, just the body.
622 * Calls audit_trail().
624 * Returns -1 if something terrible happened, which should ultimately cause
625 * route() to return MHD_NO, which should cause libmicrohttpd to close the
626 * connection to the user-agent.
628 * Returns 0 in all other cases.
630 static krb5_error_code
631 resp(kadmin_request_desc r,
632 int http_status_code,
633 krb5_error_code ret,
634 enum MHD_ResponseMemoryMode rmmode,
635 const char *content_type,
636 const void *body,
637 size_t bodylen,
638 const char *token,
639 const char *csrf)
641 struct MHD_Response *response;
642 int mret = MHD_YES;
644 if (r->response_set) {
645 krb5_log_msg(r->context, logfac, 1, NULL,
646 "Internal error; attempted to set a second response");
647 return 0;
650 (void) gettimeofday(&r->tv_end, NULL);
651 audit_trail(r, ret);
653 if (body && bodylen == BODYLEN_IS_STRLEN)
654 bodylen = strlen(body);
656 response = MHD_create_response_from_buffer(bodylen, rk_UNCONST(body),
657 rmmode);
658 if (response == NULL)
659 return -1;
660 mret = MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL,
661 "no-store, max-age=0");
662 if (mret == MHD_YES && http_status_code == MHD_HTTP_UNAUTHORIZED) {
663 size_t i;
665 if (auth_types.num_strings < 1)
666 http_status_code = MHD_HTTP_SERVICE_UNAVAILABLE;
667 else
668 for (i = 0; mret == MHD_YES && i < auth_types.num_strings; i++)
669 mret = MHD_add_response_header(response,
670 MHD_HTTP_HEADER_WWW_AUTHENTICATE,
671 auth_types.strings[i]);
672 } else if (mret == MHD_YES && http_status_code == MHD_HTTP_TEMPORARY_REDIRECT) {
673 const char *redir = make_redirect_uri(r, primary_server_URI);
675 if (redir)
676 mret = MHD_add_response_header(response, MHD_HTTP_HEADER_LOCATION,
677 redir);
678 else
679 /* XXX Find a way to set a new response body; log */
680 http_status_code = MHD_HTTP_SERVICE_UNAVAILABLE;
683 if (mret == MHD_YES && csrf)
684 mret = MHD_add_response_header(response,
685 "X-CSRF-Token",
686 csrf);
688 if (mret == MHD_YES && content_type) {
689 mret = MHD_add_response_header(response,
690 MHD_HTTP_HEADER_CONTENT_TYPE,
691 content_type);
693 if (mret != MHD_NO)
694 mret = MHD_queue_response(r->connection, http_status_code, response);
695 MHD_destroy_response(response);
696 r->response_set = 1;
697 return mret == MHD_NO ? -1 : 0;
700 static krb5_error_code
701 bad_reqv(kadmin_request_desc r,
702 krb5_error_code code,
703 int http_status_code,
704 const char *fmt,
705 va_list ap)
707 krb5_error_code ret;
708 krb5_context context = NULL;
709 const char *k5msg = NULL;
710 const char *emsg = NULL;
711 char *formatted = NULL;
712 char *msg = NULL;
714 if (r && r->context)
715 context = r->context;
716 if (r && r->hcontext && r->kv)
717 heim_audit_setkv_number((heim_svc_req_desc)r, "http-status-code",
718 http_status_code);
719 if (r)
720 (void) gettimeofday(&r->tv_end, NULL);
721 if (code == ENOMEM) {
722 if (context)
723 krb5_log_msg(context, logfac, 1, NULL, "Out of memory");
724 return resp(r, http_status_code, code, MHD_RESPMEM_PERSISTENT,
725 NULL, fmt, BODYLEN_IS_STRLEN, NULL, NULL);
728 if (code) {
729 if (context)
730 emsg = k5msg = krb5_get_error_message(context, code);
731 else
732 emsg = strerror(code);
735 ret = vasprintf(&formatted, fmt, ap) == -1;
736 if (code) {
737 if (ret > -1 && formatted)
738 ret = asprintf(&msg, "%s: %s (%d)", formatted, emsg, (int)code);
739 } else {
740 msg = formatted;
741 formatted = NULL;
743 if (r && r->hcontext)
744 heim_audit_addreason((heim_svc_req_desc)r, "%s", formatted);
745 krb5_free_error_message(context, k5msg);
747 if (ret == -1 || msg == NULL) {
748 if (context)
749 krb5_log_msg(context, logfac, 1, NULL, "Out of memory");
750 return resp(r, MHD_HTTP_SERVICE_UNAVAILABLE, ENOMEM,
751 MHD_RESPMEM_PERSISTENT, NULL,
752 "Out of memory", BODYLEN_IS_STRLEN, NULL, NULL);
755 ret = resp(r, http_status_code, code, MHD_RESPMEM_MUST_COPY,
756 NULL, msg, BODYLEN_IS_STRLEN, NULL, NULL);
757 free(formatted);
758 free(msg);
759 return ret == -1 ? -1 : code;
762 static krb5_error_code
763 bad_req(kadmin_request_desc r,
764 krb5_error_code code,
765 int http_status_code,
766 const char *fmt,
767 ...)
769 krb5_error_code ret;
770 va_list ap;
772 va_start(ap, fmt);
773 ret = bad_reqv(r, code, http_status_code, fmt, ap);
774 va_end(ap);
775 return ret;
778 static krb5_error_code
779 bad_enomem(kadmin_request_desc r, krb5_error_code ret)
781 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
782 "Out of memory");
785 static krb5_error_code
786 bad_400(kadmin_request_desc r, int ret, const char *reason)
788 return bad_req(r, ret, MHD_HTTP_BAD_REQUEST, "%s", reason);
791 static krb5_error_code
792 bad_401(kadmin_request_desc r, const char *reason)
794 return bad_req(r, EACCES, MHD_HTTP_UNAUTHORIZED, "%s", reason);
797 static krb5_error_code
798 bad_403(kadmin_request_desc r, krb5_error_code ret, const char *reason)
800 return bad_req(r, ret, MHD_HTTP_FORBIDDEN, "%s", reason);
803 static krb5_error_code
804 bad_404(kadmin_request_desc r, const char *name)
806 return bad_req(r, ENOENT, MHD_HTTP_NOT_FOUND,
807 "Resource not found: %s", name);
810 static krb5_error_code
811 bad_405(kadmin_request_desc r, const char *method)
813 return bad_req(r, EPERM, MHD_HTTP_METHOD_NOT_ALLOWED,
814 "Method not supported: %s", method);
817 static krb5_error_code
818 bad_method_want_POST(kadmin_request_desc r)
820 return bad_req(r, EPERM, MHD_HTTP_METHOD_NOT_ALLOWED,
821 "Use POST for making changes to principals");
824 #if 0
825 static krb5_error_code
826 bad_500(kadmin_request_desc r,
827 krb5_error_code ret,
828 const char *reason)
830 return bad_req(r, ret, MHD_HTTP_INTERNAL_SERVER_ERROR,
831 "Internal error: %s", reason);
833 #endif
835 static krb5_error_code
836 bad_503(kadmin_request_desc r,
837 krb5_error_code ret,
838 const char *reason)
840 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
841 "Service unavailable: %s", reason);
844 static krb5_error_code
845 good_ext_keytab(kadmin_request_desc r)
847 krb5_error_code ret;
848 size_t bodylen;
849 void *body;
850 char *p;
852 if (!r->keytab_name || !(p = strchr(r->keytab_name, ':')))
853 return bad_503(r, EINVAL, "Internal error (no keytab produced)");
854 p++;
855 if (strncmp(p, cache_dir, strlen(cache_dir)) != 0)
856 return bad_503(r, EINVAL, "Internal error");
857 ret = rk_undumpdata(p, &body, &bodylen);
858 if (ret)
859 return bad_503(r, ret, "Could not recover keytab from temp file");
861 ret = resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_MUST_COPY,
862 "application/octet-stream", body, bodylen, NULL, NULL);
863 free(body);
864 return ret;
867 static krb5_error_code
868 check_service_name(kadmin_request_desc r, const char *name)
870 if (name == NULL || name[0] == '\0' ||
871 strchr(name, '/') || strchr(name, '\\') || strchr(name, '@') ||
872 strcmp(name, "krbtgt") == 0 ||
873 strcmp(name, "iprop") == 0 ||
874 strcmp(name, "kadmin") == 0 ||
875 strcmp(name, "hprop") == 0 ||
876 strcmp(name, "WELLKNOWN") == 0 ||
877 strcmp(name, "K") == 0) {
878 krb5_set_error_message(r->context, EACCES,
879 "No one is allowed to fetch keys for "
880 "Heimdal service %s", name);
881 return EACCES;
883 if (strcmp(name, "root") != 0 &&
884 strcmp(name, "host") != 0 &&
885 strcmp(name, "exceed") != 0)
886 return 0;
887 if (krb5_config_get_bool_default(r->context, NULL, FALSE,
888 "ext_keytab",
889 "csr_authorizer_handles_svc_names",
890 NULL))
891 return 0;
892 krb5_set_error_message(r->context, EACCES,
893 "No one is allowed to fetch keys for "
894 "service \"%s\" because of authorizer "
895 "limitations", name);
896 return EACCES;
899 static heim_mhd_result
900 param_cb(void *d,
901 enum MHD_ValueKind kind,
902 const char *key,
903 const char *val)
905 kadmin_request_desc r = d;
906 krb5_error_code ret = 0;
907 heim_string_t s = NULL;
910 * Multi-valued params:
912 * - spn=<service>/<hostname>
913 * - dNSName=<hostname>
914 * - service=<service>
916 * Single-valued params:
918 * - realm=<REALM>
919 * - materialize=true -- create a concrete princ where it's virtual
920 * - enctypes=... -- key-salt types
921 * - revoke=true -- delete old keys (concrete princs only)
922 * - rotate=true -- change keys (no-op for virtual princs)
923 * - create=true -- create a concrete princ
924 * - ro=true -- perform no writes
927 if (strcmp(key, "realm") == 0 && val) {
928 if (!r->realm && !(r->realm = strdup(val)))
929 ret = krb5_enomem(r->context);
930 } else if (strcmp(key, "materialize") == 0 ||
931 strcmp(key, "revoke") == 0 ||
932 strcmp(key, "rotate") == 0 ||
933 strcmp(key, "create") == 0 ||
934 strcmp(key, "ro") == 0) {
935 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
936 "requested_option", "%s", key);
937 if (!val || strcmp(val, "true") != 0)
938 krb5_set_error_message(r->context, ret = EINVAL,
939 "get-keys \"%s\" q-param accepts "
940 "only \"true\"", key);
941 else if (strcmp(key, "materialize") == 0)
942 r->materialize = 1;
943 else if (strcmp(key, "revoke") == 0)
944 r->revoke = 1;
945 else if (strcmp(key, "rotate") == 0)
946 r->rotate = 1;
947 else if (strcmp(key, "create") == 0)
948 r->create = 1;
949 else if (strcmp(key, "ro") == 0)
950 r->ro = 1;
951 } else if (strcmp(key, "dNSName") == 0 && val) {
952 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
953 "requested_dNSName", "%s", val);
954 if (r->is_self) {
955 krb5_set_error_message(r->context, ret = EACCES,
956 "only one service may be requested for self");
957 } else if (strchr(val, '.') == NULL) {
958 krb5_set_error_message(r->context, ret = EACCES,
959 "dNSName must have at least one '.' in it");
960 } else {
961 s = heim_string_create(val);
962 if (!s)
963 ret = krb5_enomem(r->context);
964 else
965 ret = heim_array_append_value(r->hostnames, s);
967 if (ret == 0)
968 ret = hx509_request_add_dns_name(r->context->hx509ctx, r->req, val);
969 } else if (strcmp(key, "service") == 0 && val) {
970 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
971 "requested_service", "%s", val);
972 if (r->is_self)
973 krb5_set_error_message(r->context, ret = EACCES,
974 "use \"spn\" for self");
975 else
976 ret = check_service_name(r, val);
977 if (ret == 0) {
978 s = heim_string_create(val);
979 if (!s)
980 ret = krb5_enomem(r->context);
981 else
982 ret = heim_array_append_value(r->service_names, s);
984 } else if (strcmp(key, "enctypes") == 0 && val) {
985 r->enctypes = strdup(val);
986 if (!(r->enctypes = strdup(val)))
987 ret = krb5_enomem(r->context);
988 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
989 "requested_enctypes", "%s", val);
990 } else if (r->is_self && strcmp(key, "spn") == 0 && val) {
991 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
992 "requested_spn", "%s", val);
993 krb5_set_error_message(r->context, ret = EACCES,
994 "only one service may be requested for self");
995 } else if (strcmp(key, "spn") == 0 && val) {
996 krb5_principal p = NULL;
997 const char *hostname = "";
999 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
1000 "requested_spn", "%s", val);
1002 ret = krb5_parse_name_flags(r->context, val,
1003 KRB5_PRINCIPAL_PARSE_NO_DEF_REALM, &p);
1004 if (ret == 0 && krb5_principal_get_realm(r->context, p) == NULL)
1005 ret = krb5_principal_set_realm(r->context, p,
1006 r->realm ? r->realm : realm);
1009 * The SPN has to have two components.
1011 * TODO: Support more components? Support AD-style NetBIOS computer
1012 * account names?
1014 if (ret == 0 && krb5_principal_get_num_comp(r->context, p) != 2)
1015 ret = ENOTSUP;
1018 * Allow only certain service names. Except that when
1019 * the SPN == the requestor's principal name then allow the "host"
1020 * service name.
1022 if (ret == 0) {
1023 const char *service =
1024 krb5_principal_get_comp_string(r->context, p, 0);
1026 if (strcmp(service, "host") == 0 &&
1027 krb5_principal_compare(r->context, p, r->cprinc) &&
1028 !r->is_self &&
1029 heim_array_get_length(r->hostnames) == 0 &&
1030 heim_array_get_length(r->spns) == 0) {
1031 r->is_self = 1;
1032 } else
1033 ret = check_service_name(r, service);
1035 if (ret == 0 && !krb5_principal_compare(r->context, p, r->cprinc))
1036 ret = check_service_name(r,
1037 krb5_principal_get_comp_string(r->context,
1038 p, 0));
1039 if (ret == 0) {
1040 hostname = krb5_principal_get_comp_string(r->context, p, 1);
1041 if (!hostname || !strchr(hostname, '.'))
1042 krb5_set_error_message(r->context, ret = ENOTSUP,
1043 "Only host-based service names supported");
1045 if (ret == 0 && r->realm)
1046 ret = krb5_principal_set_realm(r->context, p, r->realm);
1047 else if (ret == 0 && realm)
1048 ret = krb5_principal_set_realm(r->context, p, realm);
1049 if (ret == 0)
1050 ret = hx509_request_add_dns_name(r->context->hx509ctx, r->req,
1051 hostname);
1052 if (ret == 0 && !(s = heim_string_create(val)))
1053 ret = krb5_enomem(r->context);
1054 if (ret == 0)
1055 ret = heim_array_append_value(r->spns, s);
1056 krb5_free_principal(r->context, p);
1058 #if 0
1059 /* The authorizer probably doesn't know what to do with this */
1060 ret = hx509_request_add_pkinit(r->context->hx509ctx, r->req, val);
1061 #endif
1062 } else {
1063 /* Produce error for unknown params */
1064 heim_audit_setkv_bool((heim_svc_req_desc)r, "requested_unknown", TRUE);
1065 krb5_set_error_message(r->context, ret = ENOTSUP,
1066 "Query parameter %s not supported", key);
1068 if (ret && !r->error_code)
1069 r->error_code = ret;
1070 heim_release(s);
1071 return ret ? MHD_NO /* Stop iterating */ : MHD_YES;
1074 static krb5_error_code
1075 authorize_req(kadmin_request_desc r)
1077 krb5_error_code ret;
1079 r->is_self = 0;
1080 ret = hx509_request_init(r->context->hx509ctx, &r->req);
1081 if (ret)
1082 return bad_enomem(r, ret);
1083 (void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
1084 param_cb, r);
1085 ret = r->error_code;
1086 if (ret == EACCES)
1087 return bad_403(r, ret, "Not authorized to requested principal(s)");
1088 if (ret)
1089 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
1090 "Could not handle query parameters");
1091 if (r->is_self)
1092 ret = 0;
1093 else
1094 ret = kdc_authorize_csr(r->context, "ext_keytab", r->req, r->cprinc);
1095 if (ret == EACCES || ret == EINVAL || ret == ENOTSUP ||
1096 ret == KRB5KDC_ERR_POLICY)
1097 return bad_403(r, ret, "Not authorized to requested principal(s)");
1098 if (ret)
1099 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
1100 "Error checking authorization");
1101 return ret;
1104 static krb5_error_code
1105 make_keytab(kadmin_request_desc r)
1107 krb5_error_code ret = 0;
1108 int fd = -1;
1110 r->keytab_name = NULL;
1111 if (asprintf(&r->keytab_name, "FILE:%s/kt-XXXXXX", cache_dir) == -1 ||
1112 r->keytab_name == NULL)
1113 ret = krb5_enomem(r->context);
1114 if (ret == 0)
1115 fd = mkstemp(r->keytab_name + sizeof("FILE:") - 1);
1116 if (ret == 0 && fd == -1)
1117 ret = errno;
1118 if (ret == 0)
1119 ret = krb5_kt_resolve(r->context, r->keytab_name, &r->keytab);
1120 if (fd != -1)
1121 (void) close(fd);
1122 return ret;
1125 static krb5_error_code
1126 write_keytab(kadmin_request_desc r,
1127 kadm5_principal_ent_rec *princ,
1128 const char *unparsed)
1130 krb5_error_code ret = 0;
1131 krb5_keytab_entry key;
1132 size_t i;
1134 if (princ->n_key_data <= 0)
1135 return 0;
1137 if (kadm5_some_keys_are_bogus(princ->n_key_data, &princ->key_data[0])) {
1138 krb5_warn(r->context, ret,
1139 "httpkadmind running with insufficient kadmin privilege "
1140 "for extracting keys for %s", unparsed);
1141 krb5_log_msg(r->context, logfac, 1, NULL,
1142 "httpkadmind running with insufficient kadmin privilege "
1143 "for extracting keys for %s", unparsed);
1144 return EACCES;
1147 memset(&key, 0, sizeof(key));
1148 for (i = 0; ret == 0 && i < princ->n_key_data; i++) {
1149 krb5_key_data *kd = &princ->key_data[i];
1151 key.principal = princ->principal;
1152 key.vno = kd->key_data_kvno;
1153 key.keyblock.keytype = kd->key_data_type[0];
1154 key.keyblock.keyvalue.length = kd->key_data_length[0];
1155 key.keyblock.keyvalue.data = kd->key_data_contents[0];
1158 * FIXME kadm5 doesn't give us set_time here. If it gave us the
1159 * KeyRotation metadata, we could compute it. But this might be a
1160 * concrete principal with concrete keys, in which case we can't.
1162 * To fix this we need to extend the protocol and the API.
1164 key.timestamp = time(NULL);
1166 ret = krb5_kt_add_entry(r->context, r->keytab, &key);
1168 if (ret)
1169 krb5_warn(r->context, ret,
1170 "Failed to write keytab entries for %s", unparsed);
1172 return ret;
1175 static void
1176 random_password(krb5_context context, char *buf, size_t buflen)
1178 static const char chars[] =
1179 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,";
1180 char p[32];
1181 size_t i;
1182 char b;
1184 buflen--;
1185 for (i = 0; i < buflen; i++) {
1186 if (i % sizeof(p) == 0)
1187 krb5_generate_random_block(p, sizeof(p));
1188 b = p[i % sizeof(p)];
1189 buf[i] = chars[b % (sizeof(chars) - 1)];
1191 buf[i] = '\0';
1194 static krb5_error_code
1195 make_kstuple(krb5_context context,
1196 kadm5_principal_ent_rec *p,
1197 krb5_key_salt_tuple **kstuple,
1198 size_t *n_kstuple)
1200 size_t i;
1202 *kstuple = 0;
1203 *n_kstuple = 0;
1205 if (p->n_key_data < 1)
1206 return 0;
1207 *kstuple = calloc(p->n_key_data, sizeof (**kstuple));
1208 for (i = 0; *kstuple && i < p->n_key_data; i++) {
1209 if (p->key_data[i].key_data_kvno == p->kvno) {
1210 (*kstuple)[i].ks_enctype = p->key_data[i].key_data_type[0];
1211 (*kstuple)[i].ks_salttype = p->key_data[i].key_data_type[1];
1212 (*n_kstuple)++;
1215 return *kstuple ? 0 :krb5_enomem(context);
1219 * Get keys for one principal.
1221 * Does NOT set an HTTP response.
1223 static krb5_error_code
1224 get_keys1(kadmin_request_desc r, const char *pname)
1226 kadm5_principal_ent_rec princ;
1227 krb5_key_salt_tuple *kstuple = NULL;
1228 krb5_error_code ret = 0;
1229 krb5_principal p = NULL;
1230 uint32_t mask =
1231 KADM5_PRINCIPAL | KADM5_KVNO | KADM5_MAX_LIFE | KADM5_MAX_RLIFE |
1232 KADM5_ATTRIBUTES | KADM5_KEY_DATA | KADM5_TL_DATA;
1233 uint32_t create_mask = mask & ~(KADM5_KEY_DATA | KADM5_TL_DATA);
1234 size_t nkstuple = 0;
1235 int change = 0;
1236 int refetch = 0;
1237 int freeit = 0;
1239 memset(&princ, 0, sizeof(princ));
1240 princ.key_data = NULL;
1241 princ.tl_data = NULL;
1243 ret = krb5_parse_name(r->context, pname, &p);
1244 if (ret == 0 && r->realm)
1245 ret = krb5_principal_set_realm(r->context, p, r->realm);
1246 else if (ret == 0 && realm)
1247 ret = krb5_principal_set_realm(r->context, p, realm);
1248 if (ret == 0 && r->enctypes)
1249 ret = krb5_string_to_keysalts2(r->context, r->enctypes,
1250 &nkstuple, &kstuple);
1251 if (ret == 0)
1252 ret = kadm5_get_principal(r->kadm_handle, p, &princ, mask);
1253 if (ret == 0) {
1254 freeit = 1;
1257 * If princ is virtual and we're not asked to materialize, ignore
1258 * requests to rotate.
1260 if (!r->materialize &&
1261 (princ.attributes & (KRB5_KDB_VIRTUAL_KEYS | KRB5_KDB_VIRTUAL))) {
1262 r->rotate = 0;
1263 r->revoke = 0;
1267 change = !r->ro && (r->rotate || r->revoke);
1269 /* Handle create / materialize options */
1270 if (ret == KADM5_UNK_PRINC && r->create) {
1271 char pw[128];
1273 if (read_only)
1274 ret = KADM5_READ_ONLY;
1275 else
1276 ret = strcmp(r->method, "POST") == 0 ? 0 : ENOSYS; /* XXX */
1277 if (ret == 0 && local_hdb && local_hdb_read_only) {
1278 /* Make sure we can write */
1279 kadm5_destroy(r->kadm_handle);
1280 r->kadm_handle = NULL;
1281 ret = get_kadm_handle(r->context, r->realm, 1 /* want_write */,
1282 &r->kadm_handle);
1284 memset(&princ, 0, sizeof(princ));
1286 * Some software is allergic to kvno 1, assuming that kvno 1 implies
1287 * half-baked service principal. We've some vague recollection of
1288 * something similar for kvno 2, so let's start at 3.
1290 princ.kvno = 3;
1291 princ.tl_data = NULL;
1292 princ.key_data = NULL;
1293 princ.max_life = 24 * 3600; /* XXX Make configurable */
1294 princ.max_renewable_life = princ.max_life; /* XXX Make configurable */
1296 random_password(r->context, pw, sizeof(pw));
1297 princ.principal = p; /* Borrow */
1298 if (ret == 0)
1299 ret = kadm5_create_principal_3(r->kadm_handle, &princ, create_mask,
1300 nkstuple, kstuple, pw);
1301 princ.principal = NULL; /* Return */
1302 refetch = 1;
1303 freeit = 1;
1304 } else if (ret == 0 && r->materialize &&
1305 (princ.attributes & KRB5_KDB_VIRTUAL)) {
1307 #ifndef MATERIALIZE_NOTYET
1308 ret = ENOTSUP;
1309 #else
1310 if (read_only)
1311 ret = KADM5_READ_ONLY;
1312 else
1313 ret = strcmp(r->method, "POST") == 0 ? 0 : ENOSYS; /* XXX */
1314 if (ret == 0 && local_hdb && local_hdb_read_only) {
1315 /* Make sure we can write */
1316 kadm5_destroy(r->kadm_handle);
1317 r->kadm_handle = NULL;
1318 ret = get_kadm_handle(r->context, r->realm, 1 /* want_write */,
1319 &r->kadm_handle);
1321 princ.attributes |= KRB5_KDB_MATERIALIZE;
1322 princ.attributes &= ~KRB5_KDB_VIRTUAL;
1324 * XXX If there are TL data which should be re-encoded and sent as
1325 * KRB5_TL_EXTENSION, then this call will fail with KADM5_BAD_TL_TYPE.
1327 * We should either drop those TLs, re-encode them, or make
1328 * perform_tl_data() handle them. (New extensions should generally go
1329 * as KRB5_TL_EXTENSION so that non-critical ones can be set on
1330 * principals via old kadmind programs that don't support them.)
1332 * What we really want is a kadm5 utility function to convert some TLs
1333 * to KRB5_TL_EXTENSION and drop all others.
1335 if (ret == 0)
1336 ret = kadm5_create_principal(r->kadm_handle, &princ, mask, "");
1337 refetch = 1;
1338 #endif
1339 } /* else create/materialize q-params are superfluous */
1341 /* Handle rotate / revoke options */
1342 if (ret == 0 && change) {
1343 krb5_keyblock *k = NULL;
1344 size_t i;
1345 int n_k = 0;
1346 int keepold = r->revoke ? 0 : 1;
1348 if (read_only)
1349 ret = KADM5_READ_ONLY;
1350 else
1351 ret = strcmp(r->method, "POST") == 0 ? 0 : ENOSYS; /* XXX */
1352 if (ret == 0 && local_hdb && local_hdb_read_only) {
1353 /* Make sure we can write */
1354 kadm5_destroy(r->kadm_handle);
1355 r->kadm_handle = NULL;
1356 ret = get_kadm_handle(r->context, r->realm, 1 /* want_write */,
1357 &r->kadm_handle);
1360 /* Use requested enctypes or same ones as princ already had keys for */
1361 if (ret == 0 && kstuple == NULL)
1362 ret = make_kstuple(r->context, &princ, &kstuple, &nkstuple);
1364 /* Set new keys */
1365 if (ret == 0)
1366 ret = kadm5_randkey_principal_3(r->kadm_handle, p, keepold,
1367 nkstuple, kstuple, &k, &n_k);
1368 refetch = 1;
1369 for (i = 0; n_k > 0 && i < n_k; i++)
1370 krb5_free_keyblock_contents(r->context, &k[i]);
1371 free(kstuple);
1372 free(k);
1375 if (ret == 0 && refetch) {
1376 /* Refetch changed principal */
1377 if (freeit)
1378 kadm5_free_principal_ent(r->kadm_handle, &princ);
1379 freeit = 0;
1380 ret = kadm5_get_principal(r->kadm_handle, p, &princ, mask);
1381 if (ret == 0)
1382 freeit = 1;
1385 if (ret == 0)
1386 ret = write_keytab(r, &princ, pname);
1387 if (freeit)
1388 kadm5_free_principal_ent(r->kadm_handle, &princ);
1389 krb5_free_principal(r->context, p);
1390 return ret;
1393 static krb5_error_code check_csrf(kadmin_request_desc);
1396 * Calls get_keys1() to extract each requested principal's keys.
1398 * When this returns a response will have been set.
1400 static krb5_error_code
1401 get_keysN(kadmin_request_desc r, const char *method)
1403 krb5_error_code ret;
1404 size_t nhosts;
1405 size_t nsvcs;
1406 size_t nspns;
1407 size_t i, k;
1409 /* Parses and validates the request, then checks authorization */
1410 ret = authorize_req(r);
1411 if (ret)
1412 return ret; /* authorize_req() calls bad_req() on error */
1414 ret = get_kadm_handle(r->context, r->realm ? r->realm : realm,
1415 0 /* want_write */, &r->kadm_handle);
1417 if (strcmp(method, "POST") == 0 && (ret = check_csrf(r)))
1418 return ret; /* check_csrf() calls bad_req() on error */
1420 nhosts = heim_array_get_length(r->hostnames);
1421 nsvcs = heim_array_get_length(r->service_names);
1422 nspns = heim_array_get_length(r->spns);
1423 if (!nhosts && !nspns)
1424 return bad_403(r, EINVAL, "No service principals requested");
1426 if (nhosts && !nsvcs) {
1427 heim_string_t s;
1429 if ((s = heim_string_create("HTTP")) == NULL)
1430 ret = krb5_enomem(r->context);
1431 if (ret == 0)
1432 ret = heim_array_append_value(r->service_names, s);
1433 heim_release(s);
1434 nsvcs = 1;
1435 if (ret)
1436 return bad_503(r, ret, "Out of memory");
1439 /* FIXME: Make this configurable */
1440 if (nspns + nsvcs * nhosts > 40)
1441 return bad_403(r, EINVAL, "Requested keys for too many principals");
1443 ret = make_keytab(r);
1444 for (i = 0; ret == 0 && i < nsvcs; i++) {
1445 const char *svc =
1446 heim_string_get_utf8(
1447 heim_array_get_value(r->service_names, i));
1449 for (k = 0; ret == 0 && k < nhosts; k++) {
1450 krb5_principal p = NULL;
1451 const char *hostname =
1452 heim_string_get_utf8(
1453 heim_array_get_value(r->hostnames, k));
1454 char *spn = NULL;
1456 ret = krb5_make_principal(r->context, &p,
1457 r->realm ? r->realm : realm,
1458 svc, hostname, NULL);
1459 if (ret == 0)
1460 ret = krb5_unparse_name(r->context, p, &spn);
1461 if (ret == 0)
1462 ret = get_keys1(r, spn);
1463 krb5_free_principal(r->context, p);
1464 free(spn);
1467 for (i = 0; ret == 0 && i < nspns; i++) {
1468 ret = get_keys1(r,
1469 heim_string_get_utf8(heim_array_get_value(r->spns,
1470 i)));
1472 switch (ret) {
1473 case -1:
1474 /* Can't happen */
1475 krb5_log_msg(r->context, logfac, 1, NULL,
1476 "Failed to extract keys for unknown reasons");
1477 if (r->response_set)
1478 return MHD_YES;
1479 return bad_503(r, ret, "Could not get keys");
1480 case ENOSYS:
1481 /* Our convention */
1482 return bad_method_want_POST(r);
1483 case KADM5_READ_ONLY:
1484 if (primary_server_URI) {
1485 krb5_log_msg(r->context, logfac, 1, NULL,
1486 "Redirect %s to primary server", r->cname);
1487 return resp(r, MHD_HTTP_TEMPORARY_REDIRECT, KADM5_READ_ONLY,
1488 MHD_RESPMEM_PERSISTENT, NULL, "", 0, NULL, NULL);
1489 } else {
1490 krb5_log_msg(r->context, logfac, 1, NULL, "HDB is read-only");
1491 return bad_403(r, ret, "HDB is read-only");
1493 case 0:
1494 krb5_log_msg(r->context, logfac, 1, NULL, "Sent keytab to %s",
1495 r->cname);
1496 return good_ext_keytab(r);
1497 default:
1498 return bad_503(r, ret, "Could not get keys");
1502 /* Copied from kdc/connect.c */
1503 static void
1504 addr_to_string(krb5_context context,
1505 struct sockaddr *addr,
1506 char *str,
1507 size_t len)
1509 krb5_error_code ret;
1510 krb5_address a;
1512 ret = krb5_sockaddr2address(context, addr, &a);
1513 if (ret == 0) {
1514 ret = krb5_print_address(&a, str, len, &len);
1515 krb5_free_address(context, &a);
1517 if (ret)
1518 snprintf(str, len, "<family=%d>", addr->sa_family);
1521 static krb5_error_code
1522 set_req_desc(struct MHD_Connection *connection,
1523 const char *method,
1524 const char *url,
1525 kadmin_request_desc r)
1527 const union MHD_ConnectionInfo *ci;
1528 const char *token;
1529 krb5_error_code ret;
1531 memset(r, 0, sizeof(*r));
1532 (void) gettimeofday(&r->tv_start, NULL);
1534 if ((ret = get_krb5_context(&r->context)))
1535 return ret;
1536 /* HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS fields */
1537 r->request.data = "<HTTP-REQUEST>";
1538 r->request.length = sizeof("<HTTP-REQUEST>");
1539 r->from = r->frombuf;
1540 r->config = NULL;
1541 r->logf = logfac;
1542 r->reqtype = url;
1543 r->reason = NULL;
1544 r->reply = NULL;
1545 r->sname = NULL;
1546 r->cname = NULL;
1547 r->addr = NULL;
1548 r->kv = heim_dict_create(10);
1549 r->attributes = heim_dict_create(1);
1550 /* Our fields */
1551 r->connection = connection;
1552 r->kadm_handle = NULL;
1553 r->hcontext = r->context->hcontext;
1554 r->service_names = heim_array_create();
1555 r->hostnames = heim_array_create();
1556 r->spns = heim_array_create();
1557 r->keytab_name = NULL;
1558 r->enctypes = NULL;
1559 r->freeme1 = NULL;
1560 r->method = method;
1561 r->cprinc = NULL;
1562 r->req = NULL;
1563 r->sp = NULL;
1564 ci = MHD_get_connection_info(connection,
1565 MHD_CONNECTION_INFO_CLIENT_ADDRESS);
1566 if (ci) {
1567 r->addr = ci->client_addr;
1568 addr_to_string(r->context, r->addr, r->frombuf, sizeof(r->frombuf));
1571 if (r->kv) {
1572 heim_audit_addkv((heim_svc_req_desc)r, 0, "method", "GET");
1573 heim_audit_addkv((heim_svc_req_desc)r, 0, "endpoint", "%s", r->reqtype);
1575 token = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
1576 MHD_HTTP_HEADER_AUTHORIZATION);
1577 if (token && r->kv) {
1578 const char *token_end;
1580 if ((token_end = strchr(token, ' ')) == NULL ||
1581 (token_end - token) > INT_MAX || (token_end - token) < 2)
1582 heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "<unknown>");
1583 else
1584 heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "%.*s",
1585 (int)(token_end - token), token);
1589 if (ret == 0 && r->kv == NULL) {
1590 krb5_log_msg(r->context, logfac, 1, NULL, "Out of memory");
1591 ret = r->error_code = ENOMEM;
1593 return ret;
1596 static void
1597 clean_req_desc(kadmin_request_desc r)
1599 if (!r)
1600 return;
1602 if (r->keytab)
1603 krb5_kt_destroy(r->context, r->keytab);
1604 else if (r->keytab_name && strchr(r->keytab_name, ':'))
1605 (void) unlink(strchr(r->keytab_name, ':') + 1);
1606 if (r->kadm_handle)
1607 kadm5_destroy(r->kadm_handle);
1608 hx509_request_free(&r->req);
1609 heim_release(r->service_names);
1610 heim_release(r->hostnames);
1611 heim_release(r->reason);
1612 heim_release(r->spns);
1613 heim_release(r->kv);
1614 krb5_free_principal(r->context, r->cprinc);
1615 free(r->keytab_name);
1616 free(r->enctypes);
1617 free(r->freeme1);
1618 free(r->cname);
1619 free(r->sname);
1622 /* Implements GETs of /get-keys */
1623 static krb5_error_code
1624 get_keys(kadmin_request_desc r, const char *method)
1626 krb5_error_code ret;
1628 if ((ret = validate_token(r)))
1629 return ret; /* validate_token() calls bad_req() */
1630 if (r->cname == NULL || r->cprinc == NULL)
1631 return bad_403(r, EINVAL,
1632 "Could not extract principal name from token");
1633 return get_keysN(r, method); /* Sets an HTTP response */
1636 /* Implements GETs of /get-config */
1637 static krb5_error_code
1638 get_config(kadmin_request_desc r)
1641 kadm5_principal_ent_rec princ;
1642 krb5_error_code ret;
1643 krb5_principal p = NULL;
1644 uint32_t mask = KADM5_PRINCIPAL | KADM5_TL_DATA;
1645 krb5_tl_data *tl_next;
1646 const char *pname;
1647 /* Default configuration for principals that have none set: */
1648 size_t bodylen = sizeof("include /etc/krb5.conf\n") - 1;
1649 void *body = "include /etc/krb5.conf\n";
1650 int freeit = 0;
1652 if ((ret = validate_token(r)))
1653 return ret; /* validate_token() calls bad_req() */
1654 if (r->cname == NULL || r->cprinc == NULL)
1655 return bad_403(r, EINVAL,
1656 "Could not extract principal name from token");
1658 * No authorization needed -- configs are public. Though we do require
1659 * authentication (above).
1662 ret = get_kadm_handle(r->context, r->realm ? r->realm : realm,
1663 0 /* want_write */, &r->kadm_handle);
1664 if (ret)
1665 return bad_503(r, ret, "Could not access KDC database");
1667 memset(&princ, 0, sizeof(princ));
1668 princ.key_data = NULL;
1669 princ.tl_data = NULL;
1671 pname = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
1672 "princ");
1673 if (pname == NULL)
1674 pname = r->cname;
1675 ret = krb5_parse_name(r->context, pname, &p);
1676 if (ret == 0) {
1677 ret = kadm5_get_principal(r->kadm_handle, p, &princ, mask);
1678 if (ret == 0) {
1679 freeit = 1;
1680 for (tl_next = princ.tl_data; tl_next; tl_next = tl_next->tl_data_next) {
1681 if (tl_next->tl_data_type != KRB5_TL_KRB5_CONFIG)
1682 continue;
1683 bodylen = tl_next->tl_data_length;
1684 body = tl_next->tl_data_contents;
1685 break;
1687 } else {
1688 r->error_code = ret;
1689 return bad_404(r, "/get-config");
1693 if (ret == 0) {
1694 krb5_log_msg(r->context, logfac, 1, NULL,
1695 "Returned krb5.conf contents to %s", r->cname);
1696 ret = resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_MUST_COPY,
1697 "application/text", body, bodylen, NULL, NULL);
1698 } else {
1699 ret = bad_503(r, ret, "Could not retrieve principal configuration");
1701 if (freeit)
1702 kadm5_free_principal_ent(r->kadm_handle, &princ);
1703 krb5_free_principal(r->context, p);
1704 return ret;
1707 static krb5_error_code
1708 mac_csrf_token(kadmin_request_desc r, krb5_storage *sp)
1710 kadm5_principal_ent_rec princ;
1711 krb5_error_code ret;
1712 krb5_principal p = NULL;
1713 krb5_data data;
1714 char mac[EVP_MAX_MD_SIZE];
1715 unsigned int maclen = sizeof(mac);
1716 HMAC_CTX *ctx = NULL;
1717 size_t i = 0;
1718 int freeit = 0;
1720 memset(&princ, 0, sizeof(princ));
1721 ret = krb5_storage_to_data(sp, &data);
1722 if (r->kadm_handle == NULL)
1723 ret = get_kadm_handle(r->context, r->realm, 0 /* want_write */,
1724 &r->kadm_handle);
1725 if (ret == 0)
1726 ret = krb5_make_principal(r->context, &p,
1727 r->realm ? r->realm : realm,
1728 "WELLKNOWN", "CSRFTOKEN", NULL);
1729 if (ret == 0)
1730 ret = kadm5_get_principal(r->kadm_handle, p, &princ,
1731 KADM5_PRINCIPAL | KADM5_KVNO |
1732 KADM5_KEY_DATA);
1733 if (ret == 0)
1734 freeit = 1;
1735 if (ret == 0 && princ.n_key_data < 1)
1736 ret = KADM5_UNK_PRINC;
1737 if (ret == 0)
1738 for (i = 0; i < princ.n_key_data; i++)
1739 if (princ.key_data[i].key_data_kvno == princ.kvno)
1740 break;
1741 if (ret == 0 && i == princ.n_key_data)
1742 i = 0; /* Weird, but can't happen */
1744 if (ret == 0 && (ctx = HMAC_CTX_new()) == NULL)
1745 ret = krb5_enomem(r->context);
1746 /* HMAC the token body and the client principal name */
1747 if (ret == 0) {
1748 if (HMAC_Init_ex(ctx, princ.key_data[i].key_data_contents[0],
1749 princ.key_data[i].key_data_length[0], EVP_sha256(),
1750 NULL) == 0) {
1751 HMAC_CTX_cleanup(ctx);
1752 ret = krb5_enomem(r->context);
1753 } else {
1754 HMAC_Update(ctx, data.data, data.length);
1755 HMAC_Update(ctx, r->cname, strlen(r->cname));
1756 HMAC_Final(ctx, mac, &maclen);
1757 HMAC_CTX_cleanup(ctx);
1758 krb5_data_free(&data);
1759 data.length = maclen;
1760 data.data = mac;
1761 if (krb5_storage_write(sp, mac, maclen) != maclen)
1762 ret = krb5_enomem(r->context);
1765 krb5_free_principal(r->context, p);
1766 if (freeit)
1767 kadm5_free_principal_ent(r->kadm_handle, &princ);
1768 if (ctx)
1769 HMAC_CTX_free(ctx);
1770 return ret;
1773 static krb5_error_code
1774 make_csrf_token(kadmin_request_desc r,
1775 const char *given,
1776 char **token,
1777 int64_t *age)
1779 static HEIMDAL_THREAD_LOCAL char tokenbuf[128]; /* See below, be sad */
1780 krb5_error_code ret = 0;
1781 unsigned char given_decoded[128];
1782 krb5_storage *sp = NULL;
1783 krb5_data data;
1784 ssize_t dlen = -1;
1785 uint64_t nonce;
1786 int64_t t = 0;
1789 *age = 0;
1790 data.data = NULL;
1791 data.length = 0;
1792 if (given) {
1793 size_t len = strlen(given);
1795 if (len >= sizeof(given_decoded))
1796 ret = ERANGE;
1797 if (ret == 0 && (dlen = rk_base64_decode(given, &given_decoded)) <= 0)
1798 ret = errno;
1799 if (ret == 0 &&
1800 (sp = krb5_storage_from_mem(given_decoded, dlen)) == NULL)
1801 ret = krb5_enomem(r->context);
1802 if (ret == 0)
1803 ret = krb5_ret_int64(sp, &t);
1804 if (ret == 0)
1805 ret = krb5_ret_uint64(sp, &nonce);
1806 krb5_storage_free(sp);
1807 sp = NULL;
1808 if (ret == 0)
1809 *age = time(NULL) - t;
1810 } else {
1811 t = time(NULL);
1812 krb5_generate_random_block((void *)&nonce, sizeof(nonce));
1815 if (ret == 0 && (sp = krb5_storage_emem()) == NULL)
1816 ret = krb5_enomem(r->context);
1817 if (ret == 0)
1818 ret = krb5_store_int64(sp, t);
1819 if (ret == 0)
1820 ret = krb5_store_uint64(sp, nonce);
1821 if (ret == 0)
1822 ret = mac_csrf_token(r, sp);
1823 if (ret == 0)
1824 ret = krb5_storage_to_data(sp, &data);
1825 if (ret == 0 && data.length > INT_MAX)
1826 ret = ERANGE;
1827 if (ret == 0 &&
1828 (dlen = rk_base64_encode(data.data, data.length, token)) < 0)
1829 ret = errno;
1830 if (ret == 0 && dlen >= sizeof(tokenbuf))
1831 ret = ERANGE;
1832 if (ret == 0) {
1834 * Work around for older versions of libmicrohttpd do not strdup()ing
1835 * response header values.
1837 memcpy(tokenbuf, *token, dlen);
1838 free(*token);
1839 *token = tokenbuf;
1841 krb5_storage_free(sp);
1842 krb5_data_free(&data);
1843 return ret;
1847 * Returns system or krb5_error_code on error, but also calls resp() or bad_*()
1848 * on error.
1850 static krb5_error_code
1851 check_csrf(kadmin_request_desc r)
1853 krb5_error_code ret;
1854 const char *given;
1855 int64_t age;
1856 size_t givenlen, expectedlen;
1857 char *expected = NULL;
1859 given = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
1860 "X-CSRF-Token");
1861 ret = make_csrf_token(r, given, &expected, &age);
1862 if (ret)
1863 return bad_503(r, ret, "Could not create a CSRF token");
1865 * If CSRF token needed but missing, call resp() directly, bypassing
1866 * bad_403(), to return a 403 with an expected CSRF token in the response.
1868 if (given == NULL) {
1869 (void) resp(r, MHD_HTTP_FORBIDDEN, ENOSYS, MHD_RESPMEM_PERSISTENT,
1870 NULL, "CSRF token needed; copy the X-CSRF-Token: response "
1871 "header to your next POST", BODYLEN_IS_STRLEN, NULL,
1872 expected);
1873 return ENOSYS;
1876 /* Validate the CSRF token for this request */
1877 givenlen = strlen(given);
1878 expectedlen = strlen(expected);
1879 if (givenlen != expectedlen || ct_memcmp(given, expected, givenlen)) {
1880 (void) bad_403(r, EACCES, "Invalid CSRF token");
1881 return EACCES;
1883 if (age > 300) { /* XXX */
1884 (void) bad_403(r, EACCES, "CSRF token too old");
1885 return EACCES;
1887 return 0;
1890 static krb5_error_code
1891 health(const char *method, kadmin_request_desc r)
1893 if (strcmp(method, "HEAD") == 0) {
1894 return resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_PERSISTENT, NULL, "", 0,
1895 NULL, NULL);
1897 return resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_PERSISTENT, NULL,
1898 "To determine the health of the service, use the /get-config "
1899 "end-point.\n", BODYLEN_IS_STRLEN, NULL, NULL);
1903 /* Implements the entirety of this REST service */
1904 static heim_mhd_result
1905 route(void *cls,
1906 struct MHD_Connection *connection,
1907 const char *url,
1908 const char *method,
1909 const char *version,
1910 const char *upload_data,
1911 size_t *upload_data_size,
1912 void **ctx)
1914 static int aptr = 0;
1915 struct kadmin_request_desc r;
1916 int ret;
1918 if (*ctx == NULL) {
1920 * This is the first call, right after headers were read.
1922 * We must return quickly so that any 100-Continue might be sent with
1923 * celerity.
1925 * We'll get called again to really do the processing. If we handled
1926 * POSTs then we'd also get called with upload_data != NULL between the
1927 * first and last calls. We need to keep no state between the first
1928 * and last calls, but we do need to distinguish first and last call,
1929 * so we use the ctx argument for this.
1931 *ctx = &aptr;
1932 return MHD_YES;
1936 * Note that because we attempt to connect to the HDB in set_req_desc(),
1937 * this early 503 if we fail to serves to do all of what /health should do.
1939 if ((ret = set_req_desc(connection, method, url, &r)))
1940 return bad_503(&r, ret, "Could not initialize request state");
1941 if ((strcmp(method, "HEAD") == 0 || strcmp(method, "GET") == 0) &&
1942 (strcmp(url, "/health") == 0 || strcmp(url, "/") == 0)) {
1943 ret = health(method, &r);
1944 } else if (strcmp(method, "GET") != 0 && strcmp(method, "POST") != 0) {
1945 ret = bad_405(&r, method);
1946 } else if (strcmp(url, "/get-keys") == 0) {
1947 ret = get_keys(&r, method);
1948 } else if (strcmp(url, "/get-config") == 0) {
1949 if (strcmp(method, "GET") != 0)
1950 ret = bad_405(&r, method);
1951 else
1952 ret = get_config(&r);
1953 } else {
1954 ret = bad_404(&r, url);
1957 clean_req_desc(&r);
1958 return ret == -1 ? MHD_NO : MHD_YES;
1961 static struct getargs args[] = {
1962 { "help", 'h', arg_flag, &help_flag, "Print usage message", NULL },
1963 { "version", '\0', arg_flag, &version_flag, "Print version", NULL },
1964 { NULL, 'H', arg_strings, &audiences,
1965 "expected token audience(s) of the service", "HOSTNAME" },
1966 { "daemon", 'd', arg_flag, &daemonize, "daemonize", "daemonize" },
1967 { "daemon-child", 0, arg_flag, &daemon_child_fd, NULL, NULL }, /* priv */
1968 { "reverse-proxied", 0, arg_flag, &reverse_proxied_flag,
1969 "reverse proxied", "listen on 127.0.0.1 and do not use TLS" },
1970 { NULL, 'p', arg_integer, &port, "PORT", "port number (default: 443)" },
1971 { "temp-dir", 0, arg_string, &cache_dir,
1972 "cache directory", "DIRECTORY" },
1973 { "cert", 0, arg_string, &cert_file,
1974 "certificate file path (PEM)", "HX509-STORE" },
1975 { "private-key", 0, arg_string, &priv_key_file,
1976 "private key file path (PEM)", "HX509-STORE" },
1977 { "thread-per-client", 't', arg_flag, &thread_per_client_flag, "thread per-client", NULL },
1978 { "realm", 0, arg_string, &realm, "realm", "REALM" },
1979 { "hdb", 0, arg_string, &hdb, "HDB filename", "PATH" },
1980 { "read-only-admin-server", 0, arg_string, &kadmin_server,
1981 "Name of read-only kadmin server", "HOST[:PORT]" },
1982 { "writable-admin-server", 0, arg_string, &writable_kadmin_server,
1983 "Name of writable kadmin server", "HOST[:PORT]" },
1984 { "primary-server-uri", 0, arg_string, &primary_server_URI,
1985 "Name of primary httpkadmind server for HTTP redirects", "URL" },
1986 { "local", 'l', arg_flag, &local_hdb,
1987 "Use a local HDB as read-only", NULL },
1988 { "local-read-only", 0, arg_flag, &local_hdb_read_only,
1989 "Use a local HDB as read-only", NULL },
1990 { "read-only", 0, arg_flag, &read_only, "Allow no writes", NULL },
1991 { "stash-file", 0, arg_string, &stash_file,
1992 "Stash file for HDB", "PATH" },
1993 { "kadmin-client-name", 0, arg_string, &kadmin_client_name,
1994 "Client name for remote kadmind", "PRINCIPAL" },
1995 { "kadmin-client-keytab", 0, arg_string, &kadmin_client_keytab,
1996 "Keytab with client credentials for remote kadmind", "KEYTAB" },
1997 { "token-authentication-type", 'T', arg_strings, &auth_types,
1998 "Token authentication type(s) supported", "HTTP-AUTH-TYPE" },
1999 { "verbose", 'v', arg_counter, &verbose_counter, "verbose", "run verbosely" }
2002 static int
2003 usage(int e)
2005 arg_printusage(args, sizeof(args) / sizeof(args[0]), "httpkadmind",
2006 "\nServes an HTTP API for getting (and rotating) service "
2007 "principal keys, and other kadmin-like operations\n");
2008 exit(e);
2011 static int sigpipe[2] = { -1, -1 };
2013 static void
2014 sighandler(int sig)
2016 char c = sig;
2017 while (write(sigpipe[1], &c, sizeof(c)) == -1 && errno == EINTR)
2021 static void
2022 my_openlog(krb5_context context,
2023 const char *svc,
2024 krb5_log_facility **fac)
2026 char **s = NULL, **p;
2028 krb5_initlog(context, "httpkadmind", fac);
2029 s = krb5_config_get_strings(context, NULL, svc, "logging", NULL);
2030 if (s == NULL)
2031 s = krb5_config_get_strings(context, NULL, "logging", svc, NULL);
2032 if (s) {
2033 for(p = s; *p; p++)
2034 krb5_addlog_dest(context, *fac, *p);
2035 krb5_config_free_strings(s);
2036 } else {
2037 char *ss;
2038 if (asprintf(&ss, "0-1/FILE:%s/%s", hdb_db_dir(context),
2039 KDC_LOG_FILE) < 0)
2040 err(1, "out of memory");
2041 krb5_addlog_dest(context, *fac, ss);
2042 free(ss);
2044 krb5_set_warn_dest(context, *fac);
2047 static const char *sysplugin_dirs[] = {
2048 #ifdef _WIN32
2049 "$ORIGIN",
2050 #else
2051 "$ORIGIN/../lib/plugin/kdc",
2052 #endif
2053 #ifdef __APPLE__
2054 LIBDIR "/plugin/kdc",
2055 #endif
2056 NULL
2059 static void
2060 load_plugins(krb5_context context)
2062 const char * const *dirs = sysplugin_dirs;
2063 #ifndef _WIN32
2064 char **cfdirs;
2066 cfdirs = krb5_config_get_strings(context, NULL, "kdc", "plugin_dir", NULL);
2067 if (cfdirs)
2068 dirs = (const char * const *)cfdirs;
2069 #endif
2071 /* XXX kdc? */
2072 _krb5_load_plugins(context, "kdc", (const char **)dirs);
2074 #ifndef _WIN32
2075 krb5_config_free_strings(cfdirs);
2076 #endif
2080 main(int argc, char **argv)
2082 unsigned int flags = MHD_USE_THREAD_PER_CONNECTION; /* XXX */
2083 struct sockaddr_in sin;
2084 struct MHD_Daemon *previous = NULL;
2085 struct MHD_Daemon *current = NULL;
2086 struct sigaction sa;
2087 krb5_context context = NULL;
2088 MHD_socket sock = MHD_INVALID_SOCKET;
2089 void *kadm_handle;
2090 char *priv_key_pem = NULL;
2091 char *cert_pem = NULL;
2092 char sig;
2093 int optidx = 0;
2094 int ret;
2096 setprogname("httpkadmind");
2097 if (getarg(args, sizeof(args) / sizeof(args[0]), argc, argv, &optidx))
2098 usage(1);
2099 if (help_flag)
2100 usage(0);
2101 if (version_flag) {
2102 print_version(NULL);
2103 exit(0);
2105 if (argc > optidx) /* Add option to set a URI local part prefix? */
2106 usage(1);
2107 if (port < 0)
2108 errx(1, "Port number must be given");
2110 if (audiences.num_strings == 0) {
2111 char localhost[MAXHOSTNAMELEN];
2113 ret = gethostname(localhost, sizeof(localhost));
2114 if (ret == -1)
2115 errx(1, "Could not determine local hostname; use --audience");
2117 if ((audiences.strings =
2118 calloc(1, sizeof(audiences.strings[0]))) == NULL ||
2119 (audiences.strings[0] = strdup(localhost)) == NULL)
2120 err(1, "Out of memory");
2121 audiences.num_strings = 1;
2124 if (daemonize && daemon_child_fd == -1)
2125 daemon_child_fd = roken_detach_prep(argc, argv, "--daemon-child");
2126 daemonize = 0;
2128 argc -= optidx;
2129 argv += optidx;
2130 if (argc != 0)
2131 usage(1);
2133 if ((errno = pthread_key_create(&k5ctx, k5_free_context)))
2134 err(1, "Could not create thread-specific storage");
2136 if ((errno = get_krb5_context(&context)))
2137 err(1, "Could not init krb5 context (config file issue?)");
2139 if (!realm) {
2140 char *s;
2142 ret = krb5_get_default_realm(context, &s);
2143 if (ret)
2144 krb5_err(context, 1, ret, "Could not determine default realm");
2145 realm = s;
2148 if ((errno = get_kadm_handle(context, realm, 0 /* want_write */,
2149 &kadm_handle)))
2150 err(1, "Could not connect to HDB");
2151 kadm5_destroy(kadm_handle);
2153 my_openlog(context, "httpkadmind", &logfac);
2154 load_plugins(context);
2156 if (cache_dir == NULL) {
2157 char *s = NULL;
2159 if (asprintf(&s, "%s/httpkadmind-XXXXXX",
2160 getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp") == -1 ||
2161 s == NULL ||
2162 (cache_dir = mkdtemp(s)) == NULL)
2163 err(1, "could not create temporary cache directory");
2164 if (verbose_counter)
2165 fprintf(stderr, "Note: using %s as cache directory\n", cache_dir);
2166 atexit(rm_cache_dir);
2167 setenv("TMPDIR", cache_dir, 1);
2170 again:
2171 if (cert_file && !priv_key_file)
2172 priv_key_file = cert_file;
2174 if (cert_file) {
2175 hx509_cursor cursor = NULL;
2176 hx509_certs certs = NULL;
2177 hx509_cert cert = NULL;
2178 time_t min_cert_life = 0;
2179 size_t len;
2180 void *s;
2182 ret = hx509_certs_init(context->hx509ctx, cert_file, 0, NULL, &certs);
2183 if (ret == 0)
2184 ret = hx509_certs_start_seq(context->hx509ctx, certs, &cursor);
2185 while (ret == 0 &&
2186 (ret = hx509_certs_next_cert(context->hx509ctx, certs,
2187 cursor, &cert)) == 0 && cert) {
2188 time_t notAfter = 0;
2190 if (!hx509_cert_have_private_key_only(cert) &&
2191 (notAfter = hx509_cert_get_notAfter(cert)) <= time(NULL) + 30)
2192 errx(1, "One or more certificates in %s are expired",
2193 cert_file);
2194 if (notAfter) {
2195 notAfter -= time(NULL);
2196 if (notAfter < 600)
2197 warnx("One or more certificates in %s expire soon",
2198 cert_file);
2199 /* Reload 5 minutes prior to expiration */
2200 if (notAfter < min_cert_life || min_cert_life < 1)
2201 min_cert_life = notAfter;
2203 hx509_cert_free(cert);
2205 if (certs)
2206 (void) hx509_certs_end_seq(context->hx509ctx, certs, cursor);
2207 if (min_cert_life > 4)
2208 alarm(min_cert_life >> 1);
2209 hx509_certs_free(&certs);
2210 if (ret)
2211 hx509_err(context->hx509ctx, 1, ret,
2212 "could not read certificate from %s", cert_file);
2214 if ((errno = rk_undumpdata(cert_file, &s, &len)) ||
2215 (cert_pem = strndup(s, len)) == NULL)
2216 err(1, "could not read certificate from %s", cert_file);
2217 if (strlen(cert_pem) != len)
2218 err(1, "NULs in certificate file contents: %s", cert_file);
2219 free(s);
2222 if (priv_key_file) {
2223 size_t len;
2224 void *s;
2226 if ((errno = rk_undumpdata(priv_key_file, &s, &len)) ||
2227 (priv_key_pem = strndup(s, len)) == NULL)
2228 err(1, "could not read private key from %s", priv_key_file);
2229 if (strlen(priv_key_pem) != len)
2230 err(1, "NULs in private key file contents: %s", priv_key_file);
2231 free(s);
2234 if (verbose_counter > 1)
2235 flags |= MHD_USE_DEBUG;
2236 if (thread_per_client_flag)
2237 flags |= MHD_USE_THREAD_PER_CONNECTION;
2240 if (pipe(sigpipe) == -1)
2241 err(1, "Could not set up key/cert reloading");
2242 memset(&sa, 0, sizeof(sa));
2243 sa.sa_handler = sighandler;
2244 if (reverse_proxied_flag) {
2246 * We won't use TLS in the reverse proxy case, so no need to reload
2247 * certs. But we'll still read them if given, and alarm() will get
2248 * called.
2250 * XXX We should be able to re-read krb5.conf and such on SIGHUP.
2252 (void) signal(SIGHUP, SIG_IGN);
2253 (void) signal(SIGUSR1, SIG_IGN);
2254 (void) signal(SIGALRM, SIG_IGN);
2255 } else {
2256 (void) sigaction(SIGHUP, &sa, NULL); /* Reload key & cert */
2257 (void) sigaction(SIGUSR1, &sa, NULL); /* Reload key & cert */
2258 (void) sigaction(SIGALRM, &sa, NULL); /* Reload key & cert */
2260 (void) sigaction(SIGINT, &sa, NULL); /* Graceful shutdown */
2261 (void) sigaction(SIGTERM, &sa, NULL); /* Graceful shutdown */
2262 (void) signal(SIGPIPE, SIG_IGN);
2264 if (previous)
2265 sock = MHD_quiesce_daemon(previous);
2267 if (reverse_proxied_flag) {
2269 * XXX IPv6 too. Create the sockets and tell MHD_start_daemon() about
2270 * them.
2272 sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
2273 sin.sin_family = AF_INET;
2274 sin.sin_port = htons(port);
2275 current = MHD_start_daemon(flags, port,
2276 NULL, NULL,
2277 route, (char *)NULL,
2278 MHD_OPTION_SOCK_ADDR, &sin,
2279 MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
2280 MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
2281 MHD_OPTION_END);
2282 } else if (sock != MHD_INVALID_SOCKET) {
2284 * Certificate/key rollover: reuse the listen socket returned by
2285 * MHD_quiesce_daemon().
2287 current = MHD_start_daemon(flags | MHD_USE_SSL, port,
2288 NULL, NULL,
2289 route, (char *)NULL,
2290 MHD_OPTION_HTTPS_MEM_KEY, priv_key_pem,
2291 MHD_OPTION_HTTPS_MEM_CERT, cert_pem,
2292 MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
2293 MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
2294 MHD_OPTION_LISTEN_SOCKET, sock,
2295 MHD_OPTION_END);
2296 sock = MHD_INVALID_SOCKET;
2297 } else {
2298 current = MHD_start_daemon(flags | MHD_USE_SSL, port,
2299 NULL, NULL,
2300 route, (char *)NULL,
2301 MHD_OPTION_HTTPS_MEM_KEY, priv_key_pem,
2302 MHD_OPTION_HTTPS_MEM_CERT, cert_pem,
2303 MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
2304 MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
2305 MHD_OPTION_END);
2307 if (current == NULL)
2308 err(1, "Could not start kadmin REST service");
2310 if (previous) {
2311 MHD_stop_daemon(previous);
2312 previous = NULL;
2315 if (verbose_counter)
2316 fprintf(stderr, "Ready!\n");
2317 if (daemon_child_fd != -1)
2318 roken_detach_finish(NULL, daemon_child_fd);
2320 /* Wait for signal, possibly SIGALRM, to reload certs and/or exit */
2321 while ((ret = read(sigpipe[0], &sig, sizeof(sig))) == -1 &&
2322 errno == EINTR)
2325 free(priv_key_pem);
2326 free(cert_pem);
2327 priv_key_pem = NULL;
2328 cert_pem = NULL;
2330 if (ret == 1 && (sig == SIGHUP || sig == SIGUSR1 || sig == SIGALRM)) {
2331 /* Reload certs and restart service gracefully */
2332 previous = current;
2333 current = NULL;
2334 goto again;
2337 MHD_stop_daemon(current);
2338 _krb5_unload_plugins(context, "kdc");
2339 pthread_key_delete(k5ctx);
2340 return 0;