summaryrefslogtreecommitdiffstats
path: root/third_party/heimdal/kdc/httpkadmind.c
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/heimdal/kdc/httpkadmind.c')
-rw-r--r--third_party/heimdal/kdc/httpkadmind.c2341
1 files changed, 2341 insertions, 0 deletions
diff --git a/third_party/heimdal/kdc/httpkadmind.c b/third_party/heimdal/kdc/httpkadmind.c
new file mode 100644
index 0000000..0e31b40
--- /dev/null
+++ b/third_party/heimdal/kdc/httpkadmind.c
@@ -0,0 +1,2341 @@
+/*
+ * Copyright (c) 2020 Kungliga Tekniska Högskolan
+ * (Royal Institute of Technology, Stockholm, Sweden).
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the Institute nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ */
+
+#define _XOPEN_SOURCE_EXTENDED 1
+#define _DEFAULT_SOURCE 1
+#define _BSD_SOURCE 1
+#define _GNU_SOURCE 1
+
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <ctype.h>
+#include <dlfcn.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+
+#include <microhttpd.h>
+#include "kdc_locl.h"
+#include "token_validator_plugin.h"
+#include <getarg.h>
+#include <roken.h>
+#include <krb5.h>
+#include <gssapi/gssapi.h>
+#include <gssapi/gssapi_krb5.h>
+#include <hx509.h>
+#include "../lib/hx509/hx_locl.h"
+#include <hx509-private.h>
+#include <kadm5/admin.h>
+#include <kadm5/private.h>
+#include <kadm5/kadm5_err.h>
+
+#define heim_pcontext krb5_context
+#define heim_pconfig krb5_context
+#include <heimbase-svc.h>
+
+#if MHD_VERSION < 0x00097002 || defined(MHD_YES)
+/* libmicrohttpd changed these from int valued macros to an enum in 0.9.71 */
+#ifdef MHD_YES
+#undef MHD_YES
+#undef MHD_NO
+#endif
+enum MHD_Result { MHD_NO = 0, MHD_YES = 1 };
+#define MHD_YES 1
+#define MHD_NO 0
+typedef int heim_mhd_result;
+#else
+typedef enum MHD_Result heim_mhd_result;
+#endif
+
+#define BODYLEN_IS_STRLEN (~0)
+
+/*
+ * Libmicrohttpd is not the easiest API to use. It's got issues.
+ *
+ * One of the issues is how responses are handled, and the return value of the
+ * resource handler (MHD_NO -> close the connection, MHD_YES -> send response).
+ * Note that the handler could return MHD_YES without having set an HTTP
+ * response.
+ *
+ * There's memory management issues as well.
+ *
+ * Here we have to be careful about return values.
+ *
+ * Some of the functions defined here return just a krb5_error_code without
+ * having set an HTTP response on error.
+ * Others do set an HTTP response on error.
+ * The convention is to either set an HTTP response on error, or not at all,
+ * but not a mix of errors where for some the function will set a response and
+ * for others it won't.
+ *
+ * We do use some system error codes to stand in for errors here.
+ * Specifically:
+ *
+ * - EACCES -> authorization failed
+ * - EINVAL -> bad API usage
+ * - ENOSYS -> missing CSRF token but CSRF token required
+ *
+ * FIXME: We should rely only on krb5_set_error_message() and friends and make
+ * error responses only in route(), mapping krb5_error_code values to
+ * HTTP status codes. This would simplify the error handling convention
+ * here.
+ */
+
+/* Our request description structure */
+typedef struct kadmin_request_desc {
+ HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS;
+
+ struct MHD_Connection *connection;
+ krb5_times token_times;
+ /*
+ * FIXME
+ *
+ * Currently we re-use the authz framework from bx509d, using an
+ * `hx509_request' instance (an abstraction for CSRs) to represent the
+ * request because that is what the authz plugin uses that implements the
+ * policy we want checked here.
+ *
+ * This is inappropriate in the long-term in two ways:
+ *
+ * - the policy for certificates deals in SANs and EKUs, whereas the
+ * policy for ext_keytab deals in host-based service principal names,
+ * and there is not a one-to-one mapping of service names to EKUs;
+ *
+ * - using a type from libhx509 for representing requests for things that
+ * aren't certificates is really not appropriate no matter how similar
+ * the use cases for this all might be.
+ *
+ * What we need to do is develop a library that can represent requests for
+ * credentials via naming attributes like SANs and Kerberos principal
+ * names, but more arbitrary still than what `hx509_request' supports, and
+ * then invokes a plugin.
+ *
+ * Also, we might want to develop an in-tree authorization solution that is
+ * richer than what kadmin.acl supports now, storing grants in HDB entries
+ * and/or similar places.
+ *
+ * For expediency we use `hx509_request' here for now, impedance mismatches
+ * be damned.
+ */
+ hx509_request req; /* For authz only */
+ heim_array_t service_names;
+ heim_array_t hostnames;
+ heim_array_t spns;
+ krb5_principal cprinc;
+ krb5_keytab keytab;
+ krb5_storage *sp;
+ void *kadm_handle;
+ char *realm;
+ char *keytab_name;
+ char *freeme1;
+ char *enctypes;
+ const char *method;
+ unsigned int response_set:1;
+ unsigned int materialize:1;
+ unsigned int rotate_now:1;
+ unsigned int rotate:1;
+ unsigned int revoke:1;
+ unsigned int create:1;
+ unsigned int ro:1;
+ unsigned int is_self:1;
+ char frombuf[128];
+} *kadmin_request_desc;
+
+static void
+audit_trail(kadmin_request_desc r, krb5_error_code ret)
+{
+ const char *retname = NULL;
+
+ /*
+ * Get a symbolic name for some error codes.
+ *
+ * Really, libcom_err should have a primitive for this, and ours could, but
+ * we can't use a system libcom_err if we extend ours.
+ */
+#define CASE(x) case x : retname = #x; break
+ switch (ret) {
+ case ENOSYS: retname = "ECSRFTOKENREQD"; break;
+ CASE(EINVAL);
+ CASE(ENOMEM);
+ CASE(EACCES);
+ CASE(HDB_ERR_NOT_FOUND_HERE);
+ CASE(HDB_ERR_WRONG_REALM);
+ CASE(HDB_ERR_EXISTS);
+ CASE(HDB_ERR_KVNO_NOT_FOUND);
+ CASE(HDB_ERR_NOENTRY);
+ CASE(HDB_ERR_NO_MKEY);
+ CASE(KRB5_KDC_UNREACH);
+ CASE(KADM5_FAILURE);
+ CASE(KADM5_AUTH_GET);
+ CASE(KADM5_AUTH_ADD);
+ CASE(KADM5_AUTH_MODIFY);
+ CASE(KADM5_AUTH_DELETE);
+ CASE(KADM5_AUTH_INSUFFICIENT);
+ CASE(KADM5_BAD_DB);
+ CASE(KADM5_DUP);
+ CASE(KADM5_RPC_ERROR);
+ CASE(KADM5_NO_SRV);
+ CASE(KADM5_BAD_HIST_KEY);
+ CASE(KADM5_NOT_INIT);
+ CASE(KADM5_UNK_PRINC);
+ CASE(KADM5_UNK_POLICY);
+ CASE(KADM5_BAD_MASK);
+ CASE(KADM5_BAD_CLASS);
+ CASE(KADM5_BAD_LENGTH);
+ CASE(KADM5_BAD_POLICY);
+ CASE(KADM5_BAD_PRINCIPAL);
+ CASE(KADM5_BAD_AUX_ATTR);
+ CASE(KADM5_BAD_HISTORY);
+ CASE(KADM5_BAD_MIN_PASS_LIFE);
+ CASE(KADM5_PASS_Q_TOOSHORT);
+ CASE(KADM5_PASS_Q_CLASS);
+ CASE(KADM5_PASS_Q_DICT);
+ CASE(KADM5_PASS_Q_GENERIC);
+ CASE(KADM5_PASS_REUSE);
+ CASE(KADM5_PASS_TOOSOON);
+ CASE(KADM5_POLICY_REF);
+ CASE(KADM5_INIT);
+ CASE(KADM5_BAD_PASSWORD);
+ CASE(KADM5_PROTECT_PRINCIPAL);
+ CASE(KADM5_BAD_SERVER_HANDLE);
+ CASE(KADM5_BAD_STRUCT_VERSION);
+ CASE(KADM5_OLD_STRUCT_VERSION);
+ CASE(KADM5_NEW_STRUCT_VERSION);
+ CASE(KADM5_BAD_API_VERSION);
+ CASE(KADM5_OLD_LIB_API_VERSION);
+ CASE(KADM5_OLD_SERVER_API_VERSION);
+ CASE(KADM5_NEW_LIB_API_VERSION);
+ CASE(KADM5_NEW_SERVER_API_VERSION);
+ CASE(KADM5_SECURE_PRINC_MISSING);
+ CASE(KADM5_NO_RENAME_SALT);
+ CASE(KADM5_BAD_CLIENT_PARAMS);
+ CASE(KADM5_BAD_SERVER_PARAMS);
+ CASE(KADM5_AUTH_LIST);
+ CASE(KADM5_AUTH_CHANGEPW);
+ CASE(KADM5_BAD_TL_TYPE);
+ CASE(KADM5_MISSING_CONF_PARAMS);
+ CASE(KADM5_BAD_SERVER_NAME);
+ CASE(KADM5_KS_TUPLE_NOSUPP);
+ CASE(KADM5_SETKEY3_ETYPE_MISMATCH);
+ CASE(KADM5_DECRYPT_USAGE_NOSUPP);
+ CASE(KADM5_POLICY_OP_NOSUPP);
+ CASE(KADM5_KEEPOLD_NOSUPP);
+ CASE(KADM5_AUTH_GET_KEYS);
+ CASE(KADM5_ALREADY_LOCKED);
+ CASE(KADM5_NOT_LOCKED);
+ CASE(KADM5_LOG_CORRUPT);
+ CASE(KADM5_LOG_NEEDS_UPGRADE);
+ CASE(KADM5_BAD_SERVER_HOOK);
+ CASE(KADM5_SERVER_HOOK_NOT_FOUND);
+ CASE(KADM5_OLD_SERVER_HOOK_VERSION);
+ CASE(KADM5_NEW_SERVER_HOOK_VERSION);
+ CASE(KADM5_READ_ONLY);
+ case 0:
+ retname = "SUCCESS";
+ break;
+ default:
+ retname = NULL;
+ break;
+ }
+ heim_audit_trail((heim_svc_req_desc)r, ret, retname);
+}
+
+static krb5_log_facility *logfac;
+static pthread_key_t k5ctx;
+
+static krb5_error_code
+get_krb5_context(krb5_context *contextp)
+{
+ krb5_error_code ret;
+
+ if ((*contextp = pthread_getspecific(k5ctx)))
+ return 0;
+
+ ret = krb5_init_context(contextp);
+ /* XXX krb5_set_log_dest(), warn_dest, debug_dest */
+ if (ret == 0)
+ (void) pthread_setspecific(k5ctx, *contextp);
+ return ret;
+}
+
+static int port = -1;
+static int help_flag;
+static int daemonize;
+static int daemon_child_fd = -1;
+static int local_hdb;
+static int local_hdb_read_only;
+static int read_only;
+static int verbose_counter;
+static int version_flag;
+static int reverse_proxied_flag;
+static int thread_per_client_flag;
+struct getarg_strings audiences;
+static const char *cert_file;
+static const char *priv_key_file;
+static const char *cache_dir;
+static const char *realm;
+static const char *hdb;
+static const char *primary_server_URI;
+static const char *kadmin_server;
+static const char *writable_kadmin_server;
+static const char *stash_file;
+static const char *kadmin_client_name = "httpkadmind/admin";
+static const char *kadmin_client_keytab;
+static struct getarg_strings auth_types;
+
+#define set_conf(c, f, v, b) \
+ if (v) { \
+ if (((c).f = strdup(v)) == NULL) \
+ goto enomem; \
+ conf.mask |= b; \
+ }
+
+/*
+ * Does NOT set an HTTP response, naturally, as it doesn't even have access to
+ * the connection.
+ */
+static krb5_error_code
+get_kadm_handle(krb5_context context,
+ const char *want_realm,
+ int want_write,
+ void **kadm_handle)
+{
+ kadm5_config_params conf;
+ krb5_error_code ret;
+
+ /*
+ * If the caller wants to write and we are configured to redirect in that
+ * case, then trigger a redirect by returning KADM5_READ_ONLY.
+ */
+ if (want_write && local_hdb_read_only && primary_server_URI)
+ return KADM5_READ_ONLY;
+ if (want_write && read_only)
+ return KADM5_READ_ONLY;
+
+ /*
+ * Configure kadm5 connection.
+ *
+ * Note that all of these are optional, and will be found in krb5.conf or,
+ * in some cases, in DNS, as needed.
+ */
+ memset(&conf, 0, sizeof(conf));
+ conf.realm = NULL;
+ conf.dbname = NULL;
+ conf.stash_file = NULL;
+ conf.admin_server = NULL;
+ conf.readonly_admin_server = NULL;
+ set_conf(conf, realm, want_realm, KADM5_CONFIG_REALM);
+ set_conf(conf, dbname, hdb, KADM5_CONFIG_DBNAME);
+ set_conf(conf, stash_file, stash_file, KADM5_CONFIG_STASH_FILE);
+ set_conf(conf, admin_server, writable_kadmin_server, KADM5_CONFIG_ADMIN_SERVER);
+ set_conf(conf, readonly_admin_server, kadmin_server,
+ KADM5_CONFIG_READONLY_ADMIN_SERVER);
+
+ /*
+ * If we have a local HDB we'll use it if we can. If the local HDB is
+ * read-only and the caller wants to write, then we won't use the local
+ * HDB, naturally.
+ */
+ if (local_hdb && (!local_hdb_read_only || !want_write)) {
+ ret = kadm5_s_init_with_password_ctx(context,
+ kadmin_client_name,
+ NULL, /* password */
+ NULL, /* service_name */
+ &conf,
+ 0, /* struct_version */
+ 0, /* api_version */
+ kadm_handle);
+ goto out;
+ }
+
+ /*
+ * Remote connection. This will connect to a read-only kadmind if
+ * possible, and if so, reconnect to a writable kadmind as needed.
+ *
+ * Note that kadmin_client_keytab can be an HDB: or HDBGET: keytab.
+ */
+ ret = kadm5_c_init_with_skey_ctx(context,
+ kadmin_client_name,
+ kadmin_client_keytab,
+ KADM5_ADMIN_SERVICE,
+ &conf,
+ 0, /* struct_version */
+ 0, /* api_version */
+ kadm_handle);
+ goto out;
+
+enomem:
+ ret = krb5_enomem(context);
+
+out:
+ free(conf.readonly_admin_server);
+ free(conf.admin_server);
+ free(conf.stash_file);
+ free(conf.dbname);
+ free(conf.realm);
+ return ret;
+}
+
+static krb5_error_code resp(kadmin_request_desc, int, krb5_error_code,
+ enum MHD_ResponseMemoryMode, const char *,
+ const void *, size_t, const char *, const char *);
+static krb5_error_code bad_req(kadmin_request_desc, krb5_error_code, int,
+ const char *, ...)
+ HEIMDAL_PRINTF_ATTRIBUTE((__printf__, 4, 5));
+
+static krb5_error_code bad_enomem(kadmin_request_desc, krb5_error_code);
+static krb5_error_code bad_400(kadmin_request_desc, krb5_error_code, const char *);
+static krb5_error_code bad_401(kadmin_request_desc, const char *);
+static krb5_error_code bad_403(kadmin_request_desc, krb5_error_code, const char *);
+static krb5_error_code bad_404(kadmin_request_desc, const char *);
+static krb5_error_code bad_405(kadmin_request_desc, const char *);
+/*static krb5_error_code bad_500(kadmin_request_desc, krb5_error_code, const char *);*/
+static krb5_error_code bad_503(kadmin_request_desc, krb5_error_code, const char *);
+
+static int
+validate_token(kadmin_request_desc r)
+{
+ krb5_error_code ret;
+ const char *token;
+ const char *host;
+ char token_type[64]; /* Plenty */
+ char *p;
+ krb5_data tok;
+ size_t host_len, brk, i;
+
+ memset(&r->token_times, 0, sizeof(r->token_times));
+ host = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
+ MHD_HTTP_HEADER_HOST);
+ if (host == NULL)
+ return bad_400(r, EINVAL, "Host header is missing");
+
+ /* Exclude port number here (IPv6-safe because of the below) */
+ host_len = ((p = strchr(host, ':'))) ? p - host : strlen(host);
+
+ token = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
+ MHD_HTTP_HEADER_AUTHORIZATION);
+ if (token == NULL)
+ return bad_401(r, "Authorization token is missing");
+ brk = strcspn(token, " \t");
+ if (token[brk] == '\0' || brk > sizeof(token_type) - 1)
+ return bad_401(r, "Authorization token is missing");
+ memcpy(token_type, token, brk);
+ token_type[brk] = '\0';
+ token += brk + 1;
+ tok.length = strlen(token);
+ tok.data = (void *)(uintptr_t)token;
+
+ for (i = 0; i < audiences.num_strings; i++)
+ if (strncasecmp(host, audiences.strings[i], host_len) == 0 &&
+ audiences.strings[i][host_len] == '\0')
+ break;
+ if (i == audiences.num_strings)
+ return bad_403(r, EINVAL, "Host: value is not accepted here");
+
+ r->sname = strdup(host); /* No need to check for ENOMEM here */
+
+ ret = kdc_validate_token(r->context, NULL /* realm */, token_type, &tok,
+ (const char **)&audiences.strings[i], 1,
+ &r->cprinc, &r->token_times);
+ if (ret)
+ return bad_403(r, ret, "Token validation failed");
+ if (r->cprinc == NULL)
+ return bad_403(r, ret,
+ "Could not extract a principal name from token");
+ ret = krb5_unparse_name(r->context, r->cprinc, &r->cname);
+ if (ret)
+ return bad_503(r, ret,
+ "Could not extract a principal name from token");
+ return 0;
+}
+
+static void
+k5_free_context(void *ctx)
+{
+ krb5_free_context(ctx);
+}
+
+#ifndef HAVE_UNLINKAT
+static int
+unlink1file(const char *dname, const char *name)
+{
+ char p[PATH_MAX];
+
+ if (strlcpy(p, dname, sizeof(p)) < sizeof(p) &&
+ strlcat(p, "/", sizeof(p)) < sizeof(p) &&
+ strlcat(p, name, sizeof(p)) < sizeof(p))
+ return unlink(p);
+ return ERANGE;
+}
+#endif
+
+static void
+rm_cache_dir(void)
+{
+ struct dirent *e;
+ DIR *d;
+
+ /*
+ * This works, but not on Win32:
+ *
+ * (void) simple_execlp("rm", "rm", "-rf", cache_dir, NULL);
+ *
+ * We make no directories in `cache_dir', so we need not recurse.
+ */
+ if ((d = opendir(cache_dir)) == NULL)
+ return;
+
+ while ((e = readdir(d))) {
+#ifdef HAVE_UNLINKAT
+ /*
+ * Because unlinkat() takes a directory FD, implementing one for
+ * libroken is tricky at best. Instead we might want to implement an
+ * rm_dash_rf() function in lib/roken.
+ */
+ (void) unlinkat(dirfd(d), e->d_name, 0);
+#else
+ (void) unlink1file(cache_dir, e->d_name);
+#endif
+ }
+ (void) closedir(d);
+ (void) rmdir(cache_dir);
+}
+
+/*
+ * Work around older libmicrohttpd not strduping response header values when
+ * set.
+ */
+static HEIMDAL_THREAD_LOCAL struct redirect_uri {
+ char uri[4096];
+ size_t len;
+ size_t first_param;
+ int valid;
+} redirect_uri;
+
+static void
+redirect_uri_appends(struct redirect_uri *redirect,
+ const char *s)
+{
+ size_t sz, len;
+ char *p;
+
+ if (!redirect->valid || redirect->len >= sizeof(redirect->uri) - 1) {
+ redirect->valid = 0;
+ return;
+ }
+ /* Optimize strlcpy by using redirect->uri + redirect->len */
+ p = redirect->uri + redirect->len;
+ sz = sizeof(redirect->uri) - redirect->len;
+ if ((len = strlcpy(p, s, sz)) >= sz)
+ redirect->valid = 0;
+ else
+ redirect->len += len;
+}
+
+static heim_mhd_result
+make_redirect_uri_param_cb(void *d,
+ enum MHD_ValueKind kind,
+ const char *key,
+ const char *val)
+{
+ struct redirect_uri *redirect = d;
+
+ redirect_uri_appends(redirect, redirect->first_param ? "?" : "&");
+ redirect_uri_appends(redirect, key);
+ if (val) {
+ redirect_uri_appends(redirect, "=");
+ redirect_uri_appends(redirect, val);
+ }
+ redirect->first_param = 0;
+ return MHD_YES;
+}
+
+static const char *
+make_redirect_uri(kadmin_request_desc r, const char *base)
+{
+ redirect_uri.len = 0;
+ redirect_uri.uri[0] = '\0';
+ redirect_uri.valid = 1;
+ redirect_uri.first_param = 1;
+
+ redirect_uri_appends(&redirect_uri, base); /* Redirect to primary URI base */
+ redirect_uri_appends(&redirect_uri, r->reqtype); /* URI local-part */
+ (void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
+ make_redirect_uri_param_cb,
+ &redirect_uri);
+ return redirect_uri.valid ? redirect_uri.uri : NULL;
+}
+
+
+/*
+ * XXX Shouldn't be a body, but a status message. The body should be
+ * configurable to be from a file. MHD doesn't give us a way to set the
+ * response status message though, just the body.
+ *
+ * Calls audit_trail().
+ *
+ * Returns -1 if something terrible happened, which should ultimately cause
+ * route() to return MHD_NO, which should cause libmicrohttpd to close the
+ * connection to the user-agent.
+ *
+ * Returns 0 in all other cases.
+ */
+static krb5_error_code
+resp(kadmin_request_desc r,
+ int http_status_code,
+ krb5_error_code ret,
+ enum MHD_ResponseMemoryMode rmmode,
+ const char *content_type,
+ const void *body,
+ size_t bodylen,
+ const char *token,
+ const char *csrf)
+{
+ struct MHD_Response *response;
+ int mret = MHD_YES;
+
+ if (r->response_set) {
+ krb5_log_msg(r->context, logfac, 1, NULL,
+ "Internal error; attempted to set a second response");
+ return 0;
+ }
+
+ (void) gettimeofday(&r->tv_end, NULL);
+ audit_trail(r, ret);
+
+ if (body && bodylen == BODYLEN_IS_STRLEN)
+ bodylen = strlen(body);
+
+ response = MHD_create_response_from_buffer(bodylen, rk_UNCONST(body),
+ rmmode);
+ if (response == NULL)
+ return -1;
+ mret = MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL,
+ "no-store, max-age=0");
+ if (mret == MHD_YES && http_status_code == MHD_HTTP_UNAUTHORIZED) {
+ size_t i;
+
+ if (auth_types.num_strings < 1)
+ http_status_code = MHD_HTTP_SERVICE_UNAVAILABLE;
+ else
+ for (i = 0; mret == MHD_YES && i < auth_types.num_strings; i++)
+ mret = MHD_add_response_header(response,
+ MHD_HTTP_HEADER_WWW_AUTHENTICATE,
+ auth_types.strings[i]);
+ } else if (mret == MHD_YES && http_status_code == MHD_HTTP_TEMPORARY_REDIRECT) {
+ const char *redir = make_redirect_uri(r, primary_server_URI);
+
+ if (redir)
+ mret = MHD_add_response_header(response, MHD_HTTP_HEADER_LOCATION,
+ redir);
+ else
+ /* XXX Find a way to set a new response body; log */
+ http_status_code = MHD_HTTP_SERVICE_UNAVAILABLE;
+ }
+
+ if (mret == MHD_YES && csrf)
+ mret = MHD_add_response_header(response,
+ "X-CSRF-Token",
+ csrf);
+
+ if (mret == MHD_YES && content_type) {
+ mret = MHD_add_response_header(response,
+ MHD_HTTP_HEADER_CONTENT_TYPE,
+ content_type);
+ }
+ if (mret != MHD_NO)
+ mret = MHD_queue_response(r->connection, http_status_code, response);
+ MHD_destroy_response(response);
+ r->response_set = 1;
+ return mret == MHD_NO ? -1 : 0;
+}
+
+static krb5_error_code
+bad_reqv(kadmin_request_desc r,
+ krb5_error_code code,
+ int http_status_code,
+ const char *fmt,
+ va_list ap)
+{
+ krb5_error_code ret;
+ krb5_context context = NULL;
+ const char *k5msg = NULL;
+ const char *emsg = NULL;
+ char *formatted = NULL;
+ char *msg = NULL;
+
+ if (r && r->context)
+ context = r->context;
+ if (r && r->hcontext && r->kv)
+ heim_audit_setkv_number((heim_svc_req_desc)r, "http-status-code",
+ http_status_code);
+ if (r)
+ (void) gettimeofday(&r->tv_end, NULL);
+ if (code == ENOMEM) {
+ if (context)
+ krb5_log_msg(context, logfac, 1, NULL, "Out of memory");
+ return resp(r, http_status_code, code, MHD_RESPMEM_PERSISTENT,
+ NULL, fmt, BODYLEN_IS_STRLEN, NULL, NULL);
+ }
+
+ if (code) {
+ if (context)
+ emsg = k5msg = krb5_get_error_message(context, code);
+ else
+ emsg = strerror(code);
+ }
+
+ ret = vasprintf(&formatted, fmt, ap) == -1;
+ if (code) {
+ if (ret > -1 && formatted)
+ ret = asprintf(&msg, "%s: %s (%d)", formatted, emsg, (int)code);
+ } else {
+ msg = formatted;
+ formatted = NULL;
+ }
+ if (r && r->hcontext)
+ heim_audit_addreason((heim_svc_req_desc)r, "%s", formatted);
+ krb5_free_error_message(context, k5msg);
+
+ if (ret == -1 || msg == NULL) {
+ if (context)
+ krb5_log_msg(context, logfac, 1, NULL, "Out of memory");
+ return resp(r, MHD_HTTP_SERVICE_UNAVAILABLE, ENOMEM,
+ MHD_RESPMEM_PERSISTENT, NULL,
+ "Out of memory", BODYLEN_IS_STRLEN, NULL, NULL);
+ }
+
+ ret = resp(r, http_status_code, code, MHD_RESPMEM_MUST_COPY,
+ NULL, msg, BODYLEN_IS_STRLEN, NULL, NULL);
+ free(formatted);
+ free(msg);
+ return ret == -1 ? -1 : code;
+}
+
+static krb5_error_code
+bad_req(kadmin_request_desc r,
+ krb5_error_code code,
+ int http_status_code,
+ const char *fmt,
+ ...)
+{
+ krb5_error_code ret;
+ va_list ap;
+
+ va_start(ap, fmt);
+ ret = bad_reqv(r, code, http_status_code, fmt, ap);
+ va_end(ap);
+ return ret;
+}
+
+static krb5_error_code
+bad_enomem(kadmin_request_desc r, krb5_error_code ret)
+{
+ return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
+ "Out of memory");
+}
+
+static krb5_error_code
+bad_400(kadmin_request_desc r, int ret, const char *reason)
+{
+ return bad_req(r, ret, MHD_HTTP_BAD_REQUEST, "%s", reason);
+}
+
+static krb5_error_code
+bad_401(kadmin_request_desc r, const char *reason)
+{
+ return bad_req(r, EACCES, MHD_HTTP_UNAUTHORIZED, "%s", reason);
+}
+
+static krb5_error_code
+bad_403(kadmin_request_desc r, krb5_error_code ret, const char *reason)
+{
+ return bad_req(r, ret, MHD_HTTP_FORBIDDEN, "%s", reason);
+}
+
+static krb5_error_code
+bad_404(kadmin_request_desc r, const char *name)
+{
+ return bad_req(r, ENOENT, MHD_HTTP_NOT_FOUND,
+ "Resource not found: %s", name);
+}
+
+static krb5_error_code
+bad_405(kadmin_request_desc r, const char *method)
+{
+ return bad_req(r, EPERM, MHD_HTTP_METHOD_NOT_ALLOWED,
+ "Method not supported: %s", method);
+}
+
+static krb5_error_code
+bad_method_want_POST(kadmin_request_desc r)
+{
+ return bad_req(r, EPERM, MHD_HTTP_METHOD_NOT_ALLOWED,
+ "Use POST for making changes to principals");
+}
+
+#if 0
+static krb5_error_code
+bad_500(kadmin_request_desc r,
+ krb5_error_code ret,
+ const char *reason)
+{
+ return bad_req(r, ret, MHD_HTTP_INTERNAL_SERVER_ERROR,
+ "Internal error: %s", reason);
+}
+#endif
+
+static krb5_error_code
+bad_503(kadmin_request_desc r,
+ krb5_error_code ret,
+ const char *reason)
+{
+ return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
+ "Service unavailable: %s", reason);
+}
+
+static krb5_error_code
+good_ext_keytab(kadmin_request_desc r)
+{
+ krb5_error_code ret;
+ size_t bodylen;
+ void *body;
+ char *p;
+
+ if (!r->keytab_name || !(p = strchr(r->keytab_name, ':')))
+ return bad_503(r, EINVAL, "Internal error (no keytab produced)");
+ p++;
+ if (strncmp(p, cache_dir, strlen(cache_dir)) != 0)
+ return bad_503(r, EINVAL, "Internal error");
+ ret = rk_undumpdata(p, &body, &bodylen);
+ if (ret)
+ return bad_503(r, ret, "Could not recover keytab from temp file");
+
+ ret = resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_MUST_COPY,
+ "application/octet-stream", body, bodylen, NULL, NULL);
+ free(body);
+ return ret;
+}
+
+static krb5_error_code
+check_service_name(kadmin_request_desc r, const char *name)
+{
+ if (name == NULL || name[0] == '\0' ||
+ strchr(name, '/') || strchr(name, '\\') || strchr(name, '@') ||
+ strcmp(name, "krbtgt") == 0 ||
+ strcmp(name, "iprop") == 0 ||
+ strcmp(name, "kadmin") == 0 ||
+ strcmp(name, "hprop") == 0 ||
+ strcmp(name, "WELLKNOWN") == 0 ||
+ strcmp(name, "K") == 0) {
+ krb5_set_error_message(r->context, EACCES,
+ "No one is allowed to fetch keys for "
+ "Heimdal service %s", name);
+ return EACCES;
+ }
+ if (strcmp(name, "root") != 0 &&
+ strcmp(name, "host") != 0 &&
+ strcmp(name, "exceed") != 0)
+ return 0;
+ if (krb5_config_get_bool_default(r->context, NULL, FALSE,
+ "ext_keytab",
+ "csr_authorizer_handles_svc_names",
+ NULL))
+ return 0;
+ krb5_set_error_message(r->context, EACCES,
+ "No one is allowed to fetch keys for "
+ "service \"%s\" because of authorizer "
+ "limitations", name);
+ return EACCES;
+}
+
+static heim_mhd_result
+param_cb(void *d,
+ enum MHD_ValueKind kind,
+ const char *key,
+ const char *val)
+{
+ kadmin_request_desc r = d;
+ krb5_error_code ret = 0;
+ heim_string_t s = NULL;
+
+ /*
+ * Multi-valued params:
+ *
+ * - spn=<service>/<hostname>
+ * - dNSName=<hostname>
+ * - service=<service>
+ *
+ * Single-valued params:
+ *
+ * - realm=<REALM>
+ * - materialize=true -- create a concrete princ where it's virtual
+ * - enctypes=... -- key-salt types
+ * - revoke=true -- delete old keys (concrete princs only)
+ * - rotate=true -- change keys (no-op for virtual princs)
+ * - create=true -- create a concrete princ
+ * - ro=true -- perform no writes
+ */
+
+ if (strcmp(key, "realm") == 0 && val) {
+ if (!r->realm && !(r->realm = strdup(val)))
+ ret = krb5_enomem(r->context);
+ } else if (strcmp(key, "materialize") == 0 ||
+ strcmp(key, "revoke") == 0 ||
+ strcmp(key, "rotate") == 0 ||
+ strcmp(key, "create") == 0 ||
+ strcmp(key, "ro") == 0) {
+ heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
+ "requested_option", "%s", key);
+ if (!val || strcmp(val, "true") != 0)
+ krb5_set_error_message(r->context, ret = EINVAL,
+ "get-keys \"%s\" q-param accepts "
+ "only \"true\"", key);
+ else if (strcmp(key, "materialize") == 0)
+ r->materialize = 1;
+ else if (strcmp(key, "revoke") == 0)
+ r->revoke = 1;
+ else if (strcmp(key, "rotate") == 0)
+ r->rotate = 1;
+ else if (strcmp(key, "create") == 0)
+ r->create = 1;
+ else if (strcmp(key, "ro") == 0)
+ r->ro = 1;
+ } else if (strcmp(key, "dNSName") == 0 && val) {
+ heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
+ "requested_dNSName", "%s", val);
+ if (r->is_self) {
+ krb5_set_error_message(r->context, ret = EACCES,
+ "only one service may be requested for self");
+ } else if (strchr(val, '.') == NULL) {
+ krb5_set_error_message(r->context, ret = EACCES,
+ "dNSName must have at least one '.' in it");
+ } else {
+ s = heim_string_create(val);
+ if (!s)
+ ret = krb5_enomem(r->context);
+ else
+ ret = heim_array_append_value(r->hostnames, s);
+ }
+ if (ret == 0)
+ ret = hx509_request_add_dns_name(r->context->hx509ctx, r->req, val);
+ } else if (strcmp(key, "service") == 0 && val) {
+ heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
+ "requested_service", "%s", val);
+ if (r->is_self)
+ krb5_set_error_message(r->context, ret = EACCES,
+ "use \"spn\" for self");
+ else
+ ret = check_service_name(r, val);
+ if (ret == 0) {
+ s = heim_string_create(val);
+ if (!s)
+ ret = krb5_enomem(r->context);
+ else
+ ret = heim_array_append_value(r->service_names, s);
+ }
+ } else if (strcmp(key, "enctypes") == 0 && val) {
+ r->enctypes = strdup(val);
+ if (!(r->enctypes = strdup(val)))
+ ret = krb5_enomem(r->context);
+ heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
+ "requested_enctypes", "%s", val);
+ } else if (r->is_self && strcmp(key, "spn") == 0 && val) {
+ heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
+ "requested_spn", "%s", val);
+ krb5_set_error_message(r->context, ret = EACCES,
+ "only one service may be requested for self");
+ } else if (strcmp(key, "spn") == 0 && val) {
+ krb5_principal p = NULL;
+ const char *hostname = "";
+
+ heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
+ "requested_spn", "%s", val);
+
+ ret = krb5_parse_name_flags(r->context, val,
+ KRB5_PRINCIPAL_PARSE_NO_DEF_REALM, &p);
+ if (ret == 0 && krb5_principal_get_realm(r->context, p) == NULL)
+ ret = krb5_principal_set_realm(r->context, p,
+ r->realm ? r->realm : realm);
+
+ /*
+ * The SPN has to have two components.
+ *
+ * TODO: Support more components? Support AD-style NetBIOS computer
+ * account names?
+ */
+ if (ret == 0 && krb5_principal_get_num_comp(r->context, p) != 2)
+ ret = ENOTSUP;
+
+ /*
+ * Allow only certain service names. Except that when
+ * the SPN == the requestor's principal name then allow the "host"
+ * service name.
+ */
+ if (ret == 0) {
+ const char *service =
+ krb5_principal_get_comp_string(r->context, p, 0);
+
+ if (strcmp(service, "host") == 0 &&
+ krb5_principal_compare(r->context, p, r->cprinc) &&
+ !r->is_self &&
+ heim_array_get_length(r->hostnames) == 0 &&
+ heim_array_get_length(r->spns) == 0) {
+ r->is_self = 1;
+ } else
+ ret = check_service_name(r, service);
+ }
+ if (ret == 0 && !krb5_principal_compare(r->context, p, r->cprinc))
+ ret = check_service_name(r,
+ krb5_principal_get_comp_string(r->context,
+ p, 0));
+ if (ret == 0) {
+ hostname = krb5_principal_get_comp_string(r->context, p, 1);
+ if (!hostname || !strchr(hostname, '.'))
+ krb5_set_error_message(r->context, ret = ENOTSUP,
+ "Only host-based service names supported");
+ }
+ if (ret == 0 && r->realm)
+ ret = krb5_principal_set_realm(r->context, p, r->realm);
+ else if (ret == 0 && realm)
+ ret = krb5_principal_set_realm(r->context, p, realm);
+ if (ret == 0)
+ ret = hx509_request_add_dns_name(r->context->hx509ctx, r->req,
+ hostname);
+ if (ret == 0 && !(s = heim_string_create(val)))
+ ret = krb5_enomem(r->context);
+ if (ret == 0)
+ ret = heim_array_append_value(r->spns, s);
+ krb5_free_principal(r->context, p);
+
+#if 0
+ /* The authorizer probably doesn't know what to do with this */
+ ret = hx509_request_add_pkinit(r->context->hx509ctx, r->req, val);
+#endif
+ } else {
+ /* Produce error for unknown params */
+ heim_audit_setkv_bool((heim_svc_req_desc)r, "requested_unknown", TRUE);
+ krb5_set_error_message(r->context, ret = ENOTSUP,
+ "Query parameter %s not supported", key);
+ }
+ if (ret && !r->error_code)
+ r->error_code = ret;
+ heim_release(s);
+ return ret ? MHD_NO /* Stop iterating */ : MHD_YES;
+}
+
+static krb5_error_code
+authorize_req(kadmin_request_desc r)
+{
+ krb5_error_code ret;
+
+ r->is_self = 0;
+ ret = hx509_request_init(r->context->hx509ctx, &r->req);
+ if (ret)
+ return bad_enomem(r, ret);
+ (void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
+ param_cb, r);
+ ret = r->error_code;
+ if (ret == EACCES)
+ return bad_403(r, ret, "Not authorized to requested principal(s)");
+ if (ret)
+ return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
+ "Could not handle query parameters");
+ if (r->is_self)
+ ret = 0;
+ else
+ ret = kdc_authorize_csr(r->context, "ext_keytab", r->req, r->cprinc);
+ if (ret == EACCES || ret == EINVAL || ret == ENOTSUP ||
+ ret == KRB5KDC_ERR_POLICY)
+ return bad_403(r, ret, "Not authorized to requested principal(s)");
+ if (ret)
+ return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
+ "Error checking authorization");
+ return ret;
+}
+
+static krb5_error_code
+make_keytab(kadmin_request_desc r)
+{
+ krb5_error_code ret = 0;
+ int fd = -1;
+
+ r->keytab_name = NULL;
+ if (asprintf(&r->keytab_name, "FILE:%s/kt-XXXXXX", cache_dir) == -1 ||
+ r->keytab_name == NULL)
+ ret = krb5_enomem(r->context);
+ if (ret == 0)
+ fd = mkstemp(r->keytab_name + sizeof("FILE:") - 1);
+ if (ret == 0 && fd == -1)
+ ret = errno;
+ if (ret == 0)
+ ret = krb5_kt_resolve(r->context, r->keytab_name, &r->keytab);
+ if (fd != -1)
+ (void) close(fd);
+ return ret;
+}
+
+static krb5_error_code
+write_keytab(kadmin_request_desc r,
+ kadm5_principal_ent_rec *princ,
+ const char *unparsed)
+{
+ krb5_error_code ret = 0;
+ krb5_keytab_entry key;
+ size_t i;
+
+ if (princ->n_key_data <= 0)
+ return 0;
+
+ if (kadm5_some_keys_are_bogus(princ->n_key_data, &princ->key_data[0])) {
+ krb5_warn(r->context, ret,
+ "httpkadmind running with insufficient kadmin privilege "
+ "for extracting keys for %s", unparsed);
+ krb5_log_msg(r->context, logfac, 1, NULL,
+ "httpkadmind running with insufficient kadmin privilege "
+ "for extracting keys for %s", unparsed);
+ return EACCES;
+ }
+
+ memset(&key, 0, sizeof(key));
+ for (i = 0; ret == 0 && i < princ->n_key_data; i++) {
+ krb5_key_data *kd = &princ->key_data[i];
+
+ key.principal = princ->principal;
+ key.vno = kd->key_data_kvno;
+ key.keyblock.keytype = kd->key_data_type[0];
+ key.keyblock.keyvalue.length = kd->key_data_length[0];
+ key.keyblock.keyvalue.data = kd->key_data_contents[0];
+
+ /*
+ * FIXME kadm5 doesn't give us set_time here. If it gave us the
+ * KeyRotation metadata, we could compute it. But this might be a
+ * concrete principal with concrete keys, in which case we can't.
+ *
+ * To fix this we need to extend the protocol and the API.
+ */
+ key.timestamp = time(NULL);
+
+ ret = krb5_kt_add_entry(r->context, r->keytab, &key);
+ }
+ if (ret)
+ krb5_warn(r->context, ret,
+ "Failed to write keytab entries for %s", unparsed);
+
+ return ret;
+}
+
+static void
+random_password(krb5_context context, char *buf, size_t buflen)
+{
+ static const char chars[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,";
+ char p[32];
+ size_t i;
+ char b;
+
+ buflen--;
+ for (i = 0; i < buflen; i++) {
+ if (i % sizeof(p) == 0)
+ krb5_generate_random_block(p, sizeof(p));
+ b = p[i % sizeof(p)];
+ buf[i] = chars[b % (sizeof(chars) - 1)];
+ }
+ buf[i] = '\0';
+}
+
+static krb5_error_code
+make_kstuple(krb5_context context,
+ kadm5_principal_ent_rec *p,
+ krb5_key_salt_tuple **kstuple,
+ size_t *n_kstuple)
+{
+ size_t i;
+
+ *kstuple = 0;
+ *n_kstuple = 0;
+
+ if (p->n_key_data < 1)
+ return 0;
+ *kstuple = calloc(p->n_key_data, sizeof (**kstuple));
+ for (i = 0; *kstuple && i < p->n_key_data; i++) {
+ if (p->key_data[i].key_data_kvno == p->kvno) {
+ (*kstuple)[i].ks_enctype = p->key_data[i].key_data_type[0];
+ (*kstuple)[i].ks_salttype = p->key_data[i].key_data_type[1];
+ (*n_kstuple)++;
+ }
+ }
+ return *kstuple ? 0 :krb5_enomem(context);
+}
+
+/*
+ * Get keys for one principal.
+ *
+ * Does NOT set an HTTP response.
+ */
+static krb5_error_code
+get_keys1(kadmin_request_desc r, const char *pname)
+{
+ kadm5_principal_ent_rec princ;
+ krb5_key_salt_tuple *kstuple = NULL;
+ krb5_error_code ret = 0;
+ krb5_principal p = NULL;
+ uint32_t mask =
+ KADM5_PRINCIPAL | KADM5_KVNO | KADM5_MAX_LIFE | KADM5_MAX_RLIFE |
+ KADM5_ATTRIBUTES | KADM5_KEY_DATA | KADM5_TL_DATA;
+ uint32_t create_mask = mask & ~(KADM5_KEY_DATA | KADM5_TL_DATA);
+ size_t nkstuple = 0;
+ int change = 0;
+ int refetch = 0;
+ int freeit = 0;
+
+ memset(&princ, 0, sizeof(princ));
+ princ.key_data = NULL;
+ princ.tl_data = NULL;
+
+ ret = krb5_parse_name(r->context, pname, &p);
+ if (ret == 0 && r->realm)
+ ret = krb5_principal_set_realm(r->context, p, r->realm);
+ else if (ret == 0 && realm)
+ ret = krb5_principal_set_realm(r->context, p, realm);
+ if (ret == 0 && r->enctypes)
+ ret = krb5_string_to_keysalts2(r->context, r->enctypes,
+ &nkstuple, &kstuple);
+ if (ret == 0)
+ ret = kadm5_get_principal(r->kadm_handle, p, &princ, mask);
+ if (ret == 0) {
+ freeit = 1;
+
+ /*
+ * If princ is virtual and we're not asked to materialize, ignore
+ * requests to rotate.
+ */
+ if (!r->materialize &&
+ (princ.attributes & (KRB5_KDB_VIRTUAL_KEYS | KRB5_KDB_VIRTUAL))) {
+ r->rotate = 0;
+ r->revoke = 0;
+ }
+ }
+
+ change = !r->ro && (r->rotate || r->revoke);
+
+ /* Handle create / materialize options */
+ if (ret == KADM5_UNK_PRINC && r->create) {
+ char pw[128];
+
+ if (read_only)
+ ret = KADM5_READ_ONLY;
+ else
+ ret = strcmp(r->method, "POST") == 0 ? 0 : ENOSYS; /* XXX */
+ if (ret == 0 && local_hdb && local_hdb_read_only) {
+ /* Make sure we can write */
+ kadm5_destroy(r->kadm_handle);
+ r->kadm_handle = NULL;
+ ret = get_kadm_handle(r->context, r->realm, 1 /* want_write */,
+ &r->kadm_handle);
+ }
+ memset(&princ, 0, sizeof(princ));
+ /*
+ * Some software is allergic to kvno 1, assuming that kvno 1 implies
+ * half-baked service principal. We've some vague recollection of
+ * something similar for kvno 2, so let's start at 3.
+ */
+ princ.kvno = 3;
+ princ.tl_data = NULL;
+ princ.key_data = NULL;
+ princ.max_life = 24 * 3600; /* XXX Make configurable */
+ princ.max_renewable_life = princ.max_life; /* XXX Make configurable */
+
+ random_password(r->context, pw, sizeof(pw));
+ princ.principal = p; /* Borrow */
+ if (ret == 0)
+ ret = kadm5_create_principal_3(r->kadm_handle, &princ, create_mask,
+ nkstuple, kstuple, pw);
+ princ.principal = NULL; /* Return */
+ refetch = 1;
+ freeit = 1;
+ } else if (ret == 0 && r->materialize &&
+ (princ.attributes & KRB5_KDB_VIRTUAL)) {
+
+#ifndef MATERIALIZE_NOTYET
+ ret = ENOTSUP;
+#else
+ if (read_only)
+ ret = KADM5_READ_ONLY;
+ else
+ ret = strcmp(r->method, "POST") == 0 ? 0 : ENOSYS; /* XXX */
+ if (ret == 0 && local_hdb && local_hdb_read_only) {
+ /* Make sure we can write */
+ kadm5_destroy(r->kadm_handle);
+ r->kadm_handle = NULL;
+ ret = get_kadm_handle(r->context, r->realm, 1 /* want_write */,
+ &r->kadm_handle);
+ }
+ princ.attributes |= KRB5_KDB_MATERIALIZE;
+ princ.attributes &= ~KRB5_KDB_VIRTUAL;
+ /*
+ * XXX If there are TL data which should be re-encoded and sent as
+ * KRB5_TL_EXTENSION, then this call will fail with KADM5_BAD_TL_TYPE.
+ *
+ * We should either drop those TLs, re-encode them, or make
+ * perform_tl_data() handle them. (New extensions should generally go
+ * as KRB5_TL_EXTENSION so that non-critical ones can be set on
+ * principals via old kadmind programs that don't support them.)
+ *
+ * What we really want is a kadm5 utility function to convert some TLs
+ * to KRB5_TL_EXTENSION and drop all others.
+ */
+ if (ret == 0)
+ ret = kadm5_create_principal(r->kadm_handle, &princ, mask, "");
+ refetch = 1;
+#endif
+ } /* else create/materialize q-params are superfluous */
+
+ /* Handle rotate / revoke options */
+ if (ret == 0 && change) {
+ krb5_keyblock *k = NULL;
+ size_t i;
+ int n_k = 0;
+ int keepold = r->revoke ? 0 : 1;
+
+ if (read_only)
+ ret = KADM5_READ_ONLY;
+ else
+ ret = strcmp(r->method, "POST") == 0 ? 0 : ENOSYS; /* XXX */
+ if (ret == 0 && local_hdb && local_hdb_read_only) {
+ /* Make sure we can write */
+ kadm5_destroy(r->kadm_handle);
+ r->kadm_handle = NULL;
+ ret = get_kadm_handle(r->context, r->realm, 1 /* want_write */,
+ &r->kadm_handle);
+ }
+
+ /* Use requested enctypes or same ones as princ already had keys for */
+ if (ret == 0 && kstuple == NULL)
+ ret = make_kstuple(r->context, &princ, &kstuple, &nkstuple);
+
+ /* Set new keys */
+ if (ret == 0)
+ ret = kadm5_randkey_principal_3(r->kadm_handle, p, keepold,
+ nkstuple, kstuple, &k, &n_k);
+ refetch = 1;
+ for (i = 0; n_k > 0 && i < n_k; i++)
+ krb5_free_keyblock_contents(r->context, &k[i]);
+ free(kstuple);
+ free(k);
+ }
+
+ if (ret == 0 && refetch) {
+ /* Refetch changed principal */
+ if (freeit)
+ kadm5_free_principal_ent(r->kadm_handle, &princ);
+ freeit = 0;
+ ret = kadm5_get_principal(r->kadm_handle, p, &princ, mask);
+ if (ret == 0)
+ freeit = 1;
+ }
+
+ if (ret == 0)
+ ret = write_keytab(r, &princ, pname);
+ if (freeit)
+ kadm5_free_principal_ent(r->kadm_handle, &princ);
+ krb5_free_principal(r->context, p);
+ return ret;
+}
+
+static krb5_error_code check_csrf(kadmin_request_desc);
+
+/*
+ * Calls get_keys1() to extract each requested principal's keys.
+ *
+ * When this returns a response will have been set.
+ */
+static krb5_error_code
+get_keysN(kadmin_request_desc r, const char *method)
+{
+ krb5_error_code ret;
+ size_t nhosts;
+ size_t nsvcs;
+ size_t nspns;
+ size_t i, k;
+
+ /* Parses and validates the request, then checks authorization */
+ ret = authorize_req(r);
+ if (ret)
+ return ret; /* authorize_req() calls bad_req() on error */
+
+ ret = get_kadm_handle(r->context, r->realm ? r->realm : realm,
+ 0 /* want_write */, &r->kadm_handle);
+
+ if (strcmp(method, "POST") == 0 && (ret = check_csrf(r)))
+ return ret; /* check_csrf() calls bad_req() on error */
+
+ nhosts = heim_array_get_length(r->hostnames);
+ nsvcs = heim_array_get_length(r->service_names);
+ nspns = heim_array_get_length(r->spns);
+ if (!nhosts && !nspns)
+ return bad_403(r, EINVAL, "No service principals requested");
+
+ if (nhosts && !nsvcs) {
+ heim_string_t s;
+
+ if ((s = heim_string_create("HTTP")) == NULL)
+ ret = krb5_enomem(r->context);
+ if (ret == 0)
+ ret = heim_array_append_value(r->service_names, s);
+ heim_release(s);
+ nsvcs = 1;
+ if (ret)
+ return bad_503(r, ret, "Out of memory");
+ }
+
+ /* FIXME: Make this configurable */
+ if (nspns + nsvcs * nhosts > 40)
+ return bad_403(r, EINVAL, "Requested keys for too many principals");
+
+ ret = make_keytab(r);
+ for (i = 0; ret == 0 && i < nsvcs; i++) {
+ const char *svc =
+ heim_string_get_utf8(
+ heim_array_get_value(r->service_names, i));
+
+ for (k = 0; ret == 0 && k < nhosts; k++) {
+ krb5_principal p = NULL;
+ const char *hostname =
+ heim_string_get_utf8(
+ heim_array_get_value(r->hostnames, k));
+ char *spn = NULL;
+
+ ret = krb5_make_principal(r->context, &p,
+ r->realm ? r->realm : realm,
+ svc, hostname, NULL);
+ if (ret == 0)
+ ret = krb5_unparse_name(r->context, p, &spn);
+ if (ret == 0)
+ ret = get_keys1(r, spn);
+ krb5_free_principal(r->context, p);
+ free(spn);
+ }
+ }
+ for (i = 0; ret == 0 && i < nspns; i++) {
+ ret = get_keys1(r,
+ heim_string_get_utf8(heim_array_get_value(r->spns,
+ i)));
+ }
+ switch (ret) {
+ case -1:
+ /* Can't happen */
+ krb5_log_msg(r->context, logfac, 1, NULL,
+ "Failed to extract keys for unknown reasons");
+ if (r->response_set)
+ return MHD_YES;
+ return bad_503(r, ret, "Could not get keys");
+ case ENOSYS:
+ /* Our convention */
+ return bad_method_want_POST(r);
+ case KADM5_READ_ONLY:
+ if (primary_server_URI) {
+ krb5_log_msg(r->context, logfac, 1, NULL,
+ "Redirect %s to primary server", r->cname);
+ return resp(r, MHD_HTTP_TEMPORARY_REDIRECT, KADM5_READ_ONLY,
+ MHD_RESPMEM_PERSISTENT, NULL, "", 0, NULL, NULL);
+ } else {
+ krb5_log_msg(r->context, logfac, 1, NULL, "HDB is read-only");
+ return bad_403(r, ret, "HDB is read-only");
+ }
+ case 0:
+ krb5_log_msg(r->context, logfac, 1, NULL, "Sent keytab to %s",
+ r->cname);
+ return good_ext_keytab(r);
+ default:
+ return bad_503(r, ret, "Could not get keys");
+ }
+}
+
+/* Copied from kdc/connect.c */
+static void
+addr_to_string(krb5_context context,
+ struct sockaddr *addr,
+ char *str,
+ size_t len)
+{
+ krb5_error_code ret;
+ krb5_address a;
+
+ ret = krb5_sockaddr2address(context, addr, &a);
+ if (ret == 0) {
+ ret = krb5_print_address(&a, str, len, &len);
+ krb5_free_address(context, &a);
+ }
+ if (ret)
+ snprintf(str, len, "<family=%d>", addr->sa_family);
+}
+
+static krb5_error_code
+set_req_desc(struct MHD_Connection *connection,
+ const char *method,
+ const char *url,
+ kadmin_request_desc r)
+{
+ const union MHD_ConnectionInfo *ci;
+ const char *token;
+ krb5_error_code ret;
+
+ memset(r, 0, sizeof(*r));
+ (void) gettimeofday(&r->tv_start, NULL);
+
+ if ((ret = get_krb5_context(&r->context)))
+ return ret;
+ /* HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS fields */
+ r->request.data = "<HTTP-REQUEST>";
+ r->request.length = sizeof("<HTTP-REQUEST>");
+ r->from = r->frombuf;
+ r->config = NULL;
+ r->logf = logfac;
+ r->reqtype = url;
+ r->reason = NULL;
+ r->reply = NULL;
+ r->sname = NULL;
+ r->cname = NULL;
+ r->addr = NULL;
+ r->kv = heim_dict_create(10);
+ r->attributes = heim_dict_create(1);
+ /* Our fields */
+ r->connection = connection;
+ r->kadm_handle = NULL;
+ r->hcontext = r->context->hcontext;
+ r->service_names = heim_array_create();
+ r->hostnames = heim_array_create();
+ r->spns = heim_array_create();
+ r->keytab_name = NULL;
+ r->enctypes = NULL;
+ r->freeme1 = NULL;
+ r->method = method;
+ r->cprinc = NULL;
+ r->req = NULL;
+ r->sp = NULL;
+ ci = MHD_get_connection_info(connection,
+ MHD_CONNECTION_INFO_CLIENT_ADDRESS);
+ if (ci) {
+ r->addr = ci->client_addr;
+ addr_to_string(r->context, r->addr, r->frombuf, sizeof(r->frombuf));
+ }
+
+ if (r->kv) {
+ heim_audit_addkv((heim_svc_req_desc)r, 0, "method", "GET");
+ heim_audit_addkv((heim_svc_req_desc)r, 0, "endpoint", "%s", r->reqtype);
+ }
+ token = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
+ MHD_HTTP_HEADER_AUTHORIZATION);
+ if (token && r->kv) {
+ const char *token_end;
+
+ if ((token_end = strchr(token, ' ')) == NULL ||
+ (token_end - token) > INT_MAX || (token_end - token) < 2)
+ heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "<unknown>");
+ else
+ heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "%.*s",
+ (int)(token_end - token), token);
+
+ }
+
+ if (ret == 0 && r->kv == NULL) {
+ krb5_log_msg(r->context, logfac, 1, NULL, "Out of memory");
+ ret = r->error_code = ENOMEM;
+ }
+ return ret;
+}
+
+static void
+clean_req_desc(kadmin_request_desc r)
+{
+ if (!r)
+ return;
+
+ if (r->keytab)
+ krb5_kt_destroy(r->context, r->keytab);
+ else if (r->keytab_name && strchr(r->keytab_name, ':'))
+ (void) unlink(strchr(r->keytab_name, ':') + 1);
+ if (r->kadm_handle)
+ kadm5_destroy(r->kadm_handle);
+ hx509_request_free(&r->req);
+ heim_release(r->service_names);
+ heim_release(r->hostnames);
+ heim_release(r->reason);
+ heim_release(r->spns);
+ heim_release(r->kv);
+ krb5_free_principal(r->context, r->cprinc);
+ free(r->keytab_name);
+ free(r->enctypes);
+ free(r->freeme1);
+ free(r->cname);
+ free(r->sname);
+}
+
+/* Implements GETs of /get-keys */
+static krb5_error_code
+get_keys(kadmin_request_desc r, const char *method)
+{
+ krb5_error_code ret;
+
+ if ((ret = validate_token(r)))
+ return ret; /* validate_token() calls bad_req() */
+ if (r->cname == NULL || r->cprinc == NULL)
+ return bad_403(r, EINVAL,
+ "Could not extract principal name from token");
+ return get_keysN(r, method); /* Sets an HTTP response */
+}
+
+/* Implements GETs of /get-config */
+static krb5_error_code
+get_config(kadmin_request_desc r)
+{
+
+ kadm5_principal_ent_rec princ;
+ krb5_error_code ret;
+ krb5_principal p = NULL;
+ uint32_t mask = KADM5_PRINCIPAL | KADM5_TL_DATA;
+ krb5_tl_data *tl_next;
+ const char *pname;
+ /* Default configuration for principals that have none set: */
+ size_t bodylen = sizeof("include /etc/krb5.conf\n") - 1;
+ void *body = "include /etc/krb5.conf\n";
+ int freeit = 0;
+
+ if ((ret = validate_token(r)))
+ return ret; /* validate_token() calls bad_req() */
+ if (r->cname == NULL || r->cprinc == NULL)
+ return bad_403(r, EINVAL,
+ "Could not extract principal name from token");
+ /*
+ * No authorization needed -- configs are public. Though we do require
+ * authentication (above).
+ */
+
+ ret = get_kadm_handle(r->context, r->realm ? r->realm : realm,
+ 0 /* want_write */, &r->kadm_handle);
+ if (ret)
+ return bad_503(r, ret, "Could not access KDC database");
+
+ memset(&princ, 0, sizeof(princ));
+ princ.key_data = NULL;
+ princ.tl_data = NULL;
+
+ pname = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
+ "princ");
+ if (pname == NULL)
+ pname = r->cname;
+ ret = krb5_parse_name(r->context, pname, &p);
+ if (ret == 0) {
+ ret = kadm5_get_principal(r->kadm_handle, p, &princ, mask);
+ if (ret == 0) {
+ freeit = 1;
+ for (tl_next = princ.tl_data; tl_next; tl_next = tl_next->tl_data_next) {
+ if (tl_next->tl_data_type != KRB5_TL_KRB5_CONFIG)
+ continue;
+ bodylen = tl_next->tl_data_length;
+ body = tl_next->tl_data_contents;
+ break;
+ }
+ } else {
+ r->error_code = ret;
+ return bad_404(r, "/get-config");
+ }
+ }
+
+ if (ret == 0) {
+ krb5_log_msg(r->context, logfac, 1, NULL,
+ "Returned krb5.conf contents to %s", r->cname);
+ ret = resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_MUST_COPY,
+ "application/text", body, bodylen, NULL, NULL);
+ } else {
+ ret = bad_503(r, ret, "Could not retrieve principal configuration");
+ }
+ if (freeit)
+ kadm5_free_principal_ent(r->kadm_handle, &princ);
+ krb5_free_principal(r->context, p);
+ return ret;
+}
+
+static krb5_error_code
+mac_csrf_token(kadmin_request_desc r, krb5_storage *sp)
+{
+ kadm5_principal_ent_rec princ;
+ krb5_error_code ret;
+ krb5_principal p = NULL;
+ krb5_data data;
+ char mac[EVP_MAX_MD_SIZE];
+ unsigned int maclen = sizeof(mac);
+ HMAC_CTX *ctx = NULL;
+ size_t i = 0;
+ int freeit = 0;
+
+ memset(&princ, 0, sizeof(princ));
+ ret = krb5_storage_to_data(sp, &data);
+ if (r->kadm_handle == NULL)
+ ret = get_kadm_handle(r->context, r->realm, 0 /* want_write */,
+ &r->kadm_handle);
+ if (ret == 0)
+ ret = krb5_make_principal(r->context, &p,
+ r->realm ? r->realm : realm,
+ "WELLKNOWN", "CSRFTOKEN", NULL);
+ if (ret == 0)
+ ret = kadm5_get_principal(r->kadm_handle, p, &princ,
+ KADM5_PRINCIPAL | KADM5_KVNO |
+ KADM5_KEY_DATA);
+ if (ret == 0)
+ freeit = 1;
+ if (ret == 0 && princ.n_key_data < 1)
+ ret = KADM5_UNK_PRINC;
+ if (ret == 0)
+ for (i = 0; i < princ.n_key_data; i++)
+ if (princ.key_data[i].key_data_kvno == princ.kvno)
+ break;
+ if (ret == 0 && i == princ.n_key_data)
+ i = 0; /* Weird, but can't happen */
+
+ if (ret == 0 && (ctx = HMAC_CTX_new()) == NULL)
+ ret = krb5_enomem(r->context);
+ /* HMAC the token body and the client principal name */
+ if (ret == 0) {
+ if (HMAC_Init_ex(ctx, princ.key_data[i].key_data_contents[0],
+ princ.key_data[i].key_data_length[0], EVP_sha256(),
+ NULL) == 0) {
+ HMAC_CTX_cleanup(ctx);
+ ret = krb5_enomem(r->context);
+ } else {
+ HMAC_Update(ctx, data.data, data.length);
+ HMAC_Update(ctx, r->cname, strlen(r->cname));
+ HMAC_Final(ctx, mac, &maclen);
+ HMAC_CTX_cleanup(ctx);
+ krb5_data_free(&data);
+ data.length = maclen;
+ data.data = mac;
+ if (krb5_storage_write(sp, mac, maclen) != maclen)
+ ret = krb5_enomem(r->context);
+ }
+ }
+ krb5_free_principal(r->context, p);
+ if (freeit)
+ kadm5_free_principal_ent(r->kadm_handle, &princ);
+ if (ctx)
+ HMAC_CTX_free(ctx);
+ return ret;
+}
+
+static krb5_error_code
+make_csrf_token(kadmin_request_desc r,
+ const char *given,
+ char **token,
+ int64_t *age)
+{
+ static HEIMDAL_THREAD_LOCAL char tokenbuf[128]; /* See below, be sad */
+ krb5_error_code ret = 0;
+ unsigned char given_decoded[128];
+ krb5_storage *sp = NULL;
+ krb5_data data;
+ ssize_t dlen = -1;
+ uint64_t nonce;
+ int64_t t = 0;
+
+
+ *age = 0;
+ data.data = NULL;
+ data.length = 0;
+ if (given) {
+ size_t len = strlen(given);
+
+ if (len >= sizeof(given_decoded))
+ ret = ERANGE;
+ if (ret == 0 && (dlen = rk_base64_decode(given, &given_decoded)) <= 0)
+ ret = errno;
+ if (ret == 0 &&
+ (sp = krb5_storage_from_mem(given_decoded, dlen)) == NULL)
+ ret = krb5_enomem(r->context);
+ if (ret == 0)
+ ret = krb5_ret_int64(sp, &t);
+ if (ret == 0)
+ ret = krb5_ret_uint64(sp, &nonce);
+ krb5_storage_free(sp);
+ sp = NULL;
+ if (ret == 0)
+ *age = time(NULL) - t;
+ } else {
+ t = time(NULL);
+ krb5_generate_random_block((void *)&nonce, sizeof(nonce));
+ }
+
+ if (ret == 0 && (sp = krb5_storage_emem()) == NULL)
+ ret = krb5_enomem(r->context);
+ if (ret == 0)
+ ret = krb5_store_int64(sp, t);
+ if (ret == 0)
+ ret = krb5_store_uint64(sp, nonce);
+ if (ret == 0)
+ ret = mac_csrf_token(r, sp);
+ if (ret == 0)
+ ret = krb5_storage_to_data(sp, &data);
+ if (ret == 0 && data.length > INT_MAX)
+ ret = ERANGE;
+ if (ret == 0 &&
+ (dlen = rk_base64_encode(data.data, data.length, token)) < 0)
+ ret = errno;
+ if (ret == 0 && dlen >= sizeof(tokenbuf))
+ ret = ERANGE;
+ if (ret == 0) {
+ /*
+ * Work around for older versions of libmicrohttpd do not strdup()ing
+ * response header values.
+ */
+ memcpy(tokenbuf, *token, dlen);
+ free(*token);
+ *token = tokenbuf;
+ }
+ krb5_storage_free(sp);
+ krb5_data_free(&data);
+ return ret;
+}
+
+/*
+ * Returns system or krb5_error_code on error, but also calls resp() or bad_*()
+ * on error.
+ */
+static krb5_error_code
+check_csrf(kadmin_request_desc r)
+{
+ krb5_error_code ret;
+ const char *given;
+ int64_t age;
+ size_t givenlen, expectedlen;
+ char *expected = NULL;
+
+ given = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
+ "X-CSRF-Token");
+ ret = make_csrf_token(r, given, &expected, &age);
+ if (ret)
+ return bad_503(r, ret, "Could not create a CSRF token");
+ /*
+ * If CSRF token needed but missing, call resp() directly, bypassing
+ * bad_403(), to return a 403 with an expected CSRF token in the response.
+ */
+ if (given == NULL) {
+ (void) resp(r, MHD_HTTP_FORBIDDEN, ENOSYS, MHD_RESPMEM_PERSISTENT,
+ NULL, "CSRF token needed; copy the X-CSRF-Token: response "
+ "header to your next POST", BODYLEN_IS_STRLEN, NULL,
+ expected);
+ return ENOSYS;
+ }
+
+ /* Validate the CSRF token for this request */
+ givenlen = strlen(given);
+ expectedlen = strlen(expected);
+ if (givenlen != expectedlen || ct_memcmp(given, expected, givenlen)) {
+ (void) bad_403(r, EACCES, "Invalid CSRF token");
+ return EACCES;
+ }
+ if (age > 300) { /* XXX */
+ (void) bad_403(r, EACCES, "CSRF token too old");
+ return EACCES;
+ }
+ return 0;
+}
+
+static krb5_error_code
+health(const char *method, kadmin_request_desc r)
+{
+ if (strcmp(method, "HEAD") == 0) {
+ return resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_PERSISTENT, NULL, "", 0,
+ NULL, NULL);
+ }
+ return resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_PERSISTENT, NULL,
+ "To determine the health of the service, use the /get-config "
+ "end-point.\n", BODYLEN_IS_STRLEN, NULL, NULL);
+
+}
+
+/* Implements the entirety of this REST service */
+static heim_mhd_result
+route(void *cls,
+ struct MHD_Connection *connection,
+ const char *url,
+ const char *method,
+ const char *version,
+ const char *upload_data,
+ size_t *upload_data_size,
+ void **ctx)
+{
+ static int aptr = 0;
+ struct kadmin_request_desc r;
+ int ret;
+
+ if (*ctx == NULL) {
+ /*
+ * This is the first call, right after headers were read.
+ *
+ * We must return quickly so that any 100-Continue might be sent with
+ * celerity.
+ *
+ * We'll get called again to really do the processing. If we handled
+ * POSTs then we'd also get called with upload_data != NULL between the
+ * first and last calls. We need to keep no state between the first
+ * and last calls, but we do need to distinguish first and last call,
+ * so we use the ctx argument for this.
+ */
+ *ctx = &aptr;
+ return MHD_YES;
+ }
+
+ /*
+ * Note that because we attempt to connect to the HDB in set_req_desc(),
+ * this early 503 if we fail to serves to do all of what /health should do.
+ */
+ if ((ret = set_req_desc(connection, method, url, &r)))
+ return bad_503(&r, ret, "Could not initialize request state");
+ if ((strcmp(method, "HEAD") == 0 || strcmp(method, "GET") == 0) &&
+ (strcmp(url, "/health") == 0 || strcmp(url, "/") == 0)) {
+ ret = health(method, &r);
+ } else if (strcmp(method, "GET") != 0 && strcmp(method, "POST") != 0) {
+ ret = bad_405(&r, method);
+ } else if (strcmp(url, "/get-keys") == 0) {
+ ret = get_keys(&r, method);
+ } else if (strcmp(url, "/get-config") == 0) {
+ if (strcmp(method, "GET") != 0)
+ ret = bad_405(&r, method);
+ else
+ ret = get_config(&r);
+ } else {
+ ret = bad_404(&r, url);
+ }
+
+ clean_req_desc(&r);
+ return ret == -1 ? MHD_NO : MHD_YES;
+}
+
+static struct getargs args[] = {
+ { "help", 'h', arg_flag, &help_flag, "Print usage message", NULL },
+ { "version", '\0', arg_flag, &version_flag, "Print version", NULL },
+ { NULL, 'H', arg_strings, &audiences,
+ "expected token audience(s) of the service", "HOSTNAME" },
+ { "daemon", 'd', arg_flag, &daemonize, "daemonize", "daemonize" },
+ { "daemon-child", 0, arg_flag, &daemon_child_fd, NULL, NULL }, /* priv */
+ { "reverse-proxied", 0, arg_flag, &reverse_proxied_flag,
+ "reverse proxied", "listen on 127.0.0.1 and do not use TLS" },
+ { NULL, 'p', arg_integer, &port, "PORT", "port number (default: 443)" },
+ { "temp-dir", 0, arg_string, &cache_dir,
+ "cache directory", "DIRECTORY" },
+ { "cert", 0, arg_string, &cert_file,
+ "certificate file path (PEM)", "HX509-STORE" },
+ { "private-key", 0, arg_string, &priv_key_file,
+ "private key file path (PEM)", "HX509-STORE" },
+ { "thread-per-client", 't', arg_flag, &thread_per_client_flag, "thread per-client", NULL },
+ { "realm", 0, arg_string, &realm, "realm", "REALM" },
+ { "hdb", 0, arg_string, &hdb, "HDB filename", "PATH" },
+ { "read-only-admin-server", 0, arg_string, &kadmin_server,
+ "Name of read-only kadmin server", "HOST[:PORT]" },
+ { "writable-admin-server", 0, arg_string, &writable_kadmin_server,
+ "Name of writable kadmin server", "HOST[:PORT]" },
+ { "primary-server-uri", 0, arg_string, &primary_server_URI,
+ "Name of primary httpkadmind server for HTTP redirects", "URL" },
+ { "local", 'l', arg_flag, &local_hdb,
+ "Use a local HDB as read-only", NULL },
+ { "local-read-only", 0, arg_flag, &local_hdb_read_only,
+ "Use a local HDB as read-only", NULL },
+ { "read-only", 0, arg_flag, &read_only, "Allow no writes", NULL },
+ { "stash-file", 0, arg_string, &stash_file,
+ "Stash file for HDB", "PATH" },
+ { "kadmin-client-name", 0, arg_string, &kadmin_client_name,
+ "Client name for remote kadmind", "PRINCIPAL" },
+ { "kadmin-client-keytab", 0, arg_string, &kadmin_client_keytab,
+ "Keytab with client credentials for remote kadmind", "KEYTAB" },
+ { "token-authentication-type", 'T', arg_strings, &auth_types,
+ "Token authentication type(s) supported", "HTTP-AUTH-TYPE" },
+ { "verbose", 'v', arg_counter, &verbose_counter, "verbose", "run verbosely" }
+};
+
+static int
+usage(int e)
+{
+ arg_printusage(args, sizeof(args) / sizeof(args[0]), "httpkadmind",
+ "\nServes an HTTP API for getting (and rotating) service "
+ "principal keys, and other kadmin-like operations\n");
+ exit(e);
+}
+
+static int sigpipe[2] = { -1, -1 };
+
+static void
+sighandler(int sig)
+{
+ char c = sig;
+ while (write(sigpipe[1], &c, sizeof(c)) == -1 && errno == EINTR)
+ ;
+}
+
+static void
+my_openlog(krb5_context context,
+ const char *svc,
+ krb5_log_facility **fac)
+{
+ char **s = NULL, **p;
+
+ krb5_initlog(context, "httpkadmind", fac);
+ s = krb5_config_get_strings(context, NULL, svc, "logging", NULL);
+ if (s == NULL)
+ s = krb5_config_get_strings(context, NULL, "logging", svc, NULL);
+ if (s) {
+ for(p = s; *p; p++)
+ krb5_addlog_dest(context, *fac, *p);
+ krb5_config_free_strings(s);
+ } else {
+ char *ss;
+ if (asprintf(&ss, "0-1/FILE:%s/%s", hdb_db_dir(context),
+ KDC_LOG_FILE) < 0)
+ err(1, "out of memory");
+ krb5_addlog_dest(context, *fac, ss);
+ free(ss);
+ }
+ krb5_set_warn_dest(context, *fac);
+}
+
+static const char *sysplugin_dirs[] = {
+#ifdef _WIN32
+ "$ORIGIN",
+#else
+ "$ORIGIN/../lib/plugin/kdc",
+#endif
+#ifdef __APPLE__
+ LIBDIR "/plugin/kdc",
+#endif
+ NULL
+};
+
+static void
+load_plugins(krb5_context context)
+{
+ const char * const *dirs = sysplugin_dirs;
+#ifndef _WIN32
+ char **cfdirs;
+
+ cfdirs = krb5_config_get_strings(context, NULL, "kdc", "plugin_dir", NULL);
+ if (cfdirs)
+ dirs = (const char * const *)cfdirs;
+#endif
+
+ /* XXX kdc? */
+ _krb5_load_plugins(context, "kdc", (const char **)dirs);
+
+#ifndef _WIN32
+ krb5_config_free_strings(cfdirs);
+#endif
+}
+
+int
+main(int argc, char **argv)
+{
+ unsigned int flags = MHD_USE_THREAD_PER_CONNECTION; /* XXX */
+ struct sockaddr_in sin;
+ struct MHD_Daemon *previous = NULL;
+ struct MHD_Daemon *current = NULL;
+ struct sigaction sa;
+ krb5_context context = NULL;
+ MHD_socket sock = MHD_INVALID_SOCKET;
+ void *kadm_handle;
+ char *priv_key_pem = NULL;
+ char *cert_pem = NULL;
+ char sig;
+ int optidx = 0;
+ int ret;
+
+ setprogname("httpkadmind");
+ if (getarg(args, sizeof(args) / sizeof(args[0]), argc, argv, &optidx))
+ usage(1);
+ if (help_flag)
+ usage(0);
+ if (version_flag) {
+ print_version(NULL);
+ exit(0);
+ }
+ if (argc > optidx) /* Add option to set a URI local part prefix? */
+ usage(1);
+ if (port < 0)
+ errx(1, "Port number must be given");
+
+ if (audiences.num_strings == 0) {
+ char localhost[MAXHOSTNAMELEN];
+
+ ret = gethostname(localhost, sizeof(localhost));
+ if (ret == -1)
+ errx(1, "Could not determine local hostname; use --audience");
+
+ if ((audiences.strings =
+ calloc(1, sizeof(audiences.strings[0]))) == NULL ||
+ (audiences.strings[0] = strdup(localhost)) == NULL)
+ err(1, "Out of memory");
+ audiences.num_strings = 1;
+ }
+
+ if (daemonize && daemon_child_fd == -1)
+ daemon_child_fd = roken_detach_prep(argc, argv, "--daemon-child");
+ daemonize = 0;
+
+ argc -= optidx;
+ argv += optidx;
+ if (argc != 0)
+ usage(1);
+
+ if ((errno = pthread_key_create(&k5ctx, k5_free_context)))
+ err(1, "Could not create thread-specific storage");
+
+ if ((errno = get_krb5_context(&context)))
+ err(1, "Could not init krb5 context (config file issue?)");
+
+ if (!realm) {
+ char *s;
+
+ ret = krb5_get_default_realm(context, &s);
+ if (ret)
+ krb5_err(context, 1, ret, "Could not determine default realm");
+ realm = s;
+ }
+
+ if ((errno = get_kadm_handle(context, realm, 0 /* want_write */,
+ &kadm_handle)))
+ err(1, "Could not connect to HDB");
+ kadm5_destroy(kadm_handle);
+
+ my_openlog(context, "httpkadmind", &logfac);
+ load_plugins(context);
+
+ if (cache_dir == NULL) {
+ char *s = NULL;
+
+ if (asprintf(&s, "%s/httpkadmind-XXXXXX",
+ getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp") == -1 ||
+ s == NULL ||
+ (cache_dir = mkdtemp(s)) == NULL)
+ err(1, "could not create temporary cache directory");
+ if (verbose_counter)
+ fprintf(stderr, "Note: using %s as cache directory\n", cache_dir);
+ atexit(rm_cache_dir);
+ setenv("TMPDIR", cache_dir, 1);
+ }
+
+again:
+ if (cert_file && !priv_key_file)
+ priv_key_file = cert_file;
+
+ if (cert_file) {
+ hx509_cursor cursor = NULL;
+ hx509_certs certs = NULL;
+ hx509_cert cert = NULL;
+ time_t min_cert_life = 0;
+ size_t len;
+ void *s;
+
+ ret = hx509_certs_init(context->hx509ctx, cert_file, 0, NULL, &certs);
+ if (ret == 0)
+ ret = hx509_certs_start_seq(context->hx509ctx, certs, &cursor);
+ while (ret == 0 &&
+ (ret = hx509_certs_next_cert(context->hx509ctx, certs,
+ cursor, &cert)) == 0 && cert) {
+ time_t notAfter = 0;
+
+ if (!hx509_cert_have_private_key_only(cert) &&
+ (notAfter = hx509_cert_get_notAfter(cert)) <= time(NULL) + 30)
+ errx(1, "One or more certificates in %s are expired",
+ cert_file);
+ if (notAfter) {
+ notAfter -= time(NULL);
+ if (notAfter < 600)
+ warnx("One or more certificates in %s expire soon",
+ cert_file);
+ /* Reload 5 minutes prior to expiration */
+ if (notAfter < min_cert_life || min_cert_life < 1)
+ min_cert_life = notAfter;
+ }
+ hx509_cert_free(cert);
+ }
+ if (certs)
+ (void) hx509_certs_end_seq(context->hx509ctx, certs, cursor);
+ if (min_cert_life > 4)
+ alarm(min_cert_life >> 1);
+ hx509_certs_free(&certs);
+ if (ret)
+ hx509_err(context->hx509ctx, 1, ret,
+ "could not read certificate from %s", cert_file);
+
+ if ((errno = rk_undumpdata(cert_file, &s, &len)) ||
+ (cert_pem = strndup(s, len)) == NULL)
+ err(1, "could not read certificate from %s", cert_file);
+ if (strlen(cert_pem) != len)
+ err(1, "NULs in certificate file contents: %s", cert_file);
+ free(s);
+ }
+
+ if (priv_key_file) {
+ size_t len;
+ void *s;
+
+ if ((errno = rk_undumpdata(priv_key_file, &s, &len)) ||
+ (priv_key_pem = strndup(s, len)) == NULL)
+ err(1, "could not read private key from %s", priv_key_file);
+ if (strlen(priv_key_pem) != len)
+ err(1, "NULs in private key file contents: %s", priv_key_file);
+ free(s);
+ }
+
+ if (verbose_counter > 1)
+ flags |= MHD_USE_DEBUG;
+ if (thread_per_client_flag)
+ flags |= MHD_USE_THREAD_PER_CONNECTION;
+
+
+ if (pipe(sigpipe) == -1)
+ err(1, "Could not set up key/cert reloading");
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = sighandler;
+ if (reverse_proxied_flag) {
+ /*
+ * We won't use TLS in the reverse proxy case, so no need to reload
+ * certs. But we'll still read them if given, and alarm() will get
+ * called.
+ *
+ * XXX We should be able to re-read krb5.conf and such on SIGHUP.
+ */
+ (void) signal(SIGHUP, SIG_IGN);
+ (void) signal(SIGUSR1, SIG_IGN);
+ (void) signal(SIGALRM, SIG_IGN);
+ } else {
+ (void) sigaction(SIGHUP, &sa, NULL); /* Reload key & cert */
+ (void) sigaction(SIGUSR1, &sa, NULL); /* Reload key & cert */
+ (void) sigaction(SIGALRM, &sa, NULL); /* Reload key & cert */
+ }
+ (void) sigaction(SIGINT, &sa, NULL); /* Graceful shutdown */
+ (void) sigaction(SIGTERM, &sa, NULL); /* Graceful shutdown */
+ (void) signal(SIGPIPE, SIG_IGN);
+
+ if (previous)
+ sock = MHD_quiesce_daemon(previous);
+
+ if (reverse_proxied_flag) {
+ /*
+ * XXX IPv6 too. Create the sockets and tell MHD_start_daemon() about
+ * them.
+ */
+ sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ sin.sin_family = AF_INET;
+ sin.sin_port = htons(port);
+ current = MHD_start_daemon(flags, port,
+ NULL, NULL,
+ route, (char *)NULL,
+ MHD_OPTION_SOCK_ADDR, &sin,
+ MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
+ MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
+ MHD_OPTION_END);
+ } else if (sock != MHD_INVALID_SOCKET) {
+ /*
+ * Certificate/key rollover: reuse the listen socket returned by
+ * MHD_quiesce_daemon().
+ */
+ current = MHD_start_daemon(flags | MHD_USE_SSL, port,
+ NULL, NULL,
+ route, (char *)NULL,
+ MHD_OPTION_HTTPS_MEM_KEY, priv_key_pem,
+ MHD_OPTION_HTTPS_MEM_CERT, cert_pem,
+ MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
+ MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
+ MHD_OPTION_LISTEN_SOCKET, sock,
+ MHD_OPTION_END);
+ sock = MHD_INVALID_SOCKET;
+ } else {
+ current = MHD_start_daemon(flags | MHD_USE_SSL, port,
+ NULL, NULL,
+ route, (char *)NULL,
+ MHD_OPTION_HTTPS_MEM_KEY, priv_key_pem,
+ MHD_OPTION_HTTPS_MEM_CERT, cert_pem,
+ MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
+ MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
+ MHD_OPTION_END);
+ }
+ if (current == NULL)
+ err(1, "Could not start kadmin REST service");
+
+ if (previous) {
+ MHD_stop_daemon(previous);
+ previous = NULL;
+ }
+
+ if (verbose_counter)
+ fprintf(stderr, "Ready!\n");
+ if (daemon_child_fd != -1)
+ roken_detach_finish(NULL, daemon_child_fd);
+
+ /* Wait for signal, possibly SIGALRM, to reload certs and/or exit */
+ while ((ret = read(sigpipe[0], &sig, sizeof(sig))) == -1 &&
+ errno == EINTR)
+ ;
+
+ free(priv_key_pem);
+ free(cert_pem);
+ priv_key_pem = NULL;
+ cert_pem = NULL;
+
+ if (ret == 1 && (sig == SIGHUP || sig == SIGUSR1 || sig == SIGALRM)) {
+ /* Reload certs and restart service gracefully */
+ previous = current;
+ current = NULL;
+ goto again;
+ }
+
+ MHD_stop_daemon(current);
+ _krb5_unload_plugins(context, "kdc");
+ pthread_key_delete(k5ctx);
+ return 0;
+}