2 * Copyright (c) 2020 Kungliga Tekniska Högskolan
3 * (Royal Institute of Technology, Stockholm, Sweden).
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
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
37 #define _XOPEN_SOURCE_EXTENDED 1
38 #define _DEFAULT_SOURCE 1
42 #include <sys/socket.h>
43 #include <sys/types.h>
61 #include <netinet/in.h>
62 #include <netinet/ip.h>
64 #include <microhttpd.h>
66 #include "token_validator_plugin.h"
70 #include <gssapi/gssapi.h>
71 #include <gssapi/gssapi_krb5.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 #define BODYLEN_IS_STRLEN (~0)
86 * Libmicrohttpd is not the easiest API to use. It's got issues.
88 * One of the issues is how responses are handled, and the return value of the
89 * resource handler (MHD_NO -> close the connection, MHD_YES -> send response).
90 * Note that the handler could return MHD_YES without having set an HTTP
93 * There's memory management issues as well.
95 * Here we have to be careful about return values.
97 * Some of the functions defined here return just a krb5_error_code without
98 * having set an HTTP response on error.
99 * Others do set an HTTP response on error.
100 * The convention is to either set an HTTP response on error, or not at all,
101 * but not a mix of errors where for some the function will set a response and
102 * for others it won't.
104 * We do use some system error codes to stand in for errors here.
107 * - EACCES -> authorization failed
108 * - EINVAL -> bad API usage
109 * - ENOSYS -> missing CSRF token but CSRF token required
111 * FIXME: We should rely only on krb5_set_error_message() and friends and make
112 * error responses only in route(), mapping krb5_error_code values to
113 * HTTP status codes. This would simplify the error handling convention
117 /* Our request description structure */
118 typedef struct kadmin_request_desc {
119 HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS;
121 struct MHD_Connection *connection;
122 krb5_times token_times;
126 * Currently we re-use the authz framework from bx509d, using an
127 * `hx509_request' instance (an abstraction for CSRs) to represent the
128 * request because that is what the authz plugin uses that implements the
129 * policy we want checked here.
131 * This is inappropriate in the long-term in two ways:
133 * - the policy for certificates deals in SANs and EKUs, whereas the
134 * policy for ext_keytab deals in host-based service principal names,
135 * and there is not a one-to-one mapping of service names to EKUs;
137 * - using a type from libhx509 for representing requests for things that
138 * aren't certificates is really not appropriate no matter how similar
139 * the use cases for this all might be.
141 * What we need to do is develop a library that can represent requests for
142 * credentials via naming attributes like SANs and Kerberos principal
143 * names, but more arbitrary still than what `hx509_request' supports, and
144 * then invokes a plugin.
146 * Also, we might want to develop an in-tree authorization solution that is
147 * richer than what kadmin.acl supports now, storing grants in HDB entries
148 * and/or similar places.
150 * For expediency we use `hx509_request' here for now, impedance mismatches
153 hx509_request req; /* For authz only */
154 heim_array_t service_names;
155 heim_array_t hostnames;
157 krb5_principal cprinc;
166 unsigned int response_set:1;
167 unsigned int materialize:1;
168 unsigned int rotate_now:1;
169 unsigned int rotate:1;
170 unsigned int revoke:1;
171 unsigned int create:1;
173 unsigned int is_self:1;
175 } *kadmin_request_desc;
178 audit_trail(kadmin_request_desc r, krb5_error_code ret)
180 const char *retname = NULL;
183 * Get a symbolic name for some error codes.
185 * Really, libcom_err should have a primitive for this, and ours could, but
186 * we can't use a system libcom_err if we extend ours.
188 #define CASE(x) case x : retname = #x; break
190 case ENOSYS: retname = "ECSRFTOKENREQD"; break;
194 CASE(HDB_ERR_NOT_FOUND_HERE);
195 CASE(HDB_ERR_WRONG_REALM);
196 CASE(HDB_ERR_EXISTS);
197 CASE(HDB_ERR_KVNO_NOT_FOUND);
198 CASE(HDB_ERR_NOENTRY);
199 CASE(HDB_ERR_NO_MKEY);
200 CASE(KRB5_KDC_UNREACH);
202 CASE(KADM5_AUTH_GET);
203 CASE(KADM5_AUTH_ADD);
204 CASE(KADM5_AUTH_MODIFY);
205 CASE(KADM5_AUTH_DELETE);
206 CASE(KADM5_AUTH_INSUFFICIENT);
209 CASE(KADM5_RPC_ERROR);
211 CASE(KADM5_BAD_HIST_KEY);
212 CASE(KADM5_NOT_INIT);
213 CASE(KADM5_UNK_PRINC);
214 CASE(KADM5_UNK_POLICY);
215 CASE(KADM5_BAD_MASK);
216 CASE(KADM5_BAD_CLASS);
217 CASE(KADM5_BAD_LENGTH);
218 CASE(KADM5_BAD_POLICY);
219 CASE(KADM5_BAD_PRINCIPAL);
220 CASE(KADM5_BAD_AUX_ATTR);
221 CASE(KADM5_BAD_HISTORY);
222 CASE(KADM5_BAD_MIN_PASS_LIFE);
223 CASE(KADM5_PASS_Q_TOOSHORT);
224 CASE(KADM5_PASS_Q_CLASS);
225 CASE(KADM5_PASS_Q_DICT);
226 CASE(KADM5_PASS_Q_GENERIC);
227 CASE(KADM5_PASS_REUSE);
228 CASE(KADM5_PASS_TOOSOON);
229 CASE(KADM5_POLICY_REF);
231 CASE(KADM5_BAD_PASSWORD);
232 CASE(KADM5_PROTECT_PRINCIPAL);
233 CASE(KADM5_BAD_SERVER_HANDLE);
234 CASE(KADM5_BAD_STRUCT_VERSION);
235 CASE(KADM5_OLD_STRUCT_VERSION);
236 CASE(KADM5_NEW_STRUCT_VERSION);
237 CASE(KADM5_BAD_API_VERSION);
238 CASE(KADM5_OLD_LIB_API_VERSION);
239 CASE(KADM5_OLD_SERVER_API_VERSION);
240 CASE(KADM5_NEW_LIB_API_VERSION);
241 CASE(KADM5_NEW_SERVER_API_VERSION);
242 CASE(KADM5_SECURE_PRINC_MISSING);
243 CASE(KADM5_NO_RENAME_SALT);
244 CASE(KADM5_BAD_CLIENT_PARAMS);
245 CASE(KADM5_BAD_SERVER_PARAMS);
246 CASE(KADM5_AUTH_LIST);
247 CASE(KADM5_AUTH_CHANGEPW);
248 CASE(KADM5_BAD_TL_TYPE);
249 CASE(KADM5_MISSING_CONF_PARAMS);
250 CASE(KADM5_BAD_SERVER_NAME);
251 CASE(KADM5_KS_TUPLE_NOSUPP);
252 CASE(KADM5_SETKEY3_ETYPE_MISMATCH);
253 CASE(KADM5_DECRYPT_USAGE_NOSUPP);
254 CASE(KADM5_POLICY_OP_NOSUPP);
255 CASE(KADM5_KEEPOLD_NOSUPP);
256 CASE(KADM5_AUTH_GET_KEYS);
257 CASE(KADM5_ALREADY_LOCKED);
258 CASE(KADM5_NOT_LOCKED);
259 CASE(KADM5_LOG_CORRUPT);
260 CASE(KADM5_LOG_NEEDS_UPGRADE);
261 CASE(KADM5_BAD_SERVER_HOOK);
262 CASE(KADM5_SERVER_HOOK_NOT_FOUND);
263 CASE(KADM5_OLD_SERVER_HOOK_VERSION);
264 CASE(KADM5_NEW_SERVER_HOOK_VERSION);
265 CASE(KADM5_READ_ONLY);
273 heim_audit_trail((heim_svc_req_desc)r, ret, retname);
276 static krb5_log_facility *logfac;
277 static pthread_key_t k5ctx;
279 static krb5_error_code
280 get_krb5_context(krb5_context *contextp)
284 if ((*contextp = pthread_getspecific(k5ctx)))
287 ret = krb5_init_context(contextp);
288 /* XXX krb5_set_log_dest(), warn_dest, debug_dest */
290 (void) pthread_setspecific(k5ctx, *contextp);
294 static int port = -1;
295 static int help_flag;
296 static int daemonize;
297 static int daemon_child_fd = -1;
298 static int local_hdb;
299 static int local_hdb_read_only;
300 static int read_only;
301 static int verbose_counter;
302 static int version_flag;
303 static int reverse_proxied_flag;
304 static int thread_per_client_flag;
305 struct getarg_strings audiences;
306 static const char *cert_file;
307 static const char *priv_key_file;
308 static const char *cache_dir;
309 static const char *realm;
310 static const char *hdb;
311 static const char *primary_server_URI;
312 static const char *kadmin_server;
313 static const char *writable_kadmin_server;
314 static const char *stash_file;
315 static const char *kadmin_client_name = "httpkadmind/admin";
316 static const char *kadmin_client_keytab;
317 static struct getarg_strings auth_types;
319 #define set_conf(c, f, v, b) \
321 if (((c).f = strdup(v)) == NULL) \
327 * Does NOT set an HTTP response, naturally, as it doesn't even have access to
330 static krb5_error_code
331 get_kadm_handle(krb5_context context,
332 const char *want_realm,
336 kadm5_config_params conf;
340 * If the caller wants to write and we are configured to redirect in that
341 * case, then trigger a redirect by returning KADM5_READ_ONLY.
343 if (want_write && local_hdb_read_only && primary_server_URI)
344 return KADM5_READ_ONLY;
345 if (want_write && read_only)
346 return KADM5_READ_ONLY;
349 * Configure kadm5 connection.
351 * Note that all of these are optional, and will be found in krb5.conf or,
352 * in some cases, in DNS, as needed.
354 memset(&conf, 0, sizeof(conf));
357 conf.stash_file = NULL;
358 conf.admin_server = NULL;
359 conf.readonly_admin_server = NULL;
360 set_conf(conf, realm, want_realm, KADM5_CONFIG_REALM);
361 set_conf(conf, dbname, hdb, KADM5_CONFIG_DBNAME);
362 set_conf(conf, stash_file, stash_file, KADM5_CONFIG_STASH_FILE);
363 set_conf(conf, admin_server, writable_kadmin_server, KADM5_CONFIG_ADMIN_SERVER);
364 set_conf(conf, readonly_admin_server, kadmin_server,
365 KADM5_CONFIG_READONLY_ADMIN_SERVER);
368 * If we have a local HDB we'll use it if we can. If the local HDB is
369 * read-only and the caller wants to write, then we won't use the local
372 if (local_hdb && (!local_hdb_read_only || !want_write)) {
373 ret = kadm5_s_init_with_password_ctx(context,
376 NULL, /* service_name */
378 0, /* struct_version */
385 * Remote connection. This will connect to a read-only kadmind if
386 * possible, and if so, reconnect to a writable kadmind as needed.
388 * Note that kadmin_client_keytab can be an HDB: or HDBGET: keytab.
390 ret = kadm5_c_init_with_skey_ctx(context,
392 kadmin_client_keytab,
395 0, /* struct_version */
401 ret = krb5_enomem(context);
404 free(conf.readonly_admin_server);
405 free(conf.admin_server);
406 free(conf.stash_file);
412 static krb5_error_code resp(kadmin_request_desc, int, krb5_error_code,
413 enum MHD_ResponseMemoryMode, const char *,
414 const void *, size_t, const char *, const char *);
415 static krb5_error_code bad_req(kadmin_request_desc, krb5_error_code, int,
417 HEIMDAL_PRINTF_ATTRIBUTE((__printf__, 4, 5));
419 static krb5_error_code bad_enomem(kadmin_request_desc, krb5_error_code);
420 static krb5_error_code bad_400(kadmin_request_desc, krb5_error_code, const char *);
421 static krb5_error_code bad_401(kadmin_request_desc, const char *);
422 static krb5_error_code bad_403(kadmin_request_desc, krb5_error_code, const char *);
423 static krb5_error_code bad_404(kadmin_request_desc, const char *);
424 static krb5_error_code bad_405(kadmin_request_desc, const char *);
425 /*static krb5_error_code bad_500(kadmin_request_desc, krb5_error_code, const char *);*/
426 static krb5_error_code bad_503(kadmin_request_desc, krb5_error_code, const char *);
429 validate_token(kadmin_request_desc r)
434 char token_type[64]; /* Plenty */
437 size_t host_len, brk, i;
439 memset(&r->token_times, 0, sizeof(r->token_times));
440 host = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
441 MHD_HTTP_HEADER_HOST);
443 return bad_400(r, EINVAL, "Host header is missing");
445 /* Exclude port number here (IPv6-safe because of the below) */
446 host_len = ((p = strchr(host, ':'))) ? p - host : strlen(host);
448 token = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
449 MHD_HTTP_HEADER_AUTHORIZATION);
451 return bad_401(r, "Authorization token is missing");
452 brk = strcspn(token, " \t");
453 if (token[brk] == '\0' || brk > sizeof(token_type) - 1)
454 return bad_401(r, "Authorization token is missing");
455 memcpy(token_type, token, brk);
456 token_type[brk] = '\0';
458 tok.length = strlen(token);
459 tok.data = (void *)(uintptr_t)token;
461 for (i = 0; i < audiences.num_strings; i++)
462 if (strncasecmp(host, audiences.strings[i], host_len) == 0 &&
463 audiences.strings[i][host_len] == '\0')
465 if (i == audiences.num_strings)
466 return bad_403(r, EINVAL, "Host: value is not accepted here");
468 r->sname = strdup(host); /* No need to check for ENOMEM here */
470 ret = kdc_validate_token(r->context, NULL /* realm */, token_type, &tok,
471 (const char **)&audiences.strings[i], 1,
472 &r->cprinc, &r->token_times);
474 return bad_403(r, ret, "Token validation failed");
475 if (r->cprinc == NULL)
476 return bad_403(r, ret,
477 "Could not extract a principal name from token");
478 ret = krb5_unparse_name(r->context, r->cprinc, &r->cname);
480 return bad_503(r, ret,
481 "Could not extract a principal name from token");
486 k5_free_context(void *ctx)
488 krb5_free_context(ctx);
491 #ifndef HAVE_UNLINKAT
493 unlink1file(const char *dname, const char *name)
497 if (strlcpy(p, dname, sizeof(p)) < sizeof(p) &&
498 strlcat(p, "/", sizeof(p)) < sizeof(p) &&
499 strlcat(p, name, sizeof(p)) < sizeof(p))
512 * This works, but not on Win32:
514 * (void) simple_execlp("rm", "rm", "-rf", cache_dir, NULL);
516 * We make no directories in `cache_dir', so we need not recurse.
518 if ((d = opendir(cache_dir)) == NULL)
521 while ((e = readdir(d))) {
524 * Because unlinkat() takes a directory FD, implementing one for
525 * libroken is tricky at best. Instead we might want to implement an
526 * rm_dash_rf() function in lib/roken.
528 (void) unlinkat(dirfd(d), e->d_name, 0);
530 (void) unlink1file(cache_dir, e->d_name);
534 (void) rmdir(cache_dir);
538 * Work around older libmicrohttpd not strduping response header values when
541 static HEIMDAL_THREAD_LOCAL struct redirect_uri {
549 redirect_uri_appends(struct redirect_uri *redirect,
555 if (!redirect->valid || redirect->len >= sizeof(redirect->uri) - 1) {
559 /* Optimize strlcpy by using redirect->uri + redirect->len */
560 p = redirect->uri + redirect->len;
561 sz = sizeof(redirect->uri) - redirect->len;
562 if ((len = strlcpy(p, s, sz)) >= sz)
565 redirect->len += len;
569 make_redirect_uri_param_cb(void *d,
570 enum MHD_ValueKind kind,
574 struct redirect_uri *redirect = d;
576 redirect_uri_appends(redirect, redirect->first_param ? "?" : "&");
577 redirect_uri_appends(redirect, key);
579 redirect_uri_appends(redirect, "=");
580 redirect_uri_appends(redirect, val);
582 redirect->first_param = 0;
587 make_redirect_uri(kadmin_request_desc r, const char *base)
589 redirect_uri.len = 0;
590 redirect_uri.uri[0] = '\0';
591 redirect_uri.valid = 1;
592 redirect_uri.first_param = 1;
594 redirect_uri_appends(&redirect_uri, base); /* Redirect to primary URI base */
595 redirect_uri_appends(&redirect_uri, r->reqtype); /* URI local-part */
596 (void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
597 make_redirect_uri_param_cb,
599 return redirect_uri.valid ? redirect_uri.uri : NULL;
604 * XXX Shouldn't be a body, but a status message. The body should be
605 * configurable to be from a file. MHD doesn't give us a way to set the
606 * response status message though, just the body.
608 * Calls audit_trail().
610 * Returns -1 if something terrible happened, which should ultimately cause
611 * route() to return MHD_NO, which should cause libmicrohttpd to close the
612 * connection to the user-agent.
614 * Returns 0 in all other cases.
616 static krb5_error_code
617 resp(kadmin_request_desc r,
618 int http_status_code,
620 enum MHD_ResponseMemoryMode rmmode,
621 const char *content_type,
627 struct MHD_Response *response;
630 if (r->response_set) {
631 krb5_log_msg(r->context, logfac, 1, NULL,
632 "Internal error; attempted to set a second response");
636 (void) gettimeofday(&r->tv_end, NULL);
639 if (body && bodylen == BODYLEN_IS_STRLEN)
640 bodylen = strlen(body);
642 response = MHD_create_response_from_buffer(bodylen, rk_UNCONST(body),
644 if (response == NULL)
646 mret = MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL,
647 "no-store, max-age=0");
648 if (mret == MHD_YES && http_status_code == MHD_HTTP_UNAUTHORIZED) {
651 if (auth_types.num_strings < 1)
652 http_status_code = MHD_HTTP_SERVICE_UNAVAILABLE;
654 for (i = 0; mret == MHD_YES && i < auth_types.num_strings; i++)
655 mret = MHD_add_response_header(response,
656 MHD_HTTP_HEADER_WWW_AUTHENTICATE,
657 auth_types.strings[i]);
658 } else if (mret == MHD_YES && http_status_code == MHD_HTTP_TEMPORARY_REDIRECT) {
659 const char *redir = make_redirect_uri(r, primary_server_URI);
662 mret = MHD_add_response_header(response, MHD_HTTP_HEADER_LOCATION,
665 /* XXX Find a way to set a new response body; log */
666 http_status_code = MHD_HTTP_SERVICE_UNAVAILABLE;
669 if (mret == MHD_YES && csrf)
670 mret = MHD_add_response_header(response,
674 if (mret == MHD_YES && content_type) {
675 mret = MHD_add_response_header(response,
676 MHD_HTTP_HEADER_CONTENT_TYPE,
680 mret = MHD_queue_response(r->connection, http_status_code, response);
681 MHD_destroy_response(response);
683 return mret == MHD_NO ? -1 : 0;
686 static krb5_error_code
687 bad_reqv(kadmin_request_desc r,
688 krb5_error_code code,
689 int http_status_code,
694 krb5_context context = NULL;
695 const char *k5msg = NULL;
696 const char *emsg = NULL;
697 char *formatted = NULL;
701 context = r->context;
702 if (r && r->hcontext && r->kv)
703 heim_audit_addkv((heim_svc_req_desc)r, 0, "http-status-code", "%d",
705 (void) gettimeofday(&r->tv_end, NULL);
706 if (code == ENOMEM) {
708 krb5_log_msg(context, logfac, 1, NULL, "Out of memory");
709 return resp(r, http_status_code, code, MHD_RESPMEM_PERSISTENT,
710 NULL, fmt, BODYLEN_IS_STRLEN, NULL, NULL);
715 emsg = k5msg = krb5_get_error_message(context, code);
717 emsg = strerror(code);
720 ret = vasprintf(&formatted, fmt, ap) == -1;
722 if (ret > -1 && formatted)
723 ret = asprintf(&msg, "%s: %s (%d)", formatted, emsg, (int)code);
728 if (r && r->hcontext)
729 heim_audit_addreason((heim_svc_req_desc)r, "%s", formatted);
730 krb5_free_error_message(context, k5msg);
732 if (ret == -1 || msg == NULL) {
734 krb5_log_msg(context, logfac, 1, NULL, "Out of memory");
735 return resp(r, MHD_HTTP_SERVICE_UNAVAILABLE, ENOMEM,
736 MHD_RESPMEM_PERSISTENT, NULL,
737 "Out of memory", BODYLEN_IS_STRLEN, NULL, NULL);
740 ret = resp(r, http_status_code, code, MHD_RESPMEM_MUST_COPY,
741 NULL, msg, BODYLEN_IS_STRLEN, NULL, NULL);
744 return ret == -1 ? -1 : code;
747 static krb5_error_code
748 bad_req(kadmin_request_desc r,
749 krb5_error_code code,
750 int http_status_code,
758 ret = bad_reqv(r, code, http_status_code, fmt, ap);
763 static krb5_error_code
764 bad_enomem(kadmin_request_desc r, krb5_error_code ret)
766 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
770 static krb5_error_code
771 bad_400(kadmin_request_desc r, int ret, const char *reason)
773 return bad_req(r, ret, MHD_HTTP_BAD_REQUEST, "%s", reason);
776 static krb5_error_code
777 bad_401(kadmin_request_desc r, const char *reason)
779 return bad_req(r, EACCES, MHD_HTTP_UNAUTHORIZED, "%s", reason);
782 static krb5_error_code
783 bad_403(kadmin_request_desc r, krb5_error_code ret, const char *reason)
785 return bad_req(r, ret, MHD_HTTP_FORBIDDEN, "%s", reason);
788 static krb5_error_code
789 bad_404(kadmin_request_desc r, const char *name)
791 return bad_req(r, ENOENT, MHD_HTTP_NOT_FOUND,
792 "Resource not found: %s", name);
795 static krb5_error_code
796 bad_405(kadmin_request_desc r, const char *method)
798 return bad_req(r, EPERM, MHD_HTTP_METHOD_NOT_ALLOWED,
799 "Method not supported: %s", method);
802 static krb5_error_code
803 bad_method_want_POST(kadmin_request_desc r)
805 return bad_req(r, EPERM, MHD_HTTP_METHOD_NOT_ALLOWED,
806 "Use POST for making changes to principals");
810 static krb5_error_code
811 bad_500(kadmin_request_desc r,
815 return bad_req(r, ret, MHD_HTTP_INTERNAL_SERVER_ERROR,
816 "Internal error: %s", reason);
820 static krb5_error_code
821 bad_503(kadmin_request_desc r,
825 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
826 "Service unavailable: %s", reason);
829 static krb5_error_code
830 good_ext_keytab(kadmin_request_desc r)
837 if (!r->keytab_name || !(p = strchr(r->keytab_name, ':')))
838 return bad_503(r, EINVAL, "Internal error (no keytab produced)");
840 if (strncmp(p, cache_dir, strlen(cache_dir)) != 0)
841 return bad_503(r, EINVAL, "Internal error");
842 ret = rk_undumpdata(p, &body, &bodylen);
844 return bad_503(r, ret, "Could not recover keytab from temp file");
846 ret = resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_MUST_COPY,
847 "application/octet-stream", body, bodylen, NULL, NULL);
852 static krb5_error_code
853 check_service_name(kadmin_request_desc r, const char *name)
855 if (name == NULL || name[0] == '\0' ||
856 strchr(name, '/') || strchr(name, '\\') || strchr(name, '@') ||
857 strcmp(name, "krbtgt") == 0 ||
858 strcmp(name, "iprop") == 0 ||
859 strcmp(name, "kadmin") == 0 ||
860 strcmp(name, "hprop") == 0 ||
861 strcmp(name, "WELLKNOWN") == 0 ||
862 strcmp(name, "K") == 0) {
863 krb5_set_error_message(r->context, EACCES,
864 "No one is allowed to fetch keys for "
865 "Heimdal service %s", name);
868 if (strcmp(name, "root") != 0 &&
869 strcmp(name, "host") != 0 &&
870 strcmp(name, "exceed") != 0)
872 if (krb5_config_get_bool_default(r->context, NULL, FALSE,
874 "csr_authorizer_handles_svc_names",
877 krb5_set_error_message(r->context, EACCES,
878 "No one is allowed to fetch keys for "
879 "service \"%s\" because of authorizer "
880 "limitations", name);
886 enum MHD_ValueKind kind,
890 kadmin_request_desc r = d;
891 krb5_error_code ret = 0;
892 heim_string_t s = NULL;
895 * Multi-valued params:
897 * - spn=<service>/<hostname>
898 * - dNSName=<hostname>
899 * - service=<service>
901 * Single-valued params:
904 * - materialize=true -- create a concrete princ where it's virtual
905 * - enctypes=... -- key-salt types
906 * - revoke=true -- delete old keys (concrete princs only)
907 * - rotate=true -- change keys (no-op for virtual princs)
908 * - create=true -- create a concrete princ
909 * - ro=true -- perform no writes
912 if (strcmp(key, "realm") == 0 && val) {
913 if (!r->realm && !(r->realm = strdup(val)))
914 ret = krb5_enomem(r->context);
915 } else if (strcmp(key, "materialize") == 0 ||
916 strcmp(key, "revoke") == 0 ||
917 strcmp(key, "rotate") == 0 ||
918 strcmp(key, "create") == 0 ||
919 strcmp(key, "ro") == 0) {
920 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
921 "requested_option", "%s", key);
922 if (!val || strcmp(val, "true") != 0)
923 krb5_set_error_message(r->context, ret = EINVAL,
924 "get-keys \"%s\" q-param accepts "
925 "only \"true\"", key);
926 else if (strcmp(key, "materialize") == 0)
928 else if (strcmp(key, "revoke") == 0)
930 else if (strcmp(key, "rotate") == 0)
932 else if (strcmp(key, "create") == 0)
934 else if (strcmp(key, "ro") == 0)
936 } else if (strcmp(key, "dNSName") == 0 && val) {
937 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
938 "requested_dNSName", "%s", val);
940 krb5_set_error_message(r->context, ret = EACCES,
941 "only one service may be requested for self");
942 } else if (strchr(val, '.') == NULL) {
943 krb5_set_error_message(r->context, ret = EACCES,
944 "dNSName must have at least one '.' in it");
946 s = heim_string_create(val);
948 ret = krb5_enomem(r->context);
950 ret = heim_array_append_value(r->hostnames, s);
953 ret = hx509_request_add_dns_name(r->context->hx509ctx, r->req, val);
954 } else if (strcmp(key, "service") == 0 && val) {
955 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
956 "requested_service", "%s", val);
958 krb5_set_error_message(r->context, ret = EACCES,
959 "use \"spn\" for self");
961 ret = check_service_name(r, val);
963 s = heim_string_create(val);
965 ret = krb5_enomem(r->context);
967 ret = heim_array_append_value(r->service_names, s);
969 } else if (strcmp(key, "enctypes") == 0 && val) {
970 r->enctypes = strdup(val);
971 if (!(r->enctypes = strdup(val)))
972 ret = krb5_enomem(r->context);
973 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
974 "requested_enctypes", "%s", val);
975 } else if (r->is_self && strcmp(key, "spn") == 0 && val) {
976 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
977 "requested_spn", "%s", val);
978 krb5_set_error_message(r->context, ret = EACCES,
979 "only one service may be requested for self");
980 } else if (strcmp(key, "spn") == 0 && val) {
981 krb5_principal p = NULL;
982 const char *hostname = "";
984 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
985 "requested_spn", "%s", val);
987 ret = krb5_parse_name_flags(r->context, val,
988 KRB5_PRINCIPAL_PARSE_NO_DEF_REALM, &p);
989 if (ret == 0 && krb5_principal_get_realm(r->context, p) == NULL)
990 ret = krb5_principal_set_realm(r->context, p,
991 r->realm ? r->realm : realm);
994 * The SPN has to have two components.
996 * TODO: Support more components? Support AD-style NetBIOS computer
999 if (ret == 0 && krb5_principal_get_num_comp(r->context, p) != 2)
1003 * Allow only certain service names. Except that when
1004 * the SPN == the requestor's principal name then allow the "host"
1008 const char *service =
1009 krb5_principal_get_comp_string(r->context, p, 0);
1011 if (strcmp(service, "host") == 0 &&
1012 krb5_principal_compare(r->context, p, r->cprinc) &&
1014 heim_array_get_length(r->hostnames) == 0 &&
1015 heim_array_get_length(r->spns) == 0) {
1018 ret = check_service_name(r, service);
1020 if (ret == 0 && !krb5_principal_compare(r->context, p, r->cprinc))
1021 ret = check_service_name(r,
1022 krb5_principal_get_comp_string(r->context,
1025 hostname = krb5_principal_get_comp_string(r->context, p, 1);
1026 if (!hostname || !strchr(hostname, '.'))
1027 krb5_set_error_message(r->context, ret = ENOTSUP,
1028 "Only host-based service names supported");
1030 if (ret == 0 && r->realm)
1031 ret = krb5_principal_set_realm(r->context, p, r->realm);
1032 else if (ret == 0 && realm)
1033 ret = krb5_principal_set_realm(r->context, p, realm);
1035 ret = hx509_request_add_dns_name(r->context->hx509ctx, r->req,
1037 if (ret == 0 && !(s = heim_string_create(val)))
1038 ret = krb5_enomem(r->context);
1040 ret = heim_array_append_value(r->spns, s);
1041 krb5_free_principal(r->context, p);
1044 /* The authorizer probably doesn't know what to do with this */
1045 ret = hx509_request_add_pkinit(r->context->hx509ctx, r->req, val);
1048 /* Produce error for unknown params */
1049 heim_audit_addkv((heim_svc_req_desc)r, 0, "requested_unknown", "true");
1050 krb5_set_error_message(r->context, ret = ENOTSUP,
1051 "Query parameter %s not supported", key);
1056 return ret ? MHD_NO /* Stop iterating */ : MHD_YES;
1059 static krb5_error_code
1060 authorize_req(kadmin_request_desc r)
1062 krb5_error_code ret;
1065 ret = hx509_request_init(r->context->hx509ctx, &r->req);
1067 return bad_enomem(r, ret);
1068 (void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
1072 return bad_403(r, ret, "Not authorized to requested principal(s)");
1074 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
1075 "Could not handle query parameters");
1079 ret = kdc_authorize_csr(r->context, "ext_keytab", r->req, r->cprinc);
1080 if (ret == EACCES || ret == EINVAL || ret == ENOTSUP ||
1081 ret == KRB5KDC_ERR_POLICY)
1082 return bad_403(r, ret, "Not authorized to requested principal(s)");
1084 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
1085 "Error checking authorization");
1089 static krb5_error_code
1090 make_keytab(kadmin_request_desc r)
1092 krb5_error_code ret = 0;
1095 r->keytab_name = NULL;
1096 if (asprintf(&r->keytab_name, "FILE:%s/kt-XXXXXX", cache_dir) == -1 ||
1097 r->keytab_name == NULL)
1098 ret = krb5_enomem(r->context);
1100 fd = mkstemp(r->keytab_name + sizeof("FILE:") - 1);
1101 if (ret == 0 && fd == -1)
1104 ret = krb5_kt_resolve(r->context, r->keytab_name, &r->keytab);
1110 static krb5_error_code
1111 write_keytab(kadmin_request_desc r,
1112 kadm5_principal_ent_rec *princ,
1113 const char *unparsed)
1115 krb5_error_code ret = 0;
1116 krb5_keytab_entry key;
1119 if (princ->n_key_data <= 0)
1122 if (kadm5_some_keys_are_bogus(princ->n_key_data, &princ->key_data[0])) {
1123 krb5_warn(r->context, ret,
1124 "httpkadmind running with insufficient kadmin privilege "
1125 "for extracting keys for %s", unparsed);
1126 krb5_log_msg(r->context, logfac, 1, NULL,
1127 "httpkadmind running with insufficient kadmin privilege "
1128 "for extracting keys for %s", unparsed);
1132 memset(&key, 0, sizeof(key));
1133 for (i = 0; ret == 0 && i < princ->n_key_data; i++) {
1134 krb5_key_data *kd = &princ->key_data[i];
1136 key.principal = princ->principal;
1137 key.vno = kd->key_data_kvno;
1138 key.keyblock.keytype = kd->key_data_type[0];
1139 key.keyblock.keyvalue.length = kd->key_data_length[0];
1140 key.keyblock.keyvalue.data = kd->key_data_contents[0];
1143 * FIXME kadm5 doesn't give us set_time here. If it gave us the
1144 * KeyRotation metadata, we could compute it. But this might be a
1145 * concrete principal with concrete keys, in which case we can't.
1147 * To fix this we need to extend the protocol and the API.
1149 key.timestamp = time(NULL);
1151 ret = krb5_kt_add_entry(r->context, r->keytab, &key);
1154 krb5_warn(r->context, ret,
1155 "Failed to write keytab entries for %s", unparsed);
1161 random_password(krb5_context context, char *buf, size_t buflen)
1163 static const char chars[] =
1164 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,";
1170 for (i = 0; i < buflen; i++) {
1171 if (i % sizeof(p) == 0)
1172 krb5_generate_random_block(p, sizeof(p));
1173 b = p[i % sizeof(p)];
1174 buf[i] = chars[b % (sizeof(chars) - 1)];
1179 static krb5_error_code
1180 make_kstuple(krb5_context context,
1181 kadm5_principal_ent_rec *p,
1182 krb5_key_salt_tuple **kstuple,
1190 if (p->n_key_data < 1)
1192 *kstuple = calloc(p->n_key_data, sizeof (*kstuple));
1193 for (i = 0; *kstuple && i < p->n_key_data; i++) {
1194 if (p->key_data[i].key_data_kvno == p->kvno) {
1195 (*kstuple)[i].ks_enctype = p->key_data[i].key_data_type[0];
1196 (*kstuple)[i].ks_salttype = p->key_data[i].key_data_type[1];
1200 return *kstuple ? 0 :krb5_enomem(context);
1204 * Get keys for one principal.
1206 * Does NOT set an HTTP response.
1208 static krb5_error_code
1209 get_keys1(kadmin_request_desc r, const char *pname)
1211 kadm5_principal_ent_rec princ;
1212 krb5_key_salt_tuple *kstuple = NULL;
1213 krb5_error_code ret = 0;
1214 krb5_principal p = NULL;
1216 KADM5_PRINCIPAL | KADM5_KVNO | KADM5_MAX_LIFE | KADM5_MAX_RLIFE |
1217 KADM5_ATTRIBUTES | KADM5_KEY_DATA | KADM5_TL_DATA;
1218 uint32_t create_mask = mask & ~(KADM5_KEY_DATA | KADM5_TL_DATA);
1219 size_t nkstuple = 0;
1224 memset(&princ, 0, sizeof(princ));
1225 princ.key_data = NULL;
1226 princ.tl_data = NULL;
1228 ret = krb5_parse_name(r->context, pname, &p);
1229 if (ret == 0 && r->realm)
1230 ret = krb5_principal_set_realm(r->context, p, r->realm);
1231 else if (ret == 0 && realm)
1232 ret = krb5_principal_set_realm(r->context, p, realm);
1233 if (ret == 0 && r->enctypes)
1234 ret = krb5_string_to_keysalts2(r->context, r->enctypes,
1235 &nkstuple, &kstuple);
1237 ret = kadm5_get_principal(r->kadm_handle, p, &princ, mask);
1242 * If princ is virtual and we're not asked to materialize, ignore
1243 * requests to rotate.
1245 if (!r->materialize &&
1246 (princ.attributes & (KRB5_KDB_VIRTUAL_KEYS | KRB5_KDB_VIRTUAL))) {
1252 change = !r->ro && (r->rotate || r->revoke);
1254 /* Handle create / materialize options */
1255 if (ret == KADM5_UNK_PRINC && r->create) {
1259 ret = KADM5_READ_ONLY;
1261 ret = strcmp(r->method, "POST") == 0 ? 0 : ENOSYS; /* XXX */
1262 if (ret == 0 && local_hdb && local_hdb_read_only) {
1263 /* Make sure we can write */
1264 kadm5_destroy(r->kadm_handle);
1265 r->kadm_handle = NULL;
1266 ret = get_kadm_handle(r->context, r->realm, 1 /* want_write */,
1269 memset(&princ, 0, sizeof(princ));
1271 * Some software is allergic to kvno 1, assuming that kvno 1 implies
1272 * half-baked service principal. We've some vague recollection of
1273 * something similar for kvno 2, so let's start at 3.
1276 princ.tl_data = NULL;
1277 princ.key_data = NULL;
1278 princ.max_life = 24 * 3600; /* XXX Make configurable */
1279 princ.max_renewable_life = princ.max_life; /* XXX Make configurable */
1281 random_password(r->context, pw, sizeof(pw));
1282 princ.principal = p; /* Borrow */
1284 ret = kadm5_create_principal_3(r->kadm_handle, &princ, create_mask,
1285 nkstuple, kstuple, pw);
1286 princ.principal = NULL; /* Return */
1289 } else if (ret == 0 && r->materialize &&
1290 (princ.attributes & KRB5_KDB_VIRTUAL)) {
1292 #ifndef MATERIALIZE_NOTYET
1296 ret = KADM5_READ_ONLY;
1298 ret = strcmp(r->method, "POST") == 0 ? 0 : ENOSYS; /* XXX */
1299 if (ret == 0 && local_hdb && local_hdb_read_only) {
1300 /* Make sure we can write */
1301 kadm5_destroy(r->kadm_handle);
1302 r->kadm_handle = NULL;
1303 ret = get_kadm_handle(r->context, r->realm, 1 /* want_write */,
1306 princ.attributes |= KRB5_KDB_MATERIALIZE;
1307 princ.attributes &= ~KRB5_KDB_VIRTUAL;
1309 * XXX If there are TL data which should be re-encoded and sent as
1310 * KRB5_TL_EXTENSION, then this call will fail with KADM5_BAD_TL_TYPE.
1312 * We should either drop those TLs, re-encode them, or make
1313 * perform_tl_data() handle them. (New extensions should generally go
1314 * as KRB5_TL_EXTENSION so that non-critical ones can be set on
1315 * principals via old kadmind programs that don't support them.)
1317 * What we really want is a kadm5 utility function to convert some TLs
1318 * to KRB5_TL_EXTENSION and drop all others.
1321 ret = kadm5_create_principal(r->kadm_handle, &princ, mask, "");
1324 } /* else create/materialize q-params are superfluous */
1326 /* Handle rotate / revoke options */
1327 if (ret == 0 && change) {
1328 krb5_keyblock *k = NULL;
1331 int keepold = r->revoke ? 0 : 1;
1334 ret = KADM5_READ_ONLY;
1336 ret = strcmp(r->method, "POST") == 0 ? 0 : ENOSYS; /* XXX */
1337 if (ret == 0 && local_hdb && local_hdb_read_only) {
1338 /* Make sure we can write */
1339 kadm5_destroy(r->kadm_handle);
1340 r->kadm_handle = NULL;
1341 ret = get_kadm_handle(r->context, r->realm, 1 /* want_write */,
1345 /* Use requested enctypes or same ones as princ already had keys for */
1346 if (ret == 0 && kstuple == NULL)
1347 ret = make_kstuple(r->context, &princ, &kstuple, &nkstuple);
1351 ret = kadm5_randkey_principal_3(r->kadm_handle, p, keepold,
1352 nkstuple, kstuple, &k, &n_k);
1354 for (i = 0; n_k > 0 && i < n_k; i++)
1355 krb5_free_keyblock_contents(r->context, &k[i]);
1360 if (ret == 0 && refetch) {
1361 /* Refetch changed principal */
1363 kadm5_free_principal_ent(r->kadm_handle, &princ);
1365 ret = kadm5_get_principal(r->kadm_handle, p, &princ, mask);
1371 ret = write_keytab(r, &princ, pname);
1373 kadm5_free_principal_ent(r->kadm_handle, &princ);
1374 krb5_free_principal(r->context, p);
1378 static krb5_error_code check_csrf(kadmin_request_desc);
1381 * Calls get_keys1() to extract each requested principal's keys.
1383 * When this returns a response will have been set.
1385 static krb5_error_code
1386 get_keysN(kadmin_request_desc r, const char *method)
1388 krb5_error_code ret;
1394 /* Parses and validates the request, then checks authorization */
1395 ret = authorize_req(r);
1397 return ret; /* authorize_req() calls bad_req() on error */
1399 ret = get_kadm_handle(r->context, r->realm ? r->realm : realm,
1400 0 /* want_write */, &r->kadm_handle);
1402 if (strcmp(method, "POST") == 0 && (ret = check_csrf(r)))
1403 return ret; /* check_csrf() calls bad_req() on error */
1405 nhosts = heim_array_get_length(r->hostnames);
1406 nsvcs = heim_array_get_length(r->service_names);
1407 nspns = heim_array_get_length(r->spns);
1408 if (!nhosts && !nspns)
1409 return bad_403(r, EINVAL, "No service principals requested");
1411 if (nhosts && !nsvcs) {
1414 if ((s = heim_string_create("HTTP")) == NULL)
1415 ret = krb5_enomem(r->context);
1417 ret = heim_array_append_value(r->service_names, s);
1422 /* FIXME: Make this configurable */
1423 if (nspns + nsvcs * nhosts > 40)
1424 return bad_403(r, EINVAL, "Requested keys for too many principals");
1426 ret = make_keytab(r);
1427 for (i = 0; ret == 0 && i < nsvcs; i++) {
1429 heim_string_get_utf8(
1430 heim_array_get_value(r->service_names, i));
1432 for (k = 0; ret == 0 && k < nhosts; k++) {
1433 krb5_principal p = NULL;
1434 const char *hostname =
1435 heim_string_get_utf8(
1436 heim_array_get_value(r->hostnames, k));
1439 ret = krb5_make_principal(r->context, &p,
1440 r->realm ? r->realm : realm,
1441 svc, hostname, NULL);
1443 ret = krb5_unparse_name(r->context, p, &spn);
1445 ret = get_keys1(r, spn);
1446 krb5_free_principal(r->context, p);
1450 for (i = 0; ret == 0 && i < nspns; i++) {
1452 heim_string_get_utf8(heim_array_get_value(r->spns,
1458 krb5_log_msg(r->context, logfac, 1, NULL,
1459 "Failed to extract keys for unknown reasons");
1460 if (r->response_set)
1462 return bad_503(r, ret, "Could not get keys");
1464 /* Our convention */
1465 return bad_method_want_POST(r);
1466 case KADM5_READ_ONLY:
1467 if (primary_server_URI) {
1468 krb5_log_msg(r->context, logfac, 1, NULL,
1469 "Redirect %s to primary server", r->cname);
1470 return resp(r, MHD_HTTP_TEMPORARY_REDIRECT, KADM5_READ_ONLY,
1471 MHD_RESPMEM_PERSISTENT, NULL, "", 0, NULL, NULL);
1473 krb5_log_msg(r->context, logfac, 1, NULL, "HDB is read-only");
1474 return bad_403(r, ret, "HDB is read-only");
1477 krb5_log_msg(r->context, logfac, 1, NULL, "Sent keytab to %s",
1479 return good_ext_keytab(r);
1481 return bad_503(r, ret, "Could not get keys");
1485 /* Copied from kdc/connect.c */
1487 addr_to_string(krb5_context context,
1488 struct sockaddr *addr,
1492 krb5_error_code ret;
1495 ret = krb5_sockaddr2address(context, addr, &a);
1497 ret = krb5_print_address(&a, str, len, &len);
1498 krb5_free_address(context, &a);
1501 snprintf(str, len, "<family=%d>", addr->sa_family);
1504 static krb5_error_code
1505 set_req_desc(struct MHD_Connection *connection,
1508 kadmin_request_desc r)
1510 const union MHD_ConnectionInfo *ci;
1512 krb5_error_code ret;
1514 memset(r, 0, sizeof(*r));
1515 (void) gettimeofday(&r->tv_start, NULL);
1517 if ((ret = get_krb5_context(&r->context)))
1519 /* HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS fields */
1520 r->request.data = "<HTTP-REQUEST>";
1521 r->request.length = sizeof("<HTTP-REQUEST>");
1522 r->from = r->frombuf;
1531 r->kv = heim_array_create();
1533 r->connection = connection;
1534 r->kadm_handle = NULL;
1535 r->hcontext = r->context->hcontext;
1536 r->service_names = heim_array_create();
1537 r->hostnames = heim_array_create();
1538 r->spns = heim_array_create();
1539 r->keytab_name = NULL;
1546 ci = MHD_get_connection_info(connection,
1547 MHD_CONNECTION_INFO_CLIENT_ADDRESS);
1549 r->addr = ci->client_addr;
1550 addr_to_string(r->context, r->addr, r->frombuf, sizeof(r->frombuf));
1554 heim_audit_addkv((heim_svc_req_desc)r, 0, "method", "GET");
1555 heim_audit_addkv((heim_svc_req_desc)r, 0, "endpoint", "%s", r->reqtype);
1557 token = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
1558 MHD_HTTP_HEADER_AUTHORIZATION);
1559 if (token && r->kv) {
1560 const char *token_end;
1562 if ((token_end = strchr(token, ' ')) == NULL ||
1563 (token_end - token) > INT_MAX || (token_end - token) < 2)
1564 heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "<unknown>");
1566 heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "%.*s",
1567 (int)(token_end - token), token);
1571 if (ret == 0 && r->kv == NULL) {
1572 krb5_log_msg(r->context, logfac, 1, NULL, "Out of memory");
1573 ret = r->ret = ENOMEM;
1579 clean_req_desc(kadmin_request_desc r)
1585 krb5_kt_destroy(r->context, r->keytab);
1586 else if (r->keytab_name && strchr(r->keytab_name, ':'))
1587 (void) unlink(strchr(r->keytab_name, ':') + 1);
1589 kadm5_destroy(r->kadm_handle);
1590 hx509_request_free(&r->req);
1591 heim_release(r->service_names);
1592 heim_release(r->hostnames);
1593 heim_release(r->reason);
1594 heim_release(r->spns);
1595 heim_release(r->kv);
1596 krb5_free_principal(r->context, r->cprinc);
1597 free(r->keytab_name);
1604 /* Implements GETs of /get-keys */
1605 static krb5_error_code
1606 get_keys(kadmin_request_desc r, const char *method)
1608 krb5_error_code ret;
1610 if ((ret = validate_token(r)))
1611 return ret; /* validate_token() calls bad_req() */
1612 if (r->cname == NULL || r->cprinc == NULL)
1613 return bad_403(r, EINVAL,
1614 "Could not extract principal name from token");
1615 return get_keysN(r, method); /* Sets an HTTP response */
1618 /* Implements GETs of /get-config */
1619 static krb5_error_code
1620 get_config(kadmin_request_desc r)
1623 kadm5_principal_ent_rec princ;
1624 krb5_error_code ret;
1625 krb5_principal p = NULL;
1626 uint32_t mask = KADM5_PRINCIPAL | KADM5_TL_DATA;
1627 krb5_tl_data *tl_next;
1629 /* Default configuration for principals that have none set: */
1630 size_t bodylen = sizeof("include /etc/krb5.conf\n") - 1;
1631 void *body = "include /etc/krb5.conf\n";
1634 if ((ret = validate_token(r)))
1635 return ret; /* validate_token() calls bad_req() */
1636 if (r->cname == NULL || r->cprinc == NULL)
1637 return bad_403(r, EINVAL,
1638 "Could not extract principal name from token");
1640 * No authorization needed -- configs are public. Though we do require
1641 * authentication (above).
1644 ret = get_kadm_handle(r->context, r->realm ? r->realm : realm,
1645 0 /* want_write */, &r->kadm_handle);
1647 memset(&princ, 0, sizeof(princ));
1648 princ.key_data = NULL;
1649 princ.tl_data = NULL;
1651 pname = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
1655 ret = krb5_parse_name(r->context, pname, &p);
1657 ret = kadm5_get_principal(r->kadm_handle, p, &princ, mask);
1660 for (tl_next = princ.tl_data; tl_next; tl_next = tl_next->tl_data_next) {
1661 if (tl_next->tl_data_type != KRB5_TL_KRB5_CONFIG)
1663 bodylen = tl_next->tl_data_length;
1664 body = tl_next->tl_data_contents;
1669 return bad_404(r, "/get-config");
1674 krb5_log_msg(r->context, logfac, 1, NULL,
1675 "Returned krb5.conf contents to %s", r->cname);
1676 ret = resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_MUST_COPY,
1677 "application/text", body, bodylen, NULL, NULL);
1679 ret = bad_503(r, ret, "Could not retrieve principal configuration");
1682 kadm5_free_principal_ent(r->kadm_handle, &princ);
1683 krb5_free_principal(r->context, p);
1687 static krb5_error_code
1688 mac_csrf_token(kadmin_request_desc r, krb5_storage *sp)
1690 kadm5_principal_ent_rec princ;
1691 krb5_error_code ret;
1692 krb5_principal p = NULL;
1694 char mac[EVP_MAX_MD_SIZE];
1695 unsigned int maclen = sizeof(mac);
1696 HMAC_CTX *ctx = NULL;
1700 memset(&princ, 0, sizeof(princ));
1701 ret = krb5_storage_to_data(sp, &data);
1702 if (r->kadm_handle == NULL)
1703 ret = get_kadm_handle(r->context, r->realm, 0 /* want_write */,
1706 ret = krb5_make_principal(r->context, &p,
1707 r->realm ? r->realm : realm,
1708 "WELLKNOWN", "CSRFTOKEN", NULL);
1710 ret = kadm5_get_principal(r->kadm_handle, p, &princ,
1711 KADM5_PRINCIPAL | KADM5_KVNO |
1715 if (ret == 0 && princ.n_key_data < 1)
1716 ret = KADM5_UNK_PRINC;
1718 for (i = 0; i < princ.n_key_data; i++)
1719 if (princ.key_data[i].key_data_kvno == princ.kvno)
1721 if (ret == 0 && i == princ.n_key_data)
1722 i = 0; /* Weird, but can't happen */
1724 if (ret == 0 && (ctx = HMAC_CTX_new()) == NULL)
1725 ret = krb5_enomem(r->context);
1726 /* HMAC the token body and the client principal name */
1728 HMAC_Init_ex(ctx, princ.key_data[i].key_data_contents[0], princ.key_data[i].key_data_length[0], EVP_sha256(), NULL);
1729 HMAC_Update(ctx, data.data, data.length);
1730 HMAC_Update(ctx, r->cname, strlen(r->cname));
1731 HMAC_Final(ctx, mac, &maclen);
1732 krb5_data_free(&data);
1733 data.length = maclen;
1735 if (krb5_storage_write(sp, mac, maclen) != maclen)
1736 ret = krb5_enomem(r->context);
1738 krb5_free_principal(r->context, p);
1740 kadm5_free_principal_ent(r->kadm_handle, &princ);
1746 static krb5_error_code
1747 make_csrf_token(kadmin_request_desc r,
1752 static HEIMDAL_THREAD_LOCAL char tokenbuf[128]; /* See below, be sad */
1753 krb5_error_code ret = 0;
1754 unsigned char given_decoded[128];
1755 krb5_storage *sp = NULL;
1766 size_t len = strlen(given);
1768 if (len >= sizeof(given_decoded))
1770 if (ret == 0 && (dlen = rk_base64_decode(given, &given_decoded)) <= 0)
1773 (sp = krb5_storage_from_mem(given_decoded, dlen)) == NULL)
1774 ret = krb5_enomem(r->context);
1776 ret = krb5_ret_int64(sp, &t);
1778 ret = krb5_ret_uint64(sp, &nonce);
1779 krb5_storage_free(sp);
1782 *age = time(NULL) - t;
1785 krb5_generate_random_block((void *)&nonce, sizeof(nonce));
1788 if (ret == 0 && (sp = krb5_storage_emem()) == NULL)
1789 ret = krb5_enomem(r->context);
1791 ret = krb5_store_int64(sp, t);
1793 ret = krb5_store_uint64(sp, nonce);
1795 ret = mac_csrf_token(r, sp);
1797 ret = krb5_storage_to_data(sp, &data);
1798 if (ret == 0 && data.length > INT_MAX)
1801 (dlen = rk_base64_encode(data.data, data.length, token)) < 0)
1803 if (ret == 0 && dlen >= sizeof(tokenbuf))
1807 * Work around for older versions of libmicrohttpd do not strdup()ing
1808 * response header values.
1810 memcpy(tokenbuf, *token, dlen);
1814 krb5_storage_free(sp);
1815 krb5_data_free(&data);
1820 * Returns system or krb5_error_code on error, but also calls resp() or bad_*()
1823 static krb5_error_code
1824 check_csrf(kadmin_request_desc r)
1826 krb5_error_code ret;
1829 size_t givenlen, expectedlen;
1830 char *expected = NULL;
1832 given = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
1834 ret = make_csrf_token(r, given, &expected, &age);
1836 return bad_503(r, ret, "Could not create a CSRF token");
1838 * If CSRF token needed but missing, call resp() directly, bypassing
1839 * bad_403(), to return a 403 with an expected CSRF token in the response.
1841 if (given == NULL) {
1842 (void) resp(r, MHD_HTTP_FORBIDDEN, ENOSYS, MHD_RESPMEM_PERSISTENT,
1843 NULL, "CSRF token needed; copy the X-CSRF-Token: response "
1844 "header to your next POST", BODYLEN_IS_STRLEN, NULL,
1849 /* Validate the CSRF token for this request */
1850 givenlen = strlen(given);
1851 expectedlen = strlen(expected);
1852 if (givenlen != expectedlen || ct_memcmp(given, expected, givenlen)) {
1853 (void) bad_403(r, EACCES, "Invalid CSRF token");
1856 if (age > 300) { /* XXX */
1857 (void) bad_403(r, EACCES, "CSRF token too old");
1863 static krb5_error_code
1864 health(const char *method, kadmin_request_desc r)
1866 if (strcmp(method, "HEAD") == 0) {
1867 return resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_PERSISTENT, NULL, "", 0,
1870 return resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_PERSISTENT, NULL,
1871 "To determine the health of the service, use the /get-config "
1872 "end-point.\n", BODYLEN_IS_STRLEN, NULL, NULL);
1876 /* Implements the entirety of this REST service */
1879 struct MHD_Connection *connection,
1882 const char *version,
1883 const char *upload_data,
1884 size_t *upload_data_size,
1887 static int aptr = 0;
1888 struct kadmin_request_desc r;
1893 * This is the first call, right after headers were read.
1895 * We must return quickly so that any 100-Continue might be sent with
1898 * We'll get called again to really do the processing. If we handled
1899 * POSTs then we'd also get called with upload_data != NULL between the
1900 * first and last calls. We need to keep no state between the first
1901 * and last calls, but we do need to distinguish first and last call,
1902 * so we use the ctx argument for this.
1909 * Note that because we attempt to connect to the HDB in set_req_desc(),
1910 * this early 503 if we fail to serves to do all of what /health should do.
1912 if ((ret = set_req_desc(connection, method, url, &r)))
1913 return bad_503(&r, ret, "Could not initialize request state");
1914 if ((strcmp(method, "HEAD") == 0 || strcmp(method, "GET") == 0) &&
1915 (strcmp(url, "/health") == 0 || strcmp(url, "/") == 0)) {
1916 ret = health(method, &r);
1917 } else if (strcmp(method, "GET") != 0 && strcmp(method, "POST") != 0) {
1918 ret = bad_405(&r, method);
1919 } else if (strcmp(url, "/get-keys") == 0) {
1920 ret = get_keys(&r, method);
1921 } else if (strcmp(url, "/get-config") == 0) {
1922 if (strcmp(method, "GET") != 0)
1923 ret = bad_405(&r, method);
1925 ret = get_config(&r);
1927 ret = bad_404(&r, url);
1931 return ret == -1 ? MHD_NO : MHD_YES;
1934 static struct getargs args[] = {
1935 { "help", 'h', arg_flag, &help_flag, "Print usage message", NULL },
1936 { "version", '\0', arg_flag, &version_flag, "Print version", NULL },
1937 { NULL, 'H', arg_strings, &audiences,
1938 "expected token audience(s) of the service", "HOSTNAME" },
1939 { "daemon", 'd', arg_flag, &daemonize, "daemonize", "daemonize" },
1940 { "daemon-child", 0, arg_flag, &daemon_child_fd, NULL, NULL }, /* priv */
1941 { "reverse-proxied", 0, arg_flag, &reverse_proxied_flag,
1942 "reverse proxied", "listen on 127.0.0.1 and do not use TLS" },
1943 { NULL, 'p', arg_integer, &port, "PORT", "port number (default: 443)" },
1944 { "temp-dir", 0, arg_string, &cache_dir,
1945 "cache directory", "DIRECTORY" },
1946 { "cert", 0, arg_string, &cert_file,
1947 "certificate file path (PEM)", "HX509-STORE" },
1948 { "private-key", 0, arg_string, &priv_key_file,
1949 "private key file path (PEM)", "HX509-STORE" },
1950 { "thread-per-client", 't', arg_flag, &thread_per_client_flag, "thread per-client", NULL },
1951 { "realm", 0, arg_string, &realm, "realm", "REALM" },
1952 { "hdb", 0, arg_string, &hdb, "HDB filename", "PATH" },
1953 { "read-only-admin-server", 0, arg_string, &kadmin_server,
1954 "Name of read-only kadmin server", "HOST[:PORT]" },
1955 { "writable-admin-server", 0, arg_string, &writable_kadmin_server,
1956 "Name of writable kadmin server", "HOST[:PORT]" },
1957 { "primary-server-uri", 0, arg_string, &primary_server_URI,
1958 "Name of primary httpkadmind server for HTTP redirects", "URL" },
1959 { "local", 'l', arg_flag, &local_hdb,
1960 "Use a local HDB as read-only", NULL },
1961 { "local-read-only", 0, arg_flag, &local_hdb_read_only,
1962 "Use a local HDB as read-only", NULL },
1963 { "read-only", 0, arg_flag, &read_only, "Allow no writes", NULL },
1964 { "stash-file", 0, arg_string, &stash_file,
1965 "Stash file for HDB", "PATH" },
1966 { "kadmin-client-name", 0, arg_string, &kadmin_client_name,
1967 "Client name for remote kadmind", "PRINCIPAL" },
1968 { "kadmin-client-keytab", 0, arg_string, &kadmin_client_keytab,
1969 "Keytab with client credentials for remote kadmind", "KEYTAB" },
1970 { "token-authentication-type", 'T', arg_strings, &auth_types,
1971 "Token authentication type(s) supported", "HTTP-AUTH-TYPE" },
1972 { "verbose", 'v', arg_counter, &verbose_counter, "verbose", "run verbosely" }
1978 arg_printusage(args, sizeof(args) / sizeof(args[0]), "httpkadmind",
1979 "\nServes an HTTP API for getting (and rotating) service "
1980 "principal keys, and other kadmin-like operations\n");
1984 static int sigpipe[2] = { -1, -1 };
1990 while (write(sigpipe[1], &c, sizeof(c)) == -1 && errno == EINTR)
1995 my_openlog(krb5_context context,
1997 krb5_log_facility **fac)
1999 char **s = NULL, **p;
2001 krb5_initlog(context, "httpkadmind", fac);
2002 s = krb5_config_get_strings(context, NULL, svc, "logging", NULL);
2004 s = krb5_config_get_strings(context, NULL, "logging", svc, NULL);
2007 krb5_addlog_dest(context, *fac, *p);
2008 krb5_config_free_strings(s);
2011 if (asprintf(&ss, "0-1/FILE:%s/%s", hdb_db_dir(context),
2013 err(1, "out of memory");
2014 krb5_addlog_dest(context, *fac, ss);
2017 krb5_set_warn_dest(context, *fac);
2020 static const char *sysplugin_dirs[] = {
2024 "$ORIGIN/../lib/plugin/kdc",
2027 LIBDIR "/plugin/kdc",
2033 load_plugins(krb5_context context)
2035 const char * const *dirs = sysplugin_dirs;
2039 cfdirs = krb5_config_get_strings(context, NULL, "kdc", "plugin_dir", NULL);
2041 dirs = (const char * const *)cfdirs;
2045 _krb5_load_plugins(context, "kdc", (const char **)dirs);
2048 krb5_config_free_strings(cfdirs);
2053 main(int argc, char **argv)
2055 unsigned int flags = MHD_USE_THREAD_PER_CONNECTION; /* XXX */
2056 struct sockaddr_in sin;
2057 struct MHD_Daemon *previous = NULL;
2058 struct MHD_Daemon *current = NULL;
2059 struct sigaction sa;
2060 krb5_context context = NULL;
2061 MHD_socket sock = MHD_INVALID_SOCKET;
2063 char *priv_key_pem = NULL;
2064 char *cert_pem = NULL;
2069 setprogname("httpkadmind");
2070 if (getarg(args, sizeof(args) / sizeof(args[0]), argc, argv, &optidx))
2075 print_version(NULL);
2078 if (argc > optidx) /* Add option to set a URI local part prefix? */
2081 errx(1, "Port number must be given");
2083 if (audiences.num_strings == 0) {
2084 char localhost[MAXHOSTNAMELEN];
2086 ret = gethostname(localhost, sizeof(localhost));
2088 errx(1, "Could not determine local hostname; use --audience");
2090 if ((audiences.strings =
2091 calloc(1, sizeof(audiences.strings[0]))) == NULL ||
2092 (audiences.strings[0] = strdup(localhost)) == NULL)
2093 err(1, "Out of memory");
2094 audiences.num_strings = 1;
2097 if (daemonize && daemon_child_fd == -1)
2098 daemon_child_fd = roken_detach_prep(argc, argv, "--daemon-child");
2104 if ((errno = pthread_key_create(&k5ctx, k5_free_context)))
2105 err(1, "Could not create thread-specific storage");
2107 if ((errno = get_krb5_context(&context)))
2108 err(1, "Could not init krb5 context (config file issue?)");
2113 ret = krb5_get_default_realm(context, &s);
2115 krb5_err(context, 1, ret, "Could not determine default realm");
2119 if ((errno = get_kadm_handle(context, realm, 0 /* want_write */,
2121 err(1, "Could not connect to HDB");
2122 kadm5_destroy(kadm_handle);
2124 my_openlog(context, "httpkadmind", &logfac);
2125 load_plugins(context);
2127 if (cache_dir == NULL) {
2130 if (asprintf(&s, "%s/httpkadmind-XXXXXX",
2131 getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp") == -1 ||
2133 (cache_dir = mkdtemp(s)) == NULL)
2134 err(1, "could not create temporary cache directory");
2135 if (verbose_counter)
2136 fprintf(stderr, "Note: using %s as cache directory\n", cache_dir);
2137 atexit(rm_cache_dir);
2138 setenv("TMPDIR", cache_dir, 1);
2142 if (cert_file && !priv_key_file)
2143 priv_key_file = cert_file;
2146 hx509_cursor cursor = NULL;
2147 hx509_certs certs = NULL;
2148 hx509_cert cert = NULL;
2149 time_t min_cert_life = 0;
2153 ret = hx509_certs_init(context->hx509ctx, cert_file, 0, NULL, &certs);
2155 ret = hx509_certs_start_seq(context->hx509ctx, certs, &cursor);
2157 (ret = hx509_certs_next_cert(context->hx509ctx, certs,
2158 cursor, &cert)) == 0 && cert) {
2159 time_t notAfter = 0;
2161 if (!hx509_cert_have_private_key_only(cert) &&
2162 (notAfter = hx509_cert_get_notAfter(cert)) <= time(NULL) + 30)
2163 errx(1, "One or more certificates in %s are expired",
2166 notAfter -= time(NULL);
2168 warnx("One or more certificates in %s expire soon",
2170 /* Reload 5 minutes prior to expiration */
2171 if (notAfter < min_cert_life || min_cert_life < 1)
2172 min_cert_life = notAfter;
2174 hx509_cert_free(cert);
2177 (void) hx509_certs_end_seq(context->hx509ctx, certs, cursor);
2178 if (min_cert_life > 4)
2179 alarm(min_cert_life >> 1);
2180 hx509_certs_free(&certs);
2182 hx509_err(context->hx509ctx, 1, ret,
2183 "could not read certificate from %s", cert_file);
2185 if ((errno = rk_undumpdata(cert_file, &s, &len)) ||
2186 (cert_pem = strndup(s, len)) == NULL)
2187 err(1, "could not read certificate from %s", cert_file);
2188 if (strlen(cert_pem) != len)
2189 err(1, "NULs in certificate file contents: %s", cert_file);
2193 if (priv_key_file) {
2197 if ((errno = rk_undumpdata(priv_key_file, &s, &len)) ||
2198 (priv_key_pem = strndup(s, len)) == NULL)
2199 err(1, "could not read private key from %s", priv_key_file);
2200 if (strlen(priv_key_pem) != len)
2201 err(1, "NULs in private key file contents: %s", priv_key_file);
2205 if (verbose_counter > 1)
2206 flags |= MHD_USE_DEBUG;
2207 if (thread_per_client_flag)
2208 flags |= MHD_USE_THREAD_PER_CONNECTION;
2211 if (pipe(sigpipe) == -1)
2212 err(1, "Could not set up key/cert reloading");
2213 memset(&sa, 0, sizeof(sa));
2214 sa.sa_handler = sighandler;
2215 if (reverse_proxied_flag) {
2217 * We won't use TLS in the reverse proxy case, so no need to reload
2218 * certs. But we'll still read them if given, and alarm() will get
2221 * XXX We should be able to re-read krb5.conf and such on SIGHUP.
2223 (void) signal(SIGHUP, SIG_IGN);
2224 (void) signal(SIGUSR1, SIG_IGN);
2225 (void) signal(SIGALRM, SIG_IGN);
2227 (void) sigaction(SIGHUP, &sa, NULL); /* Reload key & cert */
2228 (void) sigaction(SIGUSR1, &sa, NULL); /* Reload key & cert */
2229 (void) sigaction(SIGALRM, &sa, NULL); /* Reload key & cert */
2231 (void) sigaction(SIGINT, &sa, NULL); /* Graceful shutdown */
2232 (void) sigaction(SIGTERM, &sa, NULL); /* Graceful shutdown */
2233 (void) signal(SIGPIPE, SIG_IGN);
2236 sock = MHD_quiesce_daemon(previous);
2238 if (reverse_proxied_flag) {
2240 * XXX IPv6 too. Create the sockets and tell MHD_start_daemon() about
2243 sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
2244 sin.sin_family = AF_INET;
2245 sin.sin_port = htons(port);
2246 current = MHD_start_daemon(flags, port,
2248 route, (char *)NULL,
2249 MHD_OPTION_SOCK_ADDR, &sin,
2250 MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
2251 MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
2253 } else if (sock != MHD_INVALID_SOCKET) {
2255 * Certificate/key rollover: reuse the listen socket returned by
2256 * MHD_quiesce_daemon().
2258 current = MHD_start_daemon(flags | MHD_USE_SSL, port,
2260 route, (char *)NULL,
2261 MHD_OPTION_HTTPS_MEM_KEY, priv_key_pem,
2262 MHD_OPTION_HTTPS_MEM_CERT, cert_pem,
2263 MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
2264 MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
2265 MHD_OPTION_LISTEN_SOCKET, sock,
2267 sock = MHD_INVALID_SOCKET;
2269 current = MHD_start_daemon(flags | MHD_USE_SSL, port,
2271 route, (char *)NULL,
2272 MHD_OPTION_HTTPS_MEM_KEY, priv_key_pem,
2273 MHD_OPTION_HTTPS_MEM_CERT, cert_pem,
2274 MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
2275 MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
2278 if (current == NULL)
2279 err(1, "Could not start kadmin REST service");
2282 MHD_stop_daemon(previous);
2286 if (verbose_counter)
2287 fprintf(stderr, "Ready!\n");
2288 if (daemon_child_fd != -1)
2289 roken_detach_finish(NULL, daemon_child_fd);
2291 /* Wait for signal, possibly SIGALRM, to reload certs and/or exit */
2292 while ((ret = read(sigpipe[0], &sig, sizeof(sig))) == -1 &&
2298 priv_key_pem = NULL;
2301 if (ret == 1 && (sig == SIGHUP || sig == SIGUSR1 || sig == SIGALRM)) {
2302 /* Reload certs and restart service gracefully */
2308 MHD_stop_daemon(current);
2309 _krb5_unload_plugins(context, "kdc");
2310 pthread_key_delete(k5ctx);