/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kdc_locl.h" #include "token_validator_plugin.h" #include #include #include #include #include #include #include "../lib/hx509/hx_locl.h" #include #include #include #include #define heim_pcontext krb5_context #define heim_pconfig krb5_context #include #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=/ * - dNSName= * - service= * * Single-valued params: * * - 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, "", 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 = ""; r->request.length = sizeof(""); 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", ""); 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; }