diff --git a/kdc/Makefile.am b/kdc/Makefile.am index bbd1d2ce8..6951969b6 100644 --- a/kdc/Makefile.am +++ b/kdc/Makefile.am @@ -44,6 +44,24 @@ bx509d_LDADD = -ldl \ $(top_builddir)/lib/hx509/libhx509.la \ $(top_builddir)/lib/gssapi/libgssapi.la libexec_PROGRAMS += bx509d + +httpkadmind_SOURCES = httpkadmind.c +httpkadmind_AM_CPPFLAGS = $(AM_CPPFLAGS) $(MICROHTTPD_CFLAGS) +httpkadmind_LDADD = -ldl \ + $(top_builddir)/lib/hdb/libhdb.la \ + $(top_builddir)/lib/kadm5/libkadm5clnt.la \ + $(top_builddir)/lib/kadm5/libkadm5srv.la \ + libkdc.la \ + $(MICROHTTPD_LIBS) \ + $(LIB_roken) \ + $(LIB_heimbase) \ + $(LIB_hcrypto) \ + $(top_builddir)/lib/sl/libsl.la \ + $(top_builddir)/lib/asn1/libasn1.la \ + $(top_builddir)/lib/krb5/libkrb5.la \ + $(top_builddir)/lib/hx509/libhx509.la \ + $(top_builddir)/lib/gssapi/libgssapi.la +libexec_PROGRAMS += httpkadmind endif digest_service_SOURCES = \ @@ -116,6 +134,7 @@ ALL_OBJECTS += $(hprop_OBJECTS) ALL_OBJECTS += $(hpropd_OBJECTS) ALL_OBJECTS += $(digest_service_OBJECTS) ALL_OBJECTS += $(bx509d_OBJECTS) +ALL_OBJECTS += $(httpkadmind_OBJECTS) ALL_OBJECTS += $(cjwt_token_validator_la_OBJECTS) ALL_OBJECTS += $(simple_csr_authorizer_la_OBJECTS) ALL_OBJECTS += $(test_token_validator_OBJECTS) diff --git a/kdc/httpkadmind.8 b/kdc/httpkadmind.8 new file mode 100644 index 000000000..08edce41f --- /dev/null +++ b/kdc/httpkadmind.8 @@ -0,0 +1,407 @@ +.\" 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. +.Dd January 2, 2020 +.Dt HTTPKADMIND 8 +.Os HEIMDAL +.Sh NAME +.Nm httpkadmind +.Nd HTTP HDB Administration Interface +.Sh SYNOPSIS +.Nm +.Op Fl h | Fl Fl help +.Op Fl Fl version +.Op Fl H Ar HOSTNAME +.Op Fl d | Fl Fl daemon +.Op Fl Fl daemon-child +.Op Fl Fl reverse-proxied +.Op Fl p Ar port number (default: 443) +.Op Fl Fl temp-dir= Ns Ar DIRECTORY +.Op Fl Fl cert=HX509-STORE +.Op Fl Fl private-key=HX509-STORE +.Op Fl T | Fl Fl token-authentication-type=Negotiate|Bearer +.Op Fl Fl realm=REALM +.Op Fl Fl read-only +.Op Fl l | Fl Fl local +.Op Fl Fl local-read-only +.Op Fl Fl hdb=HDB +.Op Fl Fl stash-file=FILENAME +.Op Fl Fl primary-server-uri=URI +.Op Fl Fl read-only-admin-server=HOSTNAME[:PORT] +.Op Fl Fl writable-admin-server=HOSTNAME[:PORT] +.Op Fl Fl kadmin-client-name=PRINCIPAL +.Op Fl Fl kadmin-client-keytab=KEYTAB +.Op Fl t | Fl Fl thread-per-client +.Oo Fl v \*(Ba Xo +.Fl Fl verbose= Ns Ar run verbosely +.Xc +.Oc +.Sh DESCRIPTION +Serves the following resources: +.Ar /get-keys and +.Ar /get-config . +.Pp +The +.Ar /get-keys +end-point allows callers to get keytab content for named +principals, possibly performing write operations such as creating +a non-existent principal, or rotating its keys, if requested. +.Pp +The +.Ar /get-config +end-point allows callers to get +.Nm krb5.conf +contents for a given principal. +.Pp +This service can run against a local HDB, or against a remote HDB +via the +.Nm kadmind(8) +protocol. +Read operations are always allowed, but write operations can be +preformed either against writable +.Nm kadmind(8) +server(s) or redirected to another +.Nm httpkadmind(8). +.Pp +The +.Ar /get-config +end-point accepts a single query parameter: +.Bl -tag -width Ds -offset indent +.It Ar princ=PRINCIPAL . +.El +.Pp +The +.Ar /get-keys +end-point accepts various parameters: +.Bl -tag -width Ds -offset indent +.It Ar spn=PRINCIPAL +Names the host-based service principal whose keys to get. +May be given multiple times, and all named principal's keys will +be fetched. +.It Ar dNSName=HOSTNAME +Names the host-based service principal's hostname whose keys to get. +May be given multiple times, and all named principal's keys will +be fetched. +.It Ar service=SERVICE +Hostnames given with +.Ar dNSName=HOSTNAME +will be qualified with this service name to form a host-based +service principal. +May be given multiple times, in which case the cartesian product +of +.Ar dNSName=HOSTNAME +ad +.Ar service=SERVICE +will be used. +Defaults to +.Ar HTTP . +.It realm=REALM +Must be present if the +.Nm httpkadmind +daemon's default realm is not desired. +.It Ar enctypes=ENCTYPE,... +A comma-separated list of enctypes that the principal is expected +to support (used for Kerberos Ticket session key negotiation). +Defaults to the +.Ar supported_enctypes +configured in +.Nm krb5.conf(5) . +.It Ar materialize=true +If the named principal(s) is (are) virtual, this will cause it +(them) to be materialized as a concrete principal. +(Currently not supported.) +.It Ar create=true +If the named principal(s) does not (do not) exist, this will +cause it (them) to be created. +.It Ar rotate=true +This will cause the keys of concrete principals to be rotated. +.It Ar revoke=true +This will cause old keys of concrete principals to be deleted +if their keys are being rotated. +This means that extant service tickets with those principals as +the target will not be able to be decrypted by the caller as it +will not have the necessary keys. +.El +.Pp +Authorization is handled via the same mechanism as in +.Nm bx509d(8) +which was originally intended to authorize certification requests +(CSRs). +Authorization for extracting keys is specified like for +.Nm bx509d(8) , +but using +.Nm [ext_keytab] +as the +.Nm krb5.conf(5) section. +.Pp +Supported options: +.Bl -tag -width Ds -offset indent +.It Xo +.Fl h , +.Fl Fl help +.Xc +Print usage message. +.It Xo +.Fl Fl version +.Xc +Print version. +.It Xo +.Fl H Ar HOSTNAME +.Xc +Expected audience(s) of bearer tokens (i.e., acceptor name). +.It Xo +.Fl d , +.Fl Fl daemon +.Xc +Detach from TTY and run in the background. +.It Xo +.Fl Fl reverse-proxied +.Xc +Serves HTTP instead of HTTPS, accepting only looped-back connections. +.It Xo +.Fl p Ar port number (default: 443) +.Xc +PORT +.It Xo +.Fl Fl temp-dir= Ns Ar DIRECTORY +.Xc +Directory for temp files. +If not specified then a temporary directory will be made. +.It Xo +.Fl Fl cert= Ns Ar HX509-STORE +.Xc +Certificate file path (PEM) for HTTPS service. +May contain private key as well. +.It Xo +.Fl Fl private-key= Ns Ar HX509-STORE +.Xc +Private key file path (PEM), if the private key is not stored along with the +certificiate. +.It Xo +.Fl T Ar HTTP-AUTH-TYPE, +.Fl Fl token-authentication-type= Ns Ar HTTP-AUTH-TYPE +.Xc +HTTP bearer token authentication type(s) supported (may be given more than +once). +For example, +.Ar Negotiate +or +.Ar Bearer +(JWT). +.It Xo +.Fl t , +.Fl Fl thread-per-client +.Xc +Uses a thread per-client instead of as many threads as there are CPUs. +.It Xo +.Fl Fl realm= Ns Ar REALM +.Xc +The realm to serve, if not the default realm. +Note that clients can request keys for principals in other realms, and +.Nm httpkadmind +will attempt to satisfy those requests too. +.It Xo +.Fl Fl read-only +.Xc +Do not perform write operations. +Write operations will either fail or if a primary +.Nm httpkadmind +URI is configured, then they will be redirected there. +.It Xo +.Fl Fl local +.Xc +Use a local HDB, at least for read operations, and, if +.Fl Fl local-read-only +is not given, then also write operations. +.It Xo +.Fl Fl local-read-only +.Xc +Do not perform writes on a local HDB. +Either redirect write operations if a primary +.Nm httpkadmind +URI is configured, or use a writable remote +.Nm kadmind +server. +.It Xo +.Fl Fl hdb=HDB +.Xc +A local HDB to serve. +Note that this can be obtained from the +.Nm krb5.conf . +.It Xo +.Fl Fl stash-file=FILENAME +.Xc +A stash file containing a master key for a local HDB. +Note that this can be obtained from the +.Nm krb5.conf . +.It Xo +.Fl Fl primary-server-uri=URI +.Xc +The URL of an httpkadmind to which to redirect write operations. +.It Xo +.Fl Fl read-only-admin-server=HOSTNAME[:PORT] +.Xc +The hostname (and possibly port number) of a +.Nm kadmind(8) +service to use for read-only operations. +Recall that the +.Nm kadmind(8) +service's principal name is +.Ar kadmin/admin . +The +.Ar HOSTNAME +given here can be a name that resolves to the IP addresses of all +the +.Nm kadmind(8) +services for the +.Ar REALM . +If not specified, but needed, this will be obtained by looking for +.Nm readonly_admin_server +in +.Nm krb5.conf +or, if enabled, performing +DNS lookups for SRV resource records named +.Ar _kerberos-adm-readonly._tcp. . +.It Xo +.Fl Fl writable-admin-server=HOSTNAME[:PORT] +.Xc +The hostname (and possibly port number) of a +.Nm kadmind(8) +service to use for write operations. +If not specified, but needed, this will be obtained by looking for +.Nm admin_server +in +.Nm krb5.conf +or, if enabled, performing DNS lookups for SRV resource records named +.Ar _kerberos-adm._tcp. . +.It Xo +.Fl Fl kadmin-client-name=PRINCIPAL +.Xc +The client principal name to use when connecting to a +.Nm kadmind(8) +server. +Defaults to +.Ar httpkadmind/admin . +.It Xo +.Fl Fl kadmin-client-keytab=KEYTAB +.Xc +The keytab containing keys for the +.Ar kadmin-client-name . +Note that you may use an +.Ar HDB +as a keytab as +.Ar HDBGET:/var/heimdal/heimdal.db +(or whatever the HDB specification is). +.It Xo +.Fl v , +.Fl Fl verbose= Ns Ar run verbosely +.Xc +verbose +.El +.Sh ENVIRONMENT +.Bl -tag -width Ds +.It Ev KRB5_CONFIG +The file name of +.Pa krb5.conf , +the default being +.Pa /etc/krb5.conf . +.El +.Sh FILES +.Bl -tag -width Ds +.It Pa /etc/krb5.conf +.El +.Sh CONFIGURATION +Authorizer configuration goes in +.Br +.Ar [ext_keytab] +in +.Nm krb5.conf(5). For example: +.Pp +[ext_keytab] + simple_csr_authorizer_directory = /etc/krb5/simple_csr_authz + ipc_csr_authorizer = { + service = UNIX:/var/heimdal/csr_authorizer_sock + } +.Sh EXAMPLES +To start +.Nm httpkadmind +on a primary KDC: +.Pp +.Ar $ httpkadmind -d --cert=PEM-FILE:/etc/httpkadmind.pem +\\ +.Br + --local -T Negotiate +.Pp +To start +.Nm httpkadmind +on a secondary KDC, using redirects for write operations: +.Pp +.Ar $ httpkadmind -d --cert=PEM-FILE:/etc/httpkadmind.pem +\\ +.Br + --local-read-only -T Negotiate +\\ +.Br + --primary-server-uri=https://the-primary-server.fqdn/ +.Pp +To start +.Nm httpkadmind +on a secondary KDC, proxying kadmin to perform writes at the primary KDC, using +DNS to discover the kadmin server: +.Pp +.Ar $ httpkadmind -d --cert=PEM-FILE:/etc/httpkadmind.pem +\\ +.Br + --local-read-only -T Negotiate +\\ +.Br + --kadmin-client-keytab=FILE:/etc/krb5.keytab +.Pp +To start +.Nm httpkadmind +on a non-KDC: +.Pp +.Ar $ httpkadmind -d --cert=PEM-FILE:/etc/httpkadmind.pem +\\ +.Br + -T Negotiate --kadmin-client-keytab=FILE:/etc/krb5.keytab +.Pp +.Sh DIAGNOSTICS +See logging section of +.Nm krb5.conf.5 +.Sh SEE ALSO +.Xr bx509d 8 , +.Xr kadmin 1 , +.Xr kadmind 8 , +.Xr krb5.conf 5 . +.\".Sh STANDARDS +.\".Sh HISTORY +.\".Sh AUTHORS +.\".Sh BUGS diff --git a/kdc/httpkadmind.c b/kdc/httpkadmind.c new file mode 100644 index 000000000..d408a2c14 --- /dev/null +++ b/kdc/httpkadmind.c @@ -0,0 +1,2187 @@ +/* + * 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 + +typedef struct kadmin_request_desc { + HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS; + + struct MHD_Connection *connection; + krb5_error_code ret; + 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 materialize:1; + unsigned int rotate_now:1; + unsigned int rotate:1; + unsigned int revoke:1; + unsigned int create:1; + unsigned int ro: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(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_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; \ + } + +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, + 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"); + return krb5_unparse_name(r->context, r->cprinc, &r->cname); +} + +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 int +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. + */ +static krb5_error_code +resp(kadmin_request_desc r, + int http_status_code, + 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; + + (void) gettimeofday(&r->tv_end, NULL); + if (http_status_code == MHD_HTTP_OK) + audit_trail(r, 0); + + 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-cache"); + 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 (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); + 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_addkv((heim_svc_req_desc)r, 0, "http-status-code", "%d", + http_status_code); + (void) gettimeofday(&r->tv_end, NULL); + if (code == ENOMEM) { + if (context) + krb5_log_msg(context, logfac, 1, NULL, "Out of memory"); + audit_trail(r, code); + return resp(r, http_status_code, MHD_RESPMEM_PERSISTENT, + NULL, fmt, strlen(fmt), 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); + audit_trail(r, code); + } + 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, + MHD_RESPMEM_PERSISTENT, NULL, + "Out of memory", sizeof("Out of memory") - 1, NULL, NULL); + } + + ret = resp(r, http_status_code, MHD_RESPMEM_MUST_COPY, + NULL, msg, strlen(msg), 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))) + 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, 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 " + "Heimdal service %s because of authorizer " + "limitations", name); + return EACCES; +} + +static int +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) { + 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; + if (ret == 0) + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_option", "%s", key); + } else if (strcmp(key, "dNSName") == 0 && val) { + s = heim_string_create(val); + if (!s) + ret = krb5_enomem(r->context); + else + ret = heim_array_append_value(r->hostnames, s); + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_dNSName", "%s", val); + ret = hx509_request_add_dns_name(r->context->hx509ctx, r->req, val); + } else if (strcmp(key, "service") == 0 && val) { + 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); + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_service", "%s", val); + } + } 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 (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); + if (ret == 0 && krb5_principal_get_num_comp(r->context, p) != 2) + ret = ENOTSUP; + if (ret == 0) + 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_addkv((heim_svc_req_desc)r, 0, "requested_unknown", "true"); + krb5_set_error_message(r->context, ret = ENOTSUP, + "Query parameter %s not supported", key); + } + if (ret && !r->ret) + r->ret = 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; + + 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->ret; + if (ret) + return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE, + "Could not handle query parameters"); + 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 certificate"); + 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); + 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 */ +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); + +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() */ + + 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 bad_403(r, ret, + "CSRF token needed; copy the X-CSRF-Token: response " + "header to your next POST"); + + 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) { + krb5_set_error_message(r->context, ret = EINVAL, + "No service principals requested"); + return ret; + } + + 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; + } + + /* FIXME: Make this configurable */ + if (nsvcs > 4) { + krb5_set_error_message(r->context, ret = ERANGE, + "Requested too many service names"); + return ret; + } + + 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))); + } + return ret; +} + +/* 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); + + ret = get_krb5_context(&r->context); + /* 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_array_create(); + /* 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->ret = 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"); + switch ((ret = get_keysN(r, method))) { + case -1: /* XXX */ + return MHD_YES; + case ENOSYS: /* XXX */ + return bad_method_want_POST(r); + case KADM5_READ_ONLY: + if (primary_server_URI) { + krb5_log_msg(r->context, logfac, 1, NULL, + "Redirect for %s to primary server to " + "materialize or rotate principal", r->cname); + return resp(r, MHD_HTTP_TEMPORARY_REDIRECT, MHD_RESPMEM_PERSISTENT, + NULL, "", 0, NULL, NULL); + } else { + krb5_log_msg(r->context, logfac, 1, NULL, "HDB is read-only here " + "and no primary URI configured"); + return bad_403(r, ret, "HDB is read-only here " + "and no primary URI configured"); + } + case 0: + krb5_log_msg(r->context, logfac, 1, NULL, + "Issued service principal keys to %s", r->cname); + return good_ext_keytab(r); + default: + return bad_503(r, ret, "Could not get keys"); + } +} + +/* 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); + + 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->ret = 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, 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) { + HMAC_Init_ex(ctx, princ.key_data[i].key_data_contents[0], princ.key_data[i].key_data_length[0], EVP_sha256(), NULL); + HMAC_Update(ctx, data.data, data.length); + HMAC_Update(ctx, r->cname, strlen(r->cname)); + HMAC_Final(ctx, mac, &maclen); + 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; +} + +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) + bad_503(r, ret, "Could not create a CSRF token"); + if (given == NULL) { + (void) resp(r, MHD_HTTP_FORBIDDEN, MHD_RESPMEM_PERSISTENT, NULL, + "Request missing a CSRF token", + sizeof("Request missing a CSRF token"), NULL, + expected); + return -1; + } + + /* 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, MHD_RESPMEM_PERSISTENT, NULL, "", 0, NULL, + NULL); + return resp(r, MHD_HTTP_OK, MHD_RESPMEM_PERSISTENT, NULL, + "To determine the health of the service, use the /get-config " + "end-point.\n", + sizeof("To determine the health of the service, use the " + "/get-config end-point.\n") - 1, NULL, NULL); + +} + +/* Implements the entirety of this REST service */ +static int +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 ((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; +} diff --git a/tests/bin/setup-env.in b/tests/bin/setup-env.in index 80c119cd2..5c4b91852 100644 --- a/tests/bin/setup-env.in +++ b/tests/bin/setup-env.in @@ -19,6 +19,7 @@ NO_AFS="@NO_AFS@" # regular apps bx509d="${TESTS_ENVIRONMENT} ${top_builddir}/kdc/bx509d" +httpkadmind="${TESTS_ENVIRONMENT} ${top_builddir}/kdc/httpkadmind" hxtool="${TESTS_ENVIRONMENT} ${top_builddir}/lib/hx509/hxtool" iprop_log="${TESTS_ENVIRONMENT} ${top_builddir}/lib/kadm5/iprop-log" ipropd_master="${TESTS_ENVIRONMENT} ${top_builddir}/lib/kadm5/ipropd-master" diff --git a/tests/kdc/Makefile.am b/tests/kdc/Makefile.am index 82b75a3f0..6476367af 100644 --- a/tests/kdc/Makefile.am +++ b/tests/kdc/Makefile.am @@ -13,6 +13,7 @@ noinst_DATA = \ krb5-weak.conf \ krb5-pkinit.conf \ krb5-bx509.conf \ + krb5-httpkadmind.conf \ krb5-pkinit-win.conf \ krb5-slave2.conf \ krb5-slave.conf @@ -35,6 +36,7 @@ SCRIPT_TESTS = \ check-kpasswdd \ check-pkinit \ check-bx509 \ + check-httpkadmind \ check-iprop \ check-referral \ check-tester \ @@ -44,9 +46,11 @@ TESTS = $(SCRIPT_TESTS) port = 49188 admport = 49189 -pwport = 49190 -bx509port = 49191 -ipropport = 49192 +admport2 = 49190 +pwport = 49191 +restport = 49192 +restport2 = 49193 +ipropport = 49194 if HAVE_DLOPEN do_dlopen = -e 's,[@]DLOPEN[@],true,g' @@ -60,7 +64,10 @@ do_subst = $(heim_verbose)sed $(do_dlopen) \ -e 's,[@]srcdir[@],$(srcdir),g' \ -e 's,[@]port[@],$(port),g' \ -e 's,[@]admport[@],$(admport),g' \ - -e 's,[@]bx509port[@],$(bx509port),g' \ + -e 's,[@]admport2[@],$(admport2),g' \ + -e 's,[@]bx509port[@],$(restport),g' \ + -e 's,[@]restport[@],$(restport),g' \ + -e 's,[@]restport2[@],$(restport2),g' \ -e 's,[@]pwport[@],$(pwport),g' \ -e 's,[@]ipropport[@],$(ipropport),g' \ -e 's,[@]objdir[@],$(top_builddir)/tests/kdc,g' \ @@ -153,6 +160,11 @@ check-bx509: check-bx509.in Makefile krb5-bx509.conf $(chmod) +x check-bx509.tmp && \ mv check-bx509.tmp check-bx509 +check-httpkadmind: check-httpkadmind.in Makefile krb5-httpkadmind.conf + $(do_subst) < $(srcdir)/check-httpkadmind.in > check-httpkadmind.tmp && \ + $(chmod) +x check-httpkadmind.tmp && \ + mv check-httpkadmind.tmp check-httpkadmind + check-iprop: check-iprop.in Makefile krb5.conf krb5-slave.conf krb5-slave2.conf $(do_subst) < $(srcdir)/check-iprop.in > check-iprop.tmp && \ $(chmod) +x check-iprop.tmp && \ @@ -249,6 +261,10 @@ krb5-bx509.conf: krb5-bx509.conf.in Makefile $(do_subst) -e 's,[@]w2k[@],no,g' < $(srcdir)/krb5-bx509.conf.in > krb5-bx509.conf.tmp && \ mv krb5-bx509.conf.tmp krb5-bx509.conf +krb5-httpkadmind.conf: krb5-httpkadmind.conf.in Makefile + $(do_subst) -e 's,[@]w2k[@],no,g' < $(srcdir)/krb5-httpkadmind.conf.in > krb5-httpkadmind.conf.tmp && \ + mv krb5-httpkadmind.conf.tmp krb5-httpkadmind.conf + krb5-pkinit-win.conf: krb5-pkinit.conf.in Makefile $(do_subst) -e 's,[@]w2k[@],yes,g' < $(srcdir)/krb5-pkinit.conf.in > krb5-pkinit-win.conf.tmp && \ mv krb5-pkinit-win.conf.tmp krb5-pkinit-win.conf @@ -274,6 +290,8 @@ CLEANFILES= \ current-db* \ current.log* \ digest-reply \ + extracted_config \ + extracted_keytab* \ foopassword \ foopassword.rkpty \ iprop-stats \ @@ -290,11 +308,13 @@ CLEANFILES= \ krb5-pkinit-win.conf \ krb5-pkinit.conf \ krb5-bx509.conf \ + krb5-httpkadmind.conf \ krb5-slave2.conf \ krb5-slave.conf \ krb5-weak.conf \ krb5.conf \ krb5.conf.keys \ + kt \ leaks-log \ localname \ malloc-log \ @@ -306,6 +326,7 @@ CLEANFILES= \ o2digest-reply \ ocache.krb5 \ out-log \ + response-headers \ s2digest-reply \ sdb \ sdigest-init \ @@ -337,6 +358,7 @@ EXTRA_DIST = \ check-kpasswdd.in \ check-pkinit.in \ check-bx509.in \ + check-httpkadmind.in \ check-referral.in \ check-tester.in \ check-uu.in \ @@ -352,6 +374,7 @@ EXTRA_DIST = \ kdc-tester4.json.in \ krb5-pkinit.conf.in \ krb5-bx509.conf.in \ + krb5-httpkadmind.conf.in \ krb5.conf.in \ krb5-authz.conf.in \ krb5-authz2.conf.in \ diff --git a/tests/kdc/check-httpkadmind.in b/tests/kdc/check-httpkadmind.in new file mode 100644 index 000000000..6c6056c7a --- /dev/null +++ b/tests/kdc/check-httpkadmind.in @@ -0,0 +1,588 @@ +#!/bin/sh +# +# 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. + +top_builddir="@top_builddir@" +env_setup="@env_setup@" +objdir="@objdir@" + +testfailed="echo test failed; cat messages.log; exit 1" + +. ${env_setup} + +# If there is no useful db support compiled in, disable test +${have_db} || exit 77 + +if ! which curl > /dev/null; then + echo "curl is not available -- not testing httpkadmind" + exit 77 +fi +if ! test -x ${objdir}/../../kdc/httpkadmind; then + echo "Configured w/o libmicrohttpd -- not testing httpkadmind" + exit 77 +fi + +R=TEST.H5L.SE +domain=test.h5l.se + +port=@port@ +admport=@admport@ +admport1=@admport@ +admport2=@admport2@ +restport=@restport@ +restport1=@restport@ +restport2=@restport2@ + +server=datan.test.h5l.se +otherserver=other.test.h5l.se +cache="FILE:${objdir}/cache.krb5" +admincache="FILE:${objdir}/cache2.krb5" +keyfile="${hx509_data}/key.der" +keyfile2="${hx509_data}/key2.der" +kt=${objdir}/kt +keytab=FILE:${kt} +ukt=${objdir}/ukt +ukeytab=FILE:${ukt} + +kdc="${kdc} --addresses=localhost -P $port" +kadminr="${kadmin} -r $R -a $(uname -n)" +kadmin="${kadmin} -l -r $R" +kadmind2="${kadmind} --keytab=${keytab} --detach -p $admport2 --read-only" +kadmind="${kadmind} --keytab=${keytab} --detach -p $admport" +httpkadmind2="${httpkadmind} --reverse-proxied -T Negotiate -p $restport2" +httpkadmind="${httpkadmind} --reverse-proxied -T Negotiate -p $restport1" + +kinit="${kinit} -c $cache ${afs_no_afslog}" +adminklist="${klist} --hidden -v -c $admincache" +klist="${klist} --hidden -v -c $cache" +kgetcred="${kgetcred} -c $cache" +kdestroy="${kdestroy} -c $cache ${afs_no_unlog}" +kx509="${kx509} -c $cache" + +KRB5_CONFIG="${objdir}/krb5-httpkadmind.conf" +export KRB5_CONFIG +KRB5CCNAME=$cache +export KRB5CCNAME + +rm -f current-db* +rm -f out-* +rm -f mkey.file* +rm -f *.pem *.crt *.der +rm -rf simple_csr_authz +rm -f extracted_keytab* + +mkdir -p simple_csr_authz + +> messages.log + +# We'll avoid using a KDC for now. For testing /httpkadmind we only need keys +# for Negotiate tokens, and we'll use ktutil and kimpersonate to make it +# possible to create and accept those without a KDC. + +# grant ext-type value grantee_principal +grant() { + mkdir -p "${objdir}/simple_csr_authz/${3}" + touch "${objdir}/simple_csr_authz/${3}/${1}-${2}" +} + +revoke() { + rm -rf "${objdir}/simple_csr_authz" + mkdir -p "${objdir}/simple_csr_authz" +} + +if set -o|grep 'verbose.*on' > /dev/null || + set -o|grep 'xtrace.*on' > /dev/null; then + verbose=-vvv +else + verbose= +fi + +# HTTP curl-opts +HTTP() { + curl -g --resolve ${server}:${restport2}:127.0.0.1 \ + --resolve ${server}:${restport}:127.0.0.1 \ + -u: --negotiate $verbose "$@" +} + +# get_config QPARAMS curl-opts +get_config() { + url="http://${server}:${restport}/get-config?$1" + shift + HTTP $verbose "$@" "$url" +} + +# get_keytab QPARAMS curl-opts +get_keytab() { + url="http://${server}:${restport}/get-keys?$1" + shift + HTTP $verbose "$@" "$url" +} + +# get_keytab_POST QPARAMS curl-opts +get_keytab_POST() { + # Curl is awful, so if you don't use -f, you don't get non-zero exit codes on + # error responses, but if you do use -f then -D doesn't work. Ugh. + # + # So first we check that POST w/o CSRF token fails: + q=$1 + shift + + get_keytab "$q" -X POST --data-binary @/dev/null -f "$@" && + { echo "POST succeeded w/o CSRF token!"; return 1; } + get_keytab "$q" -X POST --data-binary @/dev/null -D response-headers "$@" + grep ^X-CSRF-Token: response-headers >/dev/null || + { echo "POST w/o CSRF token had response w/o CSRF token!"; return 1; } + get_keytab "$q" -X POST --data-binary @/dev/null -f \ + -H "$(sed -e 's/\r//' response-headers | grep ^X-CSRF-Token:)" "$@" + return $? +} + +get_keytab_POST_redir() { + url="http://${server}:${restport}/get-keys?$1" + shift + HTTP -X POST --data-binary @/dev/null -D response-headers "$@" "$url" + grep ^X-CSRF-Token: response-headers >/dev/null || + { echo "POST w/o CSRF token had response w/o CSRF token!"; return 1; } + HTTP -X POST --data-binary @/dev/null -f \ + -H "$(sed -e 's/\r//' response-headers | grep ^X-CSRF-Token:)" \ + --location --location-trusted "$@" "$url" +} + +kdcpid= +httpkadmindpid= +httpkadmind2pid= +kadmindpid= +kadmind2pid= +cleanup() { + test -n "$kdcpid" && + { echo signal killing kdc; kill -9 "$kdcpid"; } + test -n "$httpkadmindpid" && + { echo signal killing httpkadmind; kill -9 "$httpkadmindpid"; } + test -n "$httpkadmind2pid" && + { echo signal killing httpkadmind; kill -9 "$httpkadmind2pid"; } + test -n "$kadmindpid" && + { echo signal killing kadmind; kill -9 "$kadmindpid"; } + test -n "$kadmind2pid" && + { echo signal killing kadmind; kill -9 "$kadmind2pid"; } +} +trap cleanup EXIT + +rm -f extracted_keytab + +echo "Creating database" +rm -f $kt $ukt +${kadmin} init \ + --realm-max-ticket-life=1day \ + --realm-max-renewable-life=1month \ + ${R} || exit 1 +${kadmin} add -r --use-defaults foo@${R} || exit 1 +${kadmin} add -r --use-defaults httpkadmind/admin@${R} || exit 1 +${kadmin} add -r --use-defaults WELLKNOWN/CSRFTOKEN@${R} || exit 1 +${kadmin} add -r --use-defaults HTTP/localhost@${R} || exit 1 +${kadmin} add -r --use-defaults HTTP/xyz.${domain}@${R} || exit 1 +${kadmin} add_ns --key-rotation-epoch=-1d --key-rotation-period=5m \ + --max-ticket-life=1d --max-renewable-life=5d \ + --attributes= HTTP/ns.${domain}@${R} || exit 1 +${kadmin} add -r --use-defaults HTTP/${server}@${R} || exit 1 +${kadmin} ext_keytab -r -k $keytab kadmin/admin@${R} || exit 1 +${kadmin} ext_keytab -r -k $keytab httpkadmind/admin@${R} || exit 1 +${kadmin} ext_keytab -r -k $keytab HTTP/${server}@${R} || exit 1 +${kadmin} ext_keytab -r -k $keytab HTTP/localhost@${R} || exit 1 +${kadmin} add -r --use-defaults HTTP/${otherserver}@${R} || exit 1 +${kadmin} ext_keytab -r -k $ukeytab foo@${R} || exit 1 +${kdestroy} + +# For a while let's not bother with a KDC +$kimpersonate --ccache=$cache -k $keytab -R -t aes128-cts-hmac-sha1-96 \ + -c foo@${R} -s HTTP/datan.test.h5l.se@${R} || + { echo "failed to setup kimpersonate credentials"; exit 2; } +$kimpersonate -A --ccache=$cache -k $keytab -R -t aes128-cts-hmac-sha1-96 \ + -c foo@${R} -s HTTP/localhost@${R} || + { echo "failed to setup kimpersonate credentials"; exit 2; } +$klist -t >/dev/null || + { echo "failed to setup kimpersonate credentials"; exit 2; } + +echo "Starting httpkadmind" +${httpkadmind} -H $server -H localhost --local -t --daemon || + { echo "httpkadmind failed to start"; exit 2; } +httpkadmindpid=`getpid httpkadmind` +ec=0 + +echo "Checking that concrete principal exists" +${kadmin} get HTTP/xyz.${domain} > /dev/null || + { echo "Failed to create HTTP/xyz.${domain}"; exit 1; } +echo "Checking that virtual principal exists" +${kadmin} get HTTP/foo.ns.${domain} > /dev/null || + { echo "Virtual principals not working"; exit 1; } + +hn=xyz.${domain} +p=HTTP/$hn +echo "Fetching krb5.conf for $p" +get_config "princ=$p" -sf -o "${objdir}/extracted_config" || + { echo "Failed to get config for $p"; exit 1; } +read config < "${objdir}/extracted_config" +test "$config" = "include /etc/krb5.conf" || + { echo "Got unexpected default config for $p"; exit 1; } +${kadmin} mod --krb5-config-file="$KRB5_CONFIG" $p || + { echo "Failed to set config for $p"; exit 1; } +get_config "princ=$p" -sf -o "${objdir}/extracted_config" || + { echo "Failed to get config for $p"; exit 1; } +cmp "${objdir}/extracted_config" "$KRB5_CONFIG" || + { echo "Got unexpected config for $p"; exit 1; } + +hn=xyz.${domain} +p=HTTP/$hn +echo "Fetching keytab for concrete principal $p" +rm -f extracted_keytab* +grant dnsname $hn foo@${R} +${kadmin} ext_keytab -k extracted_keytab $p || + { echo "Failed to get a keytab for $p with kadmin"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.kadmin || + { echo "Failed to list keytab for $p"; exit 1; } +get_keytab "dNSName=${hn}" -sf -o "${objdir}/extracted_keytab" || + { echo "Failed to get a keytab for $p with curl"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest || + { echo "Failed to list keytab for $p"; exit 1; } +cmp extracted_keytab.kadmin extracted_keytab.rest || + { echo "Keytabs for $p don't match!"; exit 1; } + +hn=foo.ns.${domain} +p=HTTP/$hn +echo "Fetching keytab for virtual principal $p" +rm -f extracted_keytab* +grant dnsname $hn foo@${R} +${kadmin} ext_keytab -k extracted_keytab $p || + { echo "Failed to get a keytab for $p with kadmin"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.kadmin || + { echo "Failed to list keytab for $p"; exit 1; } +get_keytab "dNSName=${hn}" -sf -o "${objdir}/extracted_keytab" || + { echo "Failed to get a keytab for $p with curl"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest || + { echo "Failed to list keytab for $p"; exit 1; } +cmp extracted_keytab.kadmin extracted_keytab.rest || + { echo "Keytabs for $p don't match!"; exit 1; } + +hn1=foo.ns.${domain} +hn2=foobar.ns.${domain} +hn3=xyz.${domain} +p1=HTTP/$hn1 +p2=HTTP/$hn2 +p3=HTTP/$hn3 +echo "Fetching keytabs for more than one principal" +rm -f extracted_keytab* +grant dnsname $hn1 foo@${R} +grant dnsname $hn2 foo@${R} +grant dnsname $hn3 foo@${R} +# Note that httpkadmind will first process dNSName q-params, then the spn +# q-params. +${kadmin} ext_keytab -k extracted_keytab $p1 || + { echo "Failed to get a keytab for $p1 with kadmin"; exit 1; } +${kadmin} ext_keytab -k extracted_keytab $p3 || + { echo "Failed to get a keytab for $p3 with kadmin"; exit 1; } +${kadmin} ext_keytab -k extracted_keytab $p2 || + { echo "Failed to get a keytab for $p2 with kadmin"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.kadmin || + { echo "Failed to list keytab for multiple principals"; exit 1; } +get_keytab "dNSName=${hn1}&spn=${p2}&dNSName=${hn3}" -sf -o "${objdir}/extracted_keytab" || + { echo "Failed to get a keytab for multiple principals with curl"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest || + { echo "Failed to list keytab for $p"; exit 1; } +cmp extracted_keytab.kadmin extracted_keytab.rest || + { echo "Keytabs for $p don't match!"; exit 1; } +grep $hn1 extracted_keytab.rest > /dev/null || + { echo "Keytab does not include keys for $p1"; exit 1; } +grep $hn2 extracted_keytab.rest > /dev/null || + { echo "Keytab does not include keys for $p2"; exit 1; } +grep $hn3 extracted_keytab.rest > /dev/null || + { echo "Keytab does not include keys for $p3"; exit 1; } + +p=host/foo.ns.${domain} +echo "Checking that $p doesn't exist (no namespace for host service)" +get_keytab "svc=host&dNSName=foo.ns.${domain}" -sf -o "${objdir}/extracted_keytab.rest" && + { echo "Got a keytab for host/foo.ns.${domain} when not namespaced!"; } + +echo "Checking that authorization is enforced" +revoke +get_keytab "dNSName=xyz.${domain}" -sf -o "${objdir}/extracted_keytab" && + { echo "Got a keytab for HTTP/xyz.${domain} when not authorized!"; exit 1; } +get_keytab "dNSName=foo.ns.${domain}" -sf -o "${objdir}/extracted_keytab" && + { echo "Got a keytab for HTTP/foo.ns.${domain} when not authorized!"; exit 1; } + +hn=xyz.${domain} +p=HTTP/$hn +echo "Checking key rotation for concrete principal $p" +rm -f extracted_keytab* +grant dnsname $hn foo@${R} +get_keytab "dNSName=${hn}" -sf -o "${objdir}/extracted_keytab" || + { echo "Failed to get a keytab for $p with curl"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest1 || + { echo "Failed to list keytab for $p"; exit 1; } +test "$(grep $p extracted_keytab.rest1 | wc -l)" -eq 1 || + { echo "Wrong number of new keys!"; exit 1; } +get_keytab "dNSName=${hn}&rotate=true" -sf -o "${objdir}/extracted_keytab" && + { echo "GET succeeded for write operation!"; exit 1; } +get_keytab_POST "dNSName=${hn}&rotate=true" -s -o "${objdir}/extracted_keytab" || + { echo "Failed to rotate keys for $p"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest2 || + { echo "Failed to list keytab for $p"; exit 1; } +cmp extracted_keytab.rest1 extracted_keytab.rest2 > /dev/null && + { echo "Keys for $p did not change!"; exit 1; } +test "$(grep $p extracted_keytab.rest2 | wc -l)" -eq 2 || + { echo "Wrong number of new keys!"; exit 1; } + +hn=xyz.${domain} +p=HTTP/$hn +echo "Checking key rotation w/ revocation for concrete principal $p" +rm -f extracted_keytab* +grant dnsname $hn foo@${R} +get_keytab "dNSName=${hn}" -sf -o "${objdir}/extracted_keytab" || + { echo "Failed to get a keytab for $p with curl"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest1 || + { echo "Failed to list keytab for $p"; exit 1; } +get_keytab "dNSName=${hn}&revoke=true" -sf -o "${objdir}/extracted_keytab" && + { echo "GET succeeded for write operation!"; exit 1; } +get_keytab_POST "dNSName=${hn}&revoke=true" -s -o "${objdir}/extracted_keytab" || + { echo "Failed to get a keytab for $p with curl"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest2 || + { echo "Failed to list keytab for $p"; exit 1; } +cmp extracted_keytab.rest1 extracted_keytab.rest2 > /dev/null && + { echo "Keys for $p did not change!"; exit 1; } +test "$(grep $p extracted_keytab.rest2 | wc -l)" -eq 1 || + { echo "Wrong number of new keys!"; exit 1; } + +hn=abc.${domain} +p=HTTP/$hn +echo "Checking concrete principal creation ($p)" +rm -f extracted_keytab +grant dnsname $hn foo@${R} +get_keytab "dNSName=${hn}&create=true" -sf -o "${objdir}/extracted_keytab" && + { echo "GET succeeded for write operation!"; exit 1; } +get_keytab_POST "dNSName=${hn}&create=true" -s -o "${objdir}/extracted_keytab" || + { echo "Failed to get a keytab for $p with curl"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest || + { echo "Failed to list keytab for $p"; exit 1; } +rm -f extracted_keytab +${kadmin} ext_keytab -k extracted_keytab $p || + { echo "Failed to get a keytab for $p with kadmin"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.kadmin || + { echo "Failed to list keytab for $p"; exit 1; } +cmp extracted_keytab.kadmin extracted_keytab.rest || + { echo "Keytabs for $p don't match!"; exit 1; } + +if false; then +hn=bar.ns.${domain} +p=HTTP/$hn +echo "Checking materialization of virtual principal ($p)" +rm -f extracted_keytab +grant dnsname $hn foo@${R} +get_keytab "dNSName=${hn}&materialize=true" -sf -o "${objdir}/extracted_keytab" && + { echo "GET succeeded for write operation!"; exit 1; } +get_keytab_POST "dNSName=${hn}&materialize=true" -s -o "${objdir}/extracted_keytab" || + { echo "Failed to materialize and get a keytab for $p with curl"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest || + { echo "Failed to list keytab for $p"; exit 1; } +rm -f extracted_keytab +${kadmin} ext_keytab -k extracted_keytab $p || + { echo "Failed to get a keytab for $p with kadmin"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.kadmin || + { echo "Failed to list keytab for $p"; exit 1; } +cmp extracted_keytab.kadmin extracted_keytab.rest || + { echo "Keytabs for $p don't match!"; exit 1; } +fi + +echo "Starting secondary httpkadmind to test HTTP redirection" +${httpkadmind2} --primary-server-uri=http://localhost:$restport \ + -H $server --local --local-read-only -t --daemon || + { echo "httpkadmind failed to start"; exit 2; } +httpkadmind2pid=`getpid httpkadmind` +ec=0 + +hn=def.${domain} +p=HTTP/$hn +restport=$restport2 +echo "Checking principal creation at secondary yields redirect" +rm -f extracted_keytab +grant dnsname $hn foo@${R} +get_keytab_POST_redir "dNSName=${hn}&create=true" \ + -s -o "${objdir}/extracted_keytab" +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest || + { echo "Failed to list keytab for $p"; exit 1; } +rm -f extracted_keytab +${kadmin} ext_keytab -k extracted_keytab $p || + { echo "Failed to get a keytab for $p with kadmin"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.kadmin || + { echo "Failed to list keytab for $p"; exit 1; } +cmp extracted_keytab.kadmin extracted_keytab.rest || + { echo "Keytabs for $p don't match!"; exit 1; } + +echo "killing httpkadmind (${httpkadmindpid} ${httpkadmind2pid})" +sh ${leaks_kill} httpkadmind $httpkadmindpid || ec=1 +sh ${leaks_kill} httpkadmind $httpkadmind2pid || ec=1 +httpkadmindpid= +httpkadmind2pid= +test $ec = 1 && + { echo "Error killing httpkadmind instances or memory errors found"; exit 1; } + +echo "Starting primary kadmind for testing httpkadmind with remote HDB" +${kadmind} || + { echo "Read-write kadmind failed to start"; exit 2; } +kadmindpid=`getpid kadmind` +echo "Starting secondray (read-only) kadmind for testing httpkadmind with remote HDB" +${kadmind2} || + { echo "Read-only kadmind failed to start"; exit 2; } +kadmind2pid=`getpid kadmind` + +# Make a ccache for use with kadmin(1) +$kimpersonate --ticket-flags=initial --ccache=$admincache -k $keytab -t aes128-cts-hmac-sha1-96 \ + -c httpkadmind/admin@${R} -s kadmin/admin@${R} || + { echo "failed to setup kimpersonate credentials"; exit 2; } +$adminklist -t >/dev/null || + { echo "failed to setup kimpersonate credentials"; exit 2; } + +echo "Starting kdc needed for httpkadmind authentication to kadmind" +${kdc} --detach --testing || { echo "kdc failed to start"; exit 1; } +kdcpid=`getpid kdc` + +echo "Starting httpkadmind with remote HDBs only" +restport=$restport1 +${httpkadmind} -H $server -H localhost -t --daemon \ + --writable-admin-server=$(uname -n):$admport \ + --read-only-admin-server=$(uname -n):$admport2 \ + --kadmin-client-name=httpkadmind/admin@${R} \ + --kadmin-client-keytab=$keytab || + { echo "httpkadmind failed to start"; exit 2; } +httpkadmindpid=`getpid httpkadmind` +ec=0 + +hn=xyz.${domain} +p=HTTP/$hn +echo "Fetching keytab for concrete principal $p using remote HDB" +rm -f extracted_keytab* +grant dnsname $hn httpkadmind/admin@${R} +KRB5CCNAME=$admincache ${kadmin} ext_keytab -k extracted_keytab $p || + { echo "Failed to get a keytab for $p with kadmin"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.kadmin || + { echo "Failed to list keytab for $p"; exit 1; } +get_keytab "spn=${p}" -sf -o "${objdir}/extracted_keytab" || + { echo "Failed to get a keytab for $p with curl"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest || + { echo "Failed to list keytab for $p"; exit 1; } +cmp extracted_keytab.kadmin extracted_keytab.rest || + { echo "Keytabs for $p don't match!"; exit 1; } + +hn=xyz.${domain} +p=HTTP/$hn +echo "Checking key rotation for concrete principal $p using remote HDB" +rm -f extracted_keytab* +grant dnsname $hn foo@${R} +get_keytab "dNSName=${hn}" -sf -o "${objdir}/extracted_keytab" || + { echo "Failed to get a keytab for $p with curl"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest1 || + { echo "Failed to list keytab for $p"; exit 1; } +test "$(grep $p extracted_keytab.rest1 | wc -l)" -eq 1 || + { echo "Wrong number of new keys!"; exit 1; } +get_keytab "dNSName=${hn}&rotate=true" -sf -o "${objdir}/extracted_keytab" && + { echo "GET succeeded for write operation!"; exit 1; } +get_keytab_POST "dNSName=${hn}&rotate=true" -s -o "${objdir}/extracted_keytab" || + { echo "Failed to rotate keys for $p"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest2 || + { echo "Failed to list keytab for $p"; exit 1; } +cmp extracted_keytab.rest1 extracted_keytab.rest2 > /dev/null && + { echo "Keys for $p did not change!"; exit 1; } +test "$(grep $p extracted_keytab.rest2 | wc -l)" -eq 2 || + { echo "Wrong number of new keys!"; exit 1; } + +sh ${leaks_kill} httpkadmind $httpkadmindpid || ec=1 +httpkadmindpid= + +echo "Starting httpkadmind with local read-only HDB and remote read-write HDB" +${httpkadmind} -H $server -H localhost -t --daemon \ + --local-read-only \ + --writable-admin-server=$(uname -n):$admport \ + --kadmin-client-name=httpkadmind/admin@${R} \ + --kadmin-client-keytab=$keytab || + { echo "httpkadmind failed to start"; exit 2; } +httpkadmindpid=`getpid httpkadmind` +ec=0 + +hn=xyz.${domain} +p=HTTP/$hn +echo "Fetching keytab for concrete principal $p using local read-only HDB" +rm -f extracted_keytab* +grant dnsname $hn httpkadmind/admin@${R} +KRB5CCNAME=$admincache ${kadmin} ext_keytab -k extracted_keytab $p || + { echo "Failed to get a keytab for $p with kadmin"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.kadmin || + { echo "Failed to list keytab for $p"; exit 1; } +get_keytab "spn=${p}" -sf -o "${objdir}/extracted_keytab" || + { echo "Failed to get a keytab for $p with curl"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest || + { echo "Failed to list keytab for $p"; exit 1; } +cmp extracted_keytab.kadmin extracted_keytab.rest || + { echo "Keytabs for $p don't match!"; exit 1; } + +hn=xyz.${domain} +p=HTTP/$hn +echo "Checking key rotation for concrete principal $p using local read-only HDB and remote HDB" +rm -f extracted_keytab* +grant dnsname $hn foo@${R} +get_keytab "dNSName=${hn}" -sf -o "${objdir}/extracted_keytab" || + { echo "Failed to get a keytab for $p with curl"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest1 || + { echo "Failed to list keytab for $p"; exit 1; } +test "$(grep $p extracted_keytab.rest1 | wc -l)" -eq 2 || + { echo "Wrong number of new keys!"; exit 1; } +get_keytab "dNSName=${hn}&rotate=true" -sf -o "${objdir}/extracted_keytab" && + { echo "GET succeeded for write operation!"; exit 1; } +get_keytab_POST "dNSName=${hn}&rotate=true" -s -o "${objdir}/extracted_keytab" || + { echo "Failed to rotate keys for $p"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest2 || + { echo "Failed to list keytab for $p"; exit 1; } +cmp extracted_keytab.rest1 extracted_keytab.rest2 > /dev/null && + { echo "Keys for $p did not change!"; exit 1; } +test "$(grep $p extracted_keytab.rest2 | wc -l)" -eq 3 || + { echo "Wrong number of new keys!"; exit 1; } + +sh ${leaks_kill} httpkadmind $httpkadmindpid || ec=1 +sh ${leaks_kill} kadmind $kadmindpid || ec=1 +sh ${leaks_kill} kadmind $kadmind2pid || ec=1 +sh ${leaks_kill} kdc $kdcpid || ec=1 + +trap "" EXIT + +# TODO +# +# - implement and test that we can materialize a principal yet leave it with +# virtual keys +# - test new key delay? this one is tricky + +exit $ec diff --git a/tests/kdc/heimdal.acl b/tests/kdc/heimdal.acl index fc7133f09..2888a259e 100644 --- a/tests/kdc/heimdal.acl +++ b/tests/kdc/heimdal.acl @@ -1,4 +1,5 @@ foo/admin@TEST.H5L.SE all +httpkadmind/admin@TEST.H5L.SE all,get-keys bar@TEST.H5L.SE all baz@TEST.H5L.SE get,add * bez@TEST.H5L.SE get,add *@TEST.H5L.SE diff --git a/tests/kdc/krb5-httpkadmind.conf.in b/tests/kdc/krb5-httpkadmind.conf.in new file mode 100644 index 000000000..e3adbd2bb --- /dev/null +++ b/tests/kdc/krb5-httpkadmind.conf.in @@ -0,0 +1,100 @@ +[libdefaults] + default_realm = TEST.H5L.SE + no-addresses = TRUE + allow_weak_crypto = TRUE + rdns = false + fcache_strict_checking = false + name_canon_rules = as-is:realm=TEST.H5L.SE + +[appdefaults] + pkinit_anchors = FILE:@objdir@/pkinit-anchor.pem + pkinit_pool = FILE:@objdir@/pkinit-anchor.pem + +[realms] + TEST.H5L.SE = { + kdc = localhost:@port@ + pkinit_win2k = @w2k@ + } + +[kdc] + num-kdc-processes = 1 + strict-nametypes = true + enable-pkinit = true + pkinit_identity = PEM-FILE:@objdir@/user-issuer.pem + pkinit_anchors = PEM-FILE:@objdir@/pkinit-anchor.pem + pkinit_mappings_file = @srcdir@/pki-mapping + + # Locate kdc plugins for testing + plugin_dir = @objdir@/../../kdc/.libs + + # Configure kdc plugins for testing + simple_csr_authorizer_directory = @objdir@/simple_csr_authz + + enable-pkinit = true + pkinit_identity = PEM-FILE:@objdir@/user-issuer.pem + pkinit_anchors = PEM-FILE:@objdir@/pkinit-anchor.pem + pkinit_mappings_file = @srcdir@/pki-mapping + + database = { + dbname = @objdir@/current-db + realm = TEST.H5L.SE + mkey_file = @objdir@/mkey.file + log_file = @objdir@/log.current-db.log + acl_file = @srcdir@/heimdal.acl + } + + negotiate_token_validator = { + keytab = FILE:@objdir@/kt + } + + realms = { + TEST.H5L.SE = { + kx509 = { + user = { + include_pkinit_san = true + subject_name = CN=${principal-name-without-realm},DC=test,DC=h5l,DC=se + ekus = 1.3.6.1.5.5.7.3.2 + ca = PEM-FILE:@objdir@/user-issuer.pem + } + hostbased_service = { + HTTP = { + include_dnsname_san = true + ekus = 1.3.6.1.5.5.7.3.1 + ca = PEM-FILE:@objdir@/server-issuer.pem + } + } + client = { + ekus = 1.3.6.1.5.5.7.3.2 + ca = PEM-FILE:@objdir@/user-issuer.pem + } + server = { + ekus = 1.3.6.1.5.5.7.3.1 + ca = PEM-FILE:@objdir@/server-issuer.pem + } + mixed = { + ekus = 1.3.6.1.5.5.7.3.1 + ekus = 1.3.6.1.5.5.7.3.2 + ca = PEM-FILE:@objdir@/mixed-issuer.pem + } + } + } + } + +[hdb] + db-dir = @objdir@ + enable_virtual_hostbased_princs = true + virtual_hostbased_princ_mindots = 1 + virtual_hostbased_princ_maxdots = 3 + virtual_hostbased_princ_svcs = HTTP host + +[ext_keytab] + simple_csr_authorizer_directory = @objdir@/simple_csr_authz + +[logging] + kdc = 0-/FILE:@objdir@/messages.log + bx509d = 0-/FILE:@objdir@/messages.log + httpkadmind = 0-/FILE:@objdir@/messages.log + default = 0-/FILE:@objdir@/messages.log + +[domain_realm] + . = TEST.H5L.SE